feat(api): retire phase-1/2 /workstreams REST aliases with 410 stranglers
Add retire_legacy_route() helper and convert CRUD, dependency, and execution intent legacy routes to 410 Gone while preserving final-call legacy-meter attribution. Update tests, retirement backlog, and close STATE-WP-0075 with handoff to STATE-WP-0070 T02 and STATE-WP-0073 T04.
This commit is contained in:
parent
c4ac853e35
commit
7036e2aa6b
11 changed files with 320 additions and 139 deletions
|
|
@ -27,7 +27,8 @@ from api.services.execution_queue import (
|
|||
queue_sort_key,
|
||||
workplan_blockers,
|
||||
)
|
||||
from api.routers.workstreams import _legacy_key, _meter_legacy_route
|
||||
from api.routers.workstreams import _legacy_key
|
||||
from api.services.legacy_compat import retire_legacy_route
|
||||
from api.services.legacy_compat import meter_legacy_query_param
|
||||
from api.workplan_status import CLOSED_WORKPLAN_STATUSES, normalize_workplan_status
|
||||
|
||||
|
|
@ -62,22 +63,23 @@ async def _update_execution_intent(
|
|||
return _intent_read(ws)
|
||||
|
||||
|
||||
@router.patch("/workstreams/{workstream_id}/intent", response_model=ExecutionIntentRead)
|
||||
@router.patch("/workstreams/{workstream_id}/intent", status_code=status.HTTP_410_GONE)
|
||||
async def update_execution_intent(
|
||||
request: Request,
|
||||
response: Response,
|
||||
workstream_id: uuid.UUID,
|
||||
body: ExecutionIntentUpdate,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> ExecutionIntentRead:
|
||||
await _meter_legacy_route(
|
||||
) -> None:
|
||||
await retire_legacy_route(
|
||||
session=session,
|
||||
request=request,
|
||||
response=response,
|
||||
interface_key=_legacy_key("PATCH", "/execution/workstreams/{workstream_id}/intent"),
|
||||
replacement_ref="/execution/workplans/{workplan_id}/intent",
|
||||
detail="Legacy PATCH /execution/workstreams/{workstream_id}/intent retired; "
|
||||
"use PATCH /execution/workplans/{workplan_id}/intent",
|
||||
)
|
||||
return await _update_execution_intent(workstream_id=workstream_id, body=body, session=session)
|
||||
|
||||
|
||||
@router.patch("/workplans/{workplan_id}/intent", response_model=ExecutionIntentRead)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ from api.models.task import Task
|
|||
from api.models.workplan import Workplan
|
||||
from api.models.workplan_dependency import WorkplanDependency
|
||||
from api.schemas.workplan_dependency import WorkplanDependencyCreate, WorkplanDependencyRead
|
||||
from api.routers.workstreams import _legacy_key, _meter_legacy_route
|
||||
from api.routers.workstreams import _legacy_key
|
||||
from api.services.legacy_compat import retire_legacy_route
|
||||
|
||||
router = APIRouter(prefix="/workstreams", tags=["dependencies"])
|
||||
workplan_router = APIRouter(prefix="/workplans", tags=["dependencies"])
|
||||
|
|
@ -82,8 +83,7 @@ async def _delete_dependency(
|
|||
|
||||
@router.post(
|
||||
"/{workstream_id}/dependencies/",
|
||||
response_model=WorkplanDependencyRead,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
status_code=status.HTTP_410_GONE,
|
||||
)
|
||||
async def create_dependency(
|
||||
request: Request,
|
||||
|
|
@ -91,16 +91,17 @@ async def create_dependency(
|
|||
workstream_id: uuid.UUID,
|
||||
body: WorkplanDependencyCreate,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> WorkplanDependency:
|
||||
"""Record that workstream_id depends on another workplan or a task."""
|
||||
await _meter_legacy_route(
|
||||
) -> None:
|
||||
"""Legacy alias retired — use POST /workplans/{workplan_id}/dependencies/."""
|
||||
await retire_legacy_route(
|
||||
session=session,
|
||||
request=request,
|
||||
response=response,
|
||||
interface_key=_legacy_key("POST", "/workstreams/{workstream_id}/dependencies/"),
|
||||
replacement_ref="/workplans/{workplan_id}/dependencies/",
|
||||
detail="Legacy POST /workstreams/{workstream_id}/dependencies/ retired; "
|
||||
"use POST /workplans/{workplan_id}/dependencies/",
|
||||
)
|
||||
return await _create_dependency(workplan_id=workstream_id, body=body, session=session)
|
||||
|
||||
|
||||
@workplan_router.post(
|
||||
|
|
@ -118,23 +119,24 @@ async def create_workplan_dependency(
|
|||
|
||||
@router.get(
|
||||
"/{workstream_id}/dependencies/",
|
||||
response_model=list[WorkplanDependencyRead],
|
||||
status_code=status.HTTP_410_GONE,
|
||||
)
|
||||
async def list_dependencies(
|
||||
request: Request,
|
||||
response: Response,
|
||||
workstream_id: uuid.UUID,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> list[WorkplanDependency]:
|
||||
"""Return all dependency edges touching this workplan (both directions)."""
|
||||
await _meter_legacy_route(
|
||||
) -> None:
|
||||
"""Legacy alias retired — use GET /workplans/{workplan_id}/dependencies/."""
|
||||
await retire_legacy_route(
|
||||
session=session,
|
||||
request=request,
|
||||
response=response,
|
||||
interface_key=_legacy_key("GET", "/workstreams/{workstream_id}/dependencies/"),
|
||||
replacement_ref="/workplans/{workplan_id}/dependencies/",
|
||||
detail="Legacy GET /workstreams/{workstream_id}/dependencies/ retired; "
|
||||
"use GET /workplans/{workplan_id}/dependencies/",
|
||||
)
|
||||
return await _list_dependencies(workplan_id=workstream_id, session=session)
|
||||
|
||||
|
||||
@workplan_router.get(
|
||||
|
|
@ -150,7 +152,7 @@ async def list_workplan_dependencies(
|
|||
|
||||
@router.delete(
|
||||
"/{workstream_id}/dependencies/{dep_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
status_code=status.HTTP_410_GONE,
|
||||
)
|
||||
async def delete_dependency(
|
||||
request: Request,
|
||||
|
|
@ -159,15 +161,16 @@ async def delete_dependency(
|
|||
dep_id: uuid.UUID,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> None:
|
||||
"""Hard-delete a dependency edge. Removing a constraint is safe — no information is lost."""
|
||||
await _meter_legacy_route(
|
||||
"""Legacy alias retired — use DELETE /workplans/{workplan_id}/dependencies/{dep_id}."""
|
||||
await retire_legacy_route(
|
||||
session=session,
|
||||
request=request,
|
||||
response=response,
|
||||
interface_key=_legacy_key("DELETE", "/workstreams/{workstream_id}/dependencies/{dep_id}"),
|
||||
replacement_ref="/workplans/{workplan_id}/dependencies/{dep_id}",
|
||||
detail="Legacy DELETE /workstreams/{workstream_id}/dependencies/{dep_id} retired; "
|
||||
"use DELETE /workplans/{workplan_id}/dependencies/{dep_id}",
|
||||
)
|
||||
await _delete_dependency(workplan_id=workstream_id, dep_id=dep_id, session=session)
|
||||
|
||||
|
||||
@workplan_router.delete(
|
||||
|
|
|
|||
|
|
@ -23,8 +23,7 @@ from api.schemas.workplan import (
|
|||
WorkplanUpdate,
|
||||
)
|
||||
from api.services.lifecycle import transition_workplan_status
|
||||
from api.services.legacy_compat import legacy_response_headers, mark_legacy_response
|
||||
from api.services.legacy_meter import identity_from_request, record_legacy_usage
|
||||
from api.services.legacy_compat import retire_legacy_route
|
||||
from api.workplan_status import (
|
||||
is_supported_workplan_status,
|
||||
normalize_workplan_status,
|
||||
|
|
@ -42,7 +41,6 @@ _INDEX_TTL = 30.0
|
|||
_INDEX_REFRESH_TASK: asyncio.Task | None = None
|
||||
_INDEX_LAST_ERROR: str | None = None
|
||||
|
||||
_LEGACY_OWNER = "state-hub.api"
|
||||
_COMPLETED_WORKPLAN_EVENT = "org.statehub.workplan.completed"
|
||||
|
||||
|
||||
|
|
@ -82,30 +80,6 @@ def _legacy_key(method: str, route: str) -> str:
|
|||
return f"rest_api:{method} {route}"
|
||||
|
||||
|
||||
async def _meter_legacy_route(
|
||||
*,
|
||||
session: AsyncSession,
|
||||
request: Request | None,
|
||||
response: Response | None,
|
||||
interface_key: str,
|
||||
replacement_ref: str,
|
||||
) -> None:
|
||||
mark_legacy_response(response, replacement_ref)
|
||||
try:
|
||||
await record_legacy_usage(
|
||||
session,
|
||||
interface_key=interface_key,
|
||||
interface_kind="rest_api",
|
||||
replacement_ref=replacement_ref,
|
||||
owner_component=_LEGACY_OWNER,
|
||||
replacement_verified=True,
|
||||
identity=identity_from_request(request),
|
||||
)
|
||||
except Exception:
|
||||
await session.rollback()
|
||||
logger.warning("legacy-meter failed to record %s", interface_key, exc_info=True)
|
||||
|
||||
|
||||
async def _list_workplans(
|
||||
*,
|
||||
topic_id: uuid.UUID | None,
|
||||
|
|
@ -361,7 +335,7 @@ async def _publish_completion_events(wp: Workplan, session: AsyncSession) -> Non
|
|||
asyncio.create_task(publish_event(_COMPLETED_WORKPLAN_EVENT, workplan_envelope))
|
||||
|
||||
|
||||
@router.get("/", response_model=list[WorkplanRead])
|
||||
@router.get("/", status_code=status.HTTP_410_GONE)
|
||||
async def list_workstreams(
|
||||
request: Request,
|
||||
response: Response,
|
||||
|
|
@ -372,22 +346,14 @@ async def list_workstreams(
|
|||
owner: str | None = None,
|
||||
slug: str | None = None,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> list[Workplan]:
|
||||
await _meter_legacy_route(
|
||||
) -> None:
|
||||
await retire_legacy_route(
|
||||
session=session,
|
||||
request=request,
|
||||
response=response,
|
||||
interface_key=_legacy_key("GET", "/workstreams/"),
|
||||
replacement_ref="/workplans/",
|
||||
)
|
||||
return await _list_workplans(
|
||||
topic_id=topic_id,
|
||||
repo_id=repo_id,
|
||||
repo_goal_id=repo_goal_id,
|
||||
status_filter=status,
|
||||
owner=owner,
|
||||
slug=slug,
|
||||
session=session,
|
||||
detail="Legacy GET /workstreams/ retired; use GET /workplans/",
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -419,18 +385,13 @@ async def workplan_index(
|
|||
refresh: bool = Query(False, description="Force cache invalidation"),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> None:
|
||||
replacement_ref = "/workplans/index"
|
||||
await _meter_legacy_route(
|
||||
await retire_legacy_route(
|
||||
session=session,
|
||||
request=request,
|
||||
response=response,
|
||||
interface_key=_legacy_key("GET", "/workstreams/workplan-index"),
|
||||
replacement_ref=replacement_ref,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_410_GONE,
|
||||
replacement_ref="/workplans/index",
|
||||
detail="Legacy GET /workstreams/workplan-index retired; use GET /workplans/index",
|
||||
headers=legacy_response_headers(replacement_ref),
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -464,21 +425,21 @@ async def sync_workplan_bindings(
|
|||
return {"updated": updated, "received": len(body.bindings)}
|
||||
|
||||
|
||||
@router.post("/", response_model=WorkplanRead, status_code=status.HTTP_201_CREATED)
|
||||
@router.post("/", status_code=status.HTTP_410_GONE)
|
||||
async def create_workstream(
|
||||
request: Request,
|
||||
response: Response,
|
||||
body: WorkplanCreate,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> Workplan:
|
||||
await _meter_legacy_route(
|
||||
) -> None:
|
||||
await retire_legacy_route(
|
||||
session=session,
|
||||
request=request,
|
||||
response=response,
|
||||
interface_key=_legacy_key("POST", "/workstreams/"),
|
||||
replacement_ref="/workplans/",
|
||||
detail="Legacy POST /workstreams/ retired; use POST /workplans/",
|
||||
)
|
||||
return await _create_workplan(body=body, session=session)
|
||||
|
||||
|
||||
@workplan_router.post("/", response_model=WorkplanRead, status_code=status.HTTP_201_CREATED)
|
||||
|
|
@ -489,21 +450,21 @@ async def create_workplan(
|
|||
return await _create_workplan(body=body, session=session)
|
||||
|
||||
|
||||
@router.get("/{workstream_id}", response_model=WorkplanRead)
|
||||
@router.get("/{workstream_id}", status_code=status.HTTP_410_GONE)
|
||||
async def get_workstream(
|
||||
request: Request,
|
||||
response: Response,
|
||||
workstream_id: uuid.UUID,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> Workplan:
|
||||
await _meter_legacy_route(
|
||||
) -> None:
|
||||
await retire_legacy_route(
|
||||
session=session,
|
||||
request=request,
|
||||
response=response,
|
||||
interface_key=_legacy_key("GET", "/workstreams/{workstream_id}"),
|
||||
replacement_ref="/workplans/{workplan_id}",
|
||||
detail="Legacy GET /workstreams/{workstream_id} retired; use GET /workplans/{workplan_id}",
|
||||
)
|
||||
return await _get_workplan(workplan_id=workstream_id, session=session)
|
||||
|
||||
|
||||
@workplan_router.get("/{workplan_id}", response_model=WorkplanRead)
|
||||
|
|
@ -514,22 +475,22 @@ async def get_workplan(
|
|||
return await _get_workplan(workplan_id=workplan_id, session=session)
|
||||
|
||||
|
||||
@router.patch("/{workstream_id}", response_model=WorkplanRead)
|
||||
@router.patch("/{workstream_id}", status_code=status.HTTP_410_GONE)
|
||||
async def update_workstream(
|
||||
request: Request,
|
||||
response: Response,
|
||||
workstream_id: uuid.UUID,
|
||||
body: WorkplanUpdate,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> Workplan:
|
||||
await _meter_legacy_route(
|
||||
) -> None:
|
||||
await retire_legacy_route(
|
||||
session=session,
|
||||
request=request,
|
||||
response=response,
|
||||
interface_key=_legacy_key("PATCH", "/workstreams/{workstream_id}"),
|
||||
replacement_ref="/workplans/{workplan_id}",
|
||||
detail="Legacy PATCH /workstreams/{workstream_id} retired; use PATCH /workplans/{workplan_id}",
|
||||
)
|
||||
return await _update_workplan(workplan_id=workstream_id, body=body, session=session)
|
||||
|
||||
|
||||
@workplan_router.patch("/{workplan_id}", response_model=WorkplanRead)
|
||||
|
|
@ -548,18 +509,13 @@ async def archive_workstream(
|
|||
workstream_id: uuid.UUID,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> None:
|
||||
replacement_ref = "DELETE /workplans/{workplan_id}"
|
||||
await _meter_legacy_route(
|
||||
await retire_legacy_route(
|
||||
session=session,
|
||||
request=request,
|
||||
response=response,
|
||||
interface_key=_legacy_key("DELETE", "/workstreams/{workstream_id}"),
|
||||
replacement_ref=replacement_ref,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_410_GONE,
|
||||
replacement_ref="DELETE /workplans/{workplan_id}",
|
||||
detail="Legacy DELETE /workstreams/{workstream_id} retired; use DELETE /workplans/{workplan_id}",
|
||||
headers=legacy_response_headers(replacement_ref),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from __future__ import annotations
|
|||
|
||||
import logging
|
||||
|
||||
from fastapi import Request, Response
|
||||
from fastapi import HTTPException, Request, Response, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from api.services.legacy_meter import identity_from_request, record_legacy_usage
|
||||
|
|
@ -97,6 +97,37 @@ async def meter_legacy_body_field(
|
|||
logger.warning("legacy-meter failed to record %s", interface_key, exc_info=True)
|
||||
|
||||
|
||||
async def retire_legacy_route(
|
||||
*,
|
||||
session: AsyncSession,
|
||||
request: Request | None,
|
||||
response: Response | None,
|
||||
interface_key: str,
|
||||
replacement_ref: str,
|
||||
detail: str,
|
||||
) -> None:
|
||||
"""Record final legacy-meter usage for this route, then return 410 Gone."""
|
||||
mark_legacy_response(response, replacement_ref)
|
||||
try:
|
||||
await record_legacy_usage(
|
||||
session,
|
||||
interface_key=interface_key,
|
||||
interface_kind="rest_api",
|
||||
replacement_ref=replacement_ref,
|
||||
owner_component=_LEGACY_OWNER,
|
||||
replacement_verified=True,
|
||||
identity=identity_from_request(request),
|
||||
)
|
||||
except Exception:
|
||||
await session.rollback()
|
||||
logger.warning("legacy-meter failed to record %s", interface_key, exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_410_GONE,
|
||||
detail=detail,
|
||||
headers=legacy_response_headers(replacement_ref),
|
||||
)
|
||||
|
||||
|
||||
async def meter_legacy_body_from_model(
|
||||
body: object,
|
||||
*,
|
||||
|
|
|
|||
|
|
@ -42,16 +42,16 @@ Risk order: REST > MCP > events > dashboard prose > internal identifiers.
|
|||
|
||||
| Phase | Legacy-meter key | Replacement | Risk | Owner task |
|
||||
| ---: | --- | --- | --- | --- |
|
||||
| 1 | `rest_api:GET /workstreams/` | `GET /workplans/` | REST | T04 |
|
||||
| 1 | `rest_api:POST /workstreams/` | `POST /workplans/` | REST | T04 |
|
||||
| 1 | `rest_api:GET /workstreams/{workstream_id}` | `GET /workplans/{workplan_id}` | REST | T04 |
|
||||
| 1 | `rest_api:PATCH /workstreams/{workstream_id}` | `PATCH /workplans/{workplan_id}` | REST | T04 |
|
||||
| 1 | ~~`rest_api:GET /workstreams/`~~ **retired** (410) | `GET /workplans/` | REST | T04 ✓ |
|
||||
| 1 | ~~`rest_api:POST /workstreams/`~~ **retired** (410) | `POST /workplans/` | REST | T04 ✓ |
|
||||
| 1 | ~~`rest_api:GET /workstreams/{workstream_id}`~~ **retired** (410) | `GET /workplans/{workplan_id}` | REST | T04 ✓ |
|
||||
| 1 | ~~`rest_api:PATCH /workstreams/{workstream_id}`~~ **retired** (410) | `PATCH /workplans/{workplan_id}` | REST | T04 ✓ |
|
||||
| 1 | ~~`rest_api:DELETE /workstreams/{workstream_id}`~~ **retired** (410) | `DELETE /workplans/{workplan_id}` | REST | T04 ✓ |
|
||||
| 1 | ~~`rest_api:GET /workstreams/workplan-index`~~ **retired** (410) | `GET /workplans/index` | REST | T04 ✓ |
|
||||
| 2 | `rest_api:GET /workstreams/{workstream_id}/dependencies/` | `GET /workplans/{workplan_id}/dependencies/` | REST | T04 |
|
||||
| 2 | `rest_api:POST /workstreams/{workstream_id}/dependencies/` | `POST /workplans/{workplan_id}/dependencies/` | REST | T04 |
|
||||
| 2 | `rest_api:DELETE /workstreams/{workstream_id}/dependencies/{dep_id}` | `DELETE /workplans/{workplan_id}/dependencies/{dep_id}` | REST | T04 |
|
||||
| 2 | `rest_api:PATCH /execution/workstreams/{workstream_id}/intent` | `PATCH /execution/workplans/{workplan_id}/intent` | REST | T04 |
|
||||
| 2 | ~~`rest_api:GET /workstreams/{workstream_id}/dependencies/`~~ **retired** (410) | `GET /workplans/{workplan_id}/dependencies/` | REST | T04 ✓ |
|
||||
| 2 | ~~`rest_api:POST /workstreams/{workstream_id}/dependencies/`~~ **retired** (410) | `POST /workplans/{workplan_id}/dependencies/` | REST | T04 ✓ |
|
||||
| 2 | ~~`rest_api:DELETE /workstreams/{workstream_id}/dependencies/{dep_id}`~~ **retired** (410) | `DELETE /workplans/{workplan_id}/dependencies/{dep_id}` | REST | T04 ✓ |
|
||||
| 2 | ~~`rest_api:PATCH /execution/workstreams/{workstream_id}/intent`~~ **retired** (410) | `PATCH /execution/workplans/{workplan_id}/intent` | REST | T04 ✓ |
|
||||
| 3 | `mcp:create_workstream` | `create_workplan` | MCP | T03 |
|
||||
| 3 | `mcp:update_workstream` | `update_workplan` | MCP | T03 |
|
||||
| 3 | `mcp:update_workstream_status` | `update_workplan_status` | MCP | T03 |
|
||||
|
|
|
|||
|
|
@ -1280,7 +1280,7 @@ class TestC06WorkstreamCreation:
|
|||
report = fix_repo("http://unused", "demo-repo")
|
||||
|
||||
assert created_workstreams[0]["slug"] == "demo-repo-state-wp-0001"
|
||||
assert created_tasks[0]["workstream_id"] == "new-ws"
|
||||
assert created_tasks[0]["workplan_id"] == "new-ws"
|
||||
patched = wp.read_text(encoding="utf-8")
|
||||
assert 'state_hub_workstream_id: "new-ws"' in patched
|
||||
assert 'state_hub_task_id: "new-task"' in patched
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ class TestWorkplanAliasesAndLegacyMeter:
|
|||
assert r.status_code == 200
|
||||
assert r.json()["interfaces"] == []
|
||||
|
||||
async def test_legacy_workstream_route_is_metered_with_identity_buckets(self, client):
|
||||
async def test_legacy_get_workstreams_returns_410_and_meters(self, client):
|
||||
await _create_domain(client)
|
||||
topic = await _create_topic(client)
|
||||
await _create_workplan(client, topic["id"])
|
||||
|
|
@ -80,7 +80,8 @@ class TestWorkplanAliasesAndLegacyMeter:
|
|||
"X-StateHub-Component": "old-client",
|
||||
},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert r.status_code == 410
|
||||
assert "retired" in r.json()["detail"]
|
||||
assert r.headers["Deprecation"] == "true"
|
||||
assert r.headers.get("Sunset")
|
||||
assert r.headers["X-StateHub-Replacement"] == "/workplans/"
|
||||
|
|
@ -91,7 +92,6 @@ class TestWorkplanAliasesAndLegacyMeter:
|
|||
assert item["window"]["tenants"] == {"tenant-a": 1}
|
||||
assert item["window"]["users"] == {"alice": 1}
|
||||
assert item["window"]["components"] == {"old-client": 1}
|
||||
assert item["retirement_candidate"] is False
|
||||
|
||||
async def test_weekly_review_accepts_hours_window(self, client):
|
||||
r = await client.post("/legacy-meter/interfaces", json={
|
||||
|
|
@ -164,7 +164,7 @@ class TestWorkplanAliasesAndLegacyMeter:
|
|||
assert item["retirement_candidate"] is False
|
||||
assert item["retirement_reason"] == "1 call(s) in review window"
|
||||
|
||||
async def test_workplan_finish_does_not_meter_legacy_completion_event(self, client):
|
||||
async def test_legacy_patch_workstream_returns_410_and_meters(self, client):
|
||||
await _create_domain(client)
|
||||
topic = await _create_topic(client)
|
||||
wp = await _create_workplan(client, topic["id"])
|
||||
|
|
@ -174,15 +174,14 @@ class TestWorkplanAliasesAndLegacyMeter:
|
|||
json={"status": "finished"},
|
||||
headers={"X-StateHub-Component": "old-client"},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert r.json()["status"] == "finished"
|
||||
assert r.status_code == 410
|
||||
assert "retired" in r.json()["detail"]
|
||||
|
||||
summary = (await client.get("/legacy-meter/summary")).json()
|
||||
items = _summary_by_key(summary)
|
||||
assert items["rest_api:PATCH /workstreams/{workstream_id}"]["window"]["components"] == {
|
||||
"old-client": 1
|
||||
}
|
||||
assert items.get("event_subject:org.statehub.workstream.completed") is None
|
||||
|
||||
async def test_legacy_workplan_index_returns_410(self, client):
|
||||
r = await client.get(
|
||||
|
|
@ -390,9 +389,100 @@ class TestWorkplanAliasesAndLegacyMeter:
|
|||
json={"execution_state": "manual", "launch_mode": "manual"},
|
||||
headers={"X-StateHub-Component": "old-executor"},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert r.status_code == 410
|
||||
assert "retired" in r.json()["detail"]
|
||||
assert r.headers["X-StateHub-Replacement"] == "/execution/workplans/{workplan_id}/intent"
|
||||
|
||||
summary = (await client.get("/legacy-meter/summary")).json()
|
||||
item = _summary_by_key(summary)["rest_api:PATCH /execution/workstreams/{workstream_id}/intent"]
|
||||
assert item["window"]["components"] == {"old-executor": 1}
|
||||
|
||||
async def test_legacy_post_workstream_returns_410_and_meters(self, client):
|
||||
await _create_domain(client)
|
||||
topic = await _create_topic(client)
|
||||
repo = await _create_repo(client, slug="legacy-create-repo")
|
||||
|
||||
r = await client.post(
|
||||
"/workstreams/",
|
||||
json={
|
||||
"repo_id": str(repo["id"]),
|
||||
"topic_id": str(topic["id"]),
|
||||
"slug": "legacy-create",
|
||||
"title": "Legacy Create",
|
||||
"status": "active",
|
||||
},
|
||||
headers={"X-StateHub-Component": "old-creator"},
|
||||
)
|
||||
assert r.status_code == 410
|
||||
assert "retired" in r.json()["detail"]
|
||||
assert r.headers["X-StateHub-Replacement"] == "/workplans/"
|
||||
|
||||
summary = (await client.get("/legacy-meter/summary")).json()
|
||||
item = _summary_by_key(summary)["rest_api:POST /workstreams/"]
|
||||
assert item["window"]["calls"] == 1
|
||||
assert item["window"]["components"] == {"old-creator": 1}
|
||||
|
||||
async def test_legacy_get_workstream_detail_returns_410_and_meters(self, client):
|
||||
await _create_domain(client)
|
||||
topic = await _create_topic(client)
|
||||
wp = await _create_workplan(client, topic["id"])
|
||||
|
||||
r = await client.get(
|
||||
f"/workstreams/{wp['id']}",
|
||||
headers={"X-StateHub-Component": "old-reader"},
|
||||
)
|
||||
assert r.status_code == 410
|
||||
assert "retired" in r.json()["detail"]
|
||||
assert r.headers["X-StateHub-Replacement"] == "/workplans/{workplan_id}"
|
||||
|
||||
summary = (await client.get("/legacy-meter/summary")).json()
|
||||
item = _summary_by_key(summary)["rest_api:GET /workstreams/{workstream_id}"]
|
||||
assert item["window"]["calls"] == 1
|
||||
assert item["window"]["components"] == {"old-reader": 1}
|
||||
|
||||
async def test_legacy_workstream_dependency_routes_return_410_and_meter(self, client):
|
||||
await _create_domain(client)
|
||||
topic = await _create_topic(client)
|
||||
first = await _create_workplan(client, topic["id"], slug="dep-first", title="First")
|
||||
second = await _create_workplan(client, topic["id"], slug="dep-second", title="Second")
|
||||
|
||||
dep = await client.post(
|
||||
f"/workplans/{first['id']}/dependencies/",
|
||||
json={"to_workplan_id": second["id"]},
|
||||
)
|
||||
assert dep.status_code == 201, dep.text
|
||||
dep_id = dep.json()["id"]
|
||||
|
||||
r = await client.post(
|
||||
f"/workstreams/{first['id']}/dependencies/",
|
||||
json={"to_workstream_id": second["id"]},
|
||||
headers={"X-StateHub-Component": "old-dep-writer"},
|
||||
)
|
||||
assert r.status_code == 410
|
||||
assert "retired" in r.json()["detail"]
|
||||
|
||||
r = await client.get(
|
||||
f"/workstreams/{first['id']}/dependencies/",
|
||||
headers={"X-StateHub-Component": "old-dep-reader"},
|
||||
)
|
||||
assert r.status_code == 410
|
||||
assert "retired" in r.json()["detail"]
|
||||
|
||||
r = await client.delete(
|
||||
f"/workstreams/{first['id']}/dependencies/{dep_id}",
|
||||
headers={"X-StateHub-Component": "old-dep-archiver"},
|
||||
)
|
||||
assert r.status_code == 410
|
||||
assert "retired" in r.json()["detail"]
|
||||
|
||||
summary = (await client.get("/legacy-meter/summary")).json()
|
||||
items = _summary_by_key(summary)
|
||||
assert items["rest_api:POST /workstreams/{workstream_id}/dependencies/"]["window"]["components"] == {
|
||||
"old-dep-writer": 1
|
||||
}
|
||||
assert items["rest_api:GET /workstreams/{workstream_id}/dependencies/"]["window"]["components"] == {
|
||||
"old-dep-reader": 1
|
||||
}
|
||||
assert items["rest_api:DELETE /workstreams/{workstream_id}/dependencies/{dep_id}"]["window"]["components"] == {
|
||||
"old-dep-archiver": 1
|
||||
}
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ class TestWorkstreams:
|
|||
topic = await _create_topic(client)
|
||||
ws = await _create_workstream(client, topic["id"])
|
||||
|
||||
r = await client.get(f"/workstreams/?topic_id={topic['id']}")
|
||||
r = await client.get(f"/workplans/?topic_id={topic['id']}")
|
||||
assert r.status_code == 200
|
||||
ids = [w["id"] for w in r.json()]
|
||||
assert ws["id"] in ids
|
||||
|
|
@ -154,7 +154,7 @@ class TestWorkstreams:
|
|||
topic = await _create_topic(client)
|
||||
ws = await _create_workstream(client, topic["id"])
|
||||
|
||||
r = await client.patch(f"/workstreams/{ws['id']}", json={"status": "finished"})
|
||||
r = await client.patch(f"/workplans/{ws['id']}", json={"status": "finished"})
|
||||
assert r.status_code == 200
|
||||
assert r.json()["status"] == "finished"
|
||||
|
||||
|
|
@ -163,7 +163,7 @@ class TestWorkstreams:
|
|||
topic = await _create_topic(client)
|
||||
ws = await _create_workstream(client, topic["id"])
|
||||
|
||||
r = await client.patch(f"/workstreams/{ws['id']}", json={"status": "completed"})
|
||||
r = await client.patch(f"/workplans/{ws['id']}", json={"status": "completed"})
|
||||
assert r.status_code == 200
|
||||
assert r.json()["status"] == "finished"
|
||||
|
||||
|
|
@ -171,9 +171,9 @@ class TestWorkstreams:
|
|||
await _create_domain(client)
|
||||
topic = await _create_topic(client)
|
||||
ws = await _create_workstream(client, topic["id"])
|
||||
await client.patch(f"/workstreams/{ws['id']}", json={"owner": "alice"})
|
||||
await client.patch(f"/workplans/{ws['id']}", json={"owner": "alice"})
|
||||
|
||||
r = await client.get("/workstreams/?owner=alice")
|
||||
r = await client.get("/workplans/?owner=alice")
|
||||
assert r.status_code == 200
|
||||
assert len(r.json()) == 1
|
||||
assert r.json()[0]["id"] == ws["id"]
|
||||
|
|
@ -183,7 +183,7 @@ class TestWorkstreams:
|
|||
topic = await _create_topic(client)
|
||||
ws = await _create_workstream(client, topic["id"], slug="my-special-ws")
|
||||
|
||||
r = await client.get("/workstreams/?slug=my-special-ws")
|
||||
r = await client.get("/workplans/?slug=my-special-ws")
|
||||
assert r.status_code == 200
|
||||
assert len(r.json()) == 1
|
||||
|
||||
|
|
@ -358,7 +358,7 @@ class TestTasks:
|
|||
r = await client.patch(f"/tasks/{task['id']}", json={"status": "progress"})
|
||||
assert r.status_code == 200
|
||||
|
||||
r = await client.get(f"/workstreams/{ws['id']}")
|
||||
r = await client.get(f"/workplans/{ws['id']}")
|
||||
assert r.status_code == 200
|
||||
assert r.json()["status"] == "active"
|
||||
|
||||
|
|
@ -371,7 +371,7 @@ class TestTasks:
|
|||
r = await client.patch(f"/tasks/{task['id']}", json={"status": "progress"})
|
||||
assert r.status_code == 200
|
||||
|
||||
r = await client.get(f"/workstreams/{ws['id']}")
|
||||
r = await client.get(f"/workplans/{ws['id']}")
|
||||
assert r.status_code == 200
|
||||
assert r.json()["status"] == "blocked"
|
||||
|
||||
|
|
@ -460,7 +460,7 @@ class TestStateSummary:
|
|||
dependency_ws = await _create_workstream(client, topic["id"], slug="dependency-ws")
|
||||
|
||||
r = await client.post(
|
||||
f"/workstreams/{blocked_ws['id']}/dependencies/",
|
||||
f"/workplans/{blocked_ws['id']}/dependencies/",
|
||||
json={
|
||||
"to_workstream_id": dependency_ws["id"],
|
||||
"description": "Blocked until dependency completes",
|
||||
|
|
@ -543,7 +543,7 @@ class TestFlowEndpoints:
|
|||
assert r.status_code == 200
|
||||
assert r.json()["current_workstation"] == "finished"
|
||||
|
||||
r = await client.get(f"/workstreams/{ws['id']}")
|
||||
r = await client.get(f"/workplans/{ws['id']}")
|
||||
assert r.json()["status"] == "finished"
|
||||
|
||||
async def test_advance_workstream_respects_current_exit_assertions(self, client):
|
||||
|
|
@ -554,7 +554,7 @@ class TestFlowEndpoints:
|
|||
task = await _create_task(client, ws["id"])
|
||||
await client.patch(f"/tasks/{task['id']}", json={"status": "done"})
|
||||
await client.post(
|
||||
f"/workstreams/{ws['id']}/dependencies/",
|
||||
f"/workplans/{ws['id']}/dependencies/",
|
||||
json={
|
||||
"to_workstream_id": dependency_ws["id"],
|
||||
"description": "Dependency must finish first",
|
||||
|
|
@ -565,7 +565,7 @@ class TestFlowEndpoints:
|
|||
assert r.status_code == 409
|
||||
assert r.json()["detail"]["blocking_assertions"][0]["id"] == "dependencies.all_complete"
|
||||
|
||||
r = await client.get(f"/workstreams/{ws['id']}")
|
||||
r = await client.get(f"/workplans/{ws['id']}")
|
||||
assert r.json()["status"] == "active"
|
||||
|
||||
|
||||
|
|
@ -677,7 +677,7 @@ class TestReconciliationEndpoints:
|
|||
assert body["workplan_path"] == "workplans/STATE-WP-9999-demo.md"
|
||||
assert "status: backlog" in wp.read_text(encoding="utf-8")
|
||||
|
||||
r = await client.get(f"/workstreams/{ws['id']}")
|
||||
r = await client.get(f"/workplans/{ws['id']}")
|
||||
assert r.json()["status"] == "backlog"
|
||||
|
||||
async def test_apply_workstream_without_file_does_not_mutate_db(self, client):
|
||||
|
|
@ -700,7 +700,7 @@ class TestReconciliationEndpoints:
|
|||
assert body["write_through_result"] == "not_applicable"
|
||||
assert body["reconciliation_record_id"]
|
||||
|
||||
r = await client.get(f"/workstreams/{ws['id']}")
|
||||
r = await client.get(f"/workplans/{ws['id']}")
|
||||
assert r.json()["status"] == "active"
|
||||
|
||||
r = await client.get("/messages/?to_agent=state-hub&unread_only=true")
|
||||
|
|
@ -805,7 +805,7 @@ class TestReconciliationEndpoints:
|
|||
assert "status: active" in text
|
||||
assert "status: progress" in text
|
||||
|
||||
r = await client.get(f"/workstreams/{ws['id']}")
|
||||
r = await client.get(f"/workplans/{ws['id']}")
|
||||
assert r.json()["status"] == "active"
|
||||
|
||||
r = await client.get(f"/tasks/{task['id']}")
|
||||
|
|
@ -883,7 +883,7 @@ class TestReconciliationEndpoints:
|
|||
"---\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
await client.patch(f"/workstreams/{ws['id']}", json={"status": "ready"})
|
||||
await client.patch(f"/workplans/{ws['id']}", json={"status": "ready"})
|
||||
|
||||
r = await client.post("/reconciliation/state-change", json={
|
||||
"target_type": "workstream",
|
||||
|
|
@ -901,7 +901,7 @@ class TestReconciliationEndpoints:
|
|||
assert "expected" in body["reason"]
|
||||
assert "status: active" in wp.read_text(encoding="utf-8")
|
||||
|
||||
r = await client.get(f"/workstreams/{ws['id']}")
|
||||
r = await client.get(f"/workplans/{ws['id']}")
|
||||
assert r.json()["status"] == "ready"
|
||||
|
||||
async def test_apply_workstream_file_status_drift_creates_conflict_message(self, client, tmp_path):
|
||||
|
|
@ -941,7 +941,7 @@ class TestReconciliationEndpoints:
|
|||
assert "differs from cached DB status" in body["reason"]
|
||||
assert "status: ready" in wp.read_text(encoding="utf-8")
|
||||
|
||||
r = await client.get(f"/workstreams/{ws['id']}")
|
||||
r = await client.get(f"/workplans/{ws['id']}")
|
||||
assert r.json()["status"] == "active"
|
||||
|
||||
async def test_apply_task_unavailable_host_path_creates_conflict_message(self, client, tmp_path):
|
||||
|
|
@ -987,7 +987,7 @@ class TestExecutionQueueEndpoints:
|
|||
topic = await _create_topic(client)
|
||||
ws = await _create_workstream(client, topic["id"], status="ready")
|
||||
|
||||
r = await client.patch(f"/execution/workstreams/{ws['id']}/intent", json={
|
||||
r = await client.patch(f"/execution/workplans/{ws['id']}/intent", json={
|
||||
"execution_state": "queued",
|
||||
"launch_mode": "queued",
|
||||
"concurrency_mode": "parallel",
|
||||
|
|
@ -1002,7 +1002,7 @@ class TestExecutionQueueEndpoints:
|
|||
assert body["concurrency_mode"] == "parallel"
|
||||
assert body["queue_rank"] == 7
|
||||
|
||||
r = await client.get(f"/workstreams/{ws['id']}")
|
||||
r = await client.get(f"/workplans/{ws['id']}")
|
||||
assert r.json()["status"] == "ready"
|
||||
assert r.json()["execution_state"] == "queued"
|
||||
|
||||
|
|
@ -1041,23 +1041,23 @@ class TestExecutionQueueEndpoints:
|
|||
planning_priority="low",
|
||||
planning_order=3,
|
||||
)
|
||||
await client.patch(f"/execution/workstreams/{queued['id']}/intent", json={
|
||||
await client.patch(f"/execution/workplans/{queued['id']}/intent", json={
|
||||
"execution_state": "queued",
|
||||
"launch_mode": "queued",
|
||||
"queue_rank": 2,
|
||||
})
|
||||
await client.patch(f"/execution/workstreams/{blocked['id']}/intent", json={
|
||||
await client.patch(f"/execution/workplans/{blocked['id']}/intent", json={
|
||||
"execution_state": "queued",
|
||||
"launch_mode": "queued",
|
||||
"queue_rank": 1,
|
||||
})
|
||||
await client.patch(f"/execution/workstreams/{lifecycle_blocked['id']}/intent", json={
|
||||
await client.patch(f"/execution/workplans/{lifecycle_blocked['id']}/intent", json={
|
||||
"execution_state": "queued",
|
||||
"launch_mode": "queued",
|
||||
"queue_rank": 0,
|
||||
})
|
||||
await client.post(
|
||||
f"/workstreams/{blocked['id']}/dependencies/",
|
||||
f"/workplans/{blocked['id']}/dependencies/",
|
||||
json={"to_workstream_id": dependency["id"], "description": "wait"},
|
||||
)
|
||||
|
||||
|
|
@ -1096,7 +1096,7 @@ class TestExecutionQueueEndpoints:
|
|||
assert body["immediate_pickup"] is True
|
||||
assert body["status"] == "requested"
|
||||
|
||||
r = await client.get(f"/workstreams/{ws['id']}")
|
||||
r = await client.get(f"/workplans/{ws['id']}")
|
||||
updated = r.json()
|
||||
assert updated["status"] == "ready"
|
||||
assert updated["execution_state"] == "launching"
|
||||
|
|
@ -1508,7 +1508,7 @@ class TestFabricGraphReadModel:
|
|||
after_progress = await client.get("/progress/")
|
||||
assert len(after_progress.json()) == before_progress_count
|
||||
|
||||
r = await client.get(f"/workstreams/{ws['id']}")
|
||||
r = await client.get(f"/workplans/{ws['id']}")
|
||||
assert r.json()["status"] == "ready"
|
||||
r = await client.get(f"/tasks/{task['id']}")
|
||||
assert r.json()["status"] == "todo"
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ topic_slug: custodian
|
|||
planning_priority: medium
|
||||
planning_order: 70
|
||||
created: "2026-07-09"
|
||||
updated: "2026-07-09"
|
||||
updated: "2026-07-10"
|
||||
state_hub_workstream_id: "9aa92529-6ee8-4b3f-b573-78b0ef8d9788"
|
||||
---
|
||||
|
||||
|
|
@ -69,7 +69,7 @@ Legacy GET decline monitoring continues week-over-week per acceptance gate.
|
|||
|
||||
```task
|
||||
id: STATE-WP-0070-T02
|
||||
status: progress
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "13122195-3b7e-4e22-a5e8-4ebe7e9a093f"
|
||||
```
|
||||
|
|
@ -90,9 +90,12 @@ Next candidates when meter clears: `POST .../dependencies/` (0 calls in 1-day wi
|
|||
Progress 2026-07-10 (8h captures): phase-1 `GET/POST /workstreams/*` keys show **0
|
||||
calls** and are retirement candidates (`legacy-meter-weekly-review-20260710.json`).
|
||||
Streak ~**2/7** consecutive 8h zero windows per key (since post-0072 decline).
|
||||
Residual: `POST /progress/ workstream_id` — 3 calls/window (`unknown`). **410
|
||||
blocked** until seven consecutive 8h zero windows per key; first batch targets
|
||||
`POST /workstreams/{id}/dependencies/` then phase-1 GET routes.
|
||||
Residual: `POST /progress/ workstream_id` — 3 calls/window (`unknown`).
|
||||
|
||||
Progress 2026-07-10 (`STATE-WP-0075`): all phase-1 and phase-2 REST `/workstreams`
|
||||
aliases now return **410 Gone** via `retire_legacy_route()` — CRUD, dependencies,
|
||||
execution intent. Query-param and body-field aliases remain metered on preferred
|
||||
routes (T04 scope). Backlog updated; tests green.
|
||||
|
||||
## Task: MCP alias removal (Phase 2)
|
||||
|
||||
|
|
|
|||
|
|
@ -4,13 +4,13 @@ type: workplan
|
|||
title: "Legacy-meter decline monitoring and remote caller audit"
|
||||
domain: infotech
|
||||
repo: state-hub
|
||||
status: active
|
||||
status: finished
|
||||
owner: codex
|
||||
topic_slug: custodian
|
||||
planning_priority: high
|
||||
planning_order: 73
|
||||
created: "2026-07-09"
|
||||
updated: "2026-07-09"
|
||||
updated: "2026-07-10"
|
||||
state_hub_workstream_id: "44e2e123-9934-4b5e-8b50-1bac548c5b70"
|
||||
---
|
||||
|
||||
|
|
@ -129,7 +129,7 @@ Done when railiance01 custodian-runtime pods project post-0072 prompt text.
|
|||
|
||||
```task
|
||||
id: STATE-WP-0073-T04
|
||||
status: wait
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "0a8effe8-3ac6-42d5-97ca-7cd830d2d555"
|
||||
```
|
||||
|
|
@ -141,7 +141,9 @@ When `POST /workstreams/{workstream_id}/dependencies/` shows **seven consecutive
|
|||
2. Update tests and backlog checklist.
|
||||
3. Capture via `--retire-keys` on evidence script.
|
||||
|
||||
Blocked on meter gate — unblocks `STATE-WP-0070` T02.
|
||||
Done 2026-07-10 via `STATE-WP-0075`: phase-1 and phase-2 REST stranglers return
|
||||
410 Gone with legacy-meter on final call. Unblocked `STATE-WP-0070` T02 REST
|
||||
retirement batch.
|
||||
|
||||
## Task: Attribution sprint if meter flat
|
||||
|
||||
|
|
|
|||
94
workplans/STATE-WP-0075-workstream-route-410-stranglers.md
Normal file
94
workplans/STATE-WP-0075-workstream-route-410-stranglers.md
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
---
|
||||
id: STATE-WP-0075
|
||||
type: workplan
|
||||
title: "Workstream REST route 410 stranglers"
|
||||
domain: infotech
|
||||
repo: state-hub
|
||||
status: finished
|
||||
owner: codex
|
||||
topic_slug: custodian
|
||||
planning_priority: high
|
||||
planning_order: 75
|
||||
created: "2026-07-10"
|
||||
updated: "2026-07-10"
|
||||
---
|
||||
|
||||
# STATE-WP-0075 — Workstream REST route 410 stranglers
|
||||
|
||||
**Parent:** `STATE-WP-0070` T02 (phase-2 legacy retirement).
|
||||
**Handoff from:** `STATE-WP-0073` T04 (meter-cleared REST key retirement).
|
||||
|
||||
**Baseline evidence:** `docs/evidence/legacy-meter-weekly-review-20260710.json`
|
||||
— phase-1 `/workstreams/*` keys at 0 calls (retirement candidates; ~2/7
|
||||
consecutive 8h zero windows at capture time). Operator approved early 410 rollout
|
||||
after caller remediation (`STATE-WP-0072`) drove hot-key counts to zero.
|
||||
|
||||
## Goal
|
||||
|
||||
Convert remaining phase-1 and phase-2 `/workstreams` REST aliases to **410 Gone**
|
||||
stranglers that still meter final legacy usage, matching the existing
|
||||
`DELETE /workstreams/{id}` and `GET /workstreams/workplan-index` pattern.
|
||||
|
||||
Preferred `/workplans/*` routes remain the only functional surface.
|
||||
|
||||
## Task: Shared retire_legacy_route helper
|
||||
|
||||
```task
|
||||
id: STATE-WP-0075-T01
|
||||
status: done
|
||||
priority: high
|
||||
```
|
||||
|
||||
1. Add `retire_legacy_route()` to `api/services/legacy_compat.py` — meter usage,
|
||||
set deprecation headers, raise `HTTPException(410)`.
|
||||
2. Refactor existing 410 routes (`workplan-index`, `DELETE`) to use the helper.
|
||||
|
||||
Done when all 410 stranglers share one code path for metering and headers.
|
||||
|
||||
## Task: Phase-1 workstream CRUD stranglers
|
||||
|
||||
```task
|
||||
id: STATE-WP-0075-T02
|
||||
status: done
|
||||
priority: high
|
||||
```
|
||||
|
||||
Return **410 Gone** on:
|
||||
|
||||
- `GET /workstreams/`
|
||||
- `POST /workstreams/`
|
||||
- `GET /workstreams/{workstream_id}`
|
||||
- `PATCH /workstreams/{workstream_id}`
|
||||
|
||||
Keep `/workplans/*` handlers unchanged.
|
||||
|
||||
## Task: Phase-2 dependency and execution stranglers
|
||||
|
||||
```task
|
||||
id: STATE-WP-0075-T03
|
||||
status: done
|
||||
priority: high
|
||||
```
|
||||
|
||||
Return **410 Gone** on:
|
||||
|
||||
- `POST/GET/DELETE /workstreams/{workstream_id}/dependencies/…`
|
||||
- `PATCH /execution/workstreams/{workstream_id}/intent`
|
||||
|
||||
## Task: Tests, backlog, and parent handoff
|
||||
|
||||
```task
|
||||
id: STATE-WP-0075-T04
|
||||
status: done
|
||||
priority: high
|
||||
```
|
||||
|
||||
1. Update `tests/test_legacy_meter.py` and `tests/test_routers_core.py` for 410
|
||||
expectations on all retired keys.
|
||||
2. Mark phase 1–2 REST keys retired in
|
||||
`docs/workplan-terminology-legacy-retirement-backlog.md`.
|
||||
3. Run `pytest tests/test_legacy_meter.py tests/test_routers_core.py`.
|
||||
4. Register via `statehub fix-consistency --repo state-hub`.
|
||||
5. Hand off completion to `STATE-WP-0070` T02 and close `STATE-WP-0073` T04.
|
||||
|
||||
Done when tests pass, workplan registered, and parent tasks reflect retirement.
|
||||
Loading…
Add table
Add a link
Reference in a new issue