Skip to content

detect: bare out in _SKIP_DIRS silently drops the entire outbound layer of hexagonal codebases (196/655 .java files) #3347

Description

@Muneeb7860

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

  1. 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.
  2. 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.
  3. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions