1
0
Fork 0
Code Issues Pull requests Projects Releases 2 Packages Wiki Activity Actions Pages

Cache validated source generation receipts

This commit is contained in:
Andraxion 2026-07-29 06:07:03 -04:00
parent 529accf858
commit 6253c45a5e
3 changed files with 115 additions and 19 deletions

View file

@ -86,6 +86,15 @@ malformed, incompatible, foreign, or dirty receipt becomes a cache miss and fall
canonical load and row-verification oracle. Successful fallback verification repairs the disposable canonical load and row-verification oracle. Successful fallback verification repairs the disposable
receipt. receipt.
The final 1,000-node evidence run initially exposed a repeatable 52 ms exact-read maximum against
the 50 ms target. Profiling showed no source parsing or SQLite cost; each request rebuilt and
revalidated 1,000 `Path` objects from the unchanged JSON receipt twice. The project binding now
caches only the strictly validated receipt structure behind its device, inode, size, modification
time, and change-time signature. Canonical file and directory identities are still recaptured
before and after every query. Receipt replacement or mutation invalidates the cache and fails
closed. The same ordered exact-read profile fell from about 4052 ms to about 18 ms without
weakening stale-read refusal.
The receipt is deliberately generic-project behavior. Incremental adapter manifests retain The receipt is deliberately generic-project behavior. Incremental adapter manifests retain
authority over generated or specialist source identities. A one-method legacy adapter continues to authority over generated or specialist source identities. A one-method legacy adapter continues to
work even when it cannot provide a cheap generation. work even when it cannot provide a cheap generation.

View file

@ -88,6 +88,17 @@ class _CapturedGeneration:
directories: tuple[tuple[str, int, int, int, int, int], ...] directories: tuple[tuple[str, int, int, int, int, int], ...]
@dataclass(frozen=True)
class _ParsedGenerationReceipt:
signature: tuple[int, int, int, int, int]
source_hash: str
revision: str
files: tuple[tuple[object, ...], ...]
directories: tuple[tuple[object, ...], ...]
file_paths: tuple[Path, ...]
directory_paths: tuple[Path, ...]
def project_root_fingerprint(root: Path) -> str: def project_root_fingerprint(root: Path) -> str:
return hashlib.sha256(str(root).encode()).hexdigest()[:16] return hashlib.sha256(str(root).encode()).hexdigest()[:16]
@ -183,6 +194,22 @@ def _receipt_paths(root: Path, value: object, *, width: int) -> tuple[Path, ...]
return tuple(paths) return tuple(paths)
def _receipt_signature(path: Path) -> tuple[int, int, int, int, int] | None:
try:
status = path.lstat()
except OSError:
return None
if not stat.S_ISREG(status.st_mode):
return None
return (
status.st_dev,
status.st_ino,
status.st_size,
status.st_mtime_ns,
status.st_ctime_ns,
)
def _load_descriptor(root: Path) -> ProjectDescriptor: def _load_descriptor(root: Path) -> ProjectDescriptor:
descriptor_path = root / ".docforge" / "project.toml" descriptor_path = root / ".docforge" / "project.toml"
if not descriptor_path.is_file(): if not descriptor_path.is_file():
@ -698,6 +725,7 @@ class Project:
def __init__(self, descriptor: ProjectDescriptor) -> None: def __init__(self, descriptor: ProjectDescriptor) -> None:
self.descriptor = descriptor self.descriptor = descriptor
self._captured_generation: _CapturedGeneration | None = None self._captured_generation: _CapturedGeneration | None = None
self._generation_receipt_cache: _ParsedGenerationReceipt | None = None
@classmethod @classmethod
def open(cls, project_root: str | Path) -> Project: def open(cls, project_root: str | Path) -> Project:
@ -820,17 +848,54 @@ class Project:
def _incremental_state(self) -> ProjectState | None: def _incremental_state(self) -> ProjectState | None:
path = self.generation_path path = self.generation_path
if not path.is_file() or path.is_symlink(): signature = _receipt_signature(path)
if signature is None:
self._generation_receipt_cache = None
return None return None
receipt = self._generation_receipt_cache
if receipt is None or receipt.signature != signature:
receipt = self._parse_generation_receipt(path, signature)
self._generation_receipt_cache = receipt
if receipt is None:
return None
try:
current_directories = _directory_generation(
self.descriptor.root,
receipt.directory_paths,
)
except DocForgeError:
return None
if receipt.directories != cast(tuple[tuple[object, ...], ...], current_directories):
return None
try:
current_files = _file_generation(self.descriptor.root, receipt.file_paths)
except DocForgeError:
return None
if receipt.files != cast(tuple[tuple[object, ...], ...], current_files):
return None
if _revision(self.descriptor.root) != receipt.revision:
return None
return ProjectState(
source_hash=receipt.source_hash,
revision=receipt.revision,
)
def _parse_generation_receipt(
self,
path: Path,
signature: tuple[int, int, int, int, int],
) -> _ParsedGenerationReceipt | None:
try: try:
parsed: object = json.loads(path.read_text(encoding="utf-8")) parsed: object = json.loads(path.read_text(encoding="utf-8"))
except (OSError, UnicodeDecodeError, json.JSONDecodeError): except (OSError, UnicodeDecodeError, json.JSONDecodeError):
return None return None
if not isinstance(parsed, dict): if _receipt_signature(path) != signature or not isinstance(parsed, dict):
return None return None
payload = cast(dict[str, object], parsed) payload = cast(dict[str, object], parsed)
source_hash = payload.get("source_hash") source_hash = payload.get("source_hash")
revision = payload.get("revision") revision = payload.get("revision")
files_value = payload.get("files")
directories_value = payload.get("directories")
if ( if (
payload.get("schema_version") != SOURCE_GENERATION_SCHEMA_VERSION payload.get("schema_version") != SOURCE_GENERATION_SCHEMA_VERSION
or payload.get("source_contract") != GENERIC_SOURCE_CONTRACT or payload.get("source_contract") != GENERIC_SOURCE_CONTRACT
@ -841,35 +906,39 @@ class Project:
or not isinstance(source_hash, str) or not isinstance(source_hash, str)
or len(source_hash) != 64 or len(source_hash) != 64
or not isinstance(revision, str) or not isinstance(revision, str)
or not isinstance(files_value, list)
or not isinstance(directories_value, list)
): ):
return None return None
directory_paths = _receipt_paths( directory_paths = _receipt_paths(
self.descriptor.root, self.descriptor.root,
payload.get("directories"), cast(list[object], directories_value),
width=6, width=6,
) )
file_paths = _receipt_paths( file_paths = _receipt_paths(
self.descriptor.root, self.descriptor.root,
payload.get("files"), cast(list[object], files_value),
width=7, width=7,
) )
if directory_paths is None or file_paths is None: if directory_paths is None or file_paths is None:
return None return None
try: return _ParsedGenerationReceipt(
current_directories = _directory_generation(self.descriptor.root, directory_paths) signature=signature,
except DocForgeError: source_hash=source_hash,
return None revision=revision,
if payload.get("directories") != [list(identity) for identity in current_directories]: files=tuple(
return None tuple(cast(list[object], item))
try: for item in cast(list[object], files_value)
current_files = _file_generation(self.descriptor.root, file_paths) if isinstance(item, list)
except DocForgeError: ),
return None directories=tuple(
if payload.get("files") != [list(identity) for identity in current_files]: tuple(cast(list[object], item))
return None for item in cast(list[object], directories_value)
if _revision(self.descriptor.root) != revision: if isinstance(item, list)
return None ),
return ProjectState(source_hash=source_hash, revision=revision) file_paths=file_paths,
directory_paths=directory_paths,
)
def record_generation(self, snapshot: ProjectSnapshot) -> None: def record_generation(self, snapshot: ProjectSnapshot) -> None:
"""Persist a generation only after its complete derived index was verified.""" """Persist a generation only after its complete derived index was verified."""

View file

@ -422,6 +422,24 @@ class DocForgeCoreTests(unittest.TestCase):
): ):
index.get_node("guide.workflow") index.get_node("guide.workflow")
def test_source_generation_receipt_cache_is_signature_bound(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = self.copy_fixture("alpha", Path(directory))
project = Project.open(root)
ProjectIndex(project).build()
first = project.incremental_state()
self.assertIsNotNone(first)
with mock.patch.object(
Path,
"read_text",
side_effect=AssertionError("warm generation check reparsed its receipt"),
):
self.assertEqual(first, project.incremental_state())
project.generation_path.write_text("{", encoding="utf-8")
self.assertIsNone(project.incremental_state())
def test_source_set_change_during_load_fails_closed(self) -> None: def test_source_set_change_during_load_fails_closed(self) -> None:
project = Project.open(FIXTURES / "alpha") project = Project.open(FIXTURES / "alpha")
sources, directories = project._canonical_inventory() sources, directories = project._canonical_inventory()