Skip to content

fix(artifacts): the run's own output is not an input (#207) - #216

Merged
rahlk merged 1 commit into
mainfrom
fix/issue-207-output-dir-self-ingestion
Sep 14, 2026
Merged

rahlk merged 1 commit into
mainfrom
fix/issue-207-output-dir-self-ingestion

Conversation

@rahlk

@rahlk rahlk commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Closes #207.

The bug

discover_artifacts received only project_dir, so nothing told it where the run writes. With -o inside -i, run N ingested run N−1's analysis.json as an artifact and embedded it verbatim, squaring the payload each time. The visible symptom was a SIGKILL while decoding, which reads as a flaky test suite — the checked-out tree had accumulated 207 GB across five fixtures, including a 97 GB analysis.json grown from a 4 KB fixture.

The fix

core.analyze passes discover_artifacts the paths this run writes (Codeanalyzer._own_output_paths): the output and cache directories, plus the output files inside them. Discovery resolves them and skips any walked file that is, or sits inside, one of them.

  • Matching is on resolved paths, so relative, ..-laden, and symlinked targets exclude the same tree; a target outside the project excludes nothing.
  • A directory exclusion that holds the project itself is refused — it would empty the inventory, which is worse than the bug — with a warning. The file entries still cover that case, so -o <project root> and a --emit neo4j graph.cypher in the working directory are stable too, at the cost of one file each rather than the whole tree.
  • analysis.json and graph.cypher are now named once in codeanalyzer/options, so discovery and the writers cannot drift apart.

Definition of done

  • Two runs with -o inside -i produce the same-size analysis.json (test_cli.py::test_output_dir_inside_input_does_not_grow_across_runs; failed at [2192, 4959] before the fix).
  • A sibling artifact outside the output directory is still captured.
  • Relative and symlinked -o paths resolve to the same exclusion.
  • test_cli.py keeps writing into the fixture directory; suite green.
  • du -sk test/fixtures stable across three consecutive full-suite runs: 32456 KB baseline → 68208 → 68212 → 68216 KB. The 4 KB/run wobble is not growth — a per-file manifest across a fourth run shows changes in both directions (flask −30 B, requests +45 B, xarray −3900 B), i.e. run-to-run jitter in the whole-application fixtures.

Manual checks, three consecutive runs each: -o in a nested .output stable at 55902 B; -o at the project root stable at 2000 B with the sibling notes.md still inventoried; --emit neo4j with no -o and cwd inside the project stable at 5949 B.

Gates, re-run on 7bb7fc1

Gate Result
Fixture suite 512 passed, 11 skipped (506 before, +6 new tests)
Schema conformance schema_version=2.0.0 at L1–L4; each validates against Analysis
Monotonicity 78 → 79 → 165 → 262 ids, 0 lost at every step
Determinism two independent -a 4 runs byte-identical (126592 B)
Cross-projection 262 JSON ids vs 284 graph ids, 0 missing from the graph

Caveats

  • This is an L1 payload content change for anyone who had -o inside -i: that output no longer appears in application.artifacts. No schema shape change — no node, edge, or field added or altered; schema_version stays 2.0.0.
  • A genuinely pre-existing file living in the chosen output directory becomes invisible. Skipping the directory is the right trade, but it is a trade.
  • -o at the project root keeps the whole inventory and skips only analysis.json/graph.cypher, so an unrelated file a user happens to name analysis.json there is dropped when it is also the run's own target path.

Propagation

The same bug class exists in two siblings; follow-on issues to be filed:

  • codeanalyzer-typescript — inventoryArtifacts(opts.input, opts, …) walks with name-only SKIP_DIRS and captures text whole, with no byte cap. Identical exponential blowup for any -o not coincidentally named out/dist/build.
  • codeanalyzer-java — ArtifactDiscovery.discover(Paths.get(input), …) gets no output path and IGNORED is name-only. artifactTextMaxBytes = 262144 caps each capture, so it grows linearly rather than squared: a wrong inventory, not a SIGKILL.

Checked and not affected: codeanalyzer-iac (only classified IaC templates become candidates); the other analyzers have no artifact layer.

`discover_artifacts` walked the project with no idea where the run writes, so
`-o` (or `-c`) inside `-i` made run N ingest run N-1's `analysis.json` whole and
embed it in its own output. Each run squared the last until the analyzer was
SIGKILLed decoding tens of GB of its own output -- which reads as a flaky,
load-sensitive test suite and had been misdiagnosed as one more than once.

`core.analyze` now hands discovery the paths this run writes: the output and
cache directories, plus the output files inside them. Matching is on resolved
paths, so a relative, `..`-laden or symlinked target excludes the same tree and
a target outside the project excludes nothing. A *directory* exclusion that
holds the project itself is refused -- it would empty the inventory -- and the
file entries cover that degenerate case, so `-o <project root>` and a
`--emit neo4j` `graph.cypher` written to the working directory are stable too.

`analysis.json` and `graph.cypher` are named once in `codeanalyzer/options` now,
so discovery and the writers cannot drift apart.
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.

The analyzer ingests its own output when -o sits inside -i, growing analysis.json exponentially

1 participant