fix(retirement): close projection and launch contract gaps
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a02b7c-1c49-76a0-955a-49e7b3ddfc0d
This commit is contained in:
parent
c43266f626
commit
b0e1af24f9
13 changed files with 1717 additions and 50 deletions
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
44
api/routers/identifier_migrations.py
Normal file
44
api/routers/identifier_migrations.py
Normal 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)
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
33
api/schemas/identifier_migration.py
Normal file
33
api/schemas/identifier_migration.py
Normal 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
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
929
docs/evidence/legacy-meter-weekly-review-20260822.json
Normal file
929
docs/evidence/legacy-meter-weekly-review-20260822.json
Normal file
|
|
@ -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=<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=<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=<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
|
||||
}
|
||||
45
docs/sealed-prederivation-projection-repair.md
Normal file
45
docs/sealed-prederivation-projection-repair.md
Normal file
|
|
@ -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.
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue