From ecf0d0cbc2ffd72b35c2dda7acb180120b7ca78e Mon Sep 17 00:00:00 2001 From: tegwick Date: Mon, 28 Sep 2026 23:12:50 +0200 Subject: [PATCH] C-20 fixer: skip task-scoped edges when the hub lacks from_task_id Guards a checker newer than the hub it talks to; the primary silently drops unknown fields and would store a workplan-level edge that later duplicates the task-scoped one. Co-Authored-By: Claude Fable 5.1 Assistant: claude-code Assistant-Model: sonnet Assistant-Process: 237582@bnt-lap001 Assistant-Session: f2b3d9f1-8fb9-4b9c-bc2b-837ec5dfc826 --- scripts/consistency_check.py | 34 +++++++++++++++++++++++++++++++++ tests/test_consistency_check.py | 1 + 2 files changed, 35 insertions(+) diff --git a/scripts/consistency_check.py b/scripts/consistency_check.py index fbe0c9c..e755cc5 100644 --- a/scripts/consistency_check.py +++ b/scripts/consistency_check.py @@ -831,6 +831,31 @@ def _api_put(api_base: str, path: str, body: dict) -> Any: return {"_error": str(exc)} +_FROM_TASK_ID_SUPPORT: dict[str, bool] = {} + + +def _hub_accepts_from_task_id(api_base: str) -> bool: + """True when the hub's dependency-create schema declares from_task_id. + + Cached per api_base. An unreadable schema counts as unsupported so the + fixer never writes a task-scoped edge into a hub that would drop the + attribution silently. + """ + if api_base not in _FROM_TASK_ID_SUPPORT: + # Not via _api_get: it appends a trailing slash, which /openapi.json rejects. + try: + r = _api_client(api_base).get("/openapi.json") + spec = r.json() if r.status_code == 200 else {} + except Exception: + spec = {} + schemas = spec.get("components", {}).get("schemas", {}) if isinstance(spec, dict) else {} + _FROM_TASK_ID_SUPPORT[api_base] = any( + "from_task_id" in (schemas.get(name) or {}).get("properties", {}) + for name in ("WorkplanDependencyCreate", "WorkstreamDependencyCreate") + ) + return _FROM_TASK_ID_SUPPORT[api_base] + + def _api_post(api_base: str, path: str, body: dict) -> Any: if not _HAS_HTTPX: return {"_error": "httpx is not installed"} @@ -3659,6 +3684,15 @@ def fix_repo( "relationship_type": ctx["relationship_type"], } if ctx.get("from_task_id"): + # A hub without migration e8f9a0b1c2d3 ignores the field + # and would store a workplan-level edge that later + # duplicates the task-scoped one. Skip until deployed. + if not _hub_accepts_from_task_id(api_base): + report.fixes_applied.append( + f"C-20 SKIP: hub at {api_base} does not accept " + f"from_task_id yet (task '{ctx.get('description', '')}')" + ) + continue body["from_task_id"] = ctx["from_task_id"] body["description"] = ctx.get("description") result = _api_post(api_base, f"/workplans/{from_workstream_id}/dependencies", body) diff --git a/tests/test_consistency_check.py b/tests/test_consistency_check.py index 46f990b..e5e8ba4 100644 --- a/tests/test_consistency_check.py +++ b/tests/test_consistency_check.py @@ -2407,6 +2407,7 @@ class TestTaskWaitQualifiers: "consistency_check._api_patch", lambda _api_base, path, body: patches.append((path, body)) or {"ok": True}, ) + monkeypatch.setattr("consistency_check._hub_accepts_from_task_id", lambda _api_base: True) monkeypatch.setattr("consistency_check._detect_behind_remote", lambda _repo_path: False) monkeypatch.setattr("consistency_check._detect_ahead_of_remote", lambda _repo_path: 0) monkeypatch.setattr("consistency_check._git_commit_writeback", lambda *args, **kwargs: True)