fix(cache): scope semantic cache writes to extracted files (#1757)

This commit is contained in:
tpateeq
2026-07-13 00:35:32 +01:00
committed by safishamsi
parent a46eee49ef
commit 579ba1da3c
41 changed files with 196 additions and 41 deletions
+35 -3
View File
@@ -7,6 +7,7 @@ import json
import os
import re
import tempfile
from collections.abc import Iterable
from pathlib import Path
# Output directory name — override with GRAPHIFY_OUT env var for worktrees or
@@ -542,6 +543,7 @@ def save_semantic_cache(
hyperedges: list[dict] | None = None,
root: Path = Path("."),
merge_existing: bool = False,
allowed_source_files: Iterable[str | Path] | None = None,
) -> int:
"""Save semantic extraction results to cache, keyed by source_file.
@@ -553,6 +555,11 @@ def save_semantic_cache(
unioned with the new results before saving instead of being overwritten.
This lets callers checkpoint incrementally (e.g. once per chunk) without
dropping a prior slice of a large file that was split across chunks.
When ``allowed_source_files`` is provided, only those files may be used as
cache-write keys. Semantic nodes can legitimately mention another corpus
file, but a model must not be able to replace that file's complete cache
entry unless the file was part of the current extraction batch (#1757).
Returns the number of files cached.
"""
from collections import defaultdict
@@ -571,12 +578,37 @@ def save_semantic_cache(
if src:
by_file[src]["hyperedges"].append(h)
root_path = Path(root).resolve()
def resolved_source_path(value: str | Path) -> Path:
path = Path(value)
if not path.is_absolute():
path = root_path / path
try:
return path.resolve()
except (OSError, RuntimeError):
# Keep the cache write best-effort for inaccessible paths or a
# symlink loop emitted by an untrusted semantic result.
return Path(os.path.abspath(path))
allowed_paths = None
if allowed_source_files is not None:
allowed_paths = {resolved_source_path(path) for path in allowed_source_files}
saved = 0
for fpath, result in by_file.items():
p = Path(fpath)
if not p.is_absolute():
p = Path(root) / p
p = resolved_source_path(fpath)
if p.is_file():
if allowed_paths is not None and p not in allowed_paths:
import warnings
warnings.warn(
"semantic cache skipped out-of-scope source_file "
f"{fpath!r}; the file was not dispatched for extraction",
RuntimeWarning,
stacklevel=2,
)
continue
if merge_existing:
prev = load_cached(p, root, kind="semantic")
if prev:
+1
View File
@@ -2354,6 +2354,7 @@ def dispatch_command(cmd: str) -> None:
fresh.get("edges", []),
fresh.get("hyperedges", []),
root=out_root,
allowed_source_files=uncached_paths,
)
except Exception as exc:
print(f"[graphify extract] warning: could not write semantic cache: {exc}", file=sys.stderr)
+2 -1
View File
@@ -310,7 +310,8 @@ from graphify.cache import save_semantic_cache
from pathlib import Path
new = json.loads(Path('graphify-out/.graphify_semantic_new.json').read_text(encoding=\"utf-8\")) if Path('graphify-out/.graphify_semantic_new.json').exists() else {'nodes':[],'edges':[],'hyperedges':[]}
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH')
uncached = [line for line in Path('graphify-out/.graphify_uncached.txt').read_text(encoding=\"utf-8\").splitlines() if line]
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH', allowed_source_files=uncached)
print(f'Cached {saved} files')
"
```
+2 -1
View File
@@ -313,7 +313,8 @@ from graphify.cache import save_semantic_cache
from pathlib import Path
new = json.loads(Path('.graphify_semantic_new.json').read_text()) if Path('.graphify_semantic_new.json').exists() else {'nodes':[],'edges':[],'hyperedges':[]}
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []))
uncached = [line for line in Path('.graphify_uncached.txt').read_text().splitlines() if line]
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), allowed_source_files=uncached)
print(f'Cached {saved} files')
"
```
+2 -1
View File
@@ -310,7 +310,8 @@ from graphify.cache import save_semantic_cache
from pathlib import Path
new = json.loads(Path('graphify-out/.graphify_semantic_new.json').read_text(encoding=\"utf-8\")) if Path('graphify-out/.graphify_semantic_new.json').exists() else {'nodes':[],'edges':[],'hyperedges':[]}
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH')
uncached = [line for line in Path('graphify-out/.graphify_uncached.txt').read_text(encoding=\"utf-8\").splitlines() if line]
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH', allowed_source_files=uncached)
print(f'Cached {saved} files')
"
```
+2 -1
View File
@@ -313,7 +313,8 @@ from graphify.cache import save_semantic_cache
from pathlib import Path
new = json.loads(Path('graphify-out/.graphify_semantic_new.json').read_text(encoding=\"utf-8\")) if Path('graphify-out/.graphify_semantic_new.json').exists() else {'nodes':[],'edges':[],'hyperedges':[]}
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH')
uncached = [line for line in Path('graphify-out/.graphify_uncached.txt').read_text(encoding=\"utf-8\").splitlines() if line]
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH', allowed_source_files=uncached)
print(f'Cached {saved} files')
"
```
+2 -1
View File
@@ -310,7 +310,8 @@ from graphify.cache import save_semantic_cache
from pathlib import Path
new = json.loads(Path('graphify-out/.graphify_semantic_new.json').read_text(encoding=\"utf-8\")) if Path('graphify-out/.graphify_semantic_new.json').exists() else {'nodes':[],'edges':[],'hyperedges':[]}
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH')
uncached = [line for line in Path('graphify-out/.graphify_uncached.txt').read_text(encoding=\"utf-8\").splitlines() if line]
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH', allowed_source_files=uncached)
print(f'Cached {saved} files')
"
```
+2 -1
View File
@@ -313,7 +313,8 @@ from graphify.cache import save_semantic_cache
from pathlib import Path
new = json.loads(Path('graphify-out/.graphify_semantic_new.json').read_text(encoding=\"utf-8\")) if Path('graphify-out/.graphify_semantic_new.json').exists() else {'nodes':[],'edges':[],'hyperedges':[]}
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH')
uncached = [line for line in Path('graphify-out/.graphify_uncached.txt').read_text(encoding=\"utf-8\").splitlines() if line]
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH', allowed_source_files=uncached)
print(f'Cached {saved} files')
"
```
+2 -1
View File
@@ -374,7 +374,8 @@ from graphify.cache import save_semantic_cache
from pathlib import Path
new = json.loads(Path('graphify-out/.graphify_semantic_new.json').read_text()) if Path('graphify-out/.graphify_semantic_new.json').exists() else {'nodes':[],'edges':[],'hyperedges':[]}
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []))
uncached = [line for line in Path('graphify-out/.graphify_uncached.txt').read_text().splitlines() if line]
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), allowed_source_files=uncached)
print(f'Cached {saved} files')
"
```
+2 -1
View File
@@ -310,7 +310,8 @@ from graphify.cache import save_semantic_cache
from pathlib import Path
new = json.loads(Path('graphify-out/.graphify_semantic_new.json').read_text(encoding=\"utf-8\")) if Path('graphify-out/.graphify_semantic_new.json').exists() else {'nodes':[],'edges':[],'hyperedges':[]}
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH')
uncached = [line for line in Path('graphify-out/.graphify_uncached.txt').read_text(encoding=\"utf-8\").splitlines() if line]
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH', allowed_source_files=uncached)
print(f'Cached {saved} files')
"
```
+2 -1
View File
@@ -313,7 +313,8 @@ from graphify.cache import save_semantic_cache
from pathlib import Path
new = json.loads(Path('graphify-out/.graphify_semantic_new.json').read_text(encoding=\"utf-8\")) if Path('graphify-out/.graphify_semantic_new.json').exists() else {'nodes':[],'edges':[],'hyperedges':[]}
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH')
uncached = [line for line in Path('graphify-out/.graphify_uncached.txt').read_text(encoding=\"utf-8\").splitlines() if line]
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH', allowed_source_files=uncached)
print(f'Cached {saved} files')
"
```
+2 -1
View File
@@ -313,7 +313,8 @@ from graphify.cache import save_semantic_cache
from pathlib import Path
new = json.loads(Path('graphify-out/.graphify_semantic_new.json').read_text(encoding=\"utf-8\")) if Path('graphify-out/.graphify_semantic_new.json').exists() else {'nodes':[],'edges':[],'hyperedges':[]}
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH')
uncached = [line for line in Path('graphify-out/.graphify_uncached.txt').read_text(encoding=\"utf-8\").splitlines() if line]
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH', allowed_source_files=uncached)
print(f'Cached {saved} files')
"
```
+2 -1
View File
@@ -305,7 +305,8 @@ from graphify.cache import save_semantic_cache
from pathlib import Path
new = json.loads(Path('graphify-out/.graphify_semantic_new.json').read_text(encoding=\"utf-8\")) if Path('graphify-out/.graphify_semantic_new.json').exists() else {'nodes':[],'edges':[],'hyperedges':[]}
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH')
uncached = [line for line in Path('graphify-out/.graphify_uncached.txt').read_text(encoding=\"utf-8\").splitlines() if line]
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH', allowed_source_files=uncached)
print(f'Cached {saved} files')
"
```
+2 -1
View File
@@ -313,7 +313,8 @@ from graphify.cache import save_semantic_cache
from pathlib import Path
new = json.loads(Path('graphify-out/.graphify_semantic_new.json').read_text(encoding=\"utf-8\")) if Path('graphify-out/.graphify_semantic_new.json').exists() else {'nodes':[],'edges':[],'hyperedges':[]}
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH')
uncached = [line for line in Path('graphify-out/.graphify_uncached.txt').read_text(encoding=\"utf-8\").splitlines() if line]
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH', allowed_source_files=uncached)
print(f'Cached {saved} files')
"
```
+2 -1
View File
@@ -311,7 +311,8 @@ from graphify.cache import save_semantic_cache
from pathlib import Path
new = json.loads(Path('graphify-out/.graphify_semantic_new.json').read_text(encoding=\"utf-8\")) if Path('graphify-out/.graphify_semantic_new.json').exists() else {'nodes':[],'edges':[],'hyperedges':[]}
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH')
uncached = [line for line in Path('graphify-out/.graphify_uncached.txt').read_text(encoding=\"utf-8\").splitlines() if line]
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH', allowed_source_files=uncached)
print(f'Cached {saved} files')
"
```
+2 -1
View File
@@ -309,7 +309,8 @@ from graphify.cache import save_semantic_cache
from pathlib import Path
new = json.loads(Path('graphify-out/.graphify_semantic_new.json').read_text(encoding=\"utf-8\")) if Path('graphify-out/.graphify_semantic_new.json').exists() else {'nodes':[],'edges':[],'hyperedges':[]}
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH')
uncached = [line for line in Path('graphify-out/.graphify_uncached.txt').read_text(encoding=\"utf-8\").splitlines() if line]
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH', allowed_source_files=uncached)
print(f'Cached {saved} files')
"
```
+2 -1
View File
@@ -335,7 +335,8 @@ from graphify.cache import save_semantic_cache
from pathlib import Path
new = json.loads(Path('graphify-out/.graphify_semantic_new.json').read_text(encoding=\"utf-8\")) if Path('graphify-out/.graphify_semantic_new.json').exists() else {'nodes':[],'edges':[],'hyperedges':[]}
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH')
uncached = [line for line in Path('graphify-out/.graphify_uncached.txt').read_text(encoding=\"utf-8\").splitlines() if line]
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH', allowed_source_files=uncached)
print(f'Cached {saved} files')
"
```
+2 -1
View File
@@ -313,7 +313,8 @@ from graphify.cache import save_semantic_cache
from pathlib import Path
new = json.loads(Path('graphify-out/.graphify_semantic_new.json').read_text(encoding=\"utf-8\")) if Path('graphify-out/.graphify_semantic_new.json').exists() else {'nodes':[],'edges':[],'hyperedges':[]}
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH')
uncached = [line for line in Path('graphify-out/.graphify_uncached.txt').read_text(encoding=\"utf-8\").splitlines() if line]
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH', allowed_source_files=uncached)
print(f'Cached {saved} files')
"
```
+46
View File
@@ -524,6 +524,52 @@ def test_save_semantic_cache_overwrites_by_default(tmp_path):
assert ids == {"b"}, "default must overwrite, not accumulate"
def test_save_semantic_cache_rejects_out_of_scope_source_file(tmp_path):
"""#1757: an undispatched file must keep its complete cache entry when a
semantic result misattributes a node to it."""
from graphify.cache import save_semantic_cache
intended = tmp_path / "intended.md"
intended.write_text("# Intended\n")
protected = tmp_path / "protected.md"
protected.write_text("# Protected\n")
save_semantic_cache(
[{"id": "original", "source_file": "protected.md"}],
[],
root=tmp_path,
)
nodes = [
{"id": "expected", "source_file": str(intended.resolve())},
{"id": "stray", "source_file": "protected.md"},
]
edges = [
{"source": "stray", "target": "expected", "source_file": "protected.md"},
]
hyperedges = [
{"id": "stray_hyperedge", "nodes": ["stray"], "source_file": "protected.md"},
]
with pytest.warns(RuntimeWarning, match="out-of-scope source_file 'protected.md'"):
saved = save_semantic_cache(
nodes,
edges,
hyperedges,
root=tmp_path,
allowed_source_files=["intended.md"],
)
assert saved == 1
intended_cache = load_cached(intended, root=tmp_path, kind="semantic")
assert {node["id"] for node in intended_cache["nodes"]} == {"expected"}
protected_cache = load_cached(protected, root=tmp_path, kind="semantic")
assert {node["id"] for node in protected_cache["nodes"]} == {"original"}
assert protected_cache["edges"] == []
assert protected_cache["hyperedges"] == []
def test_save_semantic_cache_merge_existing_unions(tmp_path):
"""#1715: merge_existing=True unions with the prior entry so a file split
across chunks (checkpointed per chunk) keeps every slice."""
+12
View File
@@ -102,6 +102,15 @@ def test_extract_succeeds_when_at_least_one_chunk_completes(
monkeypatch.setattr(
"graphify.llm.extract_corpus_parallel", _one_chunk_succeeded
)
cache_call = {}
def _capture_semantic_cache(*args, **kwargs):
cache_call.update(kwargs)
return 0
monkeypatch.setattr(
"graphify.cache.save_semantic_cache", _capture_semantic_cache
)
monkeypatch.setattr(mainmod, "_check_skill_version", lambda _: None)
monkeypatch.setattr(
mainmod.sys,
@@ -121,6 +130,9 @@ def test_extract_succeeds_when_at_least_one_chunk_completes(
assert (out_dir / "graphify-out" / "graph.json").exists(), (
"graph.json must be written on the happy path"
)
assert {
str(path) for path in cache_call["allowed_source_files"]
} == {str(corpus / "README.md")}
def _code_only_corpus(tmp_path):
+12 -1
View File
@@ -487,7 +487,8 @@ def test_monoliths_change_only_sanctioned_lines():
The round-trip (multiset diff vs the pinned v8 blob) must come back clean:
each added/removed line matches one of the documented sanctioned predicates
in gen the enum unification, the unified description, the chunk-cleanup
rewrite (#1172), and the four #1392 runbook fixes. Anything else is drift.
rewrite (#1172), the four #1392 runbook fixes, and semantic-cache source
scoping (#1757). Anything else is drift.
"""
platforms = gen.load_platforms()
for key in ("aider", "devin"):
@@ -534,6 +535,16 @@ def test_monoliths_carry_the_1392_runbook_fixes():
assert "if not wrote:" in body
def test_monoliths_scope_semantic_cache_writes_to_uncached_files():
"""#1757: generated monoliths pass the dispatched-file allowlist when
replacing semantic cache entries."""
platforms = gen.load_platforms()
for key in ("aider", "devin"):
body = gen.render(platforms[key])[0].content
assert ".graphify_uncached.txt').read_text(" in body
assert "allowed_source_files=uncached" in body
def test_generated_runbooks_pass_root_to_save_manifest():
"""#1417: every save_manifest call in a shipped runbook threads root=.
@@ -310,7 +310,8 @@ from graphify.cache import save_semantic_cache
from pathlib import Path
new = json.loads(Path('graphify-out/.graphify_semantic_new.json').read_text(encoding=\"utf-8\")) if Path('graphify-out/.graphify_semantic_new.json').exists() else {'nodes':[],'edges':[],'hyperedges':[]}
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH')
uncached = [line for line in Path('graphify-out/.graphify_uncached.txt').read_text(encoding=\"utf-8\").splitlines() if line]
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH', allowed_source_files=uncached)
print(f'Cached {saved} files')
"
```
@@ -313,7 +313,8 @@ from graphify.cache import save_semantic_cache
from pathlib import Path
new = json.loads(Path('.graphify_semantic_new.json').read_text()) if Path('.graphify_semantic_new.json').exists() else {'nodes':[],'edges':[],'hyperedges':[]}
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []))
uncached = [line for line in Path('.graphify_uncached.txt').read_text().splitlines() if line]
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), allowed_source_files=uncached)
print(f'Cached {saved} files')
"
```
@@ -310,7 +310,8 @@ from graphify.cache import save_semantic_cache
from pathlib import Path
new = json.loads(Path('graphify-out/.graphify_semantic_new.json').read_text(encoding=\"utf-8\")) if Path('graphify-out/.graphify_semantic_new.json').exists() else {'nodes':[],'edges':[],'hyperedges':[]}
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH')
uncached = [line for line in Path('graphify-out/.graphify_uncached.txt').read_text(encoding=\"utf-8\").splitlines() if line]
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH', allowed_source_files=uncached)
print(f'Cached {saved} files')
"
```
@@ -313,7 +313,8 @@ from graphify.cache import save_semantic_cache
from pathlib import Path
new = json.loads(Path('graphify-out/.graphify_semantic_new.json').read_text(encoding=\"utf-8\")) if Path('graphify-out/.graphify_semantic_new.json').exists() else {'nodes':[],'edges':[],'hyperedges':[]}
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH')
uncached = [line for line in Path('graphify-out/.graphify_uncached.txt').read_text(encoding=\"utf-8\").splitlines() if line]
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH', allowed_source_files=uncached)
print(f'Cached {saved} files')
"
```
@@ -310,7 +310,8 @@ from graphify.cache import save_semantic_cache
from pathlib import Path
new = json.loads(Path('graphify-out/.graphify_semantic_new.json').read_text(encoding=\"utf-8\")) if Path('graphify-out/.graphify_semantic_new.json').exists() else {'nodes':[],'edges':[],'hyperedges':[]}
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH')
uncached = [line for line in Path('graphify-out/.graphify_uncached.txt').read_text(encoding=\"utf-8\").splitlines() if line]
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH', allowed_source_files=uncached)
print(f'Cached {saved} files')
"
```
@@ -313,7 +313,8 @@ from graphify.cache import save_semantic_cache
from pathlib import Path
new = json.loads(Path('graphify-out/.graphify_semantic_new.json').read_text(encoding=\"utf-8\")) if Path('graphify-out/.graphify_semantic_new.json').exists() else {'nodes':[],'edges':[],'hyperedges':[]}
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH')
uncached = [line for line in Path('graphify-out/.graphify_uncached.txt').read_text(encoding=\"utf-8\").splitlines() if line]
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH', allowed_source_files=uncached)
print(f'Cached {saved} files')
"
```
@@ -374,7 +374,8 @@ from graphify.cache import save_semantic_cache
from pathlib import Path
new = json.loads(Path('graphify-out/.graphify_semantic_new.json').read_text()) if Path('graphify-out/.graphify_semantic_new.json').exists() else {'nodes':[],'edges':[],'hyperedges':[]}
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []))
uncached = [line for line in Path('graphify-out/.graphify_uncached.txt').read_text().splitlines() if line]
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), allowed_source_files=uncached)
print(f'Cached {saved} files')
"
```
@@ -310,7 +310,8 @@ from graphify.cache import save_semantic_cache
from pathlib import Path
new = json.loads(Path('graphify-out/.graphify_semantic_new.json').read_text(encoding=\"utf-8\")) if Path('graphify-out/.graphify_semantic_new.json').exists() else {'nodes':[],'edges':[],'hyperedges':[]}
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH')
uncached = [line for line in Path('graphify-out/.graphify_uncached.txt').read_text(encoding=\"utf-8\").splitlines() if line]
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH', allowed_source_files=uncached)
print(f'Cached {saved} files')
"
```
@@ -313,7 +313,8 @@ from graphify.cache import save_semantic_cache
from pathlib import Path
new = json.loads(Path('graphify-out/.graphify_semantic_new.json').read_text(encoding=\"utf-8\")) if Path('graphify-out/.graphify_semantic_new.json').exists() else {'nodes':[],'edges':[],'hyperedges':[]}
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH')
uncached = [line for line in Path('graphify-out/.graphify_uncached.txt').read_text(encoding=\"utf-8\").splitlines() if line]
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH', allowed_source_files=uncached)
print(f'Cached {saved} files')
"
```
@@ -313,7 +313,8 @@ from graphify.cache import save_semantic_cache
from pathlib import Path
new = json.loads(Path('graphify-out/.graphify_semantic_new.json').read_text(encoding=\"utf-8\")) if Path('graphify-out/.graphify_semantic_new.json').exists() else {'nodes':[],'edges':[],'hyperedges':[]}
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH')
uncached = [line for line in Path('graphify-out/.graphify_uncached.txt').read_text(encoding=\"utf-8\").splitlines() if line]
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH', allowed_source_files=uncached)
print(f'Cached {saved} files')
"
```
@@ -305,7 +305,8 @@ from graphify.cache import save_semantic_cache
from pathlib import Path
new = json.loads(Path('graphify-out/.graphify_semantic_new.json').read_text(encoding=\"utf-8\")) if Path('graphify-out/.graphify_semantic_new.json').exists() else {'nodes':[],'edges':[],'hyperedges':[]}
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH')
uncached = [line for line in Path('graphify-out/.graphify_uncached.txt').read_text(encoding=\"utf-8\").splitlines() if line]
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH', allowed_source_files=uncached)
print(f'Cached {saved} files')
"
```
@@ -313,7 +313,8 @@ from graphify.cache import save_semantic_cache
from pathlib import Path
new = json.loads(Path('graphify-out/.graphify_semantic_new.json').read_text(encoding=\"utf-8\")) if Path('graphify-out/.graphify_semantic_new.json').exists() else {'nodes':[],'edges':[],'hyperedges':[]}
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH')
uncached = [line for line in Path('graphify-out/.graphify_uncached.txt').read_text(encoding=\"utf-8\").splitlines() if line]
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH', allowed_source_files=uncached)
print(f'Cached {saved} files')
"
```
@@ -311,7 +311,8 @@ from graphify.cache import save_semantic_cache
from pathlib import Path
new = json.loads(Path('graphify-out/.graphify_semantic_new.json').read_text(encoding=\"utf-8\")) if Path('graphify-out/.graphify_semantic_new.json').exists() else {'nodes':[],'edges':[],'hyperedges':[]}
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH')
uncached = [line for line in Path('graphify-out/.graphify_uncached.txt').read_text(encoding=\"utf-8\").splitlines() if line]
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH', allowed_source_files=uncached)
print(f'Cached {saved} files')
"
```
@@ -309,7 +309,8 @@ from graphify.cache import save_semantic_cache
from pathlib import Path
new = json.loads(Path('graphify-out/.graphify_semantic_new.json').read_text(encoding=\"utf-8\")) if Path('graphify-out/.graphify_semantic_new.json').exists() else {'nodes':[],'edges':[],'hyperedges':[]}
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH')
uncached = [line for line in Path('graphify-out/.graphify_uncached.txt').read_text(encoding=\"utf-8\").splitlines() if line]
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH', allowed_source_files=uncached)
print(f'Cached {saved} files')
"
```
@@ -335,7 +335,8 @@ from graphify.cache import save_semantic_cache
from pathlib import Path
new = json.loads(Path('graphify-out/.graphify_semantic_new.json').read_text(encoding=\"utf-8\")) if Path('graphify-out/.graphify_semantic_new.json').exists() else {'nodes':[],'edges':[],'hyperedges':[]}
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH')
uncached = [line for line in Path('graphify-out/.graphify_uncached.txt').read_text(encoding=\"utf-8\").splitlines() if line]
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH', allowed_source_files=uncached)
print(f'Cached {saved} files')
"
```
+2 -1
View File
@@ -313,7 +313,8 @@ from graphify.cache import save_semantic_cache
from pathlib import Path
new = json.loads(Path('graphify-out/.graphify_semantic_new.json').read_text(encoding=\"utf-8\")) if Path('graphify-out/.graphify_semantic_new.json').exists() else {'nodes':[],'edges':[],'hyperedges':[]}
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH')
uncached = [line for line in Path('graphify-out/.graphify_uncached.txt').read_text(encoding=\"utf-8\").splitlines() if line]
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH', allowed_source_files=uncached)
print(f'Cached {saved} files')
"
```
+2 -1
View File
@@ -313,7 +313,8 @@ from graphify.cache import save_semantic_cache
from pathlib import Path
new = json.loads(Path('.graphify_semantic_new.json').read_text()) if Path('.graphify_semantic_new.json').exists() else {'nodes':[],'edges':[],'hyperedges':[]}
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []))
uncached = [line for line in Path('.graphify_uncached.txt').read_text().splitlines() if line]
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), allowed_source_files=uncached)
print(f'Cached {saved} files')
"
```
+2 -1
View File
@@ -248,7 +248,8 @@ from graphify.cache import save_semantic_cache
from pathlib import Path
new = json.loads(Path('graphify-out/.graphify_semantic_new.json').read_text(encoding=\"utf-8\")) if Path('graphify-out/.graphify_semantic_new.json').exists() else {'nodes':[],'edges':[],'hyperedges':[]}
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH')
uncached = [line for line in Path('graphify-out/.graphify_uncached.txt').read_text(encoding=\"utf-8\").splitlines() if line]
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH', allowed_source_files=uncached)
print(f'Cached {saved} files')
"
```
+2 -1
View File
@@ -374,7 +374,8 @@ from graphify.cache import save_semantic_cache
from pathlib import Path
new = json.loads(Path('graphify-out/.graphify_semantic_new.json').read_text()) if Path('graphify-out/.graphify_semantic_new.json').exists() else {'nodes':[],'edges':[],'hyperedges':[]}
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []))
uncached = [line for line in Path('graphify-out/.graphify_uncached.txt').read_text().splitlines() if line]
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), allowed_source_files=uncached)
print(f'Cached {saved} files')
"
```
+20 -2
View File
@@ -902,6 +902,22 @@ def _is_uv_from_interpreter_fix_line(line: str) -> bool:
return "uv tool run" in line and "graphifyy python" in line
def _is_semantic_cache_scope_fix_line(line: str) -> bool:
"""Whether a line scopes semantic cache writes to dispatched files (#1757).
A semantic subagent can mention a corpus file outside its assigned chunk and
misattribute a node to that file. The final cache write now passes the B0
uncached-file list as an allowlist, so an incidental mention cannot replace
another file's complete cached extraction. Both the old unscoped call
(removed) and the allowlist read/call (added) are sanctioned here.
"""
stripped = line.strip()
return (
stripped.startswith("uncached = [line for line in Path(")
and ".graphify_uncached.txt" in stripped
) or stripped.startswith("saved = save_semantic_cache(")
# Every line that may differ between a rendered monolith and its pristine v8
# baseline. Each predicate documents one sanctioned change-class; a blank line is
# allowed because the multi-line fix blocks insert spacing. Anything else failing
@@ -919,6 +935,7 @@ _SANCTIONED_MONOLITH_DIFFS = (
_is_shebang_allowlist_fix_line,
_is_obsidian_usage_comment_line,
_is_uv_from_interpreter_fix_line,
_is_semantic_cache_scope_fix_line,
)
@@ -935,8 +952,9 @@ def monolith_roundtrip(platform: Platform) -> list[str]:
arbitrary edit (even a blessed one) from drifting them. Sanctioned changes are
enumerated as predicates in ``_SANCTIONED_MONOLITH_DIFFS``: the file_type enum
unification, the unified frontmatter description, the chunk-cleanup rewrite
(#1172), and the four #1392 runbook fixes (directed propagation, content-only
semantic scope, stale-cache unlink, and the zero-node/shrink-guard ordering).
(#1172), the four #1392 runbook fixes (directed propagation, content-only
semantic scope, stale-cache unlink, and the zero-node/shrink-guard ordering),
and semantic-cache source scoping (#1757).
The comparison is a multiset diff, not a positional zip: a line whose text is
unchanged but merely *moved* (the report-write line shifted below ``to_json``