fix(retirement): close projection and launch contract gaps
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Build and Publish Multi-Context Image / build-and-push (push) Successful in 23s

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a02b7c-1c49-76a0-955a-49e7b3ddfc0d
This commit is contained in:
tegwick 2026-08-23 00:52:18 +02:00
parent c43266f626
commit b0e1af24f9
13 changed files with 1717 additions and 50 deletions

View file

@ -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)

View file

@ -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])

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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,

View file

@ -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,