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

Bind visualization status to snapshot freshness

This commit is contained in:
Andraxion 2026-07-29 05:23:52 -04:00
parent 24bd13f9d9
commit 176b2d2784
9 changed files with 627 additions and 43 deletions

View file

@ -9,6 +9,7 @@ import secrets
import signal
import socket
import sqlite3
import stat
import sys
import tempfile
import threading
@ -73,6 +74,9 @@ LEASE_MONITOR_INTERVAL_SECONDS = 1.0
VISUALIZATION_REGISTRY_NAME = ".visualization.json"
VISUALIZATION_LOCK_NAME = ".visualization.lock"
VISUALIZATION_RUNTIME = "persistent-worker@1"
VISUALIZATION_SNAPSHOT_SCHEMA_VERSION = 1
IndexSignature = tuple[int, int, int, int, int]
class _VisualizationHttpServer(ThreadingHTTPServer):
@ -103,10 +107,13 @@ class VisualizationIndexSnapshot:
self.max_depth = index.project.descriptor.limits.max_traversal_depth
self.identity: dict[str, object] = {key: checked[key] for key in self._IDENTITY_KEYS}
self._stat = self._safe_stat()
self._validate_snapshot()
@classmethod
def from_spec(cls, spec: dict[str, object]) -> VisualizationIndexSnapshot:
snapshot = cls.__new__(cls)
if spec.get("schema_version") != VISUALIZATION_SNAPSHOT_SCHEMA_VERSION:
raise DocForgeError("invalid_index", "Visualization snapshot version is invalid")
path = spec["path"]
title = spec["title"]
project_root = spec["project_root"]
@ -117,11 +124,16 @@ class VisualizationIndexSnapshot:
if (
not isinstance(path, str)
or not isinstance(title, str)
or not title
or not isinstance(project_root, str)
or type(max_source_bytes) is not int
or max_source_bytes < 1
or type(max_query_chars) is not int
or max_query_chars < 1
or type(max_results) is not int
or max_results < 1
or type(max_depth) is not int
or max_depth < 1
):
raise DocForgeError("invalid_index", "Visualization snapshot is invalid")
snapshot.path = Path(path)
@ -131,6 +143,12 @@ class VisualizationIndexSnapshot:
raise DocForgeError("invalid_index", "Visualization project root is invalid") from error
if not snapshot.project_root.is_dir():
raise DocForgeError("invalid_index", "Visualization project root is invalid")
if not snapshot.path.is_absolute():
raise DocForgeError("invalid_index", "Visualization index path is invalid")
try:
snapshot.path.relative_to(snapshot.project_root)
except ValueError as error:
raise DocForgeError("invalid_index", "Visualization index path is invalid") from error
snapshot.title = title
snapshot.max_source_bytes = max_source_bytes
snapshot.max_query_chars = max_query_chars
@ -139,13 +157,59 @@ class VisualizationIndexSnapshot:
identity = spec["identity"]
if not isinstance(identity, dict):
raise DocForgeError("invalid_index", "Visualization identity is invalid")
typed_identity = cast(dict[str, object], identity)
snapshot.identity = {key: typed_identity[key] for key in cls._IDENTITY_KEYS}
snapshot._stat = snapshot._safe_stat()
snapshot.identity = cls._parse_identity(
cast(dict[str, object], identity),
snapshot.project_root,
)
snapshot._stat = cls._parse_index_signature(spec.get("index_signature"))
if snapshot.index_state() != "current":
raise DocForgeError(
"visualization_stale",
"The validated index changed before the visualization worker started",
)
snapshot._validate_snapshot()
return snapshot
@classmethod
def _parse_identity(
cls,
value: dict[str, object],
project_root: Path,
) -> dict[str, object]:
if set(value) != set(cls._IDENTITY_KEYS):
raise DocForgeError("invalid_index", "Visualization identity is invalid")
project_id = value.get("project_id")
fingerprint = value.get("project_root_fingerprint")
revision = value.get("revision")
source_hash = value.get("source_hash")
adapter = value.get("adapter")
node_count = value.get("node_count")
edge_count = value.get("edge_count")
if (
not isinstance(project_id, str)
or not project_id
or not isinstance(fingerprint, str)
or len(fingerprint) != 16
or any(character not in "0123456789abcdef" for character in fingerprint)
or fingerprint != project_root_fingerprint(project_root)
or not isinstance(revision, str)
or not revision
or not isinstance(source_hash, str)
or len(source_hash) != 64
or any(character not in "0123456789abcdef" for character in source_hash)
or not isinstance(adapter, str)
or not adapter
or type(node_count) is not int
or node_count < 0
or type(edge_count) is not int
or edge_count < 0
):
raise DocForgeError("invalid_index", "Visualization identity is invalid")
return {key: value[key] for key in cls._IDENTITY_KEYS}
def spec(self) -> dict[str, object]:
return {
"schema_version": VISUALIZATION_SNAPSHOT_SCHEMA_VERSION,
"path": str(self.path),
"project_root": str(self.project_root),
"title": self.title,
@ -154,8 +218,44 @@ class VisualizationIndexSnapshot:
"max_results": self.max_results,
"max_depth": self.max_depth,
"identity": dict(self.identity),
"index_signature": {
"schema_version": 1,
"device": self._stat[0],
"inode": self._stat[1],
"size": self._stat[2],
"mtime_ns": self._stat[3],
"ctime_ns": self._stat[4],
},
}
@staticmethod
def _parse_index_signature(value: object) -> IndexSignature:
if not isinstance(value, dict):
raise DocForgeError("invalid_index", "Visualization index signature is invalid")
payload = cast(dict[str, object], value)
if payload.get("schema_version") != 1:
raise DocForgeError("invalid_index", "Visualization index signature is invalid")
fields = ("device", "inode", "size", "mtime_ns", "ctime_ns")
values: list[int] = []
for field in fields:
item = payload.get(field)
if type(item) is not int or item < 0:
raise DocForgeError("invalid_index", "Visualization index signature is invalid")
values.append(item)
return cast(IndexSignature, tuple(values))
def index_state(self) -> str:
"""Return cheap publication freshness without opening SQLite."""
try:
return "current" if self._safe_stat() == self._stat else "stale"
except DocForgeError:
return "stale"
def _validate_snapshot(self) -> None:
with self._connection():
pass
def overview(self) -> dict[str, object]:
with self._connection() as connection:
return self._result(
@ -749,15 +849,26 @@ class VisualizationIndexSnapshot:
raise DocForgeError("invalid_limit", "Result limit is outside the configured range")
return value
def _safe_stat(self) -> tuple[int, int, int, int]:
if (
self.path.is_symlink()
or not self.path.is_file()
or self.path.resolve(strict=True) != self.path
):
raise DocForgeError("missing_index", "Validated visualization index is unavailable")
stat = self.path.stat()
return (stat.st_dev, stat.st_ino, stat.st_size, stat.st_mtime_ns)
def _safe_stat(self) -> IndexSignature:
try:
status = self.path.lstat()
if not stat.S_ISREG(status.st_mode) or self.path.resolve(strict=True) != self.path:
raise DocForgeError(
"missing_index",
"Validated visualization index is unavailable",
)
except OSError as error:
raise DocForgeError(
"missing_index",
"Validated visualization index is unavailable",
) from error
return (
status.st_dev,
status.st_ino,
status.st_size,
status.st_mtime_ns,
status.st_ctime_ns,
)
@contextmanager
def _connection(self) -> Generator[sqlite3.Connection, None, None]:
@ -1074,7 +1185,11 @@ class VisualizationRunner:
if parsed.path == f"{prefix}/api/health":
with self._lock:
last_activity = self._activity_last_seen
payload = reader.result(viewer="alive", last_activity_at=last_activity)
payload = reader.result(
viewer="alive",
last_activity_at=last_activity,
index_state=reader.index_state(),
)
elif parsed.path == f"{prefix}/api/overview":
self._touch_lease()
payload = reader.overview()