state-hub/api/schemas/managed_repo.py
tegwick 57c3e08103
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Build and Publish Multi-Context Image / build-and-push (push) Successful in 26s
fix(classification): harden registration updates
Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a02b22-9638-76d2-bbff-b7ea1770b118
2026-08-23 11:30:36 +02:00

180 lines
5.2 KiB
Python

import uuid
from datetime import date, datetime
from typing import Any, Literal
from fastapi import HTTPException
from pydantic import BaseModel, Field, model_validator
from api.classification import validate_classification
from hub_core.schemas.managed_repo import (
RepoCreate as CoreRepoCreate,
RepoPathRegister,
RepoRead as CoreRepoRead,
)
class ClassificationFields(BaseModel):
category: str | None = None
secondary_domains: list[str] | None = None
capability_tags: list[str] | None = None
business_stake: list[str] | None = None
business_mechanics: list[str] | None = None
classified_at: date | None = None
classified_by: str | None = None
standard_version: str | None = None
def classification_fields_set(data: dict[str, Any]) -> bool:
keys = (
"category",
"secondary_domains",
"capability_tags",
"business_stake",
"business_mechanics",
"classified_at",
"classified_by",
"standard_version",
)
return any(data.get(key) is not None for key in keys)
def validate_repo_classification_fields(
*,
domain_slug: str,
fields: dict[str, Any],
require_complete: bool = False,
) -> dict[str, Any]:
"""Validate classification fields and return normalized values for persistence."""
if not classification_fields_set(fields) and not require_complete:
return fields
block = {
"category": fields.get("category"),
"domain": domain_slug,
"secondary_domains": fields.get("secondary_domains") or [],
"capability_tags": fields.get("capability_tags") or [],
"business_stake": fields.get("business_stake") or [],
"business_mechanics": fields.get("business_mechanics") or [],
}
if require_complete or fields.get("category") is not None:
if block["category"] is None:
raise HTTPException(status_code=422, detail="`category` is required when classification is provided")
if classification_fields_set(fields) and block["category"] is not None:
errors, warnings = validate_classification(block)
if errors:
raise HTTPException(status_code=422, detail={"classification_errors": errors, "warnings": warnings})
return fields
class RepoCreate(CoreRepoCreate, ClassificationFields):
topic_id: uuid.UUID | None = None
@model_validator(mode="after")
def validate_classification_on_create(self) -> "RepoCreate":
validate_repo_classification_fields(
domain_slug=self.domain_slug,
fields=self.model_dump(),
require_complete=classification_fields_set(self.model_dump()),
)
return self
class RepoUpdate(ClassificationFields):
domain_slug: str | None = None
name: str | None = None
local_path: str | None = None
remote_url: str | None = None
git_fingerprint: str | None = None
description: str | None = None
topic_id: uuid.UUID | None = None
last_state_synced_at: datetime | None = None
class RepoOnboardRequest(BaseModel):
"""Start scripted onboarding for a working copy that is visible to State Hub."""
domain_slug: str
project_path: str
agent_profile: Literal["claude-code", "codex"] = "codex"
additional: bool = False
class RepoOnboardResult(BaseModel):
ok: bool
repo_slug: str | None = None
agent_profile: str
command: list[str]
stdout: str = ""
stderr: str = ""
class RepoRead(CoreRepoRead, ClassificationFields):
topic_id: uuid.UUID | None = None
sbom_source: str | None = None
last_sbom_at: datetime | None = None
last_state_synced_at: datetime | None = None
created_at: datetime
updated_at: datetime
class DispatchTask(BaseModel):
id: uuid.UUID
title: str
priority: str
status: str
needs_human: bool
class DispatchWorkplan(BaseModel):
id: uuid.UUID
title: str
status: str
pending_tasks: list[DispatchTask]
# Legacy alias
DispatchWorkstream = DispatchWorkplan
class PendingInterfaceChange(BaseModel):
id: uuid.UUID
title: str
change_type: str
interface_type: str
origin_repo_slug: str
affected_paths: list[str]
planned_for: date | None
published_at: datetime | None
class ScopeIssueDetail(BaseModel):
id: str
label: str
status: str
detail: str
missing_sections: list[str] = Field(default_factory=list)
invalid_capability_blocks: list[dict[str, Any]] = Field(default_factory=list)
needs_refresh_sections: list[str] = Field(default_factory=list)
class RepoDispatch(BaseModel):
repo_slug: str
active_goal: dict[str, Any] | None
active_workplans: list[DispatchWorkplan]
human_interventions: list[DispatchTask]
pending_interface_changes: list[PendingInterfaceChange]
scope_needs_review: bool
scope_issue_details: list[ScopeIssueDetail]
last_state_synced_at: datetime | None
@property
def active_workstreams(self) -> list[DispatchWorkplan]:
return self.active_workplans
class RepoScopeHealth(BaseModel):
repo_slug: str
domain_slug: str | None = None
local_path: str | None = None
path_available: bool
scope_needs_review: bool
scope_issue_details: list[ScopeIssueDetail]