Skip to content

Separate manifest from machine-local incremental state - #3647

Open
hopstreax wants to merge 1 commit into
Graphify-Labs:v8from
hopstreax:investigate/3643-stable-manifest
Open

hopstreax wants to merge 1 commit into
Graphify-Labs:v8from
hopstreax:investigate/3643-stable-manifest

Conversation

@hopstreax

Copy link
Copy Markdown
Contributor

Summary

Fixes #3643 by separating portable manifest data from machine-local filesystem state.

Previously, manifest.json persisted volatile mtime and seen fields, causing unnecessary changes across updates and checkouts. This change keeps the manifest content-based and moves incremental filesystem metadata into a local cache.

Changes

  • Remove mtime and seen from persisted manifest.json entries.

  • Add graphify-out/cache/incremental-state.json for:

    • size
    • mtime_ns
    • indexed_at_ns
  • Keep manifest.json deterministically ordered and byte-stable.

  • Add atomic, no-op-aware persistence for incremental state.

  • Use mtime_ns with a 2-second racily-clean window for the incremental fastpath.

  • Fall back to MD5 content verification when local state is missing, stale, malformed, or racily-clean.

  • Preserve legacy manifest compatibility and safely normalize old mtime/seen entries.

  • Preserve the existing _stat_and_hash() contract.

  • Leave the existing cache/stat-index.json infrastructure untouched.

  • Add comprehensive regression coverage for manifest stability, cache recovery, rapid rewrites, migration, deletion, exclusion, and corrupted state.

Validation

  • tests/test_stable_manifest.py — 12 passed
  • tests/test_incremental_mtime_collision.py — 5 passed
  • tests/test_long_path_hashing.py — 4 passed
  • tests/test_detect.py — 257 passed, 1 known environment-specific failure
  • tests/test_serve.py tests/test_build.py tests/test_dedup.py — 342 passed
  • git diff --check — clean

The single test_graphifyignore_hermetic_without_vcs failure is unrelated to this change and is caused by the host environment containing a .git directory under the user's home directory.

@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 4 advisory finding(s) below merit a look before merge.

Formal verification. 2 change(s) tested, no difference found (not proven).


Graphify review — findings

Adds a machine-local incremental-state file (cache/incremental-state.json) that stores per-file size, mtime_ns, and indexed_at_ns alongside the content hashes now kept in manifest.json, letting detect_incremental skip hashing via a stat fastpath while still hashing files whose write tick may not have safely closed (_is_racily_clean, tunable through GRAPHIFY_MTIME_GRANULARITY_MS). Drops the old mtime/hash fields from manifest entries, normalizing legacy rows into ast_hash/semantic_hash only, and reworks save_manifest to compute both artifacts in one stat+MD5 pass (_stat_and_hash_for_manifest) and persist state atomically with deterministic, root-relative keys. State keys re-anchor to absolute paths on load and back to forward-slash relative on save, mirroring manifest storage.

Worth a look

  • save_manifest sorts manifest keys after relativize but state keys use unrelativized keys, breaking state prunegraphify/detect.py · Escalate · high
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • save_manifest hashed-entry tuple contract changedgraphify/detect.py:2373 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • save_manifest no longer writes mtime in manifest rowsgraphify/detect.py:2317 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Incremental state save can clobber concurrent state updatesgraphify/detect.py:2664 · 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 — 2549 functions depend on the 571 functions this change touches.

Health — this change adds coupling hotspots:

  • new: extract() — 645 callers, 45 callees
  • new: _rebuild_code() — 129 callers, 54 callees
  • new: detect() — 123 callers, 15 callees
  • new: save_manifest() — 52 callers, 13 callees
  • new: _extract_generic() — 18 callers, 29 callees
  • new: extract_files_direct() — 17 callers, 20 callees
  • new: extract_js() — 85 callers, 4 callees
  • new: extract_xaml() — 19 callers, 17 callees
  • …and 45 more — each is listed as a finding

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

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: 1020 function(s) in the blast radius were not formally verified this run

Test selection

Test selection

53 of 287 test file(s) selected (18%) via static blast radius.

  • tests/test_astro_extraction.py — impact
  • tests/test_atomic_writes.py — impact
  • tests/test_build.py — impact
  • tests/test_charmap_encoding.py — impact
  • tests/test_chunking.py — impact
  • tests/test_cjs_module_extension.py — impact
  • tests/test_claude_cli_backend.py — impact
  • tests/test_cpp_nested_and_cli.py — impact
  • tests/test_detect.py — impact, changed-test
  • tests/test_dotnet.py — impact
  • tests/test_evidence_binding.py — impact
  • tests/test_extract.py — impact
  • tests/test_extract_cli.py — impact
  • tests/test_file_slice.py — impact
  • tests/test_ignore_file_encoding.py — impact
  • tests/test_image_vision.py — impact
  • tests/test_import_extension_resolution.py — impact
  • tests/test_incremental_mtime_collision.py — impact
  • tests/test_indirect_dispatch.py — impact
  • tests/test_indirect_dispatch_assign_return.py — impact
  • tests/test_indirect_dispatch_getattr.py — impact
  • tests/test_js_exported_scalar_bindings.py — impact
  • tests/test_languages.py — impact
  • tests/test_llm_backends.py — impact
  • tests/test_long_path_hashing.py — impact
  • tests/test_manifest_ingest.py — impact
  • tests/test_mcp_ingest.py — impact
  • tests/test_multilang.py — impact
  • tests/test_non_regular_files.py — impact
  • tests/test_office_incremental.py — impact
  • tests/test_office_limits.py — impact
  • tests/test_ollama.py — impact
  • tests/test_out_dir_evidence.py — impact
  • tests/test_oversized_document_slicing.py — impact
  • tests/test_package_json_subpath_imports.py — impact
  • tests/test_pdf_slicing.py — impact
  • tests/test_pdf_token_estimate.py — impact
  • tests/test_phantom_external_import.py — impact
  • tests/test_pipeline.py — impact
  • tests/test_python_underscore_resolution.py — impact
  • tests/test_rationale.py — impact
  • tests/test_ruby_resolution.py — impact
  • tests/test_scala_self_type.py — impact
  • tests/test_stable_manifest.py — impact, changed-test
  • tests/test_stale_prune.py — impact
  • tests/test_swift_computed_properties.py — impact
  • tests/test_trailing_newline_not_a_syntax_error.py — impact
  • tests/test_ts_new_expression_calls.py — impact
  • tests/test_typescript_module_extensions.py — impact
  • tests/test_unmapped_at_alias_resolution.py — impact
  • … and 3 more

Selection is safe under the controlled-regression assumption; always-run tests + a periodic full run are the backstops. Advisory — it never changes the check verdict.

Formal verification

Could not verify: Could not verify detect\_incremental.

The verifier did not have enough to check detect\_incremental, 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: no capturable inputs from the test suite; property tier: parameter `root` is annotated `Path` — outside the synthesizable primitive/collection set

No difference found (not proven): No behavior difference found in \_mtime\_may\_hide\_a\_rewrite (not a proof).

The verifier ran both versions of \_mtime\_may\_hide\_a\_rewrite 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 save\_manifest (not a proof).

The verifier ran both versions of save\_manifest 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.

· 1 grounded finding(s) anchored inline below; 52 more finding(s) on lines outside this diff (see the check run).

Comment thread graphify/detect.py
write_json_atomic(str(state_file), disk_state, indent=2)


def save_manifest(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regressionsave_manifest()

fans out to 13 callees (efferent coupling); 52 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

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.

manifest.json is never byte-stable: mtime/seen churn on every update (51k diff lines on an identical tree)

1 participant