- Strip all 31 stale seedgo bypasses, re-evaluate from zero (10 legitimate kept) - Create json_handler + wire 9 handler/module files (json_structure 0→100%) - Add test_devpulse.py (17 tests: CLI routing, module discovery, error resilience) - Add conftest fixtures: mock_logger, mock_json_handler - Fix devpulse.py: add print_help(), feedback.py: META block + introspection - README: fix stale content, update test count 130→236, add json/ handler - Global prompt: remove secrets path, collapse duplicates, add branch_json breadcrumb - Delete init pollution: aipass/status/ dir (15 files), 2 stale per-branch global prompts - Seedgo: add README content accuracy checks (test count + dead links, 26 tests) - Prax: fix _parse_agent_activity() reverse iteration (thinking entries hidden) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
200 lines
6.6 KiB
Python
200 lines
6.6 KiB
Python
# =================== AIPass ====================
|
|
# Name: json_handler.py
|
|
# Description: JSON auto-creating handler for devpulse data files
|
|
# Version: 1.0.0
|
|
# Created: 2026-05-15
|
|
# Modified: 2026-05-15
|
|
# =============================================
|
|
|
|
"""JSON auto-creating handler for devpulse data files.
|
|
|
|
Provides log_operation() for structured operation logging and
|
|
ensure_json_file() for auto-creating branch-scoped JSON files.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import inspect
|
|
import json
|
|
import os
|
|
import tempfile
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from aipass.prax import logger
|
|
|
|
_BRANCH_ROOT: Path = Path(__file__).resolve().parents[3]
|
|
_BRANCH_NAME: str = _BRANCH_ROOT.name
|
|
JSON_DIR: Path = _BRANCH_ROOT / f"{_BRANCH_NAME}_json"
|
|
|
|
_JSON_TYPES: tuple[str, ...] = ("config", "data", "log")
|
|
|
|
|
|
def _today() -> str:
|
|
"""Return today's date as ISO string."""
|
|
return datetime.now().date().isoformat()
|
|
|
|
|
|
def _get_caller_module_name() -> str:
|
|
stack = inspect.stack()
|
|
if len(stack) > 2:
|
|
caller_path = Path(stack[2].filename)
|
|
module_name = caller_path.stem
|
|
if module_name and not module_name.startswith("_"):
|
|
return module_name
|
|
return "unknown"
|
|
|
|
|
|
def _atomic_write_json(path: Path, data: Any) -> None:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
fd, tmp_path = tempfile.mkstemp(dir=str(path.parent), suffix=".tmp", prefix=".json_")
|
|
try:
|
|
with os.fdopen(fd, "w", encoding="utf-8") as fh:
|
|
json.dump(data, fh, indent=2, ensure_ascii=False)
|
|
os.replace(tmp_path, str(path))
|
|
except BaseException as exc:
|
|
logger.warning("_atomic_write_json: failed for %s: %s", path, exc)
|
|
try:
|
|
os.unlink(tmp_path)
|
|
except OSError as cleanup_exc:
|
|
logger.warning("_atomic_write_json: cleanup failed for %s: %s", tmp_path, cleanup_exc)
|
|
raise
|
|
|
|
|
|
def _default_config(module_name: str) -> dict[str, Any]:
|
|
today = _today()
|
|
return {
|
|
"module_name": module_name,
|
|
"version": "1.0.0",
|
|
"config": {"max_log_entries": 100},
|
|
"created": today,
|
|
"last_updated": today,
|
|
}
|
|
|
|
|
|
def _default_data(module_name: str) -> dict[str, Any]: # noqa: ARG001
|
|
today = _today()
|
|
return {"created": today, "last_updated": today}
|
|
|
|
|
|
def _default_log(module_name: str) -> list[Any]: # noqa: ARG001
|
|
return []
|
|
|
|
|
|
_DEFAULTS: dict[str, Any] = {
|
|
"config": _default_config,
|
|
"data": _default_data,
|
|
"log": _default_log,
|
|
}
|
|
|
|
|
|
def validate_json_structure(data: Any, json_type: str) -> bool:
|
|
"""Check that data matches expected shape for json_type."""
|
|
if json_type == "config":
|
|
if not isinstance(data, dict):
|
|
return False
|
|
return all(key in data for key in ("module_name", "version", "config"))
|
|
if json_type == "data":
|
|
if not isinstance(data, dict):
|
|
return False
|
|
return all(key in data for key in ("created", "last_updated"))
|
|
if json_type == "log":
|
|
return isinstance(data, list)
|
|
return False
|
|
|
|
|
|
def get_json_path(module_name: str, json_type: str) -> Path:
|
|
"""Return filesystem path for a module's JSON file."""
|
|
return JSON_DIR / f"{module_name}_{json_type}.json"
|
|
|
|
|
|
def ensure_json_exists(module_name: str, json_type: str) -> bool:
|
|
"""Ensure a single JSON file exists, creating with defaults if missing."""
|
|
JSON_DIR.mkdir(parents=True, exist_ok=True)
|
|
json_path = get_json_path(module_name, json_type)
|
|
if json_path.exists():
|
|
try:
|
|
if json_path.stat().st_size == 0:
|
|
logger.warning("ensure_json_exists: empty file at %s, regenerating", json_path)
|
|
else:
|
|
with open(json_path, encoding="utf-8") as fh:
|
|
data = json.load(fh)
|
|
if validate_json_structure(data, json_type):
|
|
return True
|
|
except Exception as exc: # noqa: BLE001
|
|
logger.warning("ensure_json_exists: failed to read %s, regenerating: %s", json_path, exc)
|
|
factory = _DEFAULTS.get(json_type)
|
|
if factory is None:
|
|
raise ValueError(f"Unknown json_type: {json_type!r}")
|
|
default = factory(module_name)
|
|
_atomic_write_json(json_path, default)
|
|
return True
|
|
|
|
|
|
def ensure_module_jsons(module_name: str) -> bool:
|
|
"""Ensure all three JSON files (config, data, log) exist for a module."""
|
|
for json_type in _JSON_TYPES:
|
|
ensure_json_exists(module_name, json_type)
|
|
return True
|
|
|
|
|
|
def load_json(module_name: str, json_type: str) -> Any | None:
|
|
"""Load a module's JSON file, auto-creating if missing."""
|
|
if not ensure_json_exists(module_name, json_type):
|
|
return None
|
|
json_path = get_json_path(module_name, json_type)
|
|
try:
|
|
if json_path.stat().st_size == 0:
|
|
factory = _DEFAULTS.get(json_type)
|
|
return factory(module_name) if factory else None
|
|
with open(json_path, encoding="utf-8") as fh:
|
|
return json.load(fh)
|
|
except (json.JSONDecodeError, OSError) as exc:
|
|
logger.warning("load_json: failed to read %s: %s", json_path, exc)
|
|
factory = _DEFAULTS.get(json_type)
|
|
return factory(module_name) if factory else None
|
|
|
|
|
|
def save_json(module_name: str, json_type: str, data: Any) -> bool:
|
|
"""Write data to a module's JSON file after validation."""
|
|
if not validate_json_structure(data, json_type):
|
|
raise ValueError(f"Invalid structure for {json_type} JSON")
|
|
if json_type == "data" and isinstance(data, dict):
|
|
data["last_updated"] = _today()
|
|
json_path = get_json_path(module_name, json_type)
|
|
_atomic_write_json(json_path, data)
|
|
return True
|
|
|
|
|
|
def log_operation(
|
|
operation: str,
|
|
data: dict[str, Any] | None = None,
|
|
module_name: str | None = None,
|
|
) -> bool:
|
|
"""Append an entry to a module's operation log with FIFO rotation."""
|
|
if module_name is None:
|
|
module_name = _get_caller_module_name()
|
|
try:
|
|
ensure_module_jsons(module_name)
|
|
config = load_json(module_name, "config")
|
|
max_entries = 100
|
|
if config and "config" in config:
|
|
max_entries = config["config"].get("max_log_entries", 100)
|
|
log = load_json(module_name, "log")
|
|
if log is None:
|
|
log = []
|
|
entry: dict[str, Any] = {
|
|
"timestamp": datetime.now().isoformat(),
|
|
"operation": operation,
|
|
}
|
|
if data:
|
|
entry["data"] = data
|
|
log.append(entry)
|
|
if len(log) > max_entries:
|
|
log = log[-max_entries:]
|
|
return save_json(module_name, "log", log)
|
|
except Exception as exc:
|
|
logger.warning("log_operation: failed for %s/%s: %s", module_name, operation, exc)
|
|
return False
|