orchestrata_ultimate.semantic_graph._freshness
Reference › orchestrata_ultimate › semantic_graph › _freshness
Content-hash freshness tracking for semantic graph re-ingestion (audit item 7). The enrichment pass already writes with MERGE and is cheap to repeat, but the ingestion script clears and re-embeds the whole source tree on each run; this module supplies the primitive that makes that loop incremental without touching the script. file_digest returns the SHA-256 hex digest of a file, load_manifest and save_manifest read and write a path-to-digest manifest on disk (creating parent directories, with a default location), and changed_files returns the subset of paths whose digest differs from the manifest. The intended use in the docstring is load the manifest, compute the dirty files, re-ingest only those, then save. No module imports it yet and it touches no graph.
At a glance
| Source | src/orchestrata_ultimate/semantic_graph/_freshness.py |
| Lines of code | 132 |
| Classes | 0 |
| Functions | 4 |
| Imports (internal) | none |
| Imported by | none |
| Tests that import it | tests/semantic_graph/test_spec_trace_enrichment.py |
Class diagram
No classes; see Functions.
Dependency diagram
No internal imports and no importers.
API
Functions
| Function | Signature | Summary |
|---|---|---|
file_digest |
file_digest(path: Path) -> str |
Return the sha256 hex digest of a file's contents. |
load_manifest |
load_manifest(manifest_path: Path = DEFAULT_MANIFEST_PATH) -> dict[str, str] |
Load a digest manifest from disk. |
save_manifest |
save_manifest(manifest: dict[str, str], manifest_path: Path = DEFAULT_MANIFEST_PATH) -> None |
Persist a digest manifest to disk, creating parent directories as needed. |
changed_files |
changed_files(paths: list[Path], manifest: dict[str, str]) -> list[Path] |
Return the subset of paths whose content differs from the manifest. |
Source
"""Content-hash based freshness tracking for semantic-graph re-ingestion.
Audit item 7: re-running the enrichment/ingestion pipeline should be cheap.
``spec_trace_enricher``/``scripts/enrich_semantic_graph_specs.py`` already
write via ``MERGE`` (see the Memgraph adapter), so re-running *that* pass is
idempotent and inexpensive. The costly piece is
``scripts/ingest_orchestrata_ultimate.py`` (owned by a different workstream),
which unconditionally clears and fully re-embeds the source tree on every
run.
This module provides the reusable primitive for making that loop
incremental, without reaching into that script's ownership boundary:
manifest = load_manifest()
dirty = changed_files(all_source_files, manifest)
# ... re-embed / re-ingest only `dirty` ...
for path in all_source_files:
manifest[str(path)] = file_digest(path)
save_manifest(manifest)
Intended integration for ``ingest_orchestrata_ultimate.py``: before the
clear-and-rebuild pass, compute ``changed_files()`` against the persisted
manifest and skip (or only re-embed) files whose digest is unchanged; update
and persist the manifest after a successful run. This module is pure
filesystem/hash bookkeeping -- it never talks to the graph backend -- so it
is unit-testable without any live infra and safe to adopt incrementally.
"""
from __future__ import annotations
import hashlib
import json
import logging
from pathlib import Path
logger = logging.getLogger(__name__)
#: Default location of the persisted digest manifest.
DEFAULT_MANIFEST_PATH = Path("plans/.semantic_graph_digests.json")
#: Chunk size used when streaming a file through the hash function.
_CHUNK_SIZE = 65536
def file_digest(path: Path) -> str:
"""Return the sha256 hex digest of a file's contents.
Args:
path: File to hash.
Returns:
Hex-encoded sha256 digest of the file's bytes.
Raises:
OSError: If the file cannot be opened/read.
"""
digest = hashlib.sha256()
with path.open("rb") as handle:
while chunk := handle.read(_CHUNK_SIZE):
digest.update(chunk)
return digest.hexdigest()
def load_manifest(manifest_path: Path = DEFAULT_MANIFEST_PATH) -> dict[str, str]:
"""Load a digest manifest from disk.
Args:
manifest_path: Location of the manifest JSON file.
Returns:
Mapping from (string) file path to its last-known sha256 digest.
Returns ``{}`` if the file is missing, unreadable, or not a JSON
object -- freshness tracking degrades gracefully to "everything is
changed" rather than raising.
"""
if not manifest_path.is_file():
return {}
try:
data = json.loads(manifest_path.read_text(encoding="utf-8"))
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
logger.warning("Cannot read digest manifest %s: %s", manifest_path, exc)
return {}
if not isinstance(data, dict):
logger.warning(
"Digest manifest %s is not a JSON object; ignoring", manifest_path
)
return {}
return {str(key): str(value) for key, value in data.items()}
def save_manifest(
manifest: dict[str, str], manifest_path: Path = DEFAULT_MANIFEST_PATH
) -> None:
"""Persist a digest manifest to disk, creating parent directories as needed.
Args:
manifest: Mapping from file path (string) to sha256 digest.
manifest_path: Location to write the manifest JSON file.
"""
manifest_path.parent.mkdir(parents=True, exist_ok=True)
manifest_path.write_text(
json.dumps(manifest, indent=2, sort_keys=True), encoding="utf-8"
)
def changed_files(paths: list[Path], manifest: dict[str, str]) -> list[Path]:
"""Return the subset of ``paths`` whose content differs from the manifest.
A path is considered changed if it is absent from the manifest or its
current digest differs from the recorded one. Unreadable paths fail
open (are reported as changed) so a transient read error never causes a
real change to be silently skipped.
Args:
paths: Candidate files to check.
manifest: Digest manifest as returned by :func:`load_manifest`.
Returns:
The subset of ``paths`` considered changed, in input order.
"""
changed: list[Path] = []
for path in paths:
key = str(path)
try:
digest = file_digest(path)
except OSError as exc:
logger.warning("Cannot digest %s: %s", path, exc)
changed.append(path)
continue
if manifest.get(key) != digest:
changed.append(path)
return changed