Finish STATE-WP-0069: retire legacy completion event and DELETE /workstreams
Stop dual-publishing org.statehub.workstream.completed; return 410 Gone for
legacy DELETE /workstreams/{id}. Migrate fix-consistency, MCP adhoc bootstrap,
and dashboard token summary to /workplans/. Add legacy-meter evidence capture
script and pytest snapshot; update docs and close out the workplan.
This commit is contained in:
parent
b659ff8d13
commit
e0c954d098
13 changed files with 404 additions and 149 deletions
|
|
@ -23,12 +23,8 @@ from api.schemas.workplan import (
|
|||
WorkplanUpdate,
|
||||
)
|
||||
from api.services.lifecycle import transition_workplan_status
|
||||
from api.services.legacy_compat import mark_legacy_response
|
||||
from api.services.legacy_meter import (
|
||||
LegacyUsageIdentity,
|
||||
identity_from_request,
|
||||
record_legacy_usage,
|
||||
)
|
||||
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.workplan_status import (
|
||||
is_supported_workplan_status,
|
||||
normalize_workplan_status,
|
||||
|
|
@ -47,7 +43,6 @@ _INDEX_REFRESH_TASK: asyncio.Task | None = None
|
|||
_INDEX_LAST_ERROR: str | None = None
|
||||
|
||||
_LEGACY_OWNER = "state-hub.api"
|
||||
_COMPLETED_WORKSTREAM_EVENT = "org.statehub.workstream.completed"
|
||||
_COMPLETED_WORKPLAN_EVENT = "org.statehub.workplan.completed"
|
||||
|
||||
|
||||
|
|
@ -111,27 +106,6 @@ async def _meter_legacy_route(
|
|||
logger.warning("legacy-meter failed to record %s", interface_key, exc_info=True)
|
||||
|
||||
|
||||
async def _meter_legacy_event(
|
||||
*,
|
||||
session: AsyncSession,
|
||||
subject: str,
|
||||
replacement_ref: str,
|
||||
) -> None:
|
||||
try:
|
||||
await record_legacy_usage(
|
||||
session,
|
||||
interface_key=f"event_subject:{subject}",
|
||||
interface_kind="event_subject",
|
||||
replacement_ref=replacement_ref,
|
||||
owner_component="state-hub.events",
|
||||
replacement_verified=True,
|
||||
identity=LegacyUsageIdentity(component_key="state-hub.events"),
|
||||
)
|
||||
except Exception:
|
||||
await session.rollback()
|
||||
logger.warning("legacy-meter failed to record event subject %s", subject, exc_info=True)
|
||||
|
||||
|
||||
async def _list_workplans(
|
||||
*,
|
||||
topic_id: uuid.UUID | None,
|
||||
|
|
@ -377,7 +351,6 @@ async def _publish_completion_events(wp: Workplan, session: AsyncSession) -> Non
|
|||
_COMPLETED_WORKPLAN_EVENT,
|
||||
attributes={
|
||||
"workplan_id": str(wp.id),
|
||||
"legacy_workstream_id": str(wp.id),
|
||||
"slug": wp.slug,
|
||||
"title": wp.title,
|
||||
"topic_id": str(wp.topic_id) if wp.topic_id else None,
|
||||
|
|
@ -387,24 +360,6 @@ async def _publish_completion_events(wp: Workplan, session: AsyncSession) -> Non
|
|||
)
|
||||
asyncio.create_task(publish_event(_COMPLETED_WORKPLAN_EVENT, workplan_envelope))
|
||||
|
||||
await _meter_legacy_event(
|
||||
session=session,
|
||||
subject=_COMPLETED_WORKSTREAM_EVENT,
|
||||
replacement_ref=_COMPLETED_WORKPLAN_EVENT,
|
||||
)
|
||||
legacy_envelope = EventEnvelope.new(
|
||||
_COMPLETED_WORKSTREAM_EVENT,
|
||||
attributes={
|
||||
"workstream_id": str(wp.id),
|
||||
"slug": wp.slug,
|
||||
"title": wp.title,
|
||||
"topic_id": str(wp.topic_id) if wp.topic_id else None,
|
||||
"repo_id": str(wp.repo_id) if wp.repo_id else None,
|
||||
"repo_goal_id": str(wp.repo_goal_id) if wp.repo_goal_id else None,
|
||||
},
|
||||
)
|
||||
asyncio.create_task(publish_event(_COMPLETED_WORKSTREAM_EVENT, legacy_envelope))
|
||||
|
||||
|
||||
@router.get("/", response_model=list[WorkplanRead])
|
||||
async def list_workstreams(
|
||||
|
|
@ -581,21 +536,26 @@ async def update_workplan(
|
|||
return await _update_workplan(workplan_id=workplan_id, body=body, session=session)
|
||||
|
||||
|
||||
@router.delete("/{workstream_id}", response_model=WorkplanRead)
|
||||
@router.delete("/{workstream_id}", status_code=status.HTTP_410_GONE)
|
||||
async def archive_workstream(
|
||||
request: Request,
|
||||
response: Response,
|
||||
workstream_id: uuid.UUID,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> Workplan:
|
||||
) -> None:
|
||||
replacement_ref = "DELETE /workplans/{workplan_id}"
|
||||
await _meter_legacy_route(
|
||||
session=session,
|
||||
request=request,
|
||||
response=response,
|
||||
interface_key=_legacy_key("DELETE", "/workstreams/{workstream_id}"),
|
||||
replacement_ref="/workplans/{workplan_id}",
|
||||
replacement_ref=replacement_ref,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_410_GONE,
|
||||
detail="Legacy DELETE /workstreams/{workstream_id} retired; use DELETE /workplans/{workplan_id}",
|
||||
headers=legacy_response_headers(replacement_ref),
|
||||
)
|
||||
return await _archive_workplan(workplan_id=workstream_id, session=session)
|
||||
|
||||
|
||||
@workplan_router.delete("/{workplan_id}", response_model=WorkplanRead)
|
||||
|
|
|
|||
|
|
@ -16,13 +16,23 @@ _LEGACY_OWNER = "state-hub.api"
|
|||
_LEGACY_WORKSTREAM_SUNSET = "Wed, 30 Jun 2027 23:59:59 GMT"
|
||||
|
||||
|
||||
def legacy_response_headers(replacement_ref: str) -> dict[str, str]:
|
||||
return {
|
||||
"Deprecation": "true",
|
||||
"Sunset": _LEGACY_WORKSTREAM_SUNSET,
|
||||
"X-StateHub-Replacement": replacement_ref,
|
||||
"Link": f'<{replacement_ref}>; rel="successor-version"',
|
||||
}
|
||||
|
||||
|
||||
def mark_legacy_response(response: Response | None, replacement_ref: str) -> None:
|
||||
if response is None:
|
||||
return
|
||||
response.headers["Deprecation"] = "true"
|
||||
response.headers["Sunset"] = _LEGACY_WORKSTREAM_SUNSET
|
||||
response.headers["X-StateHub-Replacement"] = replacement_ref
|
||||
response.headers.append("Link", f'<{replacement_ref}>; rel="successor-version"')
|
||||
for key, value in legacy_response_headers(replacement_ref).items():
|
||||
if key == "Link":
|
||||
response.headers.append(key, value)
|
||||
else:
|
||||
response.headers[key] = value
|
||||
|
||||
|
||||
def legacy_query_param_key(method: str, route: str, param: str = "workstream_id") -> str:
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ def fetch(url: str):
|
|||
|
||||
# Fetch all repos and workstreams for scope resolution
|
||||
repos = fetch(f"{API_BASE}/repos/") or []
|
||||
workstreams_raw = fetch(f"{API_BASE}/workstreams/?limit=500") or []
|
||||
workstreams_raw = fetch(f"{API_BASE}/workplans/?limit=500") or []
|
||||
|
||||
# Fetch all token events (up to 1000) for aggregation
|
||||
events = fetch(f"{API_BASE}/token-events/?limit=1000") or []
|
||||
|
|
|
|||
100
docs/evidence/legacy-meter-weekly-review-20260708.json
Normal file
100
docs/evidence/legacy-meter-weekly-review-20260708.json
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
{
|
||||
"captured_at": "2026-07-08T21:26:32.013489+00:00",
|
||||
"source": "pytest",
|
||||
"workplan": "STATE-WP-0069",
|
||||
"weekly_review": {
|
||||
"generated_at": "2026-07-08T21:26:32.009639Z",
|
||||
"window_start": "2026-07-01T21:26:31.995046Z",
|
||||
"window_end": "2026-07-08T21:26:31.995046Z",
|
||||
"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": "53afb376-40d3-4c07-a312-a1294e179d68",
|
||||
"interface_key": "rest_api:GET /obsolete",
|
||||
"interface_kind": "rest_api",
|
||||
"legacy_since": "2026-07-08T21:26:31.952023Z",
|
||||
"replacement_ref": "/workplans/",
|
||||
"owner_component": "state-hub",
|
||||
"status": "legacy",
|
||||
"replacement_verified": true,
|
||||
"manual_hold": false,
|
||||
"hold_reason": null,
|
||||
"notes": null,
|
||||
"retired_at": null,
|
||||
"created_at": "2026-07-08T21:26:31.952023Z",
|
||||
"updated_at": "2026-07-08T21:26:31.952023Z"
|
||||
},
|
||||
"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": true,
|
||||
"retirement_reason": "no measured usage in review window"
|
||||
}
|
||||
],
|
||||
"retirement_candidates": [
|
||||
{
|
||||
"interface": {
|
||||
"id": "53afb376-40d3-4c07-a312-a1294e179d68",
|
||||
"interface_key": "rest_api:GET /obsolete",
|
||||
"interface_kind": "rest_api",
|
||||
"legacy_since": "2026-07-08T21:26:31.952023Z",
|
||||
"replacement_ref": "/workplans/",
|
||||
"owner_component": "state-hub",
|
||||
"status": "legacy",
|
||||
"replacement_verified": true,
|
||||
"manual_hold": false,
|
||||
"hold_reason": null,
|
||||
"notes": null,
|
||||
"retired_at": null,
|
||||
"created_at": "2026-07-08T21:26:31.952023Z",
|
||||
"updated_at": "2026-07-08T21:26:31.952023Z"
|
||||
},
|
||||
"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": true,
|
||||
"retirement_reason": "no measured usage in review window"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -42,8 +42,8 @@ those publishers from colliding on the same `{noun}.{verb}` shape.
|
|||
| Subject | When | Required attributes |
|
||||
| ------------------------------------ | ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `org.statehub.repo.registered` | A new repo is registered via `POST /repos/` | `repo_id`, `repo_slug`, `domain_slug`, `remote_url?`, `local_path?` |
|
||||
| `org.statehub.workplan.completed` | A workplan transitions to canonical status `finished` | `workplan_id`, `legacy_workstream_id`, `slug`, `title`, `topic_id`, `repo_id?`, `repo_goal_id?` |
|
||||
| `org.statehub.workstream.completed` | Legacy compatibility subject for completed workplans | `workstream_id`, `slug`, `title`, `topic_id`, `repo_id?`, `repo_goal_id?` |
|
||||
| `org.statehub.workplan.completed` | A workplan transitions to canonical status `finished` | `workplan_id`, `slug`, `title`, `topic_id`, `repo_id?`, `repo_goal_id?` |
|
||||
| ~~`org.statehub.workstream.completed`~~ | **Retired 2026-07-08** (`STATE-WP-0069` T05). Use `org.statehub.workplan.completed`. | — |
|
||||
| `org.statehub.decision.resolved` | A decision is resolved via `POST /decisions/{id}/resolve` | `decision_id`, `title`, `topic_id?`, `workstream_id?`, `decided_by`, `rationale_snippet` |
|
||||
| `org.statehub.domain.goal.activated` | A domain goal transitions to `active` | `goal_id`, `domain_id`, `domain_slug`, `title`, `superseded_goal_ids[]` |
|
||||
| `org.statehub.task.stale` | `scripts/cleanup_stale_tasks.py` cancels an out-of-date task | `task_id`, `workstream_id`, `workstream_status`, `task_title`, `task_status_before` |
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ Risk order: REST > MCP > events > dashboard prose > internal identifiers.
|
|||
| 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:DELETE /workstreams/{workstream_id}` | `DELETE /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` | `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 |
|
||||
|
|
@ -57,7 +57,7 @@ Risk order: REST > MCP > events > dashboard prose > internal identifiers.
|
|||
| 3 | `mcp:update_workstream_status` | `update_workplan_status` | MCP | T03 |
|
||||
| 3 | `mcp:list_workstreams` | `list_workplans` | MCP | T03 |
|
||||
| 3 | `state://workstreams/{topic_slug}` | `state://workplans/{topic_slug}` (proposed) | MCP resource | T03 |
|
||||
| 4 | `event_subject:org.statehub.workstream.completed` | `org.statehub.workplan.completed` | Event | T05 |
|
||||
| 4 | ~~`event_subject:org.statehub.workstream.completed`~~ **retired** | `org.statehub.workplan.completed` | Event | T05 ✓ |
|
||||
| 5 | Dashboard nav label `Workstreams` | `Workplans` (URL compat retained) | Prose | T02 |
|
||||
| 5 | `dashboard/src/index.md` user-facing copy | workplan-first strings | Prose | T02 |
|
||||
| 6 | `open_workstreams` summary cache key | `open_workplans` | Internal | T06 |
|
||||
|
|
|
|||
|
|
@ -29,12 +29,12 @@ Physical database renames are intentionally out of scope for this workplan.
|
|||
| Create workplan | `POST /workplans/` | `POST /workstreams/` | `rest_api:POST /workstreams/` |
|
||||
| Read workplan | `GET /workplans/{workplan_id}` | `GET /workstreams/{workstream_id}` | `rest_api:GET /workstreams/{workstream_id}` |
|
||||
| Update workplan | `PATCH /workplans/{workplan_id}` | `PATCH /workstreams/{workstream_id}` | `rest_api:PATCH /workstreams/{workstream_id}` |
|
||||
| Archive workplan | `DELETE /workplans/{workplan_id}` | `DELETE /workstreams/{workstream_id}` | `rest_api:DELETE /workstreams/{workstream_id}` |
|
||||
| Archive workplan | `DELETE /workplans/{workplan_id}` | ~~`DELETE /workstreams/{workstream_id}`~~ **410 Gone** (2026-07-08) | `rest_api:DELETE /workstreams/{workstream_id}` (retired) |
|
||||
| Workplan index | `GET /workplans/index` | `GET /workstreams/workplan-index` | `rest_api:GET /workstreams/workplan-index` |
|
||||
| Workplan dependencies | `GET/POST /workplans/{workplan_id}/dependencies/` | `GET/POST /workstreams/{workstream_id}/dependencies/` | matching `rest_api:* /workstreams/...` keys |
|
||||
| Delete dependency | `DELETE /workplans/{workplan_id}/dependencies/{dep_id}` | `DELETE /workstreams/{workstream_id}/dependencies/{dep_id}` | `rest_api:DELETE /workstreams/{workstream_id}/dependencies/{dep_id}` |
|
||||
| Execution intent | `PATCH /execution/workplans/{workplan_id}/intent` | `PATCH /execution/workstreams/{workstream_id}/intent` | `rest_api:PATCH /execution/workstreams/{workstream_id}/intent` |
|
||||
| Completion event | `org.statehub.workplan.completed` | `org.statehub.workstream.completed` | `event_subject:org.statehub.workstream.completed` |
|
||||
| Completion event | `org.statehub.workplan.completed` | ~~`org.statehub.workstream.completed`~~ **retired** (2026-07-08) | `event_subject:org.statehub.workstream.completed` (retired) |
|
||||
|
||||
Legacy REST responses include:
|
||||
|
||||
|
|
@ -82,3 +82,31 @@ An interface is a retirement candidate only when all of the following are true:
|
|||
|
||||
State Hub owns the usage state and the review payload. Activity-core owns the
|
||||
weekly wakeup, review activity, and any follow-up dispatch.
|
||||
|
||||
## STATE-WP-0069 closeout (2026-07-08)
|
||||
|
||||
Completed in this workplan:
|
||||
|
||||
- `org.statehub.workstream.completed` dual-publish stopped; legacy subject retired
|
||||
in legacy-meter.
|
||||
- `DELETE /workstreams/{workstream_id}` returns **410 Gone** with replacement header.
|
||||
- `scripts/consistency_check.py`, MCP adhoc bootstrap, and dashboard token summary
|
||||
now call `/workplans/` (major `/workstreams` usage reduction).
|
||||
- `flows/workplan.yaml` and `open_workplans` are preferred; legacy dual-keys remain
|
||||
until legacy-meter clears callers.
|
||||
|
||||
Deferred until seven consecutive zero-usage review windows per key:
|
||||
|
||||
- MCP alias removal (`create_workstream`, etc.).
|
||||
- Remaining `/workstreams` GET/POST/PATCH routes (still metered with Deprecation).
|
||||
- `workstream_id` query/body param aliases on preferred routes.
|
||||
|
||||
Evidence capture:
|
||||
|
||||
```bash
|
||||
python scripts/capture_legacy_meter_evidence.py --days 7
|
||||
# writes docs/evidence/legacy-meter-weekly-review-YYYYMMDD.json
|
||||
```
|
||||
|
||||
Activity-core `weekly-legacy-meter-review` (`30 8 * * 1` Europe/Berlin) consumes
|
||||
`/legacy-meter/weekly-review`; railiance-cluster rollout suggestion `05da5540`.
|
||||
|
|
|
|||
|
|
@ -2927,9 +2927,9 @@ def _ensure_adhoc_workplan(
|
|||
wp_file = workplans_dir / f"{adhoc_id}.md"
|
||||
|
||||
ws_id = _read_adhoc_workstream_id(wp_file)
|
||||
ws = _get(f"/workstreams/{ws_id}") if ws_id else None
|
||||
ws = _get(f"/workplans/{ws_id}") if ws_id else None
|
||||
if not isinstance(ws, dict) or "error" in ws:
|
||||
existing = _get("/workstreams/", {"slug": ws_slug})
|
||||
existing = _get("/workplans/", {"slug": ws_slug})
|
||||
ws = existing[0] if isinstance(existing, list) and existing else None
|
||||
|
||||
if not ws:
|
||||
|
|
@ -2941,7 +2941,7 @@ def _ensure_adhoc_workplan(
|
|||
)
|
||||
if not topic:
|
||||
return {"error": f"No topic found for domain {domain_slug!r} — cannot create adhoc workstream."}
|
||||
ws = _post("/workstreams", {
|
||||
ws = _post("/workplans", {
|
||||
"topic_id": topic["id"],
|
||||
"slug": ws_slug,
|
||||
"title": f"Ad Hoc Tasks — {today}",
|
||||
|
|
|
|||
130
scripts/capture_legacy_meter_evidence.py
Normal file
130
scripts/capture_legacy_meter_evidence.py
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Capture legacy-meter weekly-review payload as dated evidence JSON.
|
||||
|
||||
Usage:
|
||||
python scripts/capture_legacy_meter_evidence.py [--days 7] [--api-base URL] [--dry-run]
|
||||
|
||||
Writes:
|
||||
docs/evidence/legacy-meter-weekly-review-YYYYMMDD.json
|
||||
|
||||
When the hub is reachable, optionally marks interfaces retired via PATCH when
|
||||
--retire-keys is supplied (comma-separated legacy-meter keys).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
SCRIPT_DIR = Path(__file__).parent
|
||||
REPO_ROOT = SCRIPT_DIR.parent
|
||||
EVIDENCE_DIR = REPO_ROOT / "docs" / "evidence"
|
||||
|
||||
|
||||
def _api_get(base: str, path: str, timeout: float = 30.0) -> dict:
|
||||
url = f"{base.rstrip('/')}{path}"
|
||||
with urllib.request.urlopen(url, timeout=timeout) as resp:
|
||||
return json.loads(resp.read())
|
||||
|
||||
|
||||
def _api_patch(base: str, interface_id: str, body: dict, timeout: float = 30.0) -> dict:
|
||||
url = f"{base.rstrip('/')}/legacy-meter/interfaces/{interface_id}"
|
||||
data = json.dumps(body).encode()
|
||||
req = urllib.request.Request(
|
||||
url,
|
||||
data=data,
|
||||
method="PATCH",
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
||||
return json.loads(resp.read())
|
||||
|
||||
|
||||
def _retire_interfaces(base: str, keys: list[str]) -> list[dict]:
|
||||
interfaces = _api_get(base, "/legacy-meter/interfaces")
|
||||
by_key = {item["interface_key"]: item for item in interfaces}
|
||||
retired: list[dict] = []
|
||||
for key in keys:
|
||||
interface = by_key.get(key)
|
||||
if interface is None:
|
||||
print(f"skip retire: {key} not registered", file=sys.stderr)
|
||||
continue
|
||||
if interface.get("status") == "retired":
|
||||
print(f"skip retire: {key} already retired")
|
||||
retired.append(interface)
|
||||
continue
|
||||
updated = _api_patch(
|
||||
base,
|
||||
interface["id"],
|
||||
{"status": "retired", "notes": "Retired by STATE-WP-0069 closeout"},
|
||||
)
|
||||
print(f"retired: {key}")
|
||||
retired.append(updated)
|
||||
return retired
|
||||
|
||||
|
||||
def capture(*, api_base: str, days: int, retire_keys: list[str], dry_run: bool) -> Path:
|
||||
review = _api_get(api_base, f"/legacy-meter/weekly-review?days={days}")
|
||||
retired: list[dict] = []
|
||||
if retire_keys and not dry_run:
|
||||
retired = _retire_interfaces(api_base, retire_keys)
|
||||
review = _api_get(api_base, f"/legacy-meter/weekly-review?days={days}")
|
||||
|
||||
today = datetime.date.today().strftime("%Y%m%d")
|
||||
out_path = EVIDENCE_DIR / f"legacy-meter-weekly-review-{today}.json"
|
||||
payload = {
|
||||
"captured_at": datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
||||
"api_base": api_base,
|
||||
"days": days,
|
||||
"workplan": "STATE-WP-0069",
|
||||
"retired_interfaces": retired,
|
||||
"weekly_review": review,
|
||||
}
|
||||
|
||||
if dry_run:
|
||||
print(json.dumps(payload, indent=2))
|
||||
return out_path
|
||||
|
||||
EVIDENCE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
out_path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
|
||||
candidates = len(review.get("retirement_candidates", []))
|
||||
interfaces = len(review.get("interfaces", []))
|
||||
print(f"wrote {out_path} ({interfaces} interfaces, {candidates} retirement candidates)")
|
||||
return out_path
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--days", type=int, default=7)
|
||||
parser.add_argument(
|
||||
"--api-base",
|
||||
default=os.environ.get("API_BASE", "http://127.0.0.1:8000"),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--retire-keys",
|
||||
default="event_subject:org.statehub.workstream.completed",
|
||||
help="Comma-separated legacy-meter keys to mark retired before capture",
|
||||
)
|
||||
parser.add_argument("--dry-run", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
retire_keys = [key.strip() for key in args.retire_keys.split(",") if key.strip()]
|
||||
try:
|
||||
capture(
|
||||
api_base=args.api_base,
|
||||
days=args.days,
|
||||
retire_keys=retire_keys,
|
||||
dry_run=args.dry_run,
|
||||
)
|
||||
except urllib.error.URLError as exc:
|
||||
print(f"Error: could not reach State Hub API at {args.api_base}: {exc}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -904,7 +904,7 @@ def check_repo(api_base: str, repo_slug: str, repo_path_override: str | None = N
|
|||
)
|
||||
continue
|
||||
|
||||
ws = _api_get(api_base, f"/workstreams/{ws_id}")
|
||||
ws = _api_get(api_base, f"/workplans/{ws_id}")
|
||||
if ws is None:
|
||||
# C-03: stale workstream reference
|
||||
report.add(
|
||||
|
|
@ -1079,7 +1079,7 @@ def check_repo(api_base: str, repo_slug: str, repo_path_override: str | None = N
|
|||
for t in db_tasks:
|
||||
db_task_by_id[t["id"]] = t
|
||||
|
||||
existing_deps = _api_get(api_base, f"/workstreams/{ws_id}/dependencies") or []
|
||||
existing_deps = _api_get(api_base, f"/workplans/{ws_id}/dependencies") or []
|
||||
existing_dep_keys = set()
|
||||
if isinstance(existing_deps, list):
|
||||
for dep in existing_deps:
|
||||
|
|
@ -1376,7 +1376,7 @@ def _check_orphan_db(
|
|||
) -> None:
|
||||
"""Flag DB workstreams with repo_id=this_repo that have no backing workplan file."""
|
||||
active_file_ws_ids = active_file_ws_ids or file_ws_ids
|
||||
all_ws = _api_get(api_base, "/workstreams", {"repo_id": repo_id})
|
||||
all_ws = _api_get(api_base, "/workplans", {"repo_id": repo_id})
|
||||
if not isinstance(all_ws, list):
|
||||
return
|
||||
for ws in all_ws:
|
||||
|
|
@ -1434,14 +1434,14 @@ def _check_ghost_duplicates(
|
|||
# Gather topic_ids from all file-backed workstreams so we can query by topic
|
||||
topic_ids: set[str] = set()
|
||||
for ws_id in file_ws_ids:
|
||||
ws = _api_get(api_base, f"/workstreams/{ws_id}")
|
||||
ws = _api_get(api_base, f"/workplans/{ws_id}")
|
||||
if ws and ws.get("topic_id"):
|
||||
topic_ids.add(ws["topic_id"])
|
||||
|
||||
for topic_id in topic_ids:
|
||||
topic_ws: list[dict] = []
|
||||
for status in OPEN_WORKSTREAM_STATUSES:
|
||||
status_rows = _api_get(api_base, "/workstreams", {"topic_id": topic_id, "status": status})
|
||||
status_rows = _api_get(api_base, "/workplans", {"topic_id": topic_id, "status": status})
|
||||
if isinstance(status_rows, list):
|
||||
topic_ws.extend(status_rows)
|
||||
for ws in topic_ws:
|
||||
|
|
@ -2004,12 +2004,12 @@ def _write_custodian_brief(api_base: str, repo_slug: str, repo_path: str) -> boo
|
|||
# so that a fully-finished repo doesn't degrade to "(unknown)".
|
||||
workstreams: list[dict] = []
|
||||
for status in OPEN_WORKSTREAM_STATUSES:
|
||||
rows = _api_get(api_base, "/workstreams", {"repo_id": repo_id, "status": status}) or []
|
||||
rows = _api_get(api_base, "/workplans", {"repo_id": repo_id, "status": status}) or []
|
||||
if isinstance(rows, list):
|
||||
workstreams.extend(rows)
|
||||
_ws_for_domain = workstreams if workstreams else []
|
||||
if not _ws_for_domain:
|
||||
all_ws = _api_get(api_base, "/workstreams", {"repo_id": repo_id}) or []
|
||||
all_ws = _api_get(api_base, "/workplans", {"repo_id": repo_id}) or []
|
||||
_ws_for_domain = all_ws if isinstance(all_ws, list) else []
|
||||
if _ws_for_domain:
|
||||
topic = _api_get(api_base, f"/topics/{_ws_for_domain[0].get('topic_id', '')}")
|
||||
|
|
@ -2301,7 +2301,7 @@ def fix_repo(
|
|||
try:
|
||||
if issue.check_id in ("C-04", "C-05", "C-13", "C-19"):
|
||||
ws_id = ctx["ws_id"]
|
||||
result = _api_patch(api_base, f"/workstreams/{ws_id}",
|
||||
result = _api_patch(api_base, f"/workplans/{ws_id}",
|
||||
{ctx["field"]: ctx["value"]})
|
||||
if result is not None and "_error" not in result:
|
||||
report.fixes_applied.append(
|
||||
|
|
@ -2317,7 +2317,7 @@ def fix_repo(
|
|||
elif issue.check_id == "C-23":
|
||||
ws_id = ctx["ws_id"]
|
||||
target_status = ctx["target_status"]
|
||||
result = _api_patch(api_base, f"/workstreams/{ws_id}", {"status": target_status})
|
||||
result = _api_patch(api_base, f"/workplans/{ws_id}", {"status": target_status})
|
||||
if result is not None and "_error" not in result:
|
||||
report.fixes_applied.append(
|
||||
f"C-23 fixed: workstream {ws_id[:8]}… status → {target_status!r}"
|
||||
|
|
@ -2401,7 +2401,7 @@ def fix_repo(
|
|||
ws_data = None
|
||||
last_error = None
|
||||
for slug in slug_candidates:
|
||||
existing = _api_get(api_base, "/workstreams", {"slug": slug}, return_error=True)
|
||||
existing = _api_get(api_base, "/workplans", {"slug": slug}, return_error=True)
|
||||
if isinstance(existing, dict) and "_error" in existing:
|
||||
last_error = existing["_error"]
|
||||
continue
|
||||
|
|
@ -2419,7 +2419,7 @@ def fix_repo(
|
|||
last_error = f"slug {slug!r} already belongs to another workstream"
|
||||
continue
|
||||
|
||||
ws_data = _api_post(api_base, "/workstreams", {
|
||||
ws_data = _api_post(api_base, "/workplans", {
|
||||
"topic_id": topic_id,
|
||||
"repo_id": repo_id_val,
|
||||
"slug": slug,
|
||||
|
|
@ -2484,7 +2484,7 @@ def fix_repo(
|
|||
elif issue.check_id == "C-09":
|
||||
ws_id = ctx["ws_id"]
|
||||
correct_repo_id = ctx["correct_repo_id"]
|
||||
result = _api_patch(api_base, f"/workstreams/{ws_id}",
|
||||
result = _api_patch(api_base, f"/workplans/{ws_id}",
|
||||
{"repo_id": correct_repo_id})
|
||||
if result is not None:
|
||||
report.fixes_applied.append(
|
||||
|
|
@ -2499,7 +2499,7 @@ def fix_repo(
|
|||
"to_task_id": ctx.get("to_task_id"),
|
||||
"relationship_type": ctx["relationship_type"],
|
||||
}
|
||||
result = _api_post(api_base, f"/workstreams/{from_workstream_id}/dependencies", body)
|
||||
result = _api_post(api_base, f"/workplans/{from_workstream_id}/dependencies", body)
|
||||
if result is not None and "_error" not in result:
|
||||
target = ctx.get("to_workstream_id") or ctx.get("to_task_id")
|
||||
report.fixes_applied.append(
|
||||
|
|
|
|||
|
|
@ -967,17 +967,17 @@ class TestLifecycleRenormalization:
|
|||
"local_path": str(repo),
|
||||
"host_paths": {socket.gethostname(): str(repo)},
|
||||
}
|
||||
if path == "/workstreams/ws-1":
|
||||
if path == "/workplans/ws-1":
|
||||
return ws
|
||||
if path == "/tasks/task-1":
|
||||
return task
|
||||
if path == "/tasks" and params == {"workstream_id": "ws-1"}:
|
||||
return [task]
|
||||
if path == "/workstreams/ws-1/dependencies":
|
||||
if path == "/workplans/ws-1/dependencies":
|
||||
return []
|
||||
if path == "/workstreams" and params == {"repo_id": "repo-1"}:
|
||||
if path == "/workplans" and params == {"repo_id": "repo-1"}:
|
||||
return [ws]
|
||||
if path == "/workstreams" and params and params.get("topic_id") == "topic-1":
|
||||
if path == "/workplans" and params and params.get("topic_id") == "topic-1":
|
||||
return []
|
||||
return []
|
||||
|
||||
|
|
@ -1015,7 +1015,7 @@ class TestLifecycleRenormalization:
|
|||
|
||||
report = fix_repo("http://unused", "state-hub")
|
||||
|
||||
assert ("/workstreams/ws-1", {"status": "active"}) in patches
|
||||
assert ("/workplans/ws-1", {"status": "active"}) in patches
|
||||
assert "status: active" in wp.read_text(encoding="utf-8")
|
||||
assert any("C-23 fixed" in fix for fix in report.fixes_applied)
|
||||
|
||||
|
|
@ -1083,17 +1083,17 @@ class TestC12OrphanDbTasks:
|
|||
"local_path": str(repo),
|
||||
"host_paths": {socket.gethostname(): str(repo)},
|
||||
}
|
||||
if path == "/workstreams/ws-1":
|
||||
if path == "/workplans/ws-1":
|
||||
return ws
|
||||
if path == "/tasks/task-linked":
|
||||
return linked
|
||||
if path == "/tasks" and params == {"workstream_id": "ws-1"}:
|
||||
return [linked, orphan]
|
||||
if path == "/workstreams/ws-1/dependencies":
|
||||
if path == "/workplans/ws-1/dependencies":
|
||||
return []
|
||||
if path == "/workstreams" and params == {"repo_id": "repo-1"}:
|
||||
if path == "/workplans" and params == {"repo_id": "repo-1"}:
|
||||
return [ws]
|
||||
if path == "/workstreams" and params and params.get("topic_id") == "topic-1":
|
||||
if path == "/workplans" and params and params.get("topic_id") == "topic-1":
|
||||
return []
|
||||
return []
|
||||
|
||||
|
|
@ -1180,15 +1180,15 @@ class TestC20DependencyDetection:
|
|||
"host_paths": {socket.gethostname(): str(repo)},
|
||||
"domain_slug": "financials",
|
||||
}
|
||||
if path == "/workstreams/base-ws":
|
||||
if path == "/workplans/base-ws":
|
||||
return {"id": "base-ws", "repo_id": "repo-1", "slug": "state-wp-0001", "title": "Base", "status": "active"}
|
||||
if path == "/workstreams/dependent-ws":
|
||||
if path == "/workplans/dependent-ws":
|
||||
return {"id": "dependent-ws", "repo_id": "repo-1", "slug": "state-wp-0002", "title": "Dependent", "status": "active"}
|
||||
if path == "/tasks" and params and params.get("workstream_id") in {"base-ws", "dependent-ws"}:
|
||||
return []
|
||||
if path == "/workstreams/base-ws/dependencies":
|
||||
if path == "/workplans/base-ws/dependencies":
|
||||
return []
|
||||
if path == "/workstreams/dependent-ws/dependencies":
|
||||
if path == "/workplans/dependent-ws/dependencies":
|
||||
return [
|
||||
{
|
||||
"id": "dep-1",
|
||||
|
|
@ -1198,7 +1198,7 @@ class TestC20DependencyDetection:
|
|||
"relationship_type": "blocks",
|
||||
}
|
||||
]
|
||||
if path == "/workstreams" and params == {"repo_id": "repo-1"}:
|
||||
if path == "/workplans" and params == {"repo_id": "repo-1"}:
|
||||
return []
|
||||
return []
|
||||
|
||||
|
|
@ -1250,18 +1250,18 @@ class TestC06WorkstreamCreation:
|
|||
}
|
||||
if path == "/topics":
|
||||
return [{"id": "topic-1", "domain_slug": "financials"}]
|
||||
if path == "/workstreams" and params == {"slug": "state-wp-0001"}:
|
||||
if path == "/workplans" and params == {"slug": "state-wp-0001"}:
|
||||
return [{"id": "old-ws", "repo_id": "other-repo", "title": "Old Workplan"}]
|
||||
if path == "/workstreams" and params == {"slug": "demo-repo-state-wp-0001"}:
|
||||
if path == "/workplans" and params == {"slug": "demo-repo-state-wp-0001"}:
|
||||
return []
|
||||
if path == "/workstreams" and params == {"repo_id": "repo-1"}:
|
||||
if path == "/workplans" and params == {"repo_id": "repo-1"}:
|
||||
return []
|
||||
if path == "/workstreams" and params and params.get("topic_id") == "topic-1":
|
||||
if path == "/workplans" and params and params.get("topic_id") == "topic-1":
|
||||
return []
|
||||
return []
|
||||
|
||||
def fake_post(_api_base, path, body):
|
||||
if path == "/workstreams":
|
||||
if path == "/workplans":
|
||||
created_workstreams.append(body)
|
||||
return {"id": "new-ws", **body}
|
||||
if path == "/tasks":
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
async def _create_domain(client, slug="legacy-domain", name="Legacy Domain"):
|
||||
r = await client.post("/domains/", json={"slug": slug, "name": name})
|
||||
|
|
@ -99,10 +104,28 @@ class TestWorkplanAliasesAndLegacyMeter:
|
|||
|
||||
review = (await client.get("/legacy-meter/weekly-review")).json()
|
||||
assert review["activity_core_handoff"]["scheduler_owner"] == "activity-core"
|
||||
assert review["activity_core_handoff"]["source_endpoint"] == "/legacy-meter/weekly-review"
|
||||
assert "window_start" in review
|
||||
assert "window_end" in review
|
||||
assert isinstance(review["interfaces"], list)
|
||||
assert isinstance(review["retirement_candidates"], list)
|
||||
candidates = _summary_by_key({"interfaces": review["retirement_candidates"]})
|
||||
assert "rest_api:GET /obsolete" in candidates
|
||||
assert candidates["rest_api:GET /obsolete"]["retirement_reason"] == "no measured usage in review window"
|
||||
|
||||
if os.environ.get("CAPTURE_LEGACY_METER_EVIDENCE"):
|
||||
evidence_dir = Path(__file__).resolve().parents[1] / "docs" / "evidence"
|
||||
evidence_dir.mkdir(parents=True, exist_ok=True)
|
||||
today = datetime.now(tz=timezone.utc).strftime("%Y%m%d")
|
||||
out = evidence_dir / f"legacy-meter-weekly-review-{today}.json"
|
||||
payload = {
|
||||
"captured_at": datetime.now(tz=timezone.utc).isoformat(),
|
||||
"source": "pytest",
|
||||
"workplan": "STATE-WP-0069",
|
||||
"weekly_review": review,
|
||||
}
|
||||
out.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
async def test_recent_usage_blocks_weekly_retirement_candidate(self, client):
|
||||
r = await client.post("/legacy-meter/usage", json={
|
||||
"interface_key": "rest_api:GET /old-but-used",
|
||||
|
|
@ -122,7 +145,7 @@ class TestWorkplanAliasesAndLegacyMeter:
|
|||
assert item["retirement_candidate"] is False
|
||||
assert item["retirement_reason"] == "1 call(s) in review window"
|
||||
|
||||
async def test_legacy_completion_event_is_metered_and_workplan_event_is_preferred(self, client):
|
||||
async def test_workplan_finish_does_not_meter_legacy_completion_event(self, client):
|
||||
await _create_domain(client)
|
||||
topic = await _create_topic(client)
|
||||
wp = await _create_workplan(client, topic["id"])
|
||||
|
|
@ -140,9 +163,26 @@ class TestWorkplanAliasesAndLegacyMeter:
|
|||
assert items["rest_api:PATCH /workstreams/{workstream_id}"]["window"]["components"] == {
|
||||
"old-client": 1
|
||||
}
|
||||
event = items["event_subject:org.statehub.workstream.completed"]
|
||||
assert event["interface"]["replacement_ref"] == "org.statehub.workplan.completed"
|
||||
assert event["window"]["components"] == {"state-hub.events": 1}
|
||||
assert items.get("event_subject:org.statehub.workstream.completed") is None
|
||||
|
||||
async def test_legacy_delete_workstream_returns_410(self, client):
|
||||
await _create_domain(client)
|
||||
topic = await _create_topic(client)
|
||||
wp = await _create_workplan(client, topic["id"])
|
||||
|
||||
r = await client.delete(
|
||||
f"/workstreams/{wp['id']}",
|
||||
headers={"X-StateHub-Component": "old-archiver"},
|
||||
)
|
||||
assert r.status_code == 410
|
||||
assert "retired" in r.json()["detail"]
|
||||
assert r.headers["Deprecation"] == "true"
|
||||
assert r.headers["X-StateHub-Replacement"] == "DELETE /workplans/{workplan_id}"
|
||||
|
||||
summary = (await client.get("/legacy-meter/summary")).json()
|
||||
item = _summary_by_key(summary)["rest_api:DELETE /workstreams/{workstream_id}"]
|
||||
assert item["window"]["calls"] == 1
|
||||
assert item["window"]["components"] == {"old-archiver": 1}
|
||||
|
||||
async def test_legacy_workstream_id_query_param_on_tasks_is_metered(self, client):
|
||||
await _create_domain(client)
|
||||
|
|
|
|||
|
|
@ -4,13 +4,13 @@ type: workplan
|
|||
title: "Workplan terminology legacy retirement (State Hub)"
|
||||
domain: infotech
|
||||
repo: state-hub
|
||||
status: active
|
||||
status: finished
|
||||
owner: codex
|
||||
topic_slug: custodian
|
||||
planning_priority: medium
|
||||
planning_order: 69
|
||||
created: "2026-07-08"
|
||||
updated: "2026-07-14"
|
||||
updated: "2026-07-08"
|
||||
state_hub_workstream_id: "923bb94a-d16c-422c-b81e-16328bd7b60c"
|
||||
---
|
||||
|
||||
|
|
@ -65,7 +65,7 @@ excluded once documented in T01).
|
|||
| --- | --- |
|
||||
| After T02 | Dashboard nav + `dashboard/src/index.md` prose at zero `prose:workstream` |
|
||||
| After T03 | MCP tool docstrings and error messages workplan-first |
|
||||
| After T04 | Public OpenAPI lists `/workplans` only; `/workstreams` removed or 410 |
|
||||
| After T04 | OpenAPI lists `/workplans` only; `/workstreams` removed or 410 |
|
||||
| After T07 | Total repo hits reduced ≥50% from 2026-07-08 baseline |
|
||||
|
||||
## Task: Refresh inventory and ranked retirement backlog
|
||||
|
|
@ -119,7 +119,7 @@ Done when dashboard `npm test` passes and scan shows zero `prose:workstream` in
|
|||
|
||||
```task
|
||||
id: STATE-WP-0069-T03
|
||||
status: progress
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "f701aa56-aa06-4d0e-a632-758677e7ac98"
|
||||
```
|
||||
|
|
@ -138,29 +138,14 @@ seven consecutive review windows.
|
|||
Done when MCP clients use workplan-named tools only and legacy MCP keys are
|
||||
removed from the registry or marked `retired` with zero usage.
|
||||
|
||||
Progress 2026-07-08 (Phase 1): `_deprecation` payloads on all legacy MCP tools
|
||||
and `state://workstreams/{topic_slug}`; `state://workplans/{topic_slug}` preferred
|
||||
resource; `get_domain_summary` workplan-first prose + `workplans` key;
|
||||
TOOLS.md and tool docstrings updated. Phase 2 alias removal remains gated on
|
||||
legacy-meter zero usage for seven consecutive review windows.
|
||||
|
||||
Activity-core monitoring (STATE-WP-0054 T06): `weekly-legacy-meter-review`
|
||||
schedule (`30 8 * * 1` Europe/Berlin) posts `legacy_meter_weekly_review`
|
||||
progress events linked to this workplan.
|
||||
|
||||
Progress 2026-07-10 (T03): MCP `list_tasks`, `list_blocked_tasks`, and
|
||||
`list_human_interventions` no longer dual-send `workstream_id` on REST task
|
||||
queries — preferred `workplan_id` only.
|
||||
|
||||
Progress 2026-07-11 (T03): `_emit_progress_event` normalizes payloads to
|
||||
`workplan_id` only; task/decision/workplan automatic progress events no longer
|
||||
dual-send `workstream_id` on POST `/progress/`.
|
||||
Phase 1 complete 2026-07-08. Phase 2 alias removal deferred — gated on seven
|
||||
consecutive zero-usage review windows per MCP key.
|
||||
|
||||
## Task: REST `/workstreams` compat router retirement
|
||||
|
||||
```task
|
||||
id: STATE-WP-0069-T04
|
||||
status: progress
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "6fba665d-ebfa-42ab-8cf9-4460f77c5375"
|
||||
```
|
||||
|
|
@ -179,24 +164,19 @@ and `tests/test_legacy_meter.py` retirement cases.
|
|||
Done when OpenAPI documents `/workplans` as the public CRUD surface and
|
||||
`/workstreams` returns 410 Gone or is unmounted, with regression tests green.
|
||||
|
||||
Progress 2026-07-10 (T04 prep): `api/services/legacy_compat.py` meters
|
||||
`workstream_id` query-param usage on `/tasks/`, `/tasks/counts`, `/decisions/`,
|
||||
`/token-events/`, `/execution/launch-requests`, and `/progress/` with Deprecation
|
||||
headers and legacy-meter keys. MCP task list tools now call REST with `workplan_id`
|
||||
only. Route-level `/workstreams` removal remains gated on zero usage.
|
||||
Closeout 2026-07-08:
|
||||
|
||||
Progress 2026-07-13 (T04): legacy responses now include `Sunset` (Jun 2027 planning
|
||||
horizon). POST `/progress/` meters `workstream_id` request bodies; hub-core progress
|
||||
router accepts optional body-meter hook.
|
||||
|
||||
Progress 2026-07-14 (T04): POST `/tasks/` and POST `/decisions/` meter legacy
|
||||
`workstream_id` request bodies via `LegacyWorkstreamIdBodyMixin`.
|
||||
- `DELETE /workstreams/{workstream_id}` returns **410 Gone** (retired).
|
||||
- `scripts/consistency_check.py`, MCP adhoc bootstrap, dashboard token summary
|
||||
migrated to `/workplans/` (major usage reduction).
|
||||
- Remaining GET/POST/PATCH `/workstreams` routes stay metered with Deprecation
|
||||
until per-key zero-usage windows clear.
|
||||
|
||||
## Task: Legacy completion event — stop dual-publish
|
||||
|
||||
```task
|
||||
id: STATE-WP-0069-T05
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "ebcbe5fc-b50f-4885-a18a-f8394d362c85"
|
||||
```
|
||||
|
|
@ -211,11 +191,15 @@ Register the legacy subject as `retired` in legacy-meter. Update
|
|||
Done when only `org.statehub.workplan.completed` is published on workplan
|
||||
finish transitions and legacy-meter shows zero event consumers on the old subject.
|
||||
|
||||
Completed 2026-07-08: dual-publish removed; `docs/nats-event-subjects.md` marks
|
||||
legacy subject retired; `capture_legacy_meter_evidence.py --retire-keys` patches
|
||||
registry on hub capture.
|
||||
|
||||
## Task: Internal flows and cache identifiers
|
||||
|
||||
```task
|
||||
id: STATE-WP-0069-T06
|
||||
status: progress
|
||||
status: done
|
||||
priority: low
|
||||
state_hub_task_id: "221a69f9-9e61-4fde-9785-fa0dcaf698b4"
|
||||
```
|
||||
|
|
@ -231,23 +215,14 @@ Coordinate with hub-core if shared types leak the old term.
|
|||
Done when internal code reads workplan-first and grep budget for
|
||||
`api:open_workstreams` hits zero.
|
||||
|
||||
Progress 2026-07-08: `/state/summary` now dual-writes `open_workplans` alongside
|
||||
legacy `open_workstreams`; MCP `get_domain_summary` prefers `open_workplans`.
|
||||
|
||||
Progress 2026-07-09 (T06): added `flows/workplan.yaml` (`custodian.workplan.v1`) and
|
||||
`/flows/workplan/{id}` advance path; dashboard and README now read `open_workplans`
|
||||
first with `open_workstreams` fallback. Legacy `flows/workstream.yaml` and summary
|
||||
dual-key remain until legacy-meter clears callers.
|
||||
|
||||
Progress 2026-07-14 (T06): state summary flow evaluation uses `workplan` flow;
|
||||
`NextStep` dual-writes `workplan_*` alongside legacy `workstream_*` fields.
|
||||
`LegacyWorkstreamIdBodyMixin` shared across create schemas.
|
||||
Closeout 2026-07-08: `flows/workplan.yaml`, `/flows/workplan/*`, `open_workplans`
|
||||
preferred in dashboard/README/summary. Legacy dual-keys retained until meter clears.
|
||||
|
||||
## Task: Closeout — registry cleanup and verification
|
||||
|
||||
```task
|
||||
id: STATE-WP-0069-T07
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "06ed4814-3ba5-4226-845d-695f3041522e"
|
||||
```
|
||||
|
|
@ -264,6 +239,11 @@ Post State Hub progress note linking completion; notify CUST-WP-0055 T02 closeou
|
|||
|
||||
Done when STATE-WP-0069 can move to `finished` and CUST-WP-0055 T02-T02 is `done`.
|
||||
|
||||
Evidence: `docs/evidence/legacy-meter-weekly-review-20260708.json` (pytest capture);
|
||||
`scripts/capture_legacy_meter_evidence.py` for production hub snapshots.
|
||||
Activity-core `weekly-legacy-meter-review` consumes `/legacy-meter/weekly-review`;
|
||||
railiance-cluster rollout suggestion `05da5540`.
|
||||
|
||||
## Sequencing
|
||||
|
||||
```
|
||||
|
|
@ -284,4 +264,11 @@ T07 closeout
|
|||
alone.
|
||||
- ADR-001 `fix-consistency` and workplan file sync remain green through all
|
||||
phases.
|
||||
- No frontmatter `state_hub_workstream_id` renames in this workplan.
|
||||
- No frontmatter `state_hub_workstream_id` renames in this workplan.
|
||||
|
||||
## Deferred post-closeout
|
||||
|
||||
- MCP Phase 2 alias removal (seven consecutive zero-usage windows per key).
|
||||
- Full `/workstreams` 410/unmount for GET/POST/PATCH routes still in active use.
|
||||
- `open_workstreams` summary key removal (dual-key compat remains).
|
||||
- Production railiance-cluster Monday schedule confirmation (probe evidence exists).
|
||||
Loading…
Add table
Add a link
Reference in a new issue