fix(consistency): preserve immutable historical binding rows
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Build and Publish Multi-Context Image / build-and-push (push) Successful in 28s

Assistant: codex
Assistant-Model: gpt-6.1-sol
Assistant-Session: 01a1468b-63fa-7710-85dc-8f2e16b3ebfa
This commit is contained in:
tegwick 2026-10-17 02:26:29 +02:00
parent 086d79a273
commit a25b62f75a
10 changed files with 586 additions and 42 deletions

View file

@ -120,7 +120,9 @@ from api.classification import ( # noqa: E402
resolve_topic_domain_slug,
)
from api.services.lifecycle import should_activate_parent_for_active_tasks # noqa: E402
from api.services.historical_projection import BINDINGS as _HISTORICAL_BINDINGS # noqa: E402
from api.services.historical_projection import ( # noqa: E402
BINDINGS as _HISTORICAL_BINDINGS, FROZEN_CLUSTER_BINDINGS, frozen_cluster_binding,
)
from api.task_status import ( # noqa: E402
CANONICAL_TASK_STATUSES,
OPEN_TASK_STATUSES,
@ -318,7 +320,7 @@ def iter_workplan_files(workplans_dir: Path, include_archived: bool = True) -> l
# C-00 marks infrastructure/automation faults (API down, repo missing in DB).
# All other FAIL severities are assessment findings for follow-up.
_AUTOMATION_ERROR_CHECKS: frozenset[str] = frozenset({"C-00"})
_AUTOMATION_ERROR_CHECKS: frozenset[str] = frozenset({"C-00", "C-44"})
_NON_WORKPLAN_WORKPLAN_FILES: frozenset[str] = frozenset({"README.md"})
@ -2484,6 +2486,8 @@ def check_repo(
_check_workplan_id_collisions(api_base, repo_slug, repo_dir, workplan_infos, report)
_check_scope_freshness(repo_dir, workplan_infos, report)
if _refuse_frozen_fix_batch(report, repo_slug):
return report
_sync_workplan_bindings(api_base, repo_slug, workplan_infos, repo_dir, report)
return report
@ -2496,23 +2500,44 @@ def _sync_workplan_bindings(
repo_dir: Path,
report: ConsistencyReport,
) -> None:
if repo_slug == "railiance-cluster":
scanned_paths = {workplan_display_path(repo_dir, path) for path, _, _ in workplan_infos}
missing = [entry["path"] for entry in FROZEN_CLUSTER_BINDINGS
if entry["path"] not in scanned_paths]
if missing:
report.add(severity="FAIL", check_id="C-44",
message="Frozen binding batch refused: bound source missing from scan",
fixable=False)
return
bindings: list[dict[str, Any]] = []
for wp_file, meta, _ in workplan_infos:
ws_id = str(meta.get("state_hub_workstream_id", "")).strip().strip('"')
if not ws_id:
continue
archived = wp_file.parent.name == "archived"
file_status = normalise_workstream_status(str(meta.get("status", "")).strip())
bindings.append(
{
"workplan_id": ws_id,
"filename": wp_file.name,
"relative_path": workplan_display_path(repo_dir, wp_file),
"repo_slug": repo_slug,
"archived": archived,
"status": file_status or None,
}
)
binding = {
"workplan_id": ws_id, "filename": wp_file.name,
"relative_path": workplan_display_path(repo_dir, wp_file),
"repo_slug": repo_slug, "archived": archived,
"status": file_status or None,
}
try:
frozen = frozen_cluster_binding(binding)
if frozen is not None:
# Verify exact source before omitting the immutable binding.
# This is safe even against a receiver predating the API guard.
from hashlib import sha256
if (wp_file.is_symlink() or not wp_file.is_file()
or sha256(wp_file.read_bytes()).hexdigest() != frozen["sha256"]):
raise ValueError("frozen historical source missing or changed")
continue
except ValueError as exc:
report.add(severity="FAIL", check_id="C-44",
message=f"Frozen binding batch refused: {exc}",
file_path=str(wp_file), fixable=False)
return # Never send a partial batch after a frozen-source refusal.
if not ws_id:
continue
bindings.append(binding)
if not bindings:
return
result = _api_put(api_base, "/workplans/index/bindings", {"bindings": bindings})
@ -3610,6 +3635,45 @@ def _write_custodian_brief(api_base: str, repo_slug: str, repo_path: str) -> boo
# Fix engine
# ---------------------------------------------------------------------------
def _refuse_frozen_fix_batch(report: ConsistencyReport, repo_slug: str) -> bool:
"""Refuse a complete repair batch touching the exact bound histories."""
if any(issue.check_id == "C-44" and issue.severity == "FAIL" for issue in report.issues):
return True
protected_ids = {entry["uuid"] for entry in FROZEN_CLUSTER_BINDINGS}
protected_ids.update(task["uuid"] for entry in FROZEN_CLUSTER_BINDINGS
for task in entry["tasks"])
paths = {entry["path"] for entry in FROZEN_CLUSTER_BINDINGS}
root = Path(report.repo_path).absolute() if report.repo_path else None
def protected(value, key=""):
if isinstance(value, dict):
return any(protected(item, name) for name, item in value.items())
if isinstance(value, (list, tuple)):
return any(protected(item, key) for item in value)
text = str(value)
if key in {"db_id", "ws_id", "task_id", "from_workstream_id",
"from_workplan_id", "from_task_id", "desired_ws_id",
"desired_task_id", "desired_record_id", "state_hub_workstream_id", "state_hub_task_id"}:
if text in protected_ids:
return True
if key in {"file_path", "wp_file", "md_path", "path"} and text:
candidate = Path(text)
if candidate.is_absolute():
return root is not None and any(candidate.absolute() == root / path for path in paths)
return repo_slug == "railiance-cluster" and candidate.as_posix() in paths
return False
for issue in report.issues:
if issue.fixable and (protected(issue.db_id, "db_id")
or protected(issue.file_path, "file_path")
or protected(issue._fix_context)):
report.add(severity="FAIL", check_id="C-44",
message="Frozen historical repair batch refused; owner review required",
fixable=False)
return True
return False
def fix_repo(
api_base: str,
repo_slug: str,
@ -3624,7 +3688,9 @@ def fix_repo(
repo_path_override,
bootstrap_empty_projection=bootstrap_empty_projection,
)
if any(i.check_id in {"C-00", "C-36"} for i in report.failures):
if _refuse_frozen_fix_batch(report, repo_slug):
return report
if any(i.check_id in {"C-00", "C-36", "C-44"} for i in report.failures):
return report
# RMGR-WP-0002: optional repo-manager reconcile proxy (index rebuild) for pilots