Skip to content

Add graphify stale — flag nodes referencing files changed since last extraction - #3130

Open
captjc9012 wants to merge 1 commit into
Graphify-Labs:v8from
captjc9012:feat/stale-command
Open

Add graphify stale — flag nodes referencing files changed since last extraction#3130
captjc9012 wants to merge 1 commit into
Graphify-Labs:v8from
captjc9012:feat/stale-command

Conversation

@captjc9012

Copy link
Copy Markdown

Summary

File-level manifest hashing (ast_hash/semantic_hash in detect.py) already skips re-extracting unchanged files on --update, but a node in an unchanged file that calls/imports/references/inherits-from a node in a file that did change keeps whatever was true about that relationship when it was last extracted — the update never revisits it. Nothing currently surfaces that.

This adds a new graphify stale subcommand that:

  • Unions the ast/semantic changed-file sets the project has actually populated (checking a hash kind that was never stamped would report every file as changed forever — e.g. a pure-code project using only graphify update never populates semantic_hash).
  • Reuses the existing reverse-traversal in affected.py (affected_nodes) to list nodes elsewhere in the graph that reference a node in a changed file.

Like graphify affected, this narrows a blast-radius query — it does not verify whether the flagged nodes' descriptions are actually wrong, only that they have a structural reason to be double-checked.

Changes

  • graphify/affected.py: new stale_from_changed_files() and format_stale(), built on the existing affected_nodes() primitive. No existing function touched.
  • graphify/cli.py: new stale subcommand, modeled directly on the existing affected subcommand's arg parsing (--graph, --depth, --relation).
  • graphify/__main__.py: help text entry.
  • tests/test_stale_from_changed_files.py: 7 new unit tests (depth control, same-file self-exclusion, no-dependents omission, unknown-file omission, formatting).

259 lines added, 0 existing lines changed.

Test plan

  • pytest tests/test_stale_from_changed_files.py — 7/7 pass
  • pytest tests/test_affected_cli.py tests/test_affected_member_seed.py tests/test_js_dynamic_import_affected.py tests/test_incremental.py tests/test_detect.py — 306/306 pass, no regressions
  • End-to-end smoke test against the real graphify update pipeline (AST-only, no LLM key needed) on a 2-file toy repo (base.py defining BaseHandler.retry(), worker.py calling it):
    • Right after graphify update .: graphify stale reports nothing.
    • After genuinely editing base.py: graphify stale correctly flags Worker.run(), the worker.py import, and the Worker class use.
    • graphify stale is read-only — rerunning without a new update reports the same result.
    • After graphify update . re-baselines: graphify stale goes quiet again.

Two real bugs were caught during this verification (not left in): an initial version hardcoded kind="semantic", which reports every file as changed forever on an AST-only project since semantic_hash is never stamped; the first fix (unconditionally checking both kinds) had the same failure in reverse. The final version only compares hash kinds the project's manifest has actually populated.

…ce last extraction

file-level manifest hashing (ast_hash/semantic_hash) already skips re-extracting
unchanged files on --update, but a node in an UNCHANGED file that calls/imports/
references/inherits-from a node in a file that DID change keeps whatever was true
about that relationship when it was last extracted — the update never revisits it.

`graphify stale` unions the ast/semantic changed-file sets a project has actually
populated (checking a kind never stamped would report every file as changed
forever), then reuses affected.py's existing reverse-traversal to list nodes
elsewhere with a structural reason to double-check. It does not verify the claims
themselves, same as `graphify affected` narrows a blast-radius query without
answering it.
@captjc9012
captjc9012 changed the base branch from main to v8 August 27, 2026 02:42

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 5 advisory finding(s) below merit a look before merge.

Formal verification. 2 change(s) alter behavior, breaking input(s) attached.

Behavior changes: \_\_getattr\_\_ changes behavior, here is the input that shows it.

The verifier found a concrete input on which \_\_getattr\_\_ behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.

Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.

Evidence: On input \{"name":"'save\_query\_result'"\}, the old code produced raises AttributeError but the new code produces \<function save\_query\_result at 0x7fc1de85d620\>. Paste that input straight into a regression test.

Behavior changes: run\_benchmark changes behavior, here is the input that shows it.

The verifier found a concrete input on which run\_benchmark behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.

Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.

Evidence: On input \{"graph\_path":"None","corpus\_words":"None","questions":"None"\}, the old code produced raises TypeError but the new code produces \{'corpus\_tokens': 1018866, 'corpus\_words': 764150, 'nodes': 15283, 'edges': 28426, 'avg\_query\_tokens': 6356, 'reduction\_ratio': 160\.3, 'per\_question': \[\{'question': 'how does authentication…. Paste that input straight into a regression test.


Graphify review — findings

Bootstraps the repo's CI/CD and project scaffolding: adds a CI workflow that runs the skillgen anti-drift check plus coverage/schema/round-trip validators and a 3.10/3.12 pytest matrix, a trusted-publishing Publish to PyPI workflow guarded by a tag-version match, and a release workflow that extracts/clusters graphify's own source into a downloadable graph asset. Adds a pre-commit config wiring the same skillgen --check guard and ruff locally, plus .dockerignore, .gitattributes (marks generated HTML as vendored so Linguist reports Python), FUNDING.yml, expanded .gitignore entries, and an AGENTS.md describing the on-disk knowledge graph. Non-blocking bandit and pip-audit security scans run on every PR.

Worth a look

  • Manual PyPI publish bypasses the release tag version guard.github/workflows/publish.yml:38 · Escalate · high
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Manual workflow dispatch can publish arbitrary checked-out code to PyPI.github/workflows/publish.yml:55 · Escalate · high
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Privileged PyPI publishing workflow uses mutable action tags.github/workflows/publish.yml:55 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • README gives contradictory query logging default contractREADME.md · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Japanese README is truncated mid-sectiondocs/translations/README.ja-JP.md:238 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 14948 functions depend on the 14948 functions this change touches.

Health — this change adds coupling hotspots:

  • new: extract() — 498 callers, 42 callees
  • new: _rebuild_code() — 98 callers, 50 callees
  • new: build_from_json() — 187 callers, 18 callees
  • new: detect() — 108 callers, 15 callees
  • new: deduplicate_entities() — 63 callers, 21 callees
  • new: build_merge() — 46 callers, 14 callees
  • new: save_semantic_cache() — 58 callers, 9 callees
  • new: to_obsidian() — 36 callers, 13 callees
  • …and 219 more — each is listed as a finding

Verification — 14948 functions in the blast radius were not formally verified this run (proofs are advisory here).

Health delta baseline: last indexed commit 43d54ac (diverged from this PR's base — delta is approximate).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 14948 function(s) in the blast radius were not formally verified this run

Formal verification

Behavior changes: \_\_getattr\_\_ changes behavior, here is the input that shows it.

The verifier found a concrete input on which \_\_getattr\_\_ behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.

Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.

Evidence: On input \{"name":"'save\_query\_result'"\}, the old code produced raises AttributeError but the new code produces \<function save\_query\_result at 0x7fc1de85d620\>. Paste that input straight into a regression test.

Behavior changes: run\_benchmark changes behavior, here is the input that shows it.

The verifier found a concrete input on which run\_benchmark behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.

Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.

Evidence: On input \{"graph\_path":"None","corpus\_words":"None","questions":"None"\}, the old code produced raises TypeError but the new code produces \{'corpus\_tokens': 1018866, 'corpus\_words': 764150, 'nodes': 15283, 'edges': 28426, 'avg\_query\_tokens': 6356, 'reduction\_ratio': 160\.3, 'per\_question': \[\{'question': 'how does authentication…. Paste that input straight into a regression test.

No difference found (not proven): No behavior difference found in main (not a proof).

The verifier ran both versions of main on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

No difference found (not proven): No behavior difference found in \_cross\_community\_surprises (not a proof).

The verifier ran both versions of \_cross\_community\_surprises on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

No difference found (not proven): No behavior difference found in \_cross\_file\_surprises (not a proof).

The verifier ran both versions of \_cross\_file\_surprises on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

No difference found (not proven): No behavior difference found in \_file\_category (not a proof).

The verifier ran both versions of \_file\_category on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

Could not verify: Could not verify \_is\_file\_node.

The verifier did not have enough to check \_is\_file\_node, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: non-vacuity: domain too small (only 1 distinct inputs exercised, need 3) — 'no divergence' would be near-vacuous

Could not verify: Could not verify \_surprise\_score.

The verifier did not have enough to check \_surprise\_score, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: not verifiable: all 200 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly TypeError — names the real obstacle, not a sampling gap)

No difference found (not proven): No behavior difference found in god\_nodes (not a proof).

The verifier ran both versions of god\_nodes on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

Could not verify: Could not verify graph\_diff.

The verifier did not have enough to check graph\_diff, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: not verifiable: the input domain has 25 values but only 5 distinct were tested — a small finite domain must be EXHAUSTED, not sampled (an untested input could invert the result)

Could not verify: Could not verify suggest\_questions.

The verifier did not have enough to check suggest\_questions, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: not verifiable: all 200 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly ValueError — names the real obstacle, not a sampling gap)

No difference found (not proven): No behavior difference found in \_query\_subgraph\_tokens (not a proof).

The verifier ran both versions of \_query\_subgraph\_tokens on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

Could not verify: Could not verify print\_benchmark.

The verifier did not have enough to check print\_benchmark, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: not verifiable: all 6 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly KeyError — names the real obstacle, not a sampling gap)

Could not verify: Could not verify build.

The verifier did not have enough to check build, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: not verifiable: all 9 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly AttributeError — names the real obstacle, not a sampling gap)

Could not verify: Could not verify build\_from\_json.

The verifier did not have enough to check build\_from\_json, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: not verifiable: all 6 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly NameError — names the real obstacle, not a sampling gap)

Could not verify: Could not verify cache\_dir.

The verifier did not have enough to check cache\_dir, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `root` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify cached\_files.

The verifier did not have enough to check cached\_files, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `root` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify check\_semantic\_cache.

The verifier did not have enough to check check\_semantic\_cache, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `root` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify clear\_cache.

The verifier did not have enough to check clear\_cache, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `root` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify file\_hash.

The verifier did not have enough to check file\_hash, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify load\_cached.

The verifier did not have enough to check load\_cached, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify save\_cached.

The verifier did not have enough to check save\_cached, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify save\_semantic\_cache.

The verifier did not have enough to check save\_semantic\_cache, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `root` is annotated `Path` — outside the synthesizable primitive/collection set

No difference found (not proven): No behavior difference found in \_split\_community (not a proof).

The verifier ran both versions of \_split\_community on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

Could not verify: Could not verify cluster.

The verifier did not have enough to check cluster, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: non-vacuity: domain too small (only 2 distinct inputs exercised, need 3) — 'no divergence' would be near-vacuous

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.

1 participant