mirror of
https://github.com/Strategic-Automation/violin.git
synced 2026-08-14 12:33:37 +02:00
141 lines
4.8 KiB
Python
141 lines
4.8 KiB
Python
"""History file I/O — append, search, staleness, and repeat detection.
|
|
|
|
All history operations are centralised here to avoid duplication between the
|
|
state machine (which previously owned append/contains/repeat) and the guard
|
|
checks (which owned the staleness check).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import UTC, datetime
|
|
from pathlib import Path
|
|
|
|
from .state import ensure_dir, lock_file, resolve_eng_dir
|
|
|
|
_COMMAND_MARKER = " | command="
|
|
_COMMAND_LENGTH_MARKER = " | command_length="
|
|
_RECEIPT_MARKER = " | receipt="
|
|
|
|
|
|
def normalize_command(command: str) -> str:
|
|
"""Normalize whitespace and newlines in a command string for reliable history matching."""
|
|
if not command:
|
|
return ""
|
|
lines = command.replace("\r\n", "\n").split("\n")
|
|
cleaned_parts = [part.strip() for part in lines if part.strip()]
|
|
return " ".join(cleaned_parts)
|
|
|
|
|
|
def _history_path(eng_dir: str | Path) -> Path:
|
|
return resolve_eng_dir(eng_dir) / "state" / "history.md"
|
|
|
|
|
|
def append_history(
|
|
eng_dir: str | Path,
|
|
command: str,
|
|
phase: str,
|
|
exit_code: int,
|
|
receipt_path: str = "",
|
|
status: str = "",
|
|
) -> None:
|
|
"""Append one execution record to history.md under an advisory lock."""
|
|
path = _history_path(eng_dir)
|
|
ensure_dir(path.parent)
|
|
stamp = datetime.now(UTC).isoformat().replace("+00:00", "Z")
|
|
clean_command = normalize_command(command) if "\n" in command else command
|
|
status_field = f" | status={status}" if status else ""
|
|
line = (
|
|
f"- {stamp} | phase={phase} | exit_code={exit_code}{status_field} | command={clean_command}"
|
|
f"{_COMMAND_LENGTH_MARKER}{len(clean_command)}"
|
|
)
|
|
if receipt_path:
|
|
line += f"{_RECEIPT_MARKER}{receipt_path}"
|
|
with lock_file(path), path.open("a", encoding="utf-8") as handle:
|
|
handle.write(line + "\n")
|
|
|
|
|
|
def history_contains(eng_dir: str | Path, command: str) -> bool:
|
|
"""Return True if ``command`` appears anywhere in history.md.
|
|
|
|
Used by the self-certify guard to prove a batch finished before review.
|
|
"""
|
|
hist = _history_path(eng_dir)
|
|
if not hist.exists():
|
|
return False
|
|
norm_target = normalize_command(command)
|
|
for line in hist.read_text(encoding="utf-8").splitlines():
|
|
rec = _recorded_command(line)
|
|
if rec == command or (rec is not None and normalize_command(rec) == norm_target):
|
|
return True
|
|
return False
|
|
|
|
|
|
def _recorded_command(line: str) -> str | None:
|
|
"""Read one command field, including legacy records without a length."""
|
|
|
|
if _COMMAND_MARKER not in line:
|
|
return None
|
|
payload = line.split(_COMMAND_MARKER, 1)[1]
|
|
command, marker, metadata = payload.rpartition(_COMMAND_LENGTH_MARKER)
|
|
if marker:
|
|
length_text = metadata.split(_RECEIPT_MARKER, 1)[0]
|
|
try:
|
|
expected_length = int(length_text)
|
|
except ValueError:
|
|
expected_length = -1
|
|
if expected_length >= 0 and len(command) == expected_length:
|
|
return command
|
|
|
|
command, marker, _receipt = payload.rpartition(_RECEIPT_MARKER)
|
|
return command if marker else payload
|
|
|
|
|
|
def check_history_staleness(
|
|
eng_dir: str | Path, command: str, *, allow_pending_repeat: bool = False
|
|
) -> tuple[list[str], list[str], list[str]]:
|
|
"""Check if the command is an exact repeat of the last recorded command.
|
|
|
|
Returns ``(errors, warnings, infos)`` tuples — compatible with GuardResult.add_* callers.
|
|
"""
|
|
errors: list[str] = []
|
|
warnings: list[str] = []
|
|
infos: list[str] = []
|
|
hist_path = _history_path(eng_dir)
|
|
|
|
if not hist_path.exists():
|
|
infos.append("history.md does not exist — will be recorded after command runs")
|
|
return errors, warnings, infos
|
|
|
|
content = hist_path.read_text(encoding="utf-8")
|
|
lines = [line.strip() for line in content.splitlines() if line.strip()]
|
|
|
|
if not lines:
|
|
infos.append("history.md is empty — first command will be recorded")
|
|
return errors, warnings, infos
|
|
|
|
# History entries are written as ``... | command=<command>``. Compare
|
|
# that field exactly instead of using substring matching, which can reject
|
|
# a command merely because it contains the previous command text.
|
|
last_line = lines[-1]
|
|
recorded_command = _recorded_command(last_line)
|
|
is_repeat = (recorded_command == command) or (
|
|
recorded_command is not None
|
|
and normalize_command(recorded_command) == normalize_command(command)
|
|
)
|
|
if is_repeat and not allow_pending_repeat:
|
|
errors.append(
|
|
f"command appears to be an exact repeat of the last recorded command: {last_line}"
|
|
)
|
|
elif is_repeat:
|
|
infos.append("exact repeat belongs to the pending batch; allowing reconciliation/retry")
|
|
|
|
return errors, warnings, infos
|
|
|
|
|
|
__all__ = [
|
|
"normalize_command",
|
|
"append_history",
|
|
"history_contains",
|
|
"check_history_staleness",
|
|
]
|