~/blogprompt-fingerprints-legacy-replay.md
cchu@nycu:~/blog$ cat prompt-fingerprints-legacy-replay.md
2026.10.236 min[agents][knowledge-graphs][caching][testing]

Prompt Fingerprints and Legacy Graph Replay

Eight cache replays show how Graphify separates prompt versions while retaining unattributed legacy entries, and why checkpoint merging applies a stricter rule.

I loaded two synthetic document extractions under the same current prompt. One returned a current node; the other returned a node from an older, unattributed cache entry. Neither document went onto the extraction queue. The cache emitted a warning about mixed extraction vintages.

That result is intentional in Graphify. Prompt fingerprints isolate entries whose extraction prompt is known, while a compatibility fallback preserves older work whose prompt is unknown. The distinction matters when an agent uses the resulting graph as context: a warm cache does not establish that every node was extracted under the current instructions.

I examined revision c3dad319ee42f42dfbe3f88b71f0a29303fcb832. Its package configuration declares graphifyy version 0.9.93; I ran the unchanged cache module from source using Python 3.12.14 on Linux. The document contents and extracted nodes were synthetic. This was a cache-layer replay, with real file writes and reads, not a model-quality test or a complete graph build.

A current prompt selects its fingerprinted cache directory first. A missing entry can fall back to an unattributed flat entry with a warning. Checkpoint merging disables that fallback.

Two Kinds of Cached Work

Graphify maps code and documents into a graph that coding agents can query. Its implementation separates syntax extraction from model-based semantic extraction. The repository has language extractors, graph construction, model backends, a CLI, and dedicated cache tests; the cache is part of an actual extraction pipeline.

The package configuration includes parser grammars and graph dependencies. Those dependencies are unnecessary for this isolated cache probe: the three imported source files use the standard library on the exercised paths. I did not install the distribution or run the upstream pytest suite.

The two cache policies reflect different costs:

Cached resultNamespace inputsReason in the implementation
Syntax extractionPackage version, key schema, grammar fingerprintParser or extractor changes can invalidate unchanged source text
Semantic extraction with a supplied promptMode and normalized prompt fingerprintKeep results across releases that leave extraction instructions unchanged
Semantic extraction without a supplied promptMode, with no prompt subdirectoryRetain the historical storage layout

The cache implementation makes this separation explicit. A semantic entry lives under cache/semantic/p<fingerprint>/; deep mode uses cache/semantic-deep/p<fingerprint>/. The fingerprint is the first 12 hexadecimal characters of SHA-256 after normalizing line endings, trailing whitespace, and outer whitespace.

The filename is a separate digest. file_hash() incorporates document content and its path relative to the corpus root. For Markdown, it excludes recognized YAML front matter. So the cache has several independent identities: document, extraction mode, and prompt. The prompt fingerprint is not a record of the model, sampling settings, or response that produced a node.

The Read Path Keeps a Compatibility Exit

With a supplied prompt, load_cached() checks the corresponding fingerprint directory. If that entry does not exist, its default allow_legacy=True permits a lookup in the flat semantic directory.

That flat entry has no prompt attribution. Serving it avoids extracting an unchanged corpus again after migration. check_semantic_cache() counts these fallback hits and emits an aggregate runtime warning. It still returns their nodes alongside current-prompt nodes.

This is narrower than saying a changed prompt always invalidates the cache. A known entry from prompt V1 does miss under V2 when no flat entry exists. An unattributed flat entry remains eligible under V2. If a V2 entry exists, it takes precedence over the flat entry.

The CLI extraction path supplies the system prompt to both cache reads and final writes. Its force branch skips reads and dispatches all semantic files. That wiring is code inspection; I did not execute the CLI extraction workflow or verify its provider calls.

Eight Executed Cache Scenarios

The replay probe verifies SHA-256 digests of graphify/__init__.py, graphify/cache.py, and graphify/paths.py before importing them. Obtain those files from the pinned revision, preserving their directory structure, then point the probe at that source root. It creates a temporary corpus and cache, calls the actual cache functions, and checks returned node IDs, misses, and warnings. No replacement cache implementation or model stub is involved.

From the portfolio checkout, I ran:

python --version
python public/blog/prompt-fingerprints-legacy-replay/cache-probe.py /tmp/graphify23

Both commands exited zero. Selected probe stdout:

Python 3.12.14; source c3dad319ee42f42dfbe3f88b71f0a29303fcb832
mixed corpus: nodes=['current_a', 'legacy_b']; misses=0; warnings=1
strict legacy read: MISS
current entries win: nodes=['current_a', 'current_b']; warnings=0
different prompt without legacy: MISS
checkpoint merge: nodes=['v2_chunk1', 'v2_chunk2']; legacy excluded
mode isolation: standard=MISS; deep=HIT
prompt normalization: line endings/trailing spaces=equal; edited text=different
unreadable prompt file: nodes=['legacy_a']; warnings=1
PASS: 8 cache scenarios; no model calls
ScenarioObserved behavior
Current entry for A, only flat legacy entry for BBoth returned; one aggregate warning; no misses
Flat entry read with allow_legacy=FalseMiss
Current and flat entries both presentCurrent entry returned without a legacy warning
V1 entry read under V2, no flat entryMiss
Checkpoint merge after a flat entry, then another current chunkLegacy node excluded; two current chunks retained
Deep entry read in standard mode, then deep modeMiss, then hit
Prompt formatting changes versus edited textTested formatting normalized away; changed text produced a different fingerprint
Missing prompt-specification fileWarning; flat legacy entry returned

The missing-file case deserves attention. An unreadable prompt_file does not abort extraction. _resolve_prompt_fp() warns and falls back to an unattributed lookup. In my fixture, a current entry existed but the flat legacy node was returned instead, because the failed prompt read supplied no fingerprint. Losing access to the specification can therefore change which stored result gets selected.

Checkpoints Refuse to Relabel Old Work

The more careful rule appears on writes. Model extraction can finish a large document in chunks, so merge_existing=True preserves earlier chunks when a later one arrives. Blindly merging the flat entry here would move unknown-prompt work into a current-prompt directory.

save_semantic_cache() prevents that by reading the previous entry with allow_legacy=False. My first current-prompt checkpoint contained only v2_chunk1, despite a flat entry containing unknown_chunk. A second current-prompt checkpoint retained both current chunks.

The incremental checkpoint caller passes the extraction system prompt and enables merging. The upstream cache tests also cover legacy replay, current-entry precedence, and merge isolation. I inspected those tests; the results above come from the separate executable probe.

This gives the migration a useful asymmetry. Reads preserve older paid work, with a warning. Writes avoid stamping that older work as if the current prompt produced it. For a graph that must contain only current-instruction extractions, a warning is insufficient: the caller needs a deliberate refresh policy. The examined CLI's force branch supplies that policy by bypassing reads, while the lower-level loader exposes strict legacy rejection. Neither policy establishes the semantic accuracy of the extracted graph; this replay only establishes which stored evidence is reused.