Summary
_SKIP_DIRS contains the bare string "out", matched by name alone in _is_noise_dir. In a hexagonal / ports-and-adapters codebase, adapter/out/ and port/out/ are where the entire outbound layer lives — persistence adapters, JPA entities, messaging adapters, outbound port interfaces. All of it is silently dropped from the scan.
On the repo where I hit this, that was 196 of 655 .java files (30%), including every @Entity class. Exit code 0, no warning, nothing in the run output.
This is the same mechanism as #2479 (which reports it for build/ and explicitly lists out as having the same problem). Filing separately because the blast radius is different in kind: build/ loses a directory, out/ loses an architectural layer, and the convention that puts source there is mainstream rather than incidental.
Environment
- graphify (
graphifyy) 0.9.54, installed via uv tool install
- macOS 26.6.2, CPython 3.11.15
- Command:
graphify extract . --code-only --timing
Mechanism
_SKIP_DIRS = {
"venv", ".venv", # "env"/".env"/"*_env" are gated on venv markers below (#2058)
"node_modules", "__pycache__", ".git",
"dist", "build", "target", "out",
...
_is_noise_dir already gates three ambiguous names on corroborating evidence, each returning False when it cannot verify:
out gets none of it — if part in _SKIP_DIRS: return True, unconditionally, before any of those checks run.
Repro
Any Spring/hexagonal layout, e.g.:
backend/src/main/java/.../domain/transaction/adapter/out/persistence/OrderEntity.java
backend/src/main/java/.../domain/transaction/port/out/OrderRepositoryPort.java
$ graphify extract . --code-only
[graphify extract] found 789 code, 0 docs, 0 papers, 0 images
[graphify extract] wrote graphify-out/graph.json: 6194 nodes, 14196 edges
>>> import json, pathlib
>>> g = json.load(open('graphify-out/graph.json'))
>>> in_graph = {n['source_file'] for n in g['nodes'] if str(n.get('source_file','')).endswith('.java')}
>>> on_disk = {str(p) for p in pathlib.Path('.').rglob('*.java') if '/target/' not in str(p)}
>>> len(on_disk), len(in_graph)
(655, 459)
>>> missing = on_disk - in_graph
>>> len(missing), sum('/out/' in m for m in missing)
(196, 196)
Every missing file contains /out/. Zero exceptions. And in that repo, every directory named out/ contained only .java files — no build artifacts anywhere:
$ find . -type d -name out -not -path "*/node_modules/*" -not -path "*/target/*" \
-exec find {} -type f \; | sed 's/.*\.//' | sort | uniq -c
196 java
Removing "out" from _SKIP_DIRS locally and re-running, same command, same repo:
|
before |
after |
| code files scanned |
789 |
986 |
.java in graph |
459 / 655 |
655 / 655 |
| nodes |
6,194 |
7,416 |
| edges |
14,196 |
17,759 |
Why this is worse than a missing directory
The dropped set is not arbitrary — it is precisely the persistence and outbound-integration layer. So the graph that survives looks coherent: controllers, use-case ports and service implementations are all present and correctly linked, and the inbound half of every hexagon traces cleanly. Nothing looks broken.
What is gone is every path from application code to the database. In my case that meant zero edges between the Java subgraph and the SQL subgraph, and I initially attributed that to JPA's @Table mapping being non-syntactic — a plausible and completely wrong diagnosis, because the entity classes were not in the graph at all. A silently incomplete graph that still answers questions confidently is a worse failure mode than one that visibly fails.
Per #2479, a .graphifyignore negation cannot recover these files either, since the prune happens during the os.walk descent before ignore-file logic sees the path. I did not verify that part myself.
Workaround
Pointing extract at the directory works, because _SKIP_DIRS only filters descendants, not the scan root:
$ graphify extract backend/src/main/java/.../adapter/out --code-only --out /tmp/gout
[graphify extract] found 5 code files ... wrote graph.json: 86 nodes, 158 edges
Combined with graphify merge-graphs that is a viable escape hatch, but it needs one invocation per out/ directory (~30 in this repo), which does not scale.
Requests
- Gate
out on evidence, like env / coverage / snapshots already are. A real build output directory is cheap to recognise — compiled artifacts (.class, .jar, .o, .wasm), a sibling build file, absence of any source file of a supported language. When it cannot be confirmed, keep the directory.
- Failing that, warn. The run already prints counts for skipped non-code, unclassified, and sensitive files. A single line —
skipped N file(s) in noise directories: out/ (196), ... — would have turned this from a silent 30% loss into a five-second diagnosis.
- Same argument extends to
dist and target, though out is the one with a mainstream architectural convention behind it.
Happy to test a patch against this repo.
Summary
_SKIP_DIRScontains the bare string"out", matched by name alone in_is_noise_dir. In a hexagonal / ports-and-adapters codebase,adapter/out/andport/out/are where the entire outbound layer lives — persistence adapters, JPA entities, messaging adapters, outbound port interfaces. All of it is silently dropped from the scan.On the repo where I hit this, that was 196 of 655
.javafiles (30%), including every@Entityclass. Exit code 0, no warning, nothing in the run output.This is the same mechanism as #2479 (which reports it for
build/and explicitly listsoutas having the same problem). Filing separately because the blast radius is different in kind:build/loses a directory,out/loses an architectural layer, and the convention that puts source there is mainstream rather than incidental.Environment
graphifyy) 0.9.54, installed viauv tool installgraphify extract . --code-only --timingMechanism
_is_noise_diralready gates three ambiguous names on corroborating evidence, each returningFalsewhen it cannot verify:env/.env/*_env→_has_venv_markers(Ruby: stable subset of files yields zero nodes in full-repo runs (0.9.6) — each extracts fine in isolation #1666/Silent data loss: env/*_env directories pruned as false-positive venvs, with zero trace in any report bucket #2058)coverage→_has_coverage_artifacts(coverage/ in _SKIP_DIRS silently drops real Python packages named 'coverage' (same class as #1666 snapshots / #2058 env) #2339)snapshots→ parent-name check plus an actual*.snapfileoutgets none of it —if part in _SKIP_DIRS: return True, unconditionally, before any of those checks run.Repro
Any Spring/hexagonal layout, e.g.:
Every missing file contains
/out/. Zero exceptions. And in that repo, every directory namedout/contained only.javafiles — no build artifacts anywhere:Removing
"out"from_SKIP_DIRSlocally and re-running, same command, same repo:.javain graphWhy this is worse than a missing directory
The dropped set is not arbitrary — it is precisely the persistence and outbound-integration layer. So the graph that survives looks coherent: controllers, use-case ports and service implementations are all present and correctly linked, and the inbound half of every hexagon traces cleanly. Nothing looks broken.
What is gone is every path from application code to the database. In my case that meant zero edges between the Java subgraph and the SQL subgraph, and I initially attributed that to JPA's
@Tablemapping being non-syntactic — a plausible and completely wrong diagnosis, because the entity classes were not in the graph at all. A silently incomplete graph that still answers questions confidently is a worse failure mode than one that visibly fails.Per #2479, a
.graphifyignorenegation cannot recover these files either, since the prune happens during theos.walkdescent before ignore-file logic sees the path. I did not verify that part myself.Workaround
Pointing
extractat the directory works, because_SKIP_DIRSonly filters descendants, not the scan root:Combined with
graphify merge-graphsthat is a viable escape hatch, but it needs one invocation perout/directory (~30 in this repo), which does not scale.Requests
outon evidence, likeenv/coverage/snapshotsalready are. A real build output directory is cheap to recognise — compiled artifacts (.class,.jar,.o,.wasm), a sibling build file, absence of any source file of a supported language. When it cannot be confirmed, keep the directory.skipped N file(s) in noise directories: out/ (196), ...— would have turned this from a silent 30% loss into a five-second diagnosis.distandtarget, thoughoutis the one with a mainstream architectural convention behind it.Happy to test a patch against this repo.