The §2b 'Done' header still read '(7 + 3 inherited)' from an earlier stage;
the tracker below it now lists 15 validated languages. Sync the count.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pascal unit/class `const` sections already extracted as `constant`, so wiring was
add-to-`VALUE_REF_LANGS` + shadow prune + two Pascal-specific fixes:
**`constant`-only targets** — the Pascal extractor emits function parameters
(`const ATarget: TControl`, `var Dest: …`) and class fields as `variable` at
enclosing scope, which would collapse to noisy file-wide targets. Genuine shared
values are all `constant` (`declConst`), so targets are restricted to that kind.
(Unit `var` globals are the rare cost; the parameter/field noise dominates.)
**Sibling-body fix (same as Dart)** — Pascal attaches a proc body (`block`) as a
next sibling of the `declProc` header (the reader scope), both under `defProc`,
not as a child. Extended the existing Dart `function_body` sibling-pull to also
walk a `block` sibling so reads inside the body are found.
Shadow prune wired for `declConst`/`declVar` — a function-local `const X`
shadows a unit `const X`.
Validated S/M/L (horse / mORMot2 / castle-engine): node count identical on/off,
precision sample 100% TP (font/crypto/DB/FFI binding consts); headline is FFI
library-name constants read by hundreds of `external` declarations (impact
2→1880, 1→358). Caveats: low same-file density on app code (cross-unit reads;
horse gave 4 edges), the `constant`-only restriction, a rare tree-sitter-pascal
misparse of `const` params in complex Delphi signatures, and case-insensitivity
(exact-text scan misses a differently-cased reference — a miss, never an FP).
Coverage now 15 languages.
Dart's grammar cleanly separates the constant idiom: a `static_final_declaration`
is exactly a top-level or class-`static` `const`/`final`, while instance fields
and `var` use `initialized_identifier` and method-locals use
`initialized_variable_definition`. A `visitNode` rule maps `static_final_declaration`
to `constant`, so there are no instance/local leaks to guard.
The reader-scan needed a Dart-specific fix: Dart attaches a method/function body
as a NEXT SIBLING of the signature node (which is stored as the reader scope),
not as a child, so the scan saw only the signature and produced zero edges until
it was taught to also walk a `function_body` next-sibling. The shadow prune counts
all three Dart declarator nodes so a method-local `const X` drops a file `const X`.
Validated S/M/L (http / flame-engine/flame / flutter/packages): node count
identical on/off; the headline is a single `static const String iconFont` read by
every Cupertino icon definition (impact 1 -> 1790). The common Dart codegen
suffixes (.g.dart/.freezed.dart/.pb.dart) are already skipped; header-only-marked
generators (JNIGEN _bindings.dart, pigeon .gen.dart) are not, so generated FFI/JNI
bindings are noisy while real source stays clean. Coverage now 15 languages.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Top-level `let` and `static let` in a `struct`/`enum`/`class` (Swift's
shared-constant idiom — it namespaces constants in `enum`/`struct`) now extract
as `constant` and participate in impact analysis; instance stored `let`s stay
`field`, and computed properties (getter, no stored value) are skipped.
Reuses the Kotlin machinery: the name nests
`property_declaration → pattern → simple_identifier` (resolved via
firstSimpleIdentifier), and the reader-scan already matched `simple_identifier`.
Two Swift-specific touches: the value-ref target gate was widened to accept
`struct:`/`enum:` parents (every other language's targets sit at
file:/class:/module:), and computed properties are detected and skipped. Node
creation slots into the existing Swift property_declaration handler, so the
property-wrapper / type-annotation dependency extraction (@Published/@State) is
untouched.
Validated S/M/L (Alamofire / swift-argument-parser / swift-nio): node count
identical on/off, genuine static-let constants, computed properties skipped,
0-1 sibling-type collisions per repo. Coverage now 14 languages.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extends same-file constant-reader impact edges to six more languages, each
requiring a different technique:
**C** — file-scope `static const` / global nodes weren't extracted at all (name
nests in `init_declarator`, the generic fallback missed it). Added a C branch in
`extractVariable` with `cDeclaratorIdentifier` to walk the declarator chain.
Skips bare-`identifier` declarators to kill the macro-prefixed-prototype misparse
FP cluster (a `MACRO RetType fn(args);` mints a spurious type-named global).
Prune switch gains `init_declarator`.
**Java** — `static final` fields already existed but as `field` kind, which the
value-ref gate rejects. An `isConst` predicate (`static` + `final` modifiers)
re-kinds the constant subset to `constant` in `extractField`. Instance `final`
fields stay `field`.
**C#** — same `field`→`constant` approach: `const` modifier or `static readonly`
modifier pair → `constant`; instance `readonly` stays `field`.
**PHP** — constants already extracted as `constant`; the only gap was the
reader-scan. PHP represents a constant reference as a `name` node (not
`identifier`), so bare `X` and the const half of `self::X` / `Foo::X` were
invisible. Added `name` to the scan. No prune wiring needed: a `$var` local is a
`variable_name` — a different namespace — and can never shadow a bare constant.
**Scala** — top-level `val` was already `constant`; `object` and `class` vals
both came out as `field`. Fixed by walking to the enclosing AST definition in the
`val_definition` handler: `object_definition` → `constant`/`variable`; `class`/
`trait`/`enum`/`given` → `field`. Prune switch gains `val_definition`/
`var_definition`.
**Kotlin** — properties weren't extracted at all (`property_declaration →
variable_declaration → simple_identifier` is too deep for the generic path).
Added a `property_declaration` handler in `visitNode`: pulls the nested name,
walks to the enclosing scope for kind (`object`/`companion object`/top-level →
`constant`/`variable`; `class` → `field`; function body → local, skipped). Reader-
scan gains `simple_identifier`; prune switch gains `property_declaration`.
Validated S/M/L across all six languages (hiredis/curl/redis, gson/commons-lang/
guava, AutoMapper/Newtonsoft/efcore, guzzle/monolog/laravel, upickle/cats/pekko,
okio/coroutines/ktor): node count identical on/off, precision guards held, 0
shadow leaks, impact wins confirmed (`INDEX_NOT_FOUND` 4→165, `Curl_ssl` 3→57,
`_resourceManager` 22→1664, `STATE_IN_QUEUE` 1→32, `BLOCKING_SHIFT` 1→24).
C++ was attempted and reverted — tree-sitter-cpp parse fidelity on
template/macro-heavy code leaks class members to file scope as bogus constants;
did not reach the precision bar.
Add a coverage section to the playbook (§2b) mapping value-reference-edge support
onto the README's 24-row Supported Languages table + the framework list:
- Done (7): TS, JS, tsx, Python, Go, Rust, Ruby.
- Done/inherited (3): Svelte, Vue, Astro — their extractors re-parse the
<script>/frontmatter as typescript/javascript (in VALUE_REF_LANGS), so a const
in a .svelte/.vue/.astro script edges its readers for free (verified on a
synthetic .svelte). Noted in the design doc too.
- Remaining (easy/file-scope): C, Kotlin (top-level), Swift (top-level), Dart
(top-level), Lua/Luau, R.
- Remaining (needs the Ruby class-scope treatment): Java, C#, Scala, PHP, C++,
Pascal/Delphi, Objective-C.
- N/A: Liquid.
Plus a note that frameworks are NOT a value-refs axis — it's language-level and
framework-agnostic (validation already ran on Rails/Django/gin/express/…); the
README's framework list is the separate route-extraction feature.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ruby keeps almost all constants inside a class/module (jekyll's lib: 0 top-level,
58 class-internal), so this needed the class-scope extension flagged since Go.
Three Ruby-specific changes:
- Extract Ruby constants as symbols at all: a `CONST = …` has a `constant`-typed
LHS (not `identifier`), so they were never extracted — including class-internal
ones (the variableTypes dispatch skipped class-internal assignments; it now
makes an exception for `constant`-LHS assignments).
- The value-ref target gate accepts `class:`/`module:` parents, not just `file:`.
- The reader-scan matches `constant` nodes (Ruby represents both a constant's
definition AND its references as `constant`, not `identifier`).
Effectively Ruby-only: Rust impl consts are already `file:`-parented (ripgrep
stayed at 144 edges) and TS/Python class members aren't constant/variable kind.
Class-scope precision uses a FILE-WIDE target map (not strict same-class) because
Ruby constant lookup is lexical+ancestor — a nested class legitimately reads an
enclosing class's constant (jekyll ERBRenderer→ThemeBuilder::SCAFFOLD_DIRECTORIES;
sinatra AcceptEntry→Request::HEADER_PARAM), and strict matching would drop those.
The only real FP is the same name in sibling classes in one file (~1.7% of
targets on rails; a NameError in valid Ruby, so rare). Net ~98–100% precision.
Validated small/medium/large on public Ruby OSS (sinatra, jekyll, rails): node
count identical on/off, ~98–100% precision, impact win holds (HEADER_PARAM 1→5).
Adds a Ruby test (top-level + class-internal const reads). Design matrix,
playbook (class-scope section now resolved), and CHANGELOG updated. Suite green
(1,558 passed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extend value-reference edges to Rust (module-level `const`/`static`). Rust's
declarators are `const_item`/`static_item` (the module consts) and
`let_declaration` (the local that shadows) — added to the per-grammar prune
switch. A synthetic probe caught the expected shadow FP (a `const TIMEOUT`
shadowed by a local `let TIMEOUT`), fixed by the addition.
No prune-RULE change needed: Rust's `#[cfg]`-conditional consts (`#[cfg(unix)]
const SEP = …; #[cfg(windows)] const SEP = …`) are kept correctly by the
Python-era file-scope-count rule (validated on tokio's io/interest.rs cfg-gated
flags). Consts written inside a config macro (`cfg_aio! { … }`) live in an
unparsed token tree, so the prune's syntax walk doesn't even see them.
Validated small/medium/large on public Rust OSS (ripgrep, tokio, rust-analyzer):
node count identical on/off (incl. 38.8k nodes on rust-analyzer), precision
samples all true positives, zero real shadow leaks, and the impact win reproduces
(rust-analyzer `INLINE_CAP` 2→183, tokio `PERMIT_SHIFT` 1→97).
Adds 2 Rust test cases (const/static read; the local-`let` shadow). Design
matrix, playbook, and CHANGELOG updated. Full suite green (1,557 passed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extend value-reference edges to Python (module-level `NAME = …`; the `assignment`
declarator added to the per-grammar shadow-prune switch). A synthetic probe found
the expected shadow FP — a module `CONFIG` shadowed by a local `CONFIG = …` —
fixed by that addition.
Python also exposed a flaw in the prune RULE. Python conditionally defines module
constants (`try: X = a; except: X = b`), a very common idiom that binds a name
twice AT MODULE SCOPE. The old "declarators > 1 → drop" over-pruned these,
dropping a real const and all its readers. Refine the rule to compare declarator
count against the number of FILE-SCOPE NODES the name has: a conditional def makes
them equal (kept); a real local shadow makes declarators exceed file-scope nodes
(pruned). Strictly more correct for every language. Track file-scope node count
per name in captureValueRefScope; also suppress same-name value-ref edges (the two
halves of a conditional def would otherwise cross-reference via their own names).
Validated small/medium/large on public Python OSS (requests, sqlalchemy, django):
node count identical on/off, precision samples all true positives, guard holds,
conditional defs now kept (django recall +18 edges), zero same-name edges, and the
impact win reproduces (django `_trans` 1→138, sqlalchemy `COMPARE_FAILED` 1→26).
Adds 3 Python test cases (module read; the local-`=` shadow, verified to fail
without the prune extension; the conditional-def keep case). Design matrix,
playbook, and CHANGELOG updated. Full suite green (1,555 passed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extend value-reference edges to Go. Go's package-level const/var are file-scope,
so the target gate fits as-is. But Go's declarators are const_spec / var_spec /
short_var_declaration, not variable_declarator, so the shadow prune was a no-op
for Go — a package `const Timeout` shadowed by a local `Timeout := …` produced a
false positive (a synthetic probe caught it immediately). Generalise the prune's
declarator scan into a per-node-type switch (a file only contains its own
grammar's nodes, so listing all languages in one switch is safe).
Validated small/medium/large/very-large on public Go OSS (gin, hugo, prometheus,
kubernetes): node count identical on/off at every size (incl. 251k nodes on k8s),
precision samples all true positives, guard invariant clean, and the impact win
reproduces (k8s KubeletSubsystem 3→138, prometheus rdsLabelInstance 1→82).
Adds 2 Go test cases (package-const read; the shadow case, verified to fail
without the prune extension). Design matrix, playbook, and CHANGELOG updated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Self-contained "Start on language X" runbook: how the feature works + code map,
the precision guards and what is/isn't an FP, the full validation recipe
(probe.sh, FP-hunt SQL, impact-delta + agent-A/B), the per-language checklist
(file-scope-vs-class-scope decision, declarator node type per grammar, extractor
kind check), the git/PR workflow, and the traps already hit (the LIKE
'%valueRef%' filename false-match, SearchResult/.node, initSync ignoring opts,
require-bindings-are-not-FPs). Cross-linked from the design doc.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
tsx is in VALUE_REF_LANGS and was exercised at scale by the excalidraw sweep
(tablerIconProps lives in icons.tsx), but those readers were normal expressions.
Add a unit test for the one tsx-specific path — a const read ONLY inside JSX
(`<Foo x={CONST}/>`), which depends on the reader-scan descending into the JSX
subtree — so it's locked against regression. Passes; no code change needed.
Note in the design doc that tsx needs no separate repo sweep.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sweep value-refs across small/medium/large JS (express, eslint, webpack),
indexing each on vs off. Clean on all three: node count identical on/off,
precision guards held, no minified-file or shadow FPs, and the impact-radius
win reproduces (webpack `LogType` 1->89, eslint `internalSlotsMap` 1->32).
No code fix needed — the #895 shadow prune and generated-file skip generalize
to JS unchanged.
Document the one JS-specific characteristic: CommonJS `var x = require(...)`
bindings and module-level `var` state are value-ref targets (hence JS's higher
~4-5% edge share vs TS's ~0.7-1.6%). These are correct, not noise — changing
such a binding genuinely affects its readers, and impact dedups against any
overlapping `calls` edge. TS `import`s dodge this (import-kind nodes, never
const/var targets).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sweep the value-refs feature across small/medium/large TypeScript repos (ky,
excalidraw, vscode), indexing each with the flag on vs off. Result: clean on all
three — node count identical on/off (edges-only), edge growth <=1.6%, precision
guards held, and the impact-radius win reproduces at every size (e.g. vscode
`LayoutStateKeys` 1->85 affected, `CORE_WEIGHT` 1->52).
No new code fix needed: the #895 shadow prune (which fixed excalidraw's 23
bundled-file false positives) generalizes — vscode showed no param-shadow or
bundle FPs in the top 200 targets, and the one apparent anomaly was a probe
artifact (a filename containing "ValueRef"), not a real edge.
Document the design, the precision guards, the validation matrix, the agent-A/B
finding (impact-API correctness is the win, not agent read-reduction), the known
limitations (param-only shadowing left unguarded by intent), and the recipe to
extend the sweep to the next language.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Value-reference edges (same-file `references` edges from a reader to the
file-scope const/var it reads) shipped behind CODEGRAPH_VALUE_REFS pending an
agent A/B. The A/B is in: on excalidraw the edges are correct and precise (node
count unchanged) and they transform the impact/blast-radius API — `impact` on a
const consumed by 103 readers goes from 1 affected symbol to the full radius.
That blast-radius API is what `codegraph impact` and CodeGraph Pro's verdict
engine consume, so the win is impact correctness; the agent path showed no
regression. Flip the default on; CODEGRAPH_VALUE_REFS=0 disables.
Also close the one precision gap the A/B surfaced: a bundled/Emscripten
`const Module` re-declared as an inner `var Module` / param produced false
positives (nested readers resolve to the inner binding). isGeneratedFile() is
path-only and can't catch content-minified bundles, so prune SHADOWED targets at
the syntax level — drop any value-ref target whose name is bound by more than one
`variable_declarator` in the file. On excalidraw this removes the 23 false
positives while preserving every real reader (impact unchanged at 170).
Adds regression coverage (there was none): same-file readers are edged, they
surface in the impact radius, shadowed consts are NOT edged, and
CODEGRAPH_VALUE_REFS=0 emits nothing.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`codegraph index` ran extraction against the already-populated DB without
clearing it first. On an unchanged tree every file's content hash still
matched, so the orchestrator skipped re-inserting all of them and the run
reported its delta (after - before = 0) as "0 nodes, 0 edges" — which read as
if `index` had wiped the graph. `init` only ever differed because it runs on a
freshly created, empty DB.
Clear the existing graph before re-indexing so `index` rebuilds from scratch
and reports the same complete result as a fresh `init`. `--force` keeps its
role as the home-dir/root-path override; `sync` stays the incremental path.
Adds an end-to-end regression test driving the built binary (init -> index),
asserting the graph stays populated and the summary is never "0 nodes, 0 edges".
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On the Linux per-directory watch path, hitting fs.inotify.max_user_watches
surfaces as ENOSPC — which the degrade logic added for #876 (EMFILE/ENFILE
only) did not catch, so it fell through to the silent "skip this directory"
branch: a large repo got a partial watch set with no hint why edits in
unwatched directories stopped auto-syncing.
ENOSPC is non-fatal — raise the limit and partial watching keeps working — so
it now warns ONCE, naming the exact knob (fs.inotify.max_user_watches, with the
sysctl to set it), instead of degrading. It also stops attempting further doomed
watches for the session (every inotify_add_watch would fail too). Installed
watches keep firing; `codegraph sync` / git sync hooks cover the remainder.
Validated on macOS (forced per-directory path) and real Linux (Docker) — the
new test asserts a single warning naming fs.inotify.max_user_watches, no
degrade, and a live partial watch.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When live file watching permanently degrades (watch-resource exhaustion, or a
write lock held past the retry budget), getPendingFiles() goes empty — so the
existing per-file staleness banner can't fire even though the index is now
frozen and silently drifting stale. The agent kept getting clean-looking
responses off a no-longer-updating index.
Read-tool responses now lead with a whole-index banner ("CodeGraph auto-sync
is DISABLED…") whenever the watcher is degraded, and codegraph_status gets a
dedicated "Auto-sync disabled" section. Both carry the degrade reason and tell
the agent to Read files directly. Expose isWatcherDegraded() /
getWatcherDegradedReason() on the CodeGraph class, and document the new banner
in the MCP server instructions.
Completes the agent-notification half of #876 (the operator-facing onDegraded
wiring shipped in #891).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The live file watcher could stay "alive" after it had stopped being
trustworthy. EMFILE/ENFILE watch-resource exhaustion only logged (and was
silently tolerated on the Linux per-directory path), and prolonged
LockUnavailableError retried forever at the normal debounce cadence — both
left auto-sync dead while the index silently drifted stale. Especially bad
for long-running MCP/daemon sessions.
Add a one-way degrade(): on watch-resource exhaustion (any watch strategy)
or on lock contention past a bounded exponential-backoff budget, log once,
fire a new onDegraded callback, and stop. start() now returns false
consistently when the per-directory path degrades at startup — it previously
returned true on Linux, so the MCP server reported the watcher "active" when
it had degraded. Wire onDegraded into the MCP server so callers are actually
told, and expose isDegraded()/getDegradedReason().
Builds on the approach in #877 by @thismilktea. Validated on macOS
(recursive), Linux (per-directory, Docker) and Windows (recursive) — 30/30
watcher + watch-policy tests on each.
Closes#876
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Emit 'references' edges from a symbol to the file-scope const/var it reads
(TS/JS), so impact analysis catches "change this table, affect its readers".
Off by default behind CODEGRAPH_VALUE_REFS pending the agent A/B; on a real PR:
+3.1% edges, 100% precision on the spot-checked target, 372/372 extraction tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A git worktree nested in a project (e.g. Claude Code's gitignored
`.claude/worktrees/<name>/`) was swept into the index as an embedded repo: its
`.git` is a FILE pointing into the host repo's `.git/worktrees/`, and embedded-
repo discovery treated any `.git` (file or directory) as a distinct repo to
index. Each worktree then duplicated the entire graph — one report went from
~1,850 files to 24,533, with search/explore flooded by stale copies.
classifyGitDir() now distinguishes:
- `.git` directory -> embedded clone, index (#193/#514/#622, unchanged)
- `.git` file → worktrees/ -> worktree, skip (#848)
- `.git` file → modules/ -> submodule, index (unchanged)
Applied at both embedded-repo entry points: findNestedGitRepos discovery (which
also covers the sync/change-detection path) and the untracked-subdir recursion
in collectGitFiles.
Verified: the reproduction drops from 6 files / betaHelper×3 to 3 files / ×1,
with a genuine embedded clone and submodules still indexed. Regression test added.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
`codegraph serve --mcp` is the stdio MCP server an AI agent launches for itself
(the installer wires it into every agent's MCP config), not a command a human
runs. Run by hand in a terminal it just hung waiting for JSON-RPC, looking
broken.
- Hide `serve` from `--help` (commander `{ hidden: true }`); it stays fully
invocable, so agents are unaffected.
- When stdin is an interactive TTY (a person — never the agent's pipe or the
detached daemon), print what it is and point to `codegraph status` /
`codegraph daemon`, then exit instead of hanging.
- README: drop `serve --mcp` from the CLI Reference and stop the troubleshooting
section from telling users to run it; keep the accurate "your agent launches
it" note.
Verified: agent path intact (22 MCP handshake/daemon tests pass), `serve` absent
from --help, and the TTY path prints the message and exits cleanly.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Cross-file `ClassName.staticMethod()` calls resolved to the class, not the
method: the import resolver matched the receiver `Foo` to the named class
import but dropped the `.bar` member, and createEdges then mis-promoted the
`calls` edge to `instantiates`. So callers/impact for the static method came
back empty. Descend from the resolved class into its `Container::member` so the
call links to the method; fall back to the class when no such member exists
(non-`::` languages and genuine class references are unaffected).
Also normalize `codegraph affected` inputs to the project-relative,
forward-slash form the index stores, so `./src/x.ts`, an absolute path, and a
Windows back-slash path all match (previously silently returned 0).
Validated on luxon (24 files): node/edge totals identical (no explosion), 69
mis-promoted `instantiates` edges become `calls`, and real static factories
(DateTime.fromISO, etc.) resolve their callers. Full suite: 1534 passed.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(cli): codegraph version command + complete CLI Reference
Add a `codegraph version` subcommand plus the `-v` and `-version`
spellings (commander already wires up `--version`/`-V`), so the version
is easy to reach however a user guesses at it. The `-v`/`-version` forms
are intercepted before commander parses — its version short flag is the
capital `-V`, and its parser rejects a multi-character single-dash flag.
A trailing `-v` on a subcommand still means `--verbose`.
Document the previously-missing commands in the README CLI Reference:
`daemon`/`daemons`, `unlock`, `telemetry`, `version`, and `help`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(changelog): reference #864 on the version-command entry
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Collapses the unreleased daemon controls into a single interactive command.
`codegraph daemon` (alias `daemons`) opens an arrow-key picker (current project's
daemon first, pre-selected), enter stops it, or pick "Stop all"; non-TTY prints a
plain list. Removes stop/list/ps; reuses the unchanged daemon-registry machinery;
the pick->stop loop is in daemon-manager.ts behind an injectable select (unit
tested). Validated live on macOS/Linux (real clack picker driven via pty) and
Windows (real runDaemonPicker + stopDaemonAt against a real daemon). Closes#845
follow-up.
Adds first-class daemon control (the #845 pain point: no clean way to stop a
runaway daemon). `codegraph stop [path]` stops the current/given project's
daemon (SIGTERM -> SIGKILL fallback, sweeps artifacts); `stop --all` stops every
daemon; `list`/`ps` shows running daemons (--json for scripts).
Discovery via a small self-healing registry: each daemon records its root under
~/.codegraph/daemons/ on start, removes it on graceful shutdown; readers prune
dead pids. Cross-platform by construction (files + process.kill). Validated live
on macOS, Linux (docker), and Windows (VM): registry unit 6/6 and real-daemon
stop/list 6/6 on each.
Running the installer or `codegraph init`/`index` from $HOME auto-indexed the
entire home tree (installer indexes process.cwd() with no guard), producing a
multi-GB ~/.codegraph/codegraph.db; the install dir sharing the ~/.codegraph
name then made every home subdir resolve its root to $HOME. On pre-1.0 macOS the
per-file watcher over that tree exhausted kern.maxfiles and crashed the machine
(#845; the fd blowup was fixed in 1.0.0, this fixes the root cause).
Add unsafeIndexRootReason() and refuse the home dir, a parent of home, and
filesystem roots at the installer auto-index, `init`, and `index`. Overridable
with --force. Closes#845.
Validated the watchdog on the Windows VM: it kills a wedged process correctly,
but Windows has no real signals — process.kill(pid,'SIGKILL') maps to
TerminateProcess, seen as signal=null + non-zero code, not 'SIGKILL'. Assert
"killed" platform-agnostically and require the own exit code in the opt-out test.
Source watchdog unchanged. Windows: fatal-handler 8/8, liveness-watchdog 7/7,
mcp-daemon 9/9; mcp-initialize EPERM is pre-existing (identical with watchdog off).
The worker-thread watchdog from #856 didn't work in the real daemon — caught by
live-testing against a real serve --mcp. V8 isolates coordinate on global
safepoints, so a main thread wedged in a tight non-allocating loop (#850's
SourcePositionTableIterator::Advance) strands the watchdog worker before it can
SIGKILL.
A separate child process shares no isolate/heap with the parent, so the wedge
can't touch it; it kills via the kernel. Parent heartbeats to the child's stdin;
silence past the timeout -> SIGKILL; parent exit closes the pipe -> child exits.
Validated live (real daemon SIGKILLed in ~timeout); regression test covers the
non-allocating-wedge-under-heap-pressure case. API/install points/CHANGELOG
unchanged; the broken worker version was never released.
Belt-and-suspenders follow-up to #855. Any non-yielding sync loop on the main
thread wedges the event loop, and nothing running on that loop (timers, signal
handlers, PPID watchdog) can recover it — only another thread can.
A tiny worker thread (in the detached daemon + direct modes) watches a
shared-memory heartbeat the main thread bumps each event-loop turn; if it stops
advancing across enough consecutive checks (~CODEGRAPH_WATCHDOG_TIMEOUT_MS,
default 60s) the worker SIGKILLs the process so a fresh daemon starts on the next
connection. Counts consecutive stale checks (not wall-clock) so it's immune to
clock jumps / sleep; tuned never to fire on real work; opt out with
CODEGRAPH_NO_WATCHDOG=1.
The process-wide uncaughtException handler logged the error and kept running. For
the detached `serve --mcp` daemon that turned any escaped fault into an
unrecoverable orphan: nothing respawns it, and when logging the raw Error hit a
V8 source-position loop while lazily formatting `.stack`, the main thread wedged
at 100% CPU so even the PPID watchdog / idle-timer could no longer fire. Same
failure mode as #799, which only fixed the stdin-'error' trigger.
Restore Node's default fatal semantics: render a bounded, hang-proof line (name +
message only — never read `.stack`) then exit non-zero, so a fresh daemon starts
on the next connection. Extracted to src/bin/fatal-handler.ts with injectable
seams; unit-tested incl. the never-touch-stack invariant.
Closes#850.
R has no declaration syntax — everything is an expression — so the
extractor works through the visitNode hook: functions in every
assignment form (incl. nested, attributed to their enclosing scope),
top-level variables/constants, library()/require() imports and
source() file references (claimed, Lua-style), S4/RefClass/R6/ggproto
classes with their methods and extends edges, setGeneric/setMethod.
Grammar vendored from r-lib/tree-sitter-r v1.2.0 (ABI 14; npm package
is a security placeholder, tree-sitter-wasms has no R).
Benchmarked on AnomalyDetection (8/8 named defs), dplyr (1027 fns),
ggplot2 (150 ggproto classes / 597 methods / 128 extends edges —
adding ggproto mid-bench flipped the large-repo A/B from a regression
to 2.4x faster than the no-codegraph arm).
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
The shipped grammar parses every record form as record_declaration (no
record_struct_declaration node), so 'record struct' mis-kinded as class.
classifyClassNode now distinguishes the value-type form by its struct
keyword child, and extractStruct accepts bodiless positional records
(the no-body gate is for C/C++ forward declarations) instead of
crashing mid-file on them.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
A workspace holding several independent git repos failed two ways:
- Enumeration: a super-repo whose .gitignore hides its child repos
(/packages/) indexed 0 of their files — git never lists ignored dirs,
and the #193 embedded-repo recursion only fired for UNTRACKED dirs.
Gitignored embedded repos are now discovered (ignored-dirs listing +
bounded .git search) and enumerated by their own git ls-files.
- Change detection: git status in the parent says nothing about embedded
repos (untracked OR ignored), so codegraph sync missed every child
change. Status now recurses per embedded repo.
ScopeIgnore is the new single source of truth for indexer + watcher
scope: parent rules for ordinary paths, the child repo's own rules for
paths inside it, built-in defaults uniformly on full paths (a git repo
inside node_modules is an npm git-dependency, not project code), and
ancestors of embedded roots are never pruned (the Linux per-directory
watcher must descend to reach them).
Non-git workspace roots already worked via the per-directory gitignore
walk — locked in by test.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(installer): opencode global config goes to ~/.config/opencode on every platform (#535)
opencode resolves its config dir with xdg-basedir (XDG_CONFIG_HOME ??
~/.config) unconditionally — it never reads %APPDATA%; that layout
belonged to the discontinued Go fork. Writing there on Windows meant
opencode never saw the MCP entry.
- globalConfigDir(): drop the win32 APPDATA branch; XDG resolution everywhere
- install/uninstall (global): sweep a stale codegraph entry + AGENTS.md block
out of the legacy %APPDATA%/opencode location (siblings/comments untouched)
- detect(global): a legacy-only dir still counts as installed so the sweep
is reachable
- tests are env-gated, not platform-gated, so the whole matrix runs on any
OS; the suite previously pointed APPDATA and XDG_CONFIG_HOME at the same
dir, which is exactly how the divergence stayed invisible
Supersedes the prefer-if-exists approach of #670 (greenfield installs --
before opencode's first run -- would still have fallen back to APPDATA).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(installer): match legacy sweep paths by dir prefix, not 'AppData' substring
On Windows os.tmpdir() lives under AppData\Local\Temp, so every harness
path contains 'AppData' and the substring assertions false-positive.
Caught on the real Windows VM.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(explore): announce dynamic-dispatch boundaries when a flow can't connect statically (#687)
When buildFlowFromNamedSymbols can't connect the agent's named symbols, scan
the disconnected symbols' bodies (query-time, deterministic, zero graph
mutation) for dynamic-dispatch forms — computed member calls, getattr,
reflection, typed message buses, runtime-keyed emits, Proxy — and announce
the exact site where the static path ends, with candidate runtime targets
when a dispatch key is statically visible. The honest alternative to
guessing edges: surface the boundary, don't fabricate the bridge.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(agent-eval): ab-new-vs-baseline survives files added since the baseline ref
A single multi-file 'git checkout <ref> --' with one unknown pathspec checks
out nothing, so the baseline arm silently ran the NEW build. Check out
per-file and remove files that don't exist on the baseline.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(playbook): boundary surfacing as the mechanism floor for non-gateable dispatch (#687)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(explore): render a direct synthesized hop between two named symbols (#687)
A 2-node chain populates pathIds but renders nothing (Flow needs >=3), and
the dynamic-links section skipped its edge as 'already in the main chain' —
so a custom EventBus emit→handler connection was invisible. Skip-as-in-chain
now applies only when a chain actually renders, and the boundary scan treats
short-chain endpoints as connected.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Adds anonymous usage statistics (commands/tools used, languages indexed,
connecting agents) with a strict, auditable allowlist. Never code, paths,
file/symbol names, queries, or IPs.
- src/telemetry/: zero-dep client — consent resolution (DO_NOT_TRACK >
CODEGRAPH_TELEMETRY > stored choice > default-on), random machine UUID,
in-memory counters → capped JSONL buffer → completed-day rollups; sync
exit-append (survives process.exit) + opportunistic bounded sends; the
first-run notice gates the first SEND, never local buffering, so the
installer's consent toggle always precedes it. Off is off: no recording,
no socket, buffered data deleted.
- codegraph telemetry status|on|off; per-command counting via preAction hook.
- MCP: tool counting after the reply is on the wire (session + proxy
in-process fallback), agent attribution from initialize clientInfo,
unref'd daemon flush interval. Zero hot-path cost, zero stdout.
- Installer: visible default-on consent toggle (asked once, never re-asked),
install/index/uninstall lifecycle events.
- telemetry-worker/: public Cloudflare Worker behind telemetry.getcodegraph.com
— allowlist validation, IP stripping, per-machine rate limit, forwards to
PostHog as anonymous events. Ships nowhere with the npm package.
- TELEMETRY.md (field-by-field contract) + README section + design doc.
- 20 unit tests; suite-wide CODEGRAPH_TELEMETRY=0 guard so tests never
pollute real telemetry. Full suite: 1448 passing.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
The spawn-based tests failed on Windows with EPERM in afterEach — the
SIGKILL'd server child briefly holds the temp cwd/SQLite handles when
rmSync runs (the documented class that fails mcp-initialize/mcp-roots
teardowns). Await the child's exit (3s cap) and retry the removal
(maxRetries/retryDelay); assertions were already passing on Windows.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
The table still listed all 8 tools; it now shows the default four
(explore/node/search/callers, with node's Read-parity file mode), the
CODEGRAPH_MCP_TOOLS re-enable path + CLI equivalents for the unlisted
four, and the inactive-when-unindexed behavior (#817).
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>