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.
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 result | Namespace inputs | Reason in the implementation |
|---|---|---|
| Syntax extraction | Package version, key schema, grammar fingerprint | Parser or extractor changes can invalidate unchanged source text |
| Semantic extraction with a supplied prompt | Mode and normalized prompt fingerprint | Keep results across releases that leave extraction instructions unchanged |
| Semantic extraction without a supplied prompt | Mode, with no prompt subdirectory | Retain 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
| Scenario | Observed behavior |
|---|---|
| Current entry for A, only flat legacy entry for B | Both returned; one aggregate warning; no misses |
Flat entry read with allow_legacy=False | Miss |
| Current and flat entries both present | Current entry returned without a legacy warning |
| V1 entry read under V2, no flat entry | Miss |
| Checkpoint merge after a flat entry, then another current chunk | Legacy node excluded; two current chunks retained |
| Deep entry read in standard mode, then deep mode | Miss, then hit |
| Prompt formatting changes versus edited text | Tested formatting normalized away; changed text produced a different fingerprint |
| Missing prompt-specification file | Warning; 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.