feat(graph): name JS/TS namespace-member functions; add --exclude-dir - #306
feat(graph): name JS/TS namespace-member functions; add --exclude-dir#306henryhans31415 wants to merge 2 commits into
Conversation
`NS.foo = (…) => …` / `NS.foo = function` / `exports.foo` / `Foo.prototype.bar` mint a method node owned by the receiver path, and an untyped `NS.foo()` call resolves owner-qualified against it (never by bare name — trailhq#35 holds). Before, such definitions had no node at all: `callers` said "no symbol", `skeleton` omitted them, and the calls in their bodies attributed to the file. `--exclude-dir <path>` is the complement of `--only-dir`, for a committed generated copy of real source that Git's ignore rules cannot hide; it is recorded in the fingerprint so check/refresh/--deep enumerate the same set. Measured on a 200-file app written in the one-namespace style: named symbols 581 → 2,518; resolved calls into the namespace 0 → 5,603; the same three functions that returned "no symbol" now list 11, 10 and 1 callers, matching a plain grep. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
🌱 graft blast radius4 areas changed → 5 areas can be affected. 14 dependent symbols, depth 2. flowchart TB
A0(("Workspace Federation<br/>4 symbols"))
A1(("MCP Server<br/>4 symbols"))
A2(("Pull Request Review<br/>4 symbols"))
A3(("Viewer Build<br/>1 symbol"))
A4(("Context Validation<br/>1 symbol"))
classDef reached fill:#D9EDF3,stroke:#3AA7C9,stroke-width:1.5px,color:#0E313C;
class A0,A1,A2,A3,A4 reached;
Who knows this code — 5 people across 9 areas
Ownership is git history over each area's own files, weighted towards recent work (120-day half-life). Merge commits and bots are dropped, and you are dropped from your own PR. A name with no All 14 dependent symbols, grouped by areaWorkspace Federation — 4 symbols in 2 files
MCP Server — 4 symbols in 2 files
Pull Request Review — 4 symbols in 4 files
Viewer Build — 1 symbol in 1 file
Context Validation — 1 symbol in 1 file
Test signal per changed area — 1 ✓ · 2 ⚠ · 1 ✗Reached = a node under a test path has a resolved edge into the changed symbol. It undercounts anything called indirectly — through a CLI, a spawned process or a dynamic import — so read a low ratio as “look here”, never as a coverage gate.
42 test suites also reference this code51 symbols, kept out of the diagram and the table so they cannot crowd out the areas a reviewer has to look at.
Open the interactive graph → — click an area to see its dependent symbols at file:line. |
…les links every caller to every definition; the Stop-hook rebuild keeps --only-dir/--exclude-dir `.graftignore` — one repo-relative path per line — is read live on every enumeration (build, check, the freshness probe, refresh, --deep), so a path Git tracks but graft must never index stays out in a fresh checkout with no flag and no fingerprint. Only the flags go into the fingerprint. `MN.foo = …` in a base file and again in an overlay is one symbol assigned twice, not two candidates to guess between: an untyped `MN.foo()` now links to both (inferred) instead of dropping as ambiguous, which read as "nothing calls this" for exactly the functions a second file overrides. Class methods with several same-named owners still drop. The Claude Code Stop hook's background sync ran a plain `graft build`, so the first turn after a --only-dir/--exclude-dir build silently widened the graph back to the whole tree; it now re-applies the fingerprint's lists, read off the sidecar with plain fs. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Problem
A codebase written in the one-namespace style —
MN.runNode = (g, n) => {…}, the pre-ESM idiom, and likewiseexports.foo = …andFoo.prototype.bar = function— gets no nodes for those functions.describe()names a TS/JS function only fromfunction_declaration/method_definitionor avariable_declaratorwith a function value, so anassignment_expressionwith a function on the right mints nothing.Measured on a 200-file app with 1,311 such definitions:
graft buildfound 581 named symbols in it,graft callers runNodeansweredno symbol "runNode" in the graph, andskeletonlisted only the inner helpers. That is a confidently wrong map — every one of those functions read as "nothing depends on this".Change
NS.foo = (…) => …,NS.foo = function,exports.foo = …andFoo.prototype.bar = functionmint a method node owned by the receiver path (NS,NS.sub,Foo;prototypeis stripped so the method belongs to the class andthis.x()inside it resolves against it).exports./module.exports.members are marked exported.bindings.tspushes the same scope segment (NS.foo) so a typed local inside the body still resolves; the helper lives in bindings.ts, which extract.ts already imports from, so no new cycle.nsReceiver(TS/JS only; a plain identifier path, neverthis/super). It resolves only owner-qualified —ownerMethod.has("MN.runNode")— so this is not the bare-name fallbackmapclusters one entry per file instead of per directory #35 ruled out:x.foo()with nox.foo = …definition still drops (test included), and cross-file ambiguity still drops.graft build --exclude-dir <path>— the complement of--only-dir. The motivating repo commits a generated copy ofsrc/(a serverless function cannot reach../../src), andgit ls-filesalways lists a tracked file, so without this every symbol appeared twice and every owner-qualified call dropped as ambiguous. Same normalization and the same home as--only-dir(the fingerprint, never.graft/config.json); honored by build,check, the hooks/refresh path and--deep.Evidence
Same repo, before → after: named symbols in the app 581 → 2,518; resolved calls into the namespace 0 → 5,603;
callers runNode/runCharge/sanitiseStatewent from "no symbol" to 11 / 10 / 1 callers, matching a plaingrep.Full suite green (1,227 pass, 0 fail). Eight new tests in
test/graph-ts-namespace.test.ts(nodes, owners, same-file/cross-file confidence, prototype +this, bindings scope, the #35 regression guard, the shapes that must mint nothing) andtest/graph-exclude-dir.test.ts(end-to-end through the CLI: fingerprint,check, the freshness probe, composition with--only-dir).Not in this PR
Arrow functions as object-literal values (
{ run: () => {…} }) are still unnamed — there is no natural qualified name for them. Happy to follow up if you would like a scheme for that.🤖 Generated with Claude Code