diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index edf5fb9..608099c 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -280,7 +280,7 @@ | task | STATE-WP-0079-T02 | done | — | workplans/STATE-WP-0079-retirement-strangler.md | | task | STATE-WP-0079-T03 | done | — | workplans/STATE-WP-0079-retirement-strangler.md | | task | STATE-WP-0079-T04 | progress | — | workplans/STATE-WP-0079-retirement-strangler.md | -| task | STATE-WP-0079-T05 | todo | — | workplans/STATE-WP-0079-retirement-strangler.md | +| task | STATE-WP-0079-T05 | progress | — | workplans/STATE-WP-0079-retirement-strangler.md | | task | STATE-WP-0079-T06 | todo | — | workplans/STATE-WP-0079-retirement-strangler.md | | task | STATE-WP-0080-T01 | done | — | workplans/STATE-WP-0080-register-project-flavor-awareness.md | | task | STATE-WP-0080-T02 | done | — | workplans/STATE-WP-0080-register-project-flavor-awareness.md | diff --git a/api/main.py b/api/main.py index 73ae3c9..cdb2f9f 100644 --- a/api/main.py +++ b/api/main.py @@ -24,6 +24,7 @@ from api.routers import execution from api.routers import fabric from api.routers import legacy_meter from api.routers import review_contracts +from api.routers import identifier_migrations class ETagMiddleware(BaseHTTPMiddleware): @@ -136,6 +137,7 @@ app.include_router(execution.router) app.include_router(fabric.router) app.include_router(legacy_meter.router) app.include_router(review_contracts.router) +app.include_router(identifier_migrations.router) app.include_router(state.router) app.include_router(ops_runs.router) app.include_router(policy.router) diff --git a/api/routers/execution.py b/api/routers/execution.py index 9f8adbc..c55a519 100644 --- a/api/routers/execution.py +++ b/api/routers/execution.py @@ -21,9 +21,9 @@ from api.services.execution_queue import ( ACTIVITY_CORE_RESPONSIBILITIES, CONCURRENCY_MODES, EXECUTION_STATES, + EXECUTION_REPLACEMENTS, LAUNCH_MODES, STATE_HUB_RESPONSIBILITIES, - execution_state_for_launch, queue_sort_key, workplan_blockers, ) @@ -43,6 +43,8 @@ async def execution_semantics() -> ExecutionSemantics: concurrency_modes=CONCURRENCY_MODES, state_hub_responsibility=STATE_HUB_RESPONSIBILITIES, activity_core_responsibility=ACTIVITY_CORE_RESPONSIBILITIES, + launch_requests_accepted=False, + replacements=EXECUTION_REPLACEMENTS, ) @@ -161,37 +163,27 @@ async def workplan_stack( @router.post( "/launch-requests", - response_model=LaunchRequestRead, - status_code=status.HTTP_201_CREATED, + status_code=status.HTTP_410_GONE, ) async def create_launch_request( + request: Request, + response: Response, body: LaunchRequestCreate, session: AsyncSession = Depends(get_session), -) -> WorkplanLaunchRequest: - ws = await session.get(Workplan, body.workplan_id) - if ws is None: - raise HTTPException(status_code=404, detail="Workplan not found") - - launch_request = WorkplanLaunchRequest( - workplan_id=ws.id, - requested_by=body.requested_by, - requested_actor=body.requested_actor, - launch_mode=body.launch_mode, - concurrency_mode=body.concurrency_mode, - priority=body.priority or ws.planning_priority, - repo_id=body.repo_id or ws.repo_id, - branch_preference=body.branch_preference, - immediate_pickup=body.immediate_pickup, - notes=body.notes, - request_metadata=body.request_metadata, +) -> None: + del body # Request shape stays documented while the retired route returns 410. + await retire_legacy_route( + session=session, + request=request, + response=response, + interface_key="rest_api:POST /execution/launch-requests", + replacement_ref="repo file queue or activity-core ActivityDefinition + ops_run", + detail=( + "State Hub workplan launch requests are retired: no consumer picks up these rows. " + "Queue development work in the authoritative repository file; use an " + "ActivityDefinition and activity-core ops_run only for recurring or operational fires." + ), ) - ws.launch_mode = body.launch_mode - ws.concurrency_mode = body.concurrency_mode - ws.execution_state = execution_state_for_launch(body.launch_mode, body.immediate_pickup) - session.add(launch_request) - await session.commit() - await session.refresh(launch_request) - return launch_request @router.get("/launch-requests", response_model=list[LaunchRequestRead]) diff --git a/api/routers/identifier_migrations.py b/api/routers/identifier_migrations.py new file mode 100644 index 0000000..25c0aac --- /dev/null +++ b/api/routers/identifier_migrations.py @@ -0,0 +1,44 @@ +from dataclasses import asdict + +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.ext.asyncio import AsyncSession + +from api.database import get_session +from api.schemas.identifier_migration import ( + SealedProjectionRepairReceiptRead, + SealedProjectionRepairSubmit, +) +from api.services.work_record_identifier_migration import ( + IdentifierMigrationError, + repair_absent_prederivation_projection, +) + +router = APIRouter(prefix="/identifier-migrations", tags=["identifier-migrations"]) + + +@router.post( + "/sealed-projection-repairs", + response_model=SealedProjectionRepairReceiptRead, +) +async def repair_sealed_projection( + body: SealedProjectionRepairSubmit, + session: AsyncSession = Depends(get_session), +) -> dict: + """Restore one exact pre-derivation workplan unit into an absent projection.""" + try: + receipt = await repair_absent_prederivation_projection( + session, + body.plan, + body.repo_slug, + body.unit, + expected_plan_sha256=body.expected_plan_sha256, + source_revision=body.source_revision, + source_fingerprint=body.source_fingerprint, + source_clean=body.source_clean, + source_synchronized=body.source_synchronized, + primary_confirmed=body.primary_confirmed, + projection_identity=body.projection_identity, + ) + except IdentifierMigrationError as exc: + raise HTTPException(status_code=409, detail=str(exc)) from exc + return asdict(receipt) diff --git a/api/schemas/execution.py b/api/schemas/execution.py index 3e0df39..68f282f 100644 --- a/api/schemas/execution.py +++ b/api/schemas/execution.py @@ -104,3 +104,5 @@ class ExecutionSemantics(BaseModel): concurrency_modes: dict[str, str] state_hub_responsibility: list[str] activity_core_responsibility: list[str] + launch_requests_accepted: bool = False + replacements: dict[str, str] = Field(default_factory=dict) diff --git a/api/schemas/identifier_migration.py b/api/schemas/identifier_migration.py new file mode 100644 index 0000000..a0556b6 --- /dev/null +++ b/api/schemas/identifier_migration.py @@ -0,0 +1,33 @@ +from typing import Any + +from pydantic import BaseModel, ConfigDict, Field + + +class SealedProjectionRepairSubmit(BaseModel): + plan: dict[str, Any] + repo_slug: str = Field(pattern=r"^[a-z0-9][a-z0-9-]{0,99}$") + unit: dict[str, Any] + expected_plan_sha256: str = Field(pattern=r"^[0-9a-f]{64}$") + source_revision: str = Field(pattern=r"^[0-9a-f]{40}$") + source_fingerprint: str = Field(pattern=r"^[0-9a-f]{64}$") + source_clean: bool + source_synchronized: bool + primary_confirmed: bool + projection_identity: str = Field(min_length=1, max_length=200) + + +class SealedProjectionRepairReceiptRead(BaseModel): + model_config = ConfigDict(populate_by_name=True) + + schema_version: str = Field(alias="schema") + outcome: str + repo_slug: str + repository_id: str + workplan_record_id: str + workplan_old_id: str + task_records: list[tuple[str, str]] + plan_sha256: str + source_revision: str + source_fingerprint: str + projection_identity: str + observed_at: str diff --git a/api/services/execution_queue.py b/api/services/execution_queue.py index 01a5263..afd4dba 100644 --- a/api/services/execution_queue.py +++ b/api/services/execution_queue.py @@ -9,7 +9,7 @@ EXECUTION_STATES = { "manual": "Not queued for autonomous pickup; humans or agents may still work manually.", "queued": "Candidate for ordered pickup when dependencies and concurrency allow it.", "scheduled": "Waiting for an external launch window; State Hub stores the requested time.", - "launching": "A launch request asks for immediate pickup or has been handed off.", + "launching": "Legacy state only; it does not prove that any consumer accepted pickup.", "paused": "Temporarily held outside the pickup stack.", "completed": "Execution intent is closed; lifecycle status remains authoritative.", "cancelled": "Execution intent was cancelled without changing lifecycle status.", @@ -19,7 +19,7 @@ LAUNCH_MODES = { "manual": "Do not request automation; keep intent visible only.", "queued": "Place in the prioritized stack for later pickup.", "scheduled": "Request pickup at or after a selected time.", - "immediate": "Request prompt activity-core or agent pickup.", + "immediate": "Legacy intent only; workplan launch-request pickup is retired.", } CONCURRENCY_MODES = { @@ -30,16 +30,26 @@ CONCURRENCY_MODES = { STATE_HUB_RESPONSIBILITIES = [ "store lifecycle status separately from execution intent", "rank candidate workplans and expose dependency-aware eligibility", - "record launch requests and handoff metadata durably", - "surface manual, queued, scheduled, and immediate intent to operators", + "preserve historical launch-request and execution-intent rows during retirement", + "reject new workplan launch requests because no pickup consumer exists", ] ACTIVITY_CORE_RESPONSIBILITIES = [ "own schedules, wakeups, and recurring automation", - "dispatch coding agents and coordinate parallel execution", - "acknowledge, run, and complete launch requests when available", + "claim and complete ops runs created by ActivityDefinition fires", + "do not consume State Hub workplan launch requests", ] +EXECUTION_REPLACEMENTS = { + "POST /execution/launch-requests": ( + "Queue work in the authoritative repository file; for recurring or operational " + "fires use an ActivityDefinition and activity-core ops_run" + ), + "GET /execution/launch-requests": "GET /ops-runs on activity-core for automation history", + "GET /execution/workplan-stack": "Repo Manager work index or hub-core projection", + "PATCH /execution/workplans/{id}/intent": "Edit the authoritative workplan file", +} + EXECUTION_STATE_RANK = { "launching": 0, "queued": 1, diff --git a/api/services/work_record_identifier_migration.py b/api/services/work_record_identifier_migration.py index 38ba776..0b773ae 100644 --- a/api/services/work_record_identifier_migration.py +++ b/api/services/work_record_identifier_migration.py @@ -10,10 +10,13 @@ from dataclasses import dataclass from datetime import UTC, datetime from typing import Any -from sqlalchemy import select, text +from sqlalchemy import func, select, text from sqlalchemy.ext.asyncio import AsyncSession +from api.models.managed_repo import ManagedRepo +from api.models.task import Task, TaskPriority, TaskStatus from api.models.work_record_identifier_alias import WorkRecordIdentifierAlias +from api.models.workplan import Workplan PLAN_SCHEMA = "repo-manager.identifier-migration-plan.v1" FLEET_NAMESPACE = "helixforge" @@ -34,6 +37,24 @@ class IdentifierMigrationResult: assignments_deferred: int +@dataclass(frozen=True) +class SealedProjectionRepairReceipt: + """Non-secret evidence for one bounded pre-derivation projection repair.""" + + schema: str + outcome: str + repo_slug: str + repository_id: str + workplan_record_id: str + workplan_old_id: str + task_records: tuple[tuple[str, str], ...] + plan_sha256: str + source_revision: str + source_fingerprint: str + projection_identity: str + observed_at: str + + def verify_plan(plan: dict[str, Any]) -> str: """Validate the immutable plan envelope and deterministic UUID mappings.""" if plan.get("schema") != PLAN_SCHEMA: @@ -117,6 +138,328 @@ def _repository_mappings( return replacements, assignments +def _repository_entry(plan: dict[str, Any], repo_slug: str) -> dict[str, Any]: + matches = [item for item in plan.get("repositories", []) if item.get("repo") == repo_slug] + if len(matches) != 1: + raise IdentifierMigrationError( + f"repository {repo_slug!r} must occur exactly once in plan" + ) + return matches[0] + + +def _repair_unit_mappings( + plan: dict[str, Any], repo_slug: str, unit: dict[str, Any] +) -> tuple[dict[str, Any], list[dict[str, Any]]]: + replacements, _assignments = _repository_mappings(plan, repo_slug) + workplan = unit.get("workplan") + tasks = unit.get("tasks") + if not isinstance(workplan, dict) or not isinstance(tasks, list): + raise IdentifierMigrationError("repair unit requires one workplan and a task list") + + workplan_record_id = workplan.get("record_id") + workplan_matches = [ + mapping + for mapping in replacements + if mapping["kind"] == "workplan" and mapping["record_id"] == workplan_record_id + ] + if len(workplan_matches) != 1: + raise IdentifierMigrationError( + "repair workplan must occur exactly once as a replacement in the sealed plan" + ) + workplan_mapping = workplan_matches[0] + raw_workplan_mapping = next( + raw + for raw in _repository_entry(plan, repo_slug).get("mappings", []) + if raw.get("kind") == "workplan" and raw.get("record_id") == workplan_record_id + ) + authoritative_path = raw_workplan_mapping.get("path") + if not isinstance(authoritative_path, str) or not authoritative_path: + raise IdentifierMigrationError("sealed workplan mapping has no authoritative path") + + task_record_ids = [task.get("record_id") for task in tasks if isinstance(task, dict)] + if len(task_record_ids) != len(tasks) or len(set(task_record_ids)) != len(tasks): + raise IdentifierMigrationError("repair task record ids must be explicit and unique") + task_mappings = [ + mapping + for mapping in replacements + if mapping["kind"] == "task" and mapping["record_id"] in task_record_ids + ] + mapped_task_ids = {mapping["record_id"] for mapping in task_mappings} + if mapped_task_ids != set(task_record_ids): + raise IdentifierMigrationError( + "repair tasks do not exactly match replacement mappings in the sealed plan" + ) + + raw_unit_task_ids = { + raw.get("record_id") + for raw in _repository_entry(plan, repo_slug).get("mappings", []) + if raw.get("kind") == "task" + and raw.get("action") == "replace" + and raw.get("path") == authoritative_path + } + if raw_unit_task_ids != set(task_record_ids): + raise IdentifierMigrationError( + "repair must include every replaced task in the sealed workplan unit" + ) + return workplan_mapping, task_mappings + + +def _uuid_field(value: Any, label: str) -> uuid.UUID: + try: + return uuid.UUID(str(value)) + except (TypeError, ValueError) as exc: + raise IdentifierMigrationError(f"invalid UUID for {label}") from exc + + +def _repair_payloads( + unit: dict[str, Any], + workplan_mapping: dict[str, Any], + task_mappings: list[dict[str, Any]], +) -> tuple[dict[str, Any], list[dict[str, Any]]]: + raw_workplan = unit["workplan"] + if _uuid_field(raw_workplan.get("id"), "workplan") != workplan_mapping["old_id"]: + raise IdentifierMigrationError("repair workplan UUID does not match sealed old UUID") + slug = raw_workplan.get("slug") + title = raw_workplan.get("title") + status = raw_workplan.get("status") + if not isinstance(slug, str) or not slug or not isinstance(title, str) or not title: + raise IdentifierMigrationError("repair workplan requires explicit slug and title") + if status not in { + "proposed", "ready", "active", "blocked", "backlog", "finished", "archived" + }: + raise IdentifierMigrationError(f"unsupported repair workplan status {status!r}") + workplan_payload = { + "id": workplan_mapping["old_id"], + "topic_id": _uuid_field(raw_workplan.get("topic_id"), "workplan topic"), + "slug": slug, + "title": title, + "description": raw_workplan.get("description"), + "status": status, + "owner": raw_workplan.get("owner"), + "planning_priority": raw_workplan.get("planning_priority"), + "planning_order": raw_workplan.get("planning_order"), + } + + mappings_by_record = {mapping["record_id"]: mapping for mapping in task_mappings} + task_payloads: list[dict[str, Any]] = [] + for raw_task in unit["tasks"]: + mapping = mappings_by_record[raw_task["record_id"]] + if _uuid_field(raw_task.get("id"), raw_task["record_id"]) != mapping["old_id"]: + raise IdentifierMigrationError( + f"repair task UUID does not match sealed old UUID for {raw_task['record_id']}" + ) + title = raw_task.get("title") + if not isinstance(title, str) or not title: + raise IdentifierMigrationError( + f"repair task {raw_task['record_id']} requires an explicit title" + ) + try: + status_value = TaskStatus(raw_task.get("status", "todo")) + priority_value = TaskPriority(raw_task.get("priority", "medium")) + except ValueError as exc: + raise IdentifierMigrationError( + f"invalid task state for {raw_task['record_id']}" + ) from exc + parent_record_id = raw_task.get("parent_record_id") + if parent_record_id is not None and parent_record_id not in mappings_by_record: + raise IdentifierMigrationError( + f"parent task for {raw_task['record_id']} is outside the sealed repair unit" + ) + task_payloads.append( + { + "record_id": raw_task["record_id"], + "id": mapping["old_id"], + "title": title, + "description": raw_task.get("description"), + "status": status_value, + "priority": priority_value, + "assignee": raw_task.get("assignee"), + "blocking_reason": raw_task.get("blocking_reason"), + "needs_human": bool(raw_task.get("needs_human", False)), + "intervention_note": raw_task.get("intervention_note"), + "parent_task_id": ( + mappings_by_record[parent_record_id]["old_id"] + if parent_record_id is not None + else None + ), + } + ) + if task_payloads[-1]["needs_human"] and not task_payloads[-1]["intervention_note"]: + raise IdentifierMigrationError( + f"repair task {raw_task['record_id']} needs an intervention note" + ) + if ( + task_payloads[-1]["status"] == TaskStatus.wait + and task_payloads[-1]["needs_human"] + and not task_payloads[-1]["blocking_reason"] + ): + raise IdentifierMigrationError( + f"repair task {raw_task['record_id']} needs a blocking reason" + ) + + pending = list(task_payloads) + ordered: list[dict[str, Any]] = [] + emitted: set[uuid.UUID] = set() + while pending: + ready = [ + task + for task in pending + if task["parent_task_id"] is None or task["parent_task_id"] in emitted + ] + if not ready: + raise IdentifierMigrationError("repair task parentage contains a cycle") + for task in ready: + pending.remove(task) + ordered.append(task) + emitted.add(task["id"]) + return workplan_payload, ordered + + +def _same_workplan(row: Workplan, expected: dict[str, Any], repo_id: uuid.UUID) -> bool: + fields = ( + "id", "topic_id", "slug", "title", "description", "status", "owner", + "planning_priority", "planning_order", + ) + return row.repo_id == repo_id and all( + getattr(row, field) == expected[field] for field in fields + ) + + +def _same_task(row: Task, expected: dict[str, Any], workplan_id: uuid.UUID) -> bool: + fields = ( + "id", "title", "description", "status", "priority", "assignee", + "blocking_reason", "needs_human", "intervention_note", "parent_task_id", + ) + return row.workplan_id == workplan_id and all( + getattr(row, field) == expected[field] for field in fields + ) + + +async def repair_absent_prederivation_projection( + session: AsyncSession, + plan: dict[str, Any], + repo_slug: str, + unit: dict[str, Any], + *, + expected_plan_sha256: str, + source_revision: str, + source_fingerprint: str, + source_clean: bool, + source_synchronized: bool, + primary_confirmed: bool, + projection_identity: str, + observed_at: datetime | None = None, +) -> SealedProjectionRepairReceipt: + """Restore one exact random-ID workplan unit into an absent projection. + + This is intentionally separate from ordinary consistency repair. The caller + must independently prove Git cleanliness/synchronization and provide the + authoritative unit parsed from the pinned source revision. + """ + plan_sha256 = verify_plan(plan) + if plan_sha256 != expected_plan_sha256: + raise IdentifierMigrationError("explicit plan SHA-256 does not match sealed plan") + if not primary_confirmed: + raise IdentifierMigrationError("projection repair requires explicit primary confirmation") + if not source_clean or not source_synchronized: + raise IdentifierMigrationError("projection repair requires a clean synchronized source") + if not re.fullmatch(r"[0-9a-f]{40}", source_revision): + raise IdentifierMigrationError("source revision must be a full Git SHA") + if not re.fullmatch(r"[0-9a-f]{64}", source_fingerprint): + raise IdentifierMigrationError("source fingerprint must be SHA-256") + if not projection_identity.strip(): + raise IdentifierMigrationError("projection identity is required") + + repository_entry = _repository_entry(plan, repo_slug) + if source_revision != repository_entry.get("planned_head_sha"): + raise IdentifierMigrationError("source revision drifted from sealed plan") + if source_fingerprint != repository_entry.get("source_fingerprint"): + raise IdentifierMigrationError("source fingerprint drifted from sealed plan") + workplan_mapping, task_mappings = _repair_unit_mappings(plan, repo_slug, unit) + workplan_payload, task_payloads = _repair_payloads( + unit, workplan_mapping, task_mappings + ) + if session.in_transaction(): + raise IdentifierMigrationError("projection repair requires a fresh database session") + + outcome = "repaired" + async with session.begin(): + repo = await session.scalar( + select(ManagedRepo).where(ManagedRepo.slug == repo_slug).with_for_update() + ) + if repo is None: + raise IdentifierMigrationError(f"repository projection is absent: {repo_slug}") + if repo.topic_id != workplan_payload["topic_id"]: + raise IdentifierMigrationError("repair topic does not match repository projection") + + old_ids = [workplan_mapping["old_id"], *(m["old_id"] for m in task_mappings)] + new_workplan = await session.get(Workplan, workplan_mapping["new_id"]) + new_tasks = [await session.get(Task, mapping["new_id"]) for mapping in task_mappings] + if new_workplan is not None or any(task is not None for task in new_tasks): + raise IdentifierMigrationError("derived target presence blocks projection repair") + alias_count = await session.scalar( + select(func.count()).select_from(WorkRecordIdentifierAlias).where( + WorkRecordIdentifierAlias.old_id.in_(old_ids) + ) + ) + if alias_count: + raise IdentifierMigrationError("durable alias presence blocks projection repair") + + old_workplan = await session.get(Workplan, workplan_mapping["old_id"]) + old_tasks = [await session.get(Task, mapping["old_id"]) for mapping in task_mappings] + present_count = int(old_workplan is not None) + sum(task is not None for task in old_tasks) + expected_count = 1 + len(task_mappings) + if present_count not in {0, expected_count}: + raise IdentifierMigrationError("partial old projection presence blocks repair") + + if present_count == expected_count: + if not _same_workplan(old_workplan, workplan_payload, repo.id): + raise IdentifierMigrationError( + "existing old workplan differs from sealed repair unit" + ) + expected_tasks = {task["id"]: task for task in task_payloads} + if any( + not _same_task(task, expected_tasks[task.id], workplan_mapping["old_id"]) + for task in old_tasks + ): + raise IdentifierMigrationError("existing old tasks differ from sealed repair unit") + outcome = "verified_noop" + else: + slug_conflict = await session.scalar( + select(Workplan.id).where(Workplan.slug == workplan_payload["slug"]) + ) + if slug_conflict is not None: + raise IdentifierMigrationError("workplan slug is already projected") + session.add( + Workplan(repo_id=repo.id, **workplan_payload) + ) + await session.flush() + for task_payload in task_payloads: + values = {key: value for key, value in task_payload.items() if key != "record_id"} + session.add(Task(workplan_id=workplan_mapping["old_id"], **values)) + await session.flush() + + timestamp = observed_at or datetime.now(UTC) + if timestamp.tzinfo is None: + raise IdentifierMigrationError("receipt observation time must be timezone-aware") + return SealedProjectionRepairReceipt( + schema="state-hub.sealed-prederivation-projection-repair-receipt.v1", + outcome=outcome, + repo_slug=repo_slug, + repository_id=str(repo.id), + workplan_record_id=workplan_mapping["record_id"], + workplan_old_id=str(workplan_mapping["old_id"]), + task_records=tuple( + (mapping["record_id"], str(mapping["old_id"])) for mapping in task_mappings + ), + plan_sha256=plan_sha256, + source_revision=source_revision, + source_fingerprint=source_fingerprint, + projection_identity=projection_identity, + observed_at=timestamp.astimezone(UTC).isoformat().replace("+00:00", "Z"), + ) + + async def _assert_projection_preconditions( session: AsyncSession, repo_slug: str, diff --git a/docs/evidence/legacy-meter-weekly-review-20260822.json b/docs/evidence/legacy-meter-weekly-review-20260822.json new file mode 100644 index 0000000..c374f7b --- /dev/null +++ b/docs/evidence/legacy-meter-weekly-review-20260822.json @@ -0,0 +1,929 @@ +{ + "captured_at": "2026-08-22T14:00:05.083125+00:00", + "api_base": "http://127.0.0.1:8000", + "workplan": "STATE-WP-0070", + "retired_interfaces": [], + "weekly_review": { + "generated_at": "2026-08-22T14:00:05.076969Z", + "window_start": "2026-08-15T14:00:04.994574Z", + "window_end": "2026-08-22T14:00:04.994574Z", + "cadence": "weekly", + "activity_core_handoff": { + "activity_id": "statehub-legacy-interface-review", + "cadence": "weekly", + "source_endpoint": "/legacy-meter/weekly-review", + "state_owner": "state-hub", + "scheduler_owner": "activity-core" + }, + "interfaces": [ + { + "interface": { + "id": "8051fb40-66c9-4dc4-a413-13e8573e04a3", + "interface_key": "event_subject:org.statehub.workstream.completed", + "interface_kind": "event_subject", + "legacy_since": "2026-06-04T06:09:42.198193Z", + "replacement_ref": "org.statehub.workplan.completed", + "owner_component": "state-hub.events", + "status": "retired", + "replacement_verified": true, + "manual_hold": false, + "hold_reason": null, + "notes": "Retired by STATE-WP-0069 closeout", + "retired_at": "2026-07-08T21:38:52.773601Z", + "created_at": "2026-06-04T06:09:42.198193Z", + "updated_at": "2026-07-08T21:38:52.761243Z" + }, + "all_time": { + "calls": 255, + "tenant_count": 1, + "user_count": 1, + "component_count": 1, + "tenants": { + "unknown": 255 + }, + "users": { + "unknown": 255 + }, + "components": { + "state-hub.events": 255 + } + }, + "window": { + "calls": 0, + "tenant_count": 0, + "user_count": 0, + "component_count": 0, + "tenants": {}, + "users": {}, + "components": {} + }, + "last_seen_at": "2026-07-08T20:49:12.839383Z", + "retirement_candidate": false, + "retirement_reason": "already retired" + }, + { + "interface": { + "id": "63ed90e3-42b4-431a-94db-e1254a52f9e2", + "interface_key": "mcp:create_workstream", + "interface_kind": "mcp_tool", + "legacy_since": "2026-07-10T13:19:44.205900Z", + "replacement_ref": "create_workplan", + "owner_component": "state-hub.mcp", + "status": "retired", + "replacement_verified": true, + "manual_hold": false, + "hold_reason": null, + "notes": "Retired by STATE-WP-0079-T05 (cutover slice E2); evidence: 7d review window + volume-scaled quiet ladder, capture 2026-08-20", + "retired_at": "2026-08-20T05:27:19.851030Z", + "created_at": "2026-07-10T13:19:44.205900Z", + "updated_at": "2026-08-20T05:27:19.844807Z" + }, + "all_time": { + "calls": 2, + "tenant_count": 1, + "user_count": 1, + "component_count": 1, + "tenants": { + "unknown": 2 + }, + "users": { + "unknown": 2 + }, + "components": { + "state-hub.mcp": 2 + } + }, + "window": { + "calls": 0, + "tenant_count": 0, + "user_count": 0, + "component_count": 0, + "tenants": {}, + "users": {}, + "components": {} + }, + "last_seen_at": "2026-07-10T13:20:43.378546Z", + "retirement_candidate": false, + "retirement_reason": "already retired" + }, + { + "interface": { + "id": "b6a5fe44-fb85-4ef7-a273-78d09da777af", + "interface_key": "mcp:list_workstreams", + "interface_kind": "mcp_tool", + "legacy_since": "2026-07-10T13:19:26.075491Z", + "replacement_ref": "list_workplans", + "owner_component": "state-hub.mcp", + "status": "retired", + "replacement_verified": true, + "manual_hold": false, + "hold_reason": null, + "notes": "Retired by STATE-WP-0079-T05 (cutover slice E2); evidence: 7d review window + volume-scaled quiet ladder, capture 2026-08-20", + "retired_at": "2026-08-20T05:27:19.883196Z", + "created_at": "2026-07-10T13:19:26.075491Z", + "updated_at": "2026-08-20T05:27:19.876635Z" + }, + "all_time": { + "calls": 2, + "tenant_count": 1, + "user_count": 1, + "component_count": 1, + "tenants": { + "unknown": 2 + }, + "users": { + "unknown": 2 + }, + "components": { + "state-hub.mcp": 2 + } + }, + "window": { + "calls": 0, + "tenant_count": 0, + "user_count": 0, + "component_count": 0, + "tenants": {}, + "users": {}, + "components": {} + }, + "last_seen_at": "2026-07-10T13:20:42.374940Z", + "retirement_candidate": false, + "retirement_reason": "already retired" + }, + { + "interface": { + "id": "53c3372b-307d-4d6a-8ea4-bc36cacdcaf2", + "interface_key": "mcp:update_workstream", + "interface_kind": "mcp_tool", + "legacy_since": "2026-07-10T13:19:44.255216Z", + "replacement_ref": "update_workplan", + "owner_component": "state-hub.mcp", + "status": "retired", + "replacement_verified": true, + "manual_hold": false, + "hold_reason": null, + "notes": "Retired by STATE-WP-0079-T05 (cutover slice E2); evidence: 7d review window + volume-scaled quiet ladder, capture 2026-08-20", + "retired_at": "2026-08-20T05:27:19.907081Z", + "created_at": "2026-07-10T13:19:44.255216Z", + "updated_at": "2026-08-20T05:27:19.902177Z" + }, + "all_time": { + "calls": 0, + "tenant_count": 0, + "user_count": 0, + "component_count": 0, + "tenants": {}, + "users": {}, + "components": {} + }, + "window": { + "calls": 0, + "tenant_count": 0, + "user_count": 0, + "component_count": 0, + "tenants": {}, + "users": {}, + "components": {} + }, + "last_seen_at": null, + "retirement_candidate": false, + "retirement_reason": "already retired" + }, + { + "interface": { + "id": "bed4d08a-3471-48f5-9f0e-f9d550383a26", + "interface_key": "mcp:update_workstream_status", + "interface_kind": "mcp_tool", + "legacy_since": "2026-07-10T13:19:44.303612Z", + "replacement_ref": "update_workplan_status", + "owner_component": "state-hub.mcp", + "status": "retired", + "replacement_verified": true, + "manual_hold": false, + "hold_reason": null, + "notes": "Retired by STATE-WP-0079-T05 (cutover slice E2); evidence: 7d review window + volume-scaled quiet ladder, capture 2026-08-20", + "retired_at": "2026-08-20T05:27:19.928988Z", + "created_at": "2026-07-10T13:19:44.303612Z", + "updated_at": "2026-08-20T05:27:19.925652Z" + }, + "all_time": { + "calls": 0, + "tenant_count": 0, + "user_count": 0, + "component_count": 0, + "tenants": {}, + "users": {}, + "components": {} + }, + "window": { + "calls": 0, + "tenant_count": 0, + "user_count": 0, + "component_count": 0, + "tenants": {}, + "users": {}, + "components": {} + }, + "last_seen_at": null, + "retirement_candidate": false, + "retirement_reason": "already retired" + }, + { + "interface": { + "id": "46900dc2-0893-405b-ba59-80f31fb6ebb1", + "interface_key": "state://workstreams/{topic_slug}", + "interface_kind": "mcp_tool", + "legacy_since": "2026-07-10T13:19:44.344493Z", + "replacement_ref": "state://workplans/{topic_slug}", + "owner_component": "state-hub.mcp", + "status": "retired", + "replacement_verified": true, + "manual_hold": false, + "hold_reason": null, + "notes": "Retired by STATE-WP-0079-T05 (cutover slice E2); evidence: 7d review window + volume-scaled quiet ladder, capture 2026-08-20", + "retired_at": "2026-08-20T05:27:19.948467Z", + "created_at": "2026-07-10T13:19:44.344493Z", + "updated_at": "2026-08-20T05:27:19.944874Z" + }, + "all_time": { + "calls": 1, + "tenant_count": 1, + "user_count": 1, + "component_count": 1, + "tenants": { + "unknown": 1 + }, + "users": { + "unknown": 1 + }, + "components": { + "state-hub.mcp": 1 + } + }, + "window": { + "calls": 0, + "tenant_count": 0, + "user_count": 0, + "component_count": 0, + "tenants": {}, + "users": {}, + "components": {} + }, + "last_seen_at": "2026-07-10T13:20:34.491811Z", + "retirement_candidate": false, + "retirement_reason": "already retired" + }, + { + "interface": { + "id": "cd9342fd-27e7-4a46-a1be-d41499e106a3", + "interface_key": "rest_api:DELETE /workstreams/{workstream_id}", + "interface_kind": "rest_api", + "legacy_since": "2026-06-06T17:28:13.052813Z", + "replacement_ref": "/workplans/{workplan_id}", + "owner_component": "state-hub.api", + "status": "retired", + "replacement_verified": true, + "manual_hold": false, + "hold_reason": null, + "notes": "Retired by STATE-WP-0079-T05 (cutover slice E2); evidence: 7d review window + volume-scaled quiet ladder, capture 2026-08-20", + "retired_at": "2026-08-20T05:27:19.972086Z", + "created_at": "2026-06-06T17:28:13.052813Z", + "updated_at": "2026-08-20T05:27:19.966289Z" + }, + "all_time": { + "calls": 1, + "tenant_count": 1, + "user_count": 1, + "component_count": 1, + "tenants": { + "unknown": 1 + }, + "users": { + "unknown": 1 + }, + "components": { + "unknown": 1 + } + }, + "window": { + "calls": 0, + "tenant_count": 0, + "user_count": 0, + "component_count": 0, + "tenants": {}, + "users": {}, + "components": {} + }, + "last_seen_at": "2026-06-06T17:28:13.047529Z", + "retirement_candidate": false, + "retirement_reason": "already retired" + }, + { + "interface": { + "id": "a4e0948c-cd4a-461e-8ae2-7fd7b51d04a7", + "interface_key": "rest_api:GET /decisions/?workstream_id", + "interface_kind": "rest_api", + "legacy_since": "2026-07-09T23:07:53.093223Z", + "replacement_ref": "/decisions/?workplan_id=", + "owner_component": "state-hub.api", + "status": "retired", + "replacement_verified": true, + "manual_hold": false, + "hold_reason": null, + "notes": "Retired by STATE-WP-0079-T05 (cutover slice E2); evidence: 7d review window + volume-scaled quiet ladder, capture 2026-08-20", + "retired_at": "2026-08-20T05:27:19.996130Z", + "created_at": "2026-07-09T23:07:53.093223Z", + "updated_at": "2026-08-20T05:27:19.991877Z" + }, + "all_time": { + "calls": 1, + "tenant_count": 1, + "user_count": 1, + "component_count": 1, + "tenants": { + "unknown": 1 + }, + "users": { + "unknown": 1 + }, + "components": { + "unknown": 1 + } + }, + "window": { + "calls": 0, + "tenant_count": 0, + "user_count": 0, + "component_count": 0, + "tenants": {}, + "users": {}, + "components": {} + }, + "last_seen_at": "2026-07-09T23:07:53.083299Z", + "retirement_candidate": false, + "retirement_reason": "already retired" + }, + { + "interface": { + "id": "70a9346e-e0ee-42ac-874f-f160580b54dd", + "interface_key": "rest_api:GET /progress/?workstream_id", + "interface_kind": "rest_api", + "legacy_since": "2026-07-13T22:04:03.911449Z", + "replacement_ref": "/progress/?workplan_id=", + "owner_component": "state-hub.api", + "status": "retired", + "replacement_verified": true, + "manual_hold": false, + "hold_reason": null, + "notes": "Retired by STATE-WP-0079-T05 (cutover slice E2); evidence: 7d review window + volume-scaled quiet ladder, capture 2026-08-20", + "retired_at": "2026-08-20T05:27:20.017065Z", + "created_at": "2026-07-13T22:04:03.911449Z", + "updated_at": "2026-08-20T05:27:20.013500Z" + }, + "all_time": { + "calls": 2, + "tenant_count": 1, + "user_count": 1, + "component_count": 1, + "tenants": { + "unknown": 2 + }, + "users": { + "unknown": 2 + }, + "components": { + "unknown": 2 + } + }, + "window": { + "calls": 0, + "tenant_count": 0, + "user_count": 0, + "component_count": 0, + "tenants": {}, + "users": {}, + "components": {} + }, + "last_seen_at": "2026-08-12T08:55:33.912995Z", + "retirement_candidate": false, + "retirement_reason": "already retired" + }, + { + "interface": { + "id": "be3cdcf8-250b-4bf0-801f-02ffd5f57e00", + "interface_key": "rest_api:GET /tasks/?workstream_id", + "interface_kind": "rest_api", + "legacy_since": "2026-07-08T19:32:21.628513Z", + "replacement_ref": "/tasks/?workplan_id=", + "owner_component": "state-hub.api", + "status": "legacy", + "replacement_verified": true, + "manual_hold": false, + "hold_reason": null, + "notes": null, + "retired_at": null, + "created_at": "2026-07-08T19:32:21.628513Z", + "updated_at": "2026-07-08T19:32:21.628513Z" + }, + "all_time": { + "calls": 596, + "tenant_count": 1, + "user_count": 1, + "component_count": 1, + "tenants": { + "unknown": 596 + }, + "users": { + "unknown": 596 + }, + "components": { + "unknown": 596 + } + }, + "window": { + "calls": 6, + "tenant_count": 1, + "user_count": 1, + "component_count": 1, + "tenants": { + "unknown": 6 + }, + "users": { + "unknown": 6 + }, + "components": { + "unknown": 6 + } + }, + "last_seen_at": "2026-08-20T20:57:26.985884Z", + "retirement_candidate": false, + "retirement_reason": "6 call(s) in review window" + }, + { + "interface": { + "id": "3e4a3d0b-08fa-45c3-91e2-4f514a914b97", + "interface_key": "rest_api:GET /workstreams/", + "interface_kind": "rest_api", + "legacy_since": "2026-06-04T00:26:14.533764Z", + "replacement_ref": "/workplans/", + "owner_component": "state-hub.api", + "status": "legacy", + "replacement_verified": true, + "manual_hold": false, + "hold_reason": null, + "notes": null, + "retired_at": null, + "created_at": "2026-06-04T00:26:14.533764Z", + "updated_at": "2026-06-04T00:26:14.533764Z" + }, + "all_time": { + "calls": 144087, + "tenant_count": 1, + "user_count": 1, + "component_count": 1, + "tenants": { + "unknown": 144087 + }, + "users": { + "unknown": 144087 + }, + "components": { + "unknown": 144087 + } + }, + "window": { + "calls": 1, + "tenant_count": 1, + "user_count": 1, + "component_count": 1, + "tenants": { + "unknown": 1 + }, + "users": { + "unknown": 1 + }, + "components": { + "unknown": 1 + } + }, + "last_seen_at": "2026-08-21T22:29:25.068158Z", + "retirement_candidate": false, + "retirement_reason": "1 call(s) in review window" + }, + { + "interface": { + "id": "e4c140ec-7479-46f9-b185-4b533f19d639", + "interface_key": "rest_api:GET /workstreams/workplan-index", + "interface_kind": "rest_api", + "legacy_since": "2026-06-04T05:20:58.321869Z", + "replacement_ref": "/workplans/index", + "owner_component": "state-hub.api", + "status": "retired", + "replacement_verified": true, + "manual_hold": false, + "hold_reason": null, + "notes": "Retired by STATE-WP-0079-T05 (cutover slice E2); evidence: 7d review window + volume-scaled quiet ladder, capture 2026-08-20", + "retired_at": "2026-08-20T05:27:20.041850Z", + "created_at": "2026-06-04T05:20:58.321869Z", + "updated_at": "2026-08-20T05:27:20.035160Z" + }, + "all_time": { + "calls": 30, + "tenant_count": 1, + "user_count": 1, + "component_count": 1, + "tenants": { + "unknown": 30 + }, + "users": { + "unknown": 30 + }, + "components": { + "unknown": 30 + } + }, + "window": { + "calls": 0, + "tenant_count": 0, + "user_count": 0, + "component_count": 0, + "tenants": {}, + "users": {}, + "components": {} + }, + "last_seen_at": "2026-07-03T05:20:34.534439Z", + "retirement_candidate": false, + "retirement_reason": "already retired" + }, + { + "interface": { + "id": "3a00b19e-e7db-4403-98aa-a77c9ab61ecc", + "interface_key": "rest_api:GET /workstreams/{workstream_id}", + "interface_kind": "rest_api", + "legacy_since": "2026-06-04T00:25:59.014966Z", + "replacement_ref": "/workplans/{workplan_id}", + "owner_component": "state-hub.api", + "status": "legacy", + "replacement_verified": true, + "manual_hold": false, + "hold_reason": null, + "notes": null, + "retired_at": null, + "created_at": "2026-06-04T00:25:59.014966Z", + "updated_at": "2026-06-04T00:25:59.014966Z" + }, + "all_time": { + "calls": 511407, + "tenant_count": 1, + "user_count": 1, + "component_count": 1, + "tenants": { + "unknown": 511407 + }, + "users": { + "unknown": 511407 + }, + "components": { + "unknown": 511407 + } + }, + "window": { + "calls": 1, + "tenant_count": 1, + "user_count": 1, + "component_count": 1, + "tenants": { + "unknown": 1 + }, + "users": { + "unknown": 1 + }, + "components": { + "unknown": 1 + } + }, + "last_seen_at": "2026-08-20T23:32:31.368781Z", + "retirement_candidate": false, + "retirement_reason": "1 call(s) in review window" + }, + { + "interface": { + "id": "69e1a255-a8b9-4101-8a68-b1bed23abfda", + "interface_key": "rest_api:GET /workstreams/{workstream_id}/dependencies/", + "interface_kind": "rest_api", + "legacy_since": "2026-06-04T00:25:59.291135Z", + "replacement_ref": "/workplans/{workplan_id}/dependencies/", + "owner_component": "state-hub.api", + "status": "legacy", + "replacement_verified": true, + "manual_hold": false, + "hold_reason": null, + "notes": null, + "retired_at": null, + "created_at": "2026-06-04T00:25:59.291135Z", + "updated_at": "2026-06-04T00:25:59.291135Z" + }, + "all_time": { + "calls": 255865, + "tenant_count": 1, + "user_count": 1, + "component_count": 1, + "tenants": { + "unknown": 255865 + }, + "users": { + "unknown": 255865 + }, + "components": { + "unknown": 255865 + } + }, + "window": { + "calls": 0, + "tenant_count": 0, + "user_count": 0, + "component_count": 0, + "tenants": {}, + "users": {}, + "components": {} + }, + "last_seen_at": "2026-07-08T21:14:12.747005Z", + "retirement_candidate": false, + "retirement_reason": "quiet 44d of 60d required for 255865 all-time call(s)" + }, + { + "interface": { + "id": "b1aae931-b51f-4a85-a723-865dd65f6132", + "interface_key": "rest_api:PATCH /workstreams/{workstream_id}", + "interface_kind": "rest_api", + "legacy_since": "2026-06-04T06:09:41.901035Z", + "replacement_ref": "/workplans/{workplan_id}", + "owner_component": "state-hub.api", + "status": "retired", + "replacement_verified": true, + "manual_hold": false, + "hold_reason": null, + "notes": "Retired by STATE-WP-0079-T05 (cutover slice E2); evidence: 7d review window + volume-scaled quiet ladder, capture 2026-08-20", + "retired_at": "2026-08-20T05:27:20.069758Z", + "created_at": "2026-06-04T06:09:41.901035Z", + "updated_at": "2026-08-20T05:27:20.062648Z" + }, + "all_time": { + "calls": 571, + "tenant_count": 1, + "user_count": 1, + "component_count": 1, + "tenants": { + "unknown": 571 + }, + "users": { + "unknown": 571 + }, + "components": { + "unknown": 571 + } + }, + "window": { + "calls": 0, + "tenant_count": 0, + "user_count": 0, + "component_count": 0, + "tenants": {}, + "users": {}, + "components": {} + }, + "last_seen_at": "2026-07-08T20:49:12.335710Z", + "retirement_candidate": false, + "retirement_reason": "already retired" + }, + { + "interface": { + "id": "4aea9591-970b-4918-ac4a-4da95a0a1a8c", + "interface_key": "rest_api:POST /decisions/ workstream_id", + "interface_kind": "rest_api", + "legacy_since": "2026-07-09T22:56:35.473130Z", + "replacement_ref": "POST /decisions/ with workplan_id", + "owner_component": "state-hub.api", + "status": "retired", + "replacement_verified": true, + "manual_hold": false, + "hold_reason": null, + "notes": "Retired by STATE-WP-0079-T05 (cutover slice E2); evidence: 7d review window + volume-scaled quiet ladder, capture 2026-08-20", + "retired_at": "2026-08-20T05:27:20.093372Z", + "created_at": "2026-07-09T22:56:35.473130Z", + "updated_at": "2026-08-20T05:27:20.090180Z" + }, + "all_time": { + "calls": 3, + "tenant_count": 1, + "user_count": 1, + "component_count": 1, + "tenants": { + "unknown": 3 + }, + "users": { + "unknown": 3 + }, + "components": { + "unknown": 3 + } + }, + "window": { + "calls": 0, + "tenant_count": 0, + "user_count": 0, + "component_count": 0, + "tenants": {}, + "users": {}, + "components": {} + }, + "last_seen_at": "2026-07-09T22:57:05.912456Z", + "retirement_candidate": false, + "retirement_reason": "already retired" + }, + { + "interface": { + "id": "4a70583c-1e41-4ec1-99a9-728856a7d79c", + "interface_key": "rest_api:POST /progress/ workstream_id", + "interface_kind": "rest_api", + "legacy_since": "2026-07-08T23:35:09.148333Z", + "replacement_ref": "POST /progress/ with workplan_id", + "owner_component": "state-hub.api", + "status": "retired", + "replacement_verified": true, + "manual_hold": false, + "hold_reason": null, + "notes": "Retired by STATE-WP-0079-T05 (cutover slice E2); evidence: 7d review window + volume-scaled quiet ladder, capture 2026-08-20", + "retired_at": "2026-08-20T05:27:20.114395Z", + "created_at": "2026-07-08T23:35:09.148333Z", + "updated_at": "2026-08-20T05:27:20.110090Z" + }, + "all_time": { + "calls": 14, + "tenant_count": 1, + "user_count": 1, + "component_count": 1, + "tenants": { + "unknown": 14 + }, + "users": { + "unknown": 14 + }, + "components": { + "unknown": 14 + } + }, + "window": { + "calls": 0, + "tenant_count": 0, + "user_count": 0, + "component_count": 0, + "tenants": {}, + "users": {}, + "components": {} + }, + "last_seen_at": "2026-07-10T08:34:42.959724Z", + "retirement_candidate": false, + "retirement_reason": "already retired" + }, + { + "interface": { + "id": "6df4e443-7bf1-461a-923b-a6eb52fe7b07", + "interface_key": "rest_api:POST /tasks/ workstream_id", + "interface_kind": "rest_api", + "legacy_since": "2026-07-08T22:38:29.405720Z", + "replacement_ref": "POST /tasks/ with workplan_id", + "owner_component": "state-hub.api", + "status": "retired", + "replacement_verified": true, + "manual_hold": false, + "hold_reason": null, + "notes": "Retired by STATE-WP-0079-T05 (cutover slice E2); evidence: 7d review window + volume-scaled quiet ladder, capture 2026-08-20", + "retired_at": "2026-08-20T05:27:20.134796Z", + "created_at": "2026-07-08T22:38:29.405720Z", + "updated_at": "2026-08-20T05:27:20.131430Z" + }, + "all_time": { + "calls": 5, + "tenant_count": 1, + "user_count": 1, + "component_count": 1, + "tenants": { + "unknown": 5 + }, + "users": { + "unknown": 5 + }, + "components": { + "state-hub.fix-consistency": 5 + } + }, + "window": { + "calls": 0, + "tenant_count": 0, + "user_count": 0, + "component_count": 0, + "tenants": {}, + "users": {}, + "components": {} + }, + "last_seen_at": "2026-07-08T22:38:29.976918Z", + "retirement_candidate": false, + "retirement_reason": "already retired" + }, + { + "interface": { + "id": "ed6451c9-d2cc-4486-9afe-d3852782cabc", + "interface_key": "rest_api:POST /workstreams/", + "interface_kind": "rest_api", + "legacy_since": "2026-06-04T07:40:24.168535Z", + "replacement_ref": "/workplans/", + "owner_component": "state-hub.api", + "status": "retired", + "replacement_verified": true, + "manual_hold": false, + "hold_reason": null, + "notes": "Retired by STATE-WP-0079-T05 (cutover slice E2); evidence: 7d review window + volume-scaled quiet ladder, capture 2026-08-20", + "retired_at": "2026-08-20T05:27:20.157133Z", + "created_at": "2026-06-04T07:40:24.168535Z", + "updated_at": "2026-08-20T05:27:20.150639Z" + }, + "all_time": { + "calls": 824, + "tenant_count": 1, + "user_count": 1, + "component_count": 1, + "tenants": { + "unknown": 824 + }, + "users": { + "unknown": 824 + }, + "components": { + "unknown": 824 + } + }, + "window": { + "calls": 0, + "tenant_count": 0, + "user_count": 0, + "component_count": 0, + "tenants": {}, + "users": {}, + "components": {} + }, + "last_seen_at": "2026-07-08T20:38:05.903144Z", + "retirement_candidate": false, + "retirement_reason": "already retired" + }, + { + "interface": { + "id": "281fd706-b192-4194-8055-2d8733c115f9", + "interface_key": "rest_api:POST /workstreams/{workstream_id}/dependencies/", + "interface_kind": "rest_api", + "legacy_since": "2026-06-04T22:57:37.149207Z", + "replacement_ref": "/workplans/{workplan_id}/dependencies/", + "owner_component": "state-hub.api", + "status": "retired", + "replacement_verified": true, + "manual_hold": false, + "hold_reason": null, + "notes": "Retired by STATE-WP-0079-T05 (cutover slice E2); evidence: 7d review window + volume-scaled quiet ladder, capture 2026-08-20", + "retired_at": "2026-08-20T05:27:20.186201Z", + "created_at": "2026-06-04T22:57:37.149207Z", + "updated_at": "2026-08-20T05:27:20.181406Z" + }, + "all_time": { + "calls": 4971, + "tenant_count": 1, + "user_count": 1, + "component_count": 1, + "tenants": { + "unknown": 4971 + }, + "users": { + "unknown": 4971 + }, + "components": { + "unknown": 4971 + } + }, + "window": { + "calls": 0, + "tenant_count": 0, + "user_count": 0, + "component_count": 0, + "tenants": {}, + "users": {}, + "components": {} + }, + "last_seen_at": "2026-07-01T22:09:24.229011Z", + "retirement_candidate": false, + "retirement_reason": "already retired" + } + ], + "retirement_candidates": [] + }, + "days": 7 +} diff --git a/docs/sealed-prederivation-projection-repair.md b/docs/sealed-prederivation-projection-repair.md new file mode 100644 index 0000000..85a140a --- /dev/null +++ b/docs/sealed-prederivation-projection-repair.md @@ -0,0 +1,45 @@ +# Sealed pre-derivation projection repair + +This temporary compatibility surface supports the deterministic-identifier +cutover in `STATE-WP-0079-T04`. It restores one exact random-UUID workplan unit +into a partial State Hub projection so the existing transactional migration can +replace the old UUIDs with their deterministic UUIDv5 targets. + +Ordinary `statehub fix-consistency` behavior is unchanged: a missing random UUID +remains a non-fixable C-03 stale reference. + +## Endpoint + +`POST /identifier-migrations/sealed-projection-repairs` + +The request must contain: + +- the complete sealed Repo Manager migration plan; +- the exact plan SHA-256, repository slug, pinned 40-character Git revision, + and source fingerprint recorded in that plan; +- `source_clean`, `source_synchronized`, and `primary_confirmed` set to true; +- one explicit authoritative workplan projection and every `replace` task + mapped to the same sealed workplan file; and +- a non-secret projection identity for the receipt. + +The workplan projection supplies its canonical `record_id`, exact old UUID, +topic UUID, projection slug, title, status, and optional projection fields. +Each task supplies its canonical `record_id`, exact old UUID, title, state, and +optional projection fields. Parentage uses `parent_record_id`, never a path- or +position-derived identity. + +The server rejects source drift, plan-seal drift, repository/topic mismatch, +partial old-row presence, any derived-target presence, alias conflicts, slug +conflicts, incomplete task units, and non-identical retries. The workplan and +tasks are created in one database transaction. An exact retry returns a +`verified_noop` receipt. + +The receipt contains no source content or secrets: repository/projection +identity, canonical IDs and old UUIDs, plan/source seals, outcome, and a UTC +observation timestamp. + +## Removal condition + +Remove this endpoint with the identifier-migration executor after every sealed +pre-derivation unit has either completed deterministic cutover or been rejected +and residual-owned. It is not a general projection-import API. diff --git a/tests/test_routers_core.py b/tests/test_routers_core.py index a60a7e6..bd30076 100644 --- a/tests/test_routers_core.py +++ b/tests/test_routers_core.py @@ -1006,7 +1006,7 @@ class TestReconciliationEndpoints: class TestExecutionQueueEndpoints: - async def test_execution_semantics_separates_state_hub_and_activity_core(self, client): + async def test_execution_semantics_retires_workplan_launch_pickup(self, client): r = await client.get("/execution/semantics") assert r.status_code == 200 @@ -1014,8 +1014,15 @@ class TestExecutionQueueEndpoints: assert "queued" in body["execution_states"] assert "immediate" in body["launch_modes"] assert "parallel" in body["concurrency_modes"] - assert any("launch requests" in item for item in body["state_hub_responsibility"]) - assert any("dispatch" in item for item in body["activity_core_responsibility"]) + assert body["launch_requests_accepted"] is False + assert "authoritative repository file" in body["replacements"][ + "POST /execution/launch-requests" + ] + assert any( + "reject new workplan launch" in item + for item in body["state_hub_responsibility"] + ) + assert any("do not consume" in item for item in body["activity_core_responsibility"]) async def test_execution_intent_update_does_not_change_lifecycle_status(self, client): await _create_domain(client) @@ -1107,7 +1114,7 @@ class TestExecutionQueueEndpoints: assert rows[2]["eligible"] is False assert rows[2]["blocked_by_workstream_ids"] == [dependency["id"]] - async def test_launch_request_records_handoff_and_updates_execution_intent(self, client): + async def test_launch_request_is_gone_and_does_not_update_execution_intent(self, client): await _create_domain(client) topic = await _create_topic(client) ws = await _create_workstream(client, topic["id"], status="ready") @@ -1124,21 +1131,18 @@ class TestExecutionQueueEndpoints: "notes": "start now", }) - assert r.status_code == 201, r.text - body = r.json() - assert body["workstream_id"] == ws["id"] - assert body["launch_mode"] == "immediate" - assert body["immediate_pickup"] is True - assert body["status"] == "requested" + assert r.status_code == 410, r.text + assert "no consumer picks up these rows" in r.json()["detail"] + assert "repo file queue" in r.headers["x-statehub-replacement"] r = await client.get(f"/workplans/{ws['id']}") updated = r.json() assert updated["status"] == "ready" - assert updated["execution_state"] == "launching" - assert updated["launch_mode"] == "immediate" + assert updated["execution_state"] == "manual" + assert updated["launch_mode"] == "manual" r = await client.get(f"/execution/launch-requests?workstream_id={ws['id']}") - assert len(r.json()) == 1 + assert r.json() == [] def _fabric_graph_export(generated_at="2026-05-23T12:00:00Z", extra_node=False): diff --git a/tests/test_work_record_identifier_migration.py b/tests/test_work_record_identifier_migration.py index 8d525c0..4dd2d3a 100644 --- a/tests/test_work_record_identifier_migration.py +++ b/tests/test_work_record_identifier_migration.py @@ -13,6 +13,7 @@ from api.models.domain import Domain from api.models.managed_repo import ManagedRepo from api.models.progress_event import ProgressEvent from api.models.task import Task +from api.models.topic import Topic from api.models.token_event import TokenEvent from api.models.work_record_identifier_alias import WorkRecordIdentifierAlias from api.models.workplan import Workplan @@ -21,6 +22,7 @@ from api.services.work_record_identifier_migration import ( DERIVATION_NAMESPACE_UUID, IdentifierMigrationError, apply_repository_identifier_migration, + repair_absent_prederivation_projection, reverse_repository_identifier_migration, ) @@ -296,3 +298,233 @@ async def test_repository_migration_is_atomic_when_a_source_is_missing(test_engi assert await session.scalar( text("SELECT count(*) FROM work_record_identifier_aliases") ) == 0 + + +async def _seed_empty_repository(factory, repo_slug: str): + domain_id = uuid.uuid4() + topic_id = uuid.uuid4() + repo_id = uuid.uuid4() + async with factory() as session: + session.add(Domain(id=domain_id, slug="infotech", name="Infotech", status="active")) + await session.flush() + session.add( + Topic( + id=topic_id, + domain_id=domain_id, + slug="infotech", + title="Infotech", + status="active", + ) + ) + await session.flush() + session.add( + ManagedRepo( + id=repo_id, + domain_id=domain_id, + topic_id=topic_id, + slug=repo_slug, + name="Repair Repository", + status="active", + ) + ) + await session.commit() + return repo_id, topic_id + + +def _repair_unit(plan: dict, topic_id: uuid.UUID) -> dict: + repository = plan["repositories"][0] + replacements = [m for m in repository["mappings"] if m["action"] == "replace"] + workplan = next(m for m in replacements if m["kind"] == "workplan") + task = next(m for m in replacements if m["kind"] == "task") + return { + "workplan": { + "record_id": workplan["record_id"], + "id": workplan["current_uuid"], + "topic_id": str(topic_id), + "slug": "test-wp-0001", + "title": "Migration test", + "description": "Exact authoritative projection", + "status": "active", + "owner": "codex", + "planning_priority": "high", + "planning_order": 1, + }, + "tasks": [ + { + "record_id": task["record_id"], + "id": task["current_uuid"], + "title": "Mapped task", + "description": "Exact task projection", + "status": "progress", + "priority": "high", + "assignee": "codex", + } + ], + } + + +def _repair_kwargs(plan: dict) -> dict: + repository = plan["repositories"][0] + return { + "expected_plan_sha256": plan["plan_sha256"], + "source_revision": repository["planned_head_sha"], + "source_fingerprint": repository["source_fingerprint"], + "source_clean": True, + "source_synchronized": True, + "primary_confirmed": True, + "projection_identity": "test-projection", + } + + +@pytest.mark.asyncio +async def test_sealed_repair_restores_absent_projection_and_retries_as_noop(test_engine): + factory = async_sessionmaker(test_engine, class_=AsyncSession, expire_on_commit=False) + repo_id, topic_id = await _seed_empty_repository(factory, "repair-repo") + workplan_old = uuid.uuid4() + task_old = uuid.uuid4() + plan = _sealed_plan("repair-repo", workplan_old, task_old) + unit = _repair_unit(plan, topic_id) + + async with factory() as session: + receipt = await repair_absent_prederivation_projection( + session, plan, "repair-repo", unit, **_repair_kwargs(plan) + ) + assert receipt.outcome == "repaired" + assert receipt.repository_id == str(repo_id) + assert receipt.workplan_old_id == str(workplan_old) + assert receipt.task_records == (("TEST-WP-0001-T01", str(task_old)),) + + async with factory() as session: + assert await session.get(Workplan, workplan_old) is not None + assert await session.get(Task, task_old) is not None + assert await session.get(Workplan, _derived("TEST-WP-0001")) is None + assert await session.get(Task, _derived("TEST-WP-0001-T01")) is None + + async with factory() as session: + retry = await repair_absent_prederivation_projection( + session, plan, "repair-repo", unit, **_repair_kwargs(plan) + ) + assert retry.outcome == "verified_noop" + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + ("override", "message"), + [ + ({"source_clean": False}, "clean synchronized"), + ({"source_synchronized": False}, "clean synchronized"), + ({"source_revision": "f" * 40}, "source revision drifted"), + ({"source_fingerprint": "e" * 64}, "source fingerprint drifted"), + ({"expected_plan_sha256": "0" * 64}, "explicit plan SHA-256"), + ({"primary_confirmed": False}, "primary confirmation"), + ], +) +async def test_sealed_repair_rejects_untrusted_source(test_engine, override, message): + factory = async_sessionmaker(test_engine, class_=AsyncSession, expire_on_commit=False) + _repo_id, topic_id = await _seed_empty_repository(factory, "untrusted-repo") + workplan_old = uuid.uuid4() + task_old = uuid.uuid4() + plan = _sealed_plan("untrusted-repo", workplan_old, task_old) + kwargs = _repair_kwargs(plan) | override + + async with factory() as session: + with pytest.raises(IdentifierMigrationError, match=message): + await repair_absent_prederivation_projection( + session, plan, "untrusted-repo", _repair_unit(plan, topic_id), **kwargs + ) + async with factory() as session: + assert await session.get(Workplan, workplan_old) is None + assert await session.get(Task, task_old) is None + + +@pytest.mark.asyncio +async def test_sealed_repair_rejects_partial_or_derived_projection_presence(test_engine): + factory = async_sessionmaker(test_engine, class_=AsyncSession, expire_on_commit=False) + repo_id, topic_id = await _seed_empty_repository(factory, "partial-repo") + workplan_old = uuid.uuid4() + task_old = uuid.uuid4() + plan = _sealed_plan("partial-repo", workplan_old, task_old) + unit = _repair_unit(plan, topic_id) + + async with factory() as session: + session.add( + Workplan( + id=workplan_old, + repo_id=repo_id, + topic_id=topic_id, + slug="test-wp-0001", + title="Migration test", + status="active", + ) + ) + await session.commit() + async with factory() as session: + with pytest.raises(IdentifierMigrationError, match="partial old projection"): + await repair_absent_prederivation_projection( + session, plan, "partial-repo", unit, **_repair_kwargs(plan) + ) + + async with factory() as session: + await session.execute(text("DELETE FROM workplans WHERE id = :id"), {"id": workplan_old}) + session.add( + Workplan( + id=_derived("TEST-WP-0001"), + repo_id=repo_id, + topic_id=topic_id, + slug="derived-conflict", + title="Derived conflict", + status="active", + ) + ) + await session.commit() + async with factory() as session: + with pytest.raises(IdentifierMigrationError, match="derived target presence"): + await repair_absent_prederivation_projection( + session, plan, "partial-repo", unit, **_repair_kwargs(plan) + ) + + +@pytest.mark.asyncio +async def test_sealed_repair_http_surface_returns_receipt(client, test_engine): + factory = async_sessionmaker(test_engine, class_=AsyncSession, expire_on_commit=False) + repo_id, topic_id = await _seed_empty_repository(factory, "http-repair-repo") + workplan_old = uuid.uuid4() + task_old = uuid.uuid4() + plan = _sealed_plan("http-repair-repo", workplan_old, task_old) + response = await client.post( + "/identifier-migrations/sealed-projection-repairs", + json={ + "plan": plan, + "repo_slug": "http-repair-repo", + "unit": _repair_unit(plan, topic_id), + **_repair_kwargs(plan), + }, + ) + assert response.status_code == 200, response.text + receipt = response.json() + assert receipt["outcome"] == "repaired" + assert receipt["repository_id"] == str(repo_id) + assert receipt["task_records"] == [["TEST-WP-0001-T01", str(task_old)]] + assert (await client.get(f"/workplans/{workplan_old}")).status_code == 200 + assert (await client.get(f"/tasks/{task_old}")).status_code == 200 + assert (await client.get(f"/workplans/{_derived('TEST-WP-0001')}")).status_code == 404 + assert (await client.get(f"/tasks/{_derived('TEST-WP-0001-T01')}")).status_code == 404 + + +@pytest.mark.asyncio +async def test_sealed_repair_http_surface_rejects_missing_confirmation(client, test_engine): + factory = async_sessionmaker(test_engine, class_=AsyncSession, expire_on_commit=False) + _repo_id, topic_id = await _seed_empty_repository(factory, "http-reject-repo") + plan = _sealed_plan("http-reject-repo", uuid.uuid4(), uuid.uuid4()) + payload = { + "plan": plan, + "repo_slug": "http-reject-repo", + "unit": _repair_unit(plan, topic_id), + **_repair_kwargs(plan), + } + payload["primary_confirmed"] = False + response = await client.post( + "/identifier-migrations/sealed-projection-repairs", json=payload + ) + assert response.status_code == 409 + assert "primary confirmation" in response.json()["detail"] diff --git a/workplans/STATE-WP-0079-retirement-strangler.md b/workplans/STATE-WP-0079-retirement-strangler.md index b0ebb50..16dc194 100644 --- a/workplans/STATE-WP-0079-retirement-strangler.md +++ b/workplans/STATE-WP-0079-retirement-strangler.md @@ -8,7 +8,7 @@ status: active owner: codex topic_slug: infotech created: "2026-08-09" -updated: "2026-08-22" +updated: "2026-08-23" parent_project: prj-state-hub-retirement parent_workplan: SHR-WP-0001 related: @@ -247,6 +247,25 @@ and were not ingested. Production aliases remain empty as expected for a rebuild rather than an in-place rewrite. Widening beyond this one-repository pilot still requires a separate fleet cutover decision. +**Sealed pre-derivation repair interface approved with safeguards +(2026-08-23):** Repo Manager interface +`helixforge.identifiers.state-hub-sealed-projection-repair.v1` is accepted as a +temporary T04 compatibility sub-slice. The owner implementation adds +`POST /identifier-migrations/sealed-projection-repairs` and keeps ordinary C-03 +refusal unchanged. Repair is limited to one explicitly named workplan and all +of its sealed `replace` task mappings; it requires the exact plan seal, clean +synchronized Git revision and source fingerprint, explicit primary +confirmation, existing repository/topic projection, and complete absence of +both the old unit and derived targets. Creation is one database transaction; +an exact retry returns a verified no-op receipt, while partial presence, source +drift, aliases, target rows, or content mismatch fail closed. Operational and +removal contract: `docs/sealed-prederivation-projection-repair.md`. + +Verification: focused identifier-migration coverage passes (`13 passed`), the +ordinary consistency suite passes (`129 passed`), and the full API plus +dashboard build passes (`650 passed`). The full run retained one pre-existing +SQLAlchemy resource warning and one pre-existing dashboard link warning. + **A2a executed (2026-08-20) — first live cutover slice.** Dual-run is on for the pilot repo: @@ -331,7 +350,7 @@ re-scaffolding. ```task id: STATE-WP-0079-T05 -status: todo +status: progress priority: medium state_hub_task_id: "02e508ed-3cde-4487-907e-d324a8a877d6" ``` @@ -398,6 +417,18 @@ freeze window, not now. Then `dashboard-meta` (E3, 1); `legacy-meter` itself (E4, 9) retires last, being the instrument. +**Execution launch-request defect closed (2026-08-23):** the surviving +`POST /execution/launch-requests` route claimed Activity Core pickup after that +mapping was explicitly retired by `ACTIVITY-WP-0029-T04`. New submissions now +return 410 with the repository-file / ActivityDefinition+ops_run replacement; +`GET /execution/semantics` explicitly says launch requests are not accepted, +that Activity Core does not consume them, and that legacy `launching` does not +prove pickup. Historical launch rows remain readable. The two reported +requests (`7052b20f…`, `5628fe55…`) were cancelled as never consumed and their +workplans restored from `launching/immediate` to `manual/manual`. +Focused execution coverage passes (`4 passed`); the combined full regression +and dashboard build remains green (`650 passed`). + ## Stabilization window and archive prep ```task