feat(classification-spine): implement STATE-WP-0065 repo-anchored model

Replace the ad-hoc coordination-domain spine with the Repo Classification
Standard: 14 market domains, classification columns on managed_repos, and
workplans anchored by repo_id (topic_id optional).

- Add Alembic migration d8e9f0a1b2c3 with data backfill and workstream→workplan rename
- Add api/classification.py validation and register-from-classification tooling
- Expose workplan-first REST/MCP surface with legacy workstream aliases
- Add C-24 consistency rule and legacy domain frontmatter mapping
- Update dashboard repos page with category/capability/stake filters
- Update orientation docs; mark STATE-WP-0065 finished
This commit is contained in:
2026-06-22 13:52:13 +02:00
parent 279be4ffbd
commit 0949d4c0d8
84 changed files with 4494 additions and 1111 deletions

View File

@@ -19,15 +19,16 @@ async def _call_tool(name: str, arguments: dict[str, Any]) -> dict[str, Any]:
class TestMCPWriteTools:
async def test_create_workstream_returns_rest_shape_and_emits_progress(self, monkeypatch):
async def test_create_workplan_returns_rest_shape_and_emits_progress(self, monkeypatch):
calls: list[tuple[str, dict[str, Any]]] = []
def fake_post(path: str, body: dict[str, Any]) -> dict[str, Any]:
calls.append((path, body))
if path == "/workstreams":
if path == "/workplans":
return {
"id": "ws-1",
"topic_id": body["topic_id"],
"id": "wp-1",
"repo_id": body["repo_id"],
"topic_id": body.get("topic_id"),
"title": body["title"],
"slug": body["slug"],
"status": body["status"],
@@ -39,20 +40,42 @@ class TestMCPWriteTools:
monkeypatch.setattr(server, "_post", fake_post)
body = await _call_tool(
"create_workstream",
{"topic_id": "topic-1", "title": "MCP Reliable Write"},
"create_workplan",
{"repo_id": "repo-1", "topic_id": "topic-1", "title": "MCP Reliable Write"},
)
assert body == {
"id": "ws-1",
"id": "wp-1",
"repo_id": "repo-1",
"topic_id": "topic-1",
"title": "MCP Reliable Write",
"slug": "mcp-reliable-write",
"status": "active",
}
assert [path for path, _ in calls] == ["/workstreams", "/progress"]
assert calls[1][1]["workstream_id"] == "ws-1"
assert calls[1][1]["event_type"] == "workstream_created"
assert [path for path, _ in calls] == ["/workplans", "/progress"]
assert calls[1][1]["workplan_id"] == "wp-1"
assert calls[1][1]["event_type"] == "workplan_created"
async def test_create_workstream_legacy_alias_uses_workplans_endpoint(self, monkeypatch):
calls: list[tuple[str, dict[str, Any]]] = []
def fake_post(path: str, body: dict[str, Any]) -> dict[str, Any]:
calls.append((path, body))
if path == "/workplans":
return {"id": "wp-1", "repo_id": body["repo_id"], "title": body["title"], "slug": body["slug"], "status": "active"}
if path == "/progress":
return {"id": "event-1", **body}
raise AssertionError(f"unexpected POST {path}")
monkeypatch.setattr(server, "_post", fake_post)
body = await _call_tool(
"create_workstream",
{"repo_id": "repo-1", "title": "Legacy alias"},
)
assert body["id"] == "wp-1"
assert [path for path, _ in calls] == ["/workplans", "/progress"]
async def test_create_task_returns_rest_shape_and_emits_progress(self, monkeypatch):
calls: list[tuple[str, dict[str, Any]]] = []
@@ -62,7 +85,8 @@ class TestMCPWriteTools:
if path == "/tasks":
return {
"id": "task-1",
"workstream_id": body["workstream_id"],
"workplan_id": body.get("workplan_id") or body.get("workstream_id"),
"workstream_id": body.get("workplan_id") or body.get("workstream_id"),
"title": body["title"],
"priority": body["priority"],
"status": "todo",
@@ -80,6 +104,7 @@ class TestMCPWriteTools:
assert body == {
"id": "task-1",
"workplan_id": "ws-1",
"workstream_id": "ws-1",
"title": "MCP task",
"priority": "high",
@@ -266,18 +291,18 @@ class TestMCPWriteTools:
def fake_post(path: str, body: dict[str, Any]) -> dict[str, Any]:
calls.append((path, body))
return {"error": "API 422: invalid topic"}
return {"error": "API 422: invalid repo"}
monkeypatch.setattr(server, "_post", fake_post)
body = await _call_tool(
"create_workstream",
{"topic_id": "bad-topic", "title": "No progress on failure"},
{"repo_id": "bad-repo", "title": "No progress on failure"},
)
assert body["tool"] == "create_workstream"
assert body["error"] == "API 422: invalid topic"
assert [path for path, _ in calls] == ["/workstreams"]
assert body["error"] == "API 422: invalid repo"
assert [path for path, _ in calls] == ["/workplans"]
async def test_record_decision_missing_id_is_clear_and_skips_progress(self, monkeypatch):
calls: list[tuple[str, dict[str, Any]]] = []