fix(consistency): preserve immutable historical binding rows
Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a1468b-63fa-7710-85dc-8f2e16b3ebfa
This commit is contained in:
parent
086d79a273
commit
a25b62f75a
10 changed files with 586 additions and 42 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue