feat(state-hub): Interface Change Registry (CUST-WP-0033 T01-T06)
Adds first-class tracking for API and interface mutations across the
agent ecosystem. Breaking changes are documented, affected repos are
notified via inbox, and agents discover pending changes at session
start via the dispatch endpoint.
- Migration q4l5m6n7o8p9: interface_changes table
- Model/schema: InterfaceChange with draft→published→resolved lifecycle
- Router: POST/GET/PATCH /interface-changes/, /publish, /resolve actions
(auto-notify affected repo agents on publish; progress event on origin)
- Dispatch: GET /repos/{slug}/dispatch now returns pending_interface_changes
- MCP tools: register_interface_change, list_interface_changes,
publish_interface_change, resolve_interface_change
- Dashboard: /interface-changes page with type badges, planned calendar,
published cards, and draft table
- EP-CUST-ICR-001 registered: webhook subscriptions (deliberately deferred)
First record: trailing-slash normalisation (2026-04-26), published,
affecting repo-registry — visible in repo-registry dispatch immediately.
223 tests passing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 15:29:08 +02:00
|
|
|
import uuid
|
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
|
|
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
|
|
|
|
from sqlalchemy import select
|
|
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
|
|
|
|
|
|
from api.database import get_session
|
|
|
|
|
from api.models.agent_message import AgentMessage
|
|
|
|
|
from api.models.interface_change import InterfaceChange
|
|
|
|
|
from api.models.progress_event import ProgressEvent
|
|
|
|
|
from api.schemas.interface_change import (
|
|
|
|
|
InterfaceChangeCreate,
|
|
|
|
|
InterfaceChangePatch,
|
|
|
|
|
InterfaceChangeRead,
|
|
|
|
|
)
|
2026-08-29 10:47:51 +02:00
|
|
|
from api.services.repository_aliases import (
|
|
|
|
|
affected_slug_predicate,
|
|
|
|
|
canonicalize_repository_slug,
|
|
|
|
|
resolve_repository_slug,
|
|
|
|
|
)
|
feat(state-hub): Interface Change Registry (CUST-WP-0033 T01-T06)
Adds first-class tracking for API and interface mutations across the
agent ecosystem. Breaking changes are documented, affected repos are
notified via inbox, and agents discover pending changes at session
start via the dispatch endpoint.
- Migration q4l5m6n7o8p9: interface_changes table
- Model/schema: InterfaceChange with draft→published→resolved lifecycle
- Router: POST/GET/PATCH /interface-changes/, /publish, /resolve actions
(auto-notify affected repo agents on publish; progress event on origin)
- Dispatch: GET /repos/{slug}/dispatch now returns pending_interface_changes
- MCP tools: register_interface_change, list_interface_changes,
publish_interface_change, resolve_interface_change
- Dashboard: /interface-changes page with type badges, planned calendar,
published cards, and draft table
- EP-CUST-ICR-001 registered: webhook subscriptions (deliberately deferred)
First record: trailing-slash normalisation (2026-04-26), published,
affecting repo-registry — visible in repo-registry dispatch immediately.
223 tests passing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 15:29:08 +02:00
|
|
|
|
|
|
|
|
router = APIRouter(prefix="/interface-changes", tags=["interface-changes"])
|
|
|
|
|
|
|
|
|
|
_VALID_INTERFACE_TYPES = {"rest_api", "mcp_tool", "cli", "schema", "capability"}
|
|
|
|
|
_VALID_CHANGE_TYPES = {"breaking", "additive", "deprecation", "removal"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/", response_model=InterfaceChangeRead, status_code=status.HTTP_201_CREATED)
|
|
|
|
|
async def create_interface_change(
|
|
|
|
|
body: InterfaceChangeCreate,
|
|
|
|
|
session: AsyncSession = Depends(get_session),
|
|
|
|
|
) -> InterfaceChangeRead:
|
|
|
|
|
if body.interface_type not in _VALID_INTERFACE_TYPES:
|
|
|
|
|
raise HTTPException(status_code=422, detail=f"interface_type must be one of {sorted(_VALID_INTERFACE_TYPES)}")
|
|
|
|
|
if body.change_type not in _VALID_CHANGE_TYPES:
|
|
|
|
|
raise HTTPException(status_code=422, detail=f"change_type must be one of {sorted(_VALID_CHANGE_TYPES)}")
|
|
|
|
|
|
2026-08-29 10:47:51 +02:00
|
|
|
resolution = await resolve_repository_slug(session, body.repo_slug)
|
|
|
|
|
repo = resolution.repo
|
|
|
|
|
affected_repo_slugs = [
|
|
|
|
|
await canonicalize_repository_slug(session, slug)
|
|
|
|
|
for slug in body.affected_repo_slugs
|
|
|
|
|
]
|
feat(state-hub): Interface Change Registry (CUST-WP-0033 T01-T06)
Adds first-class tracking for API and interface mutations across the
agent ecosystem. Breaking changes are documented, affected repos are
notified via inbox, and agents discover pending changes at session
start via the dispatch endpoint.
- Migration q4l5m6n7o8p9: interface_changes table
- Model/schema: InterfaceChange with draft→published→resolved lifecycle
- Router: POST/GET/PATCH /interface-changes/, /publish, /resolve actions
(auto-notify affected repo agents on publish; progress event on origin)
- Dispatch: GET /repos/{slug}/dispatch now returns pending_interface_changes
- MCP tools: register_interface_change, list_interface_changes,
publish_interface_change, resolve_interface_change
- Dashboard: /interface-changes page with type badges, planned calendar,
published cards, and draft table
- EP-CUST-ICR-001 registered: webhook subscriptions (deliberately deferred)
First record: trailing-slash normalisation (2026-04-26), published,
affecting repo-registry — visible in repo-registry dispatch immediately.
223 tests passing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 15:29:08 +02:00
|
|
|
change = InterfaceChange(
|
|
|
|
|
repo_id=repo.id,
|
|
|
|
|
interface_type=body.interface_type,
|
|
|
|
|
change_type=body.change_type,
|
|
|
|
|
title=body.title,
|
|
|
|
|
description=body.description,
|
|
|
|
|
affected_paths=body.affected_paths,
|
2026-08-29 10:47:51 +02:00
|
|
|
affected_repo_slugs=affected_repo_slugs,
|
feat(state-hub): Interface Change Registry (CUST-WP-0033 T01-T06)
Adds first-class tracking for API and interface mutations across the
agent ecosystem. Breaking changes are documented, affected repos are
notified via inbox, and agents discover pending changes at session
start via the dispatch endpoint.
- Migration q4l5m6n7o8p9: interface_changes table
- Model/schema: InterfaceChange with draft→published→resolved lifecycle
- Router: POST/GET/PATCH /interface-changes/, /publish, /resolve actions
(auto-notify affected repo agents on publish; progress event on origin)
- Dispatch: GET /repos/{slug}/dispatch now returns pending_interface_changes
- MCP tools: register_interface_change, list_interface_changes,
publish_interface_change, resolve_interface_change
- Dashboard: /interface-changes page with type badges, planned calendar,
published cards, and draft table
- EP-CUST-ICR-001 registered: webhook subscriptions (deliberately deferred)
First record: trailing-slash normalisation (2026-04-26), published,
affecting repo-registry — visible in repo-registry dispatch immediately.
223 tests passing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 15:29:08 +02:00
|
|
|
planned_for=body.planned_for,
|
|
|
|
|
author=body.author,
|
|
|
|
|
status="draft",
|
|
|
|
|
)
|
|
|
|
|
session.add(change)
|
|
|
|
|
await session.commit()
|
|
|
|
|
await session.refresh(change)
|
|
|
|
|
return InterfaceChangeRead.from_orm_with_slug(change)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/", response_model=list[InterfaceChangeRead])
|
|
|
|
|
async def list_interface_changes(
|
|
|
|
|
repo_slug: str | None = Query(None),
|
|
|
|
|
status: str | None = Query(None),
|
|
|
|
|
change_type: str | None = Query(None),
|
|
|
|
|
affected_repo: str | None = Query(None, description="Return changes that affect this repo slug"),
|
|
|
|
|
session: AsyncSession = Depends(get_session),
|
|
|
|
|
) -> list[InterfaceChangeRead]:
|
|
|
|
|
q = select(InterfaceChange).order_by(InterfaceChange.created_at.desc())
|
|
|
|
|
if repo_slug:
|
|
|
|
|
repo = await _repo_by_slug(repo_slug, session)
|
|
|
|
|
q = q.where(InterfaceChange.repo_id == repo.id)
|
|
|
|
|
if status:
|
|
|
|
|
q = q.where(InterfaceChange.status == status)
|
|
|
|
|
if change_type:
|
|
|
|
|
q = q.where(InterfaceChange.change_type == change_type)
|
|
|
|
|
if affected_repo:
|
2026-08-29 10:47:51 +02:00
|
|
|
resolution = await resolve_repository_slug(
|
|
|
|
|
session, affected_repo, required=False
|
|
|
|
|
)
|
|
|
|
|
values = resolution.slug_values if resolution else (affected_repo,)
|
|
|
|
|
q = q.where(affected_slug_predicate(InterfaceChange.affected_repo_slugs, values))
|
feat(state-hub): Interface Change Registry (CUST-WP-0033 T01-T06)
Adds first-class tracking for API and interface mutations across the
agent ecosystem. Breaking changes are documented, affected repos are
notified via inbox, and agents discover pending changes at session
start via the dispatch endpoint.
- Migration q4l5m6n7o8p9: interface_changes table
- Model/schema: InterfaceChange with draft→published→resolved lifecycle
- Router: POST/GET/PATCH /interface-changes/, /publish, /resolve actions
(auto-notify affected repo agents on publish; progress event on origin)
- Dispatch: GET /repos/{slug}/dispatch now returns pending_interface_changes
- MCP tools: register_interface_change, list_interface_changes,
publish_interface_change, resolve_interface_change
- Dashboard: /interface-changes page with type badges, planned calendar,
published cards, and draft table
- EP-CUST-ICR-001 registered: webhook subscriptions (deliberately deferred)
First record: trailing-slash normalisation (2026-04-26), published,
affecting repo-registry — visible in repo-registry dispatch immediately.
223 tests passing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 15:29:08 +02:00
|
|
|
result = await session.execute(q)
|
|
|
|
|
return [InterfaceChangeRead.from_orm_with_slug(c) for c in result.scalars().all()]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/{change_id}", response_model=InterfaceChangeRead)
|
|
|
|
|
async def get_interface_change(
|
|
|
|
|
change_id: uuid.UUID,
|
|
|
|
|
session: AsyncSession = Depends(get_session),
|
|
|
|
|
) -> InterfaceChangeRead:
|
|
|
|
|
change = await _get_or_404(change_id, session)
|
|
|
|
|
return InterfaceChangeRead.from_orm_with_slug(change)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.patch("/{change_id}", response_model=InterfaceChangeRead)
|
|
|
|
|
async def patch_interface_change(
|
|
|
|
|
change_id: uuid.UUID,
|
|
|
|
|
body: InterfaceChangePatch,
|
|
|
|
|
session: AsyncSession = Depends(get_session),
|
|
|
|
|
) -> InterfaceChangeRead:
|
|
|
|
|
change = await _get_or_404(change_id, session)
|
|
|
|
|
if change.status != "draft":
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=409,
|
|
|
|
|
detail=f"Cannot edit a change with status '{change.status}'. Only draft records are mutable.",
|
|
|
|
|
)
|
2026-08-29 10:47:51 +02:00
|
|
|
payload = body.model_dump(exclude_unset=True)
|
|
|
|
|
if payload.get("affected_repo_slugs") is not None:
|
|
|
|
|
payload["affected_repo_slugs"] = [
|
|
|
|
|
await canonicalize_repository_slug(session, slug)
|
|
|
|
|
for slug in payload["affected_repo_slugs"]
|
|
|
|
|
]
|
|
|
|
|
for field, value in payload.items():
|
feat(state-hub): Interface Change Registry (CUST-WP-0033 T01-T06)
Adds first-class tracking for API and interface mutations across the
agent ecosystem. Breaking changes are documented, affected repos are
notified via inbox, and agents discover pending changes at session
start via the dispatch endpoint.
- Migration q4l5m6n7o8p9: interface_changes table
- Model/schema: InterfaceChange with draft→published→resolved lifecycle
- Router: POST/GET/PATCH /interface-changes/, /publish, /resolve actions
(auto-notify affected repo agents on publish; progress event on origin)
- Dispatch: GET /repos/{slug}/dispatch now returns pending_interface_changes
- MCP tools: register_interface_change, list_interface_changes,
publish_interface_change, resolve_interface_change
- Dashboard: /interface-changes page with type badges, planned calendar,
published cards, and draft table
- EP-CUST-ICR-001 registered: webhook subscriptions (deliberately deferred)
First record: trailing-slash normalisation (2026-04-26), published,
affecting repo-registry — visible in repo-registry dispatch immediately.
223 tests passing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 15:29:08 +02:00
|
|
|
setattr(change, field, value)
|
|
|
|
|
await session.commit()
|
|
|
|
|
await session.refresh(change)
|
|
|
|
|
return InterfaceChangeRead.from_orm_with_slug(change)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/{change_id}/publish", response_model=InterfaceChangeRead)
|
|
|
|
|
async def publish_interface_change(
|
|
|
|
|
change_id: uuid.UUID,
|
|
|
|
|
session: AsyncSession = Depends(get_session),
|
|
|
|
|
) -> InterfaceChangeRead:
|
|
|
|
|
change = await _get_or_404(change_id, session)
|
|
|
|
|
if change.status != "draft":
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=409,
|
|
|
|
|
detail=f"Cannot publish a change with status '{change.status}'. Must be 'draft'.",
|
|
|
|
|
)
|
|
|
|
|
now = datetime.now(tz=timezone.utc)
|
|
|
|
|
change.status = "published"
|
|
|
|
|
change.published_at = now
|
|
|
|
|
|
|
|
|
|
# Send inbox notifications to agents of affected repos
|
|
|
|
|
affected = change.affected_repo_slugs or []
|
|
|
|
|
for slug in affected:
|
2026-08-29 10:47:51 +02:00
|
|
|
target_slug = await canonicalize_repository_slug(session, slug)
|
feat(state-hub): Interface Change Registry (CUST-WP-0033 T01-T06)
Adds first-class tracking for API and interface mutations across the
agent ecosystem. Breaking changes are documented, affected repos are
notified via inbox, and agents discover pending changes at session
start via the dispatch endpoint.
- Migration q4l5m6n7o8p9: interface_changes table
- Model/schema: InterfaceChange with draft→published→resolved lifecycle
- Router: POST/GET/PATCH /interface-changes/, /publish, /resolve actions
(auto-notify affected repo agents on publish; progress event on origin)
- Dispatch: GET /repos/{slug}/dispatch now returns pending_interface_changes
- MCP tools: register_interface_change, list_interface_changes,
publish_interface_change, resolve_interface_change
- Dashboard: /interface-changes page with type badges, planned calendar,
published cards, and draft table
- EP-CUST-ICR-001 registered: webhook subscriptions (deliberately deferred)
First record: trailing-slash normalisation (2026-04-26), published,
affecting repo-registry — visible in repo-registry dispatch immediately.
223 tests passing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 15:29:08 +02:00
|
|
|
paths_summary = ", ".join(change.affected_paths[:5]) if change.affected_paths else "see description"
|
|
|
|
|
if len(change.affected_paths) > 5:
|
|
|
|
|
paths_summary += f" (+{len(change.affected_paths) - 5} more)"
|
|
|
|
|
msg = AgentMessage(
|
|
|
|
|
from_agent=change.repo.slug,
|
2026-08-29 10:47:51 +02:00
|
|
|
to_agent=target_slug,
|
feat(state-hub): Interface Change Registry (CUST-WP-0033 T01-T06)
Adds first-class tracking for API and interface mutations across the
agent ecosystem. Breaking changes are documented, affected repos are
notified via inbox, and agents discover pending changes at session
start via the dispatch endpoint.
- Migration q4l5m6n7o8p9: interface_changes table
- Model/schema: InterfaceChange with draft→published→resolved lifecycle
- Router: POST/GET/PATCH /interface-changes/, /publish, /resolve actions
(auto-notify affected repo agents on publish; progress event on origin)
- Dispatch: GET /repos/{slug}/dispatch now returns pending_interface_changes
- MCP tools: register_interface_change, list_interface_changes,
publish_interface_change, resolve_interface_change
- Dashboard: /interface-changes page with type badges, planned calendar,
published cards, and draft table
- EP-CUST-ICR-001 registered: webhook subscriptions (deliberately deferred)
First record: trailing-slash normalisation (2026-04-26), published,
affecting repo-registry — visible in repo-registry dispatch immediately.
223 tests passing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 15:29:08 +02:00
|
|
|
subject=f"[{change.change_type.upper()}] {change.title}",
|
|
|
|
|
body=(
|
|
|
|
|
f"**Interface change published by `{change.repo.slug}`**\n\n"
|
|
|
|
|
f"- Type: `{change.interface_type}` / `{change.change_type}`\n"
|
|
|
|
|
f"- Affected paths: {paths_summary}\n\n"
|
|
|
|
|
f"{change.description}\n\n"
|
|
|
|
|
f"Change ID: `{change.id}` — resolve with "
|
|
|
|
|
f"`POST /interface-changes/{change.id}/resolve` once adapted."
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
session.add(msg)
|
|
|
|
|
|
|
|
|
|
# Progress event on the originating repo
|
|
|
|
|
session.add(ProgressEvent(
|
|
|
|
|
event_type="milestone",
|
|
|
|
|
summary=f"Interface change published: {change.title}",
|
|
|
|
|
detail={
|
|
|
|
|
"change_id": str(change.id),
|
|
|
|
|
"change_type": change.change_type,
|
|
|
|
|
"interface_type": change.interface_type,
|
|
|
|
|
"affected_repos": affected,
|
|
|
|
|
"notifications_sent": len(affected),
|
|
|
|
|
},
|
|
|
|
|
author=change.author,
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
await session.commit()
|
|
|
|
|
await session.refresh(change)
|
|
|
|
|
return InterfaceChangeRead.from_orm_with_slug(change)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/{change_id}/resolve", response_model=InterfaceChangeRead)
|
|
|
|
|
async def resolve_interface_change(
|
|
|
|
|
change_id: uuid.UUID,
|
|
|
|
|
session: AsyncSession = Depends(get_session),
|
|
|
|
|
) -> InterfaceChangeRead:
|
|
|
|
|
change = await _get_or_404(change_id, session)
|
|
|
|
|
if change.status != "published":
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=409,
|
|
|
|
|
detail=f"Cannot resolve a change with status '{change.status}'. Must be 'published'.",
|
|
|
|
|
)
|
|
|
|
|
change.status = "resolved"
|
|
|
|
|
change.resolved_at = datetime.now(tz=timezone.utc)
|
|
|
|
|
await session.commit()
|
|
|
|
|
await session.refresh(change)
|
|
|
|
|
return InterfaceChangeRead.from_orm_with_slug(change)
|
|
|
|
|
|
|
|
|
|
|
2026-08-29 10:47:51 +02:00
|
|
|
async def _repo_by_slug(slug: str, session: AsyncSession):
|
|
|
|
|
resolution = await resolve_repository_slug(session, slug)
|
|
|
|
|
return resolution.repo
|
feat(state-hub): Interface Change Registry (CUST-WP-0033 T01-T06)
Adds first-class tracking for API and interface mutations across the
agent ecosystem. Breaking changes are documented, affected repos are
notified via inbox, and agents discover pending changes at session
start via the dispatch endpoint.
- Migration q4l5m6n7o8p9: interface_changes table
- Model/schema: InterfaceChange with draft→published→resolved lifecycle
- Router: POST/GET/PATCH /interface-changes/, /publish, /resolve actions
(auto-notify affected repo agents on publish; progress event on origin)
- Dispatch: GET /repos/{slug}/dispatch now returns pending_interface_changes
- MCP tools: register_interface_change, list_interface_changes,
publish_interface_change, resolve_interface_change
- Dashboard: /interface-changes page with type badges, planned calendar,
published cards, and draft table
- EP-CUST-ICR-001 registered: webhook subscriptions (deliberately deferred)
First record: trailing-slash normalisation (2026-04-26), published,
affecting repo-registry — visible in repo-registry dispatch immediately.
223 tests passing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 15:29:08 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _get_or_404(change_id: uuid.UUID, session: AsyncSession) -> InterfaceChange:
|
|
|
|
|
result = await session.execute(
|
|
|
|
|
select(InterfaceChange).where(InterfaceChange.id == change_id)
|
|
|
|
|
)
|
|
|
|
|
change = result.scalar_one_or_none()
|
|
|
|
|
if change is None:
|
|
|
|
|
raise HTTPException(status_code=404, detail=f"InterfaceChange '{change_id}' not found")
|
|
|
|
|
return change
|