generated from coulomb/repo-seed
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:
@@ -1,4 +1,5 @@
|
||||
from api.schemas.topic import TopicCreate, TopicUpdate, TopicRead, TopicWithWorkstreams
|
||||
from api.schemas.workplan import WorkplanCreate, WorkplanUpdate, WorkplanRead
|
||||
from api.schemas.workstream import WorkstreamCreate, WorkstreamUpdate, WorkstreamRead
|
||||
from api.schemas.task import TaskCreate, TaskUpdate, TaskRead
|
||||
from api.schemas.decision import DecisionCreate, DecisionUpdate, DecisionRead
|
||||
@@ -9,6 +10,7 @@ from api.schemas.technical_debt import TDCreate, TDUpdate, TDRead
|
||||
|
||||
__all__ = [
|
||||
"TopicCreate", "TopicUpdate", "TopicRead", "TopicWithWorkstreams",
|
||||
"WorkplanCreate", "WorkplanUpdate", "WorkplanRead",
|
||||
"WorkstreamCreate", "WorkstreamUpdate", "WorkstreamRead",
|
||||
"TaskCreate", "TaskUpdate", "TaskRead",
|
||||
"DecisionCreate", "DecisionUpdate", "DecisionRead",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from pydantic import AliasChoices, BaseModel, ConfigDict, Field, computed_field
|
||||
|
||||
from hub_core.schemas.capability import (
|
||||
CapabilityRequestDispute,
|
||||
@@ -23,20 +23,29 @@ class CapabilityRequestCreate(BaseModel):
|
||||
priority: str = "medium"
|
||||
requesting_domain: str # slug, resolved to domain_id in router
|
||||
requesting_agent: str
|
||||
requesting_workstream_id: uuid.UUID | None = None
|
||||
requesting_workplan_id: uuid.UUID | None = Field(
|
||||
default=None,
|
||||
validation_alias=AliasChoices("requesting_workplan_id", "requesting_workstream_id"),
|
||||
)
|
||||
blocking_task_id: uuid.UUID | None = None
|
||||
|
||||
|
||||
class CapabilityRequestAccept(BaseModel):
|
||||
fulfilling_agent: str
|
||||
fulfilling_workstream_id: uuid.UUID | None = None
|
||||
fulfilling_workplan_id: uuid.UUID | None = Field(
|
||||
default=None,
|
||||
validation_alias=AliasChoices("fulfilling_workplan_id", "fulfilling_workstream_id"),
|
||||
)
|
||||
|
||||
|
||||
class CapabilityRequestPatch(BaseModel):
|
||||
catalog_entry_id: uuid.UUID | None = None
|
||||
priority: str | None = None
|
||||
blocking_task_id: uuid.UUID | None = None
|
||||
fulfilling_workstream_id: uuid.UUID | None = None
|
||||
fulfilling_workplan_id: uuid.UUID | None = Field(
|
||||
default=None,
|
||||
validation_alias=AliasChoices("fulfilling_workplan_id", "fulfilling_workstream_id"),
|
||||
)
|
||||
|
||||
|
||||
class CapabilityRequestReroute(BaseModel):
|
||||
@@ -57,10 +66,10 @@ class CapabilityRequestRead(BaseModel):
|
||||
status: str
|
||||
requesting_domain_slug: str
|
||||
requesting_agent: str
|
||||
requesting_workstream_id: uuid.UUID | None = None
|
||||
requesting_workplan_id: uuid.UUID | None = None
|
||||
fulfilling_domain_slug: str | None = None
|
||||
fulfilling_agent: str | None = None
|
||||
fulfilling_workstream_id: uuid.UUID | None = None
|
||||
fulfilling_workplan_id: uuid.UUID | None = None
|
||||
blocking_task_id: uuid.UUID | None = None
|
||||
catalog_entry_id: uuid.UUID | None = None
|
||||
resolution_note: str | None = None
|
||||
@@ -73,3 +82,13 @@ class CapabilityRequestRead(BaseModel):
|
||||
completed_at: datetime | None = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
@computed_field # type: ignore[prop-decorator]
|
||||
@property
|
||||
def requesting_workstream_id(self) -> uuid.UUID | None:
|
||||
return self.requesting_workplan_id
|
||||
|
||||
@computed_field # type: ignore[prop-decorator]
|
||||
@property
|
||||
def fulfilling_workstream_id(self) -> uuid.UUID | None:
|
||||
return self.fulfilling_workplan_id
|
||||
|
||||
43
api/schemas/compat.py
Normal file
43
api/schemas/compat.py
Normal file
@@ -0,0 +1,43 @@
|
||||
"""Shared Pydantic field helpers for workplan / workstream compatibility."""
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
|
||||
from pydantic import AliasChoices, Field, computed_field, model_validator
|
||||
|
||||
|
||||
def workplan_id_field(*, default: uuid.UUID | None = None) -> uuid.UUID | None:
|
||||
return Field(
|
||||
default=default,
|
||||
validation_alias=AliasChoices("workplan_id", "workstream_id"),
|
||||
)
|
||||
|
||||
|
||||
class WorkplanIdCompatMixin:
|
||||
"""Accept ``workplan_id`` or legacy ``workstream_id`` on input; emit both on output."""
|
||||
|
||||
workplan_id: uuid.UUID = workplan_id_field()
|
||||
|
||||
@computed_field # type: ignore[prop-decorator]
|
||||
@property
|
||||
def workstream_id(self) -> uuid.UUID:
|
||||
return self.workplan_id
|
||||
|
||||
|
||||
class WorkplanIdCreateMixin:
|
||||
workplan_id: uuid.UUID | None = workplan_id_field(default=None)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _require_workplan_id(self):
|
||||
if self.workplan_id is None:
|
||||
raise ValueError("workplan_id is required")
|
||||
return self
|
||||
|
||||
|
||||
class OptionalWorkplanIdCompatMixin:
|
||||
workplan_id: uuid.UUID | None = workplan_id_field(default=None)
|
||||
|
||||
@computed_field # type: ignore[prop-decorator]
|
||||
@property
|
||||
def workstream_id(self) -> uuid.UUID | None:
|
||||
return self.workplan_id
|
||||
@@ -1,7 +1,7 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from pydantic import AliasChoices, BaseModel, ConfigDict, Field, computed_field
|
||||
|
||||
from api.models.contribution import ContributionStatus, ContributionType
|
||||
|
||||
@@ -14,7 +14,10 @@ class ContributionCreate(BaseModel):
|
||||
title: str
|
||||
body_path: str | None = None
|
||||
related_topic_id: uuid.UUID | None = None
|
||||
related_workstream_id: uuid.UUID | None = None
|
||||
related_workplan_id: uuid.UUID | None = Field(
|
||||
default=None,
|
||||
validation_alias=AliasChoices("related_workplan_id", "related_workstream_id"),
|
||||
)
|
||||
repo_id: uuid.UUID | None = None
|
||||
notes: str | None = None
|
||||
|
||||
@@ -36,10 +39,15 @@ class ContributionRead(BaseModel):
|
||||
status: ContributionStatus
|
||||
body_path: str | None = None
|
||||
related_topic_id: uuid.UUID | None = None
|
||||
related_workstream_id: uuid.UUID | None = None
|
||||
related_workplan_id: uuid.UUID | None = None
|
||||
repo_id: uuid.UUID | None = None
|
||||
submitted_at: datetime | None = None
|
||||
resolved_at: datetime | None = None
|
||||
notes: str | None = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
@computed_field # type: ignore[prop-decorator]
|
||||
@property
|
||||
def related_workstream_id(self) -> uuid.UUID | None:
|
||||
return self.related_workplan_id
|
||||
|
||||
@@ -4,11 +4,16 @@ from datetime import datetime
|
||||
from pydantic import BaseModel, ConfigDict, model_validator
|
||||
|
||||
from api.models.decision import DecisionStatus, DecisionType
|
||||
from api.schemas.compat import OptionalWorkplanIdCompatMixin
|
||||
from pydantic import AliasChoices, Field
|
||||
|
||||
|
||||
class DecisionCreate(BaseModel):
|
||||
topic_id: uuid.UUID | None = None
|
||||
workstream_id: uuid.UUID | None = None
|
||||
workplan_id: uuid.UUID | None = Field(
|
||||
default=None,
|
||||
validation_alias=AliasChoices("workplan_id", "workstream_id"),
|
||||
)
|
||||
title: str
|
||||
description: str | None = None
|
||||
decision_type: DecisionType = DecisionType.pending
|
||||
@@ -20,9 +25,9 @@ class DecisionCreate(BaseModel):
|
||||
escalation_note: str | None = None
|
||||
|
||||
@model_validator(mode="after")
|
||||
def topic_or_workstream_required(self) -> "DecisionCreate":
|
||||
if self.topic_id is None and self.workstream_id is None:
|
||||
raise ValueError("At least one of topic_id or workstream_id must be set")
|
||||
def topic_or_workplan_required(self) -> "DecisionCreate":
|
||||
if self.topic_id is None and self.workplan_id is None:
|
||||
raise ValueError("At least one of topic_id or workplan_id must be set")
|
||||
return self
|
||||
|
||||
|
||||
@@ -45,11 +50,10 @@ class DecisionUpdate(BaseModel):
|
||||
superseded_by: uuid.UUID | None = None
|
||||
|
||||
|
||||
class DecisionRead(BaseModel):
|
||||
class DecisionRead(OptionalWorkplanIdCompatMixin, BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
id: uuid.UUID
|
||||
topic_id: uuid.UUID | None = None
|
||||
workstream_id: uuid.UUID | None = None
|
||||
title: str
|
||||
description: str | None = None
|
||||
decision_type: DecisionType
|
||||
@@ -61,4 +65,4 @@ class DecisionRead(BaseModel):
|
||||
escalation_note: str | None = None
|
||||
superseded_by: uuid.UUID | None = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
updated_at: datetime
|
||||
@@ -2,7 +2,7 @@ import uuid
|
||||
from datetime import datetime
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from pydantic import AliasChoices, BaseModel, ConfigDict, Field, computed_field
|
||||
|
||||
|
||||
ExecutionState = Literal["manual", "queued", "scheduled", "launching", "paused", "completed", "cancelled"]
|
||||
@@ -21,7 +21,12 @@ class ExecutionIntentUpdate(BaseModel):
|
||||
|
||||
|
||||
class ExecutionIntentRead(BaseModel):
|
||||
workstream_id: uuid.UUID
|
||||
workplan_id: uuid.UUID
|
||||
|
||||
@computed_field # type: ignore[prop-decorator]
|
||||
@property
|
||||
def workstream_id(self) -> uuid.UUID:
|
||||
return self.workplan_id
|
||||
execution_state: ExecutionState
|
||||
launch_mode: LaunchMode
|
||||
concurrency_mode: ConcurrencyMode
|
||||
@@ -31,7 +36,7 @@ class ExecutionIntentRead(BaseModel):
|
||||
|
||||
|
||||
class WorkplanQueueItem(BaseModel):
|
||||
workstream_id: uuid.UUID
|
||||
workplan_id: uuid.UUID
|
||||
slug: str
|
||||
title: str
|
||||
status: str
|
||||
@@ -45,13 +50,18 @@ class WorkplanQueueItem(BaseModel):
|
||||
execution_group: str | None = None
|
||||
scheduled_for: datetime | None = None
|
||||
eligible: bool
|
||||
blocked_by_workstream_ids: list[uuid.UUID] = Field(default_factory=list)
|
||||
blocked_by_workplan_ids: list[uuid.UUID] = Field(default_factory=list)
|
||||
|
||||
@computed_field # type: ignore[prop-decorator]
|
||||
@property
|
||||
def blocked_by_workstream_ids(self) -> list[uuid.UUID]:
|
||||
return self.blocked_by_workplan_ids
|
||||
blocked_by_task_ids: list[uuid.UUID] = Field(default_factory=list)
|
||||
sort_key: list[str | int] = Field(default_factory=list)
|
||||
|
||||
|
||||
class LaunchRequestCreate(BaseModel):
|
||||
workstream_id: uuid.UUID
|
||||
workplan_id: uuid.UUID = Field(validation_alias=AliasChoices("workplan_id", "workstream_id"))
|
||||
requested_by: str = "dashboard"
|
||||
requested_actor: str | None = None
|
||||
launch_mode: LaunchMode = "queued"
|
||||
@@ -67,10 +77,15 @@ class LaunchRequestCreate(BaseModel):
|
||||
class LaunchRequestRead(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
id: uuid.UUID
|
||||
workstream_id: uuid.UUID
|
||||
workplan_id: uuid.UUID
|
||||
requested_by: str
|
||||
requested_actor: str | None = None
|
||||
launch_mode: LaunchMode
|
||||
|
||||
@computed_field # type: ignore[prop-decorator]
|
||||
@property
|
||||
def workstream_id(self) -> uuid.UUID:
|
||||
return self.workplan_id
|
||||
concurrency_mode: ConcurrencyMode
|
||||
priority: str | None = None
|
||||
repo_id: uuid.UUID | None = None
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from pydantic import AliasChoices, BaseModel, ConfigDict, Field
|
||||
|
||||
from api.models.extension_point import EPStatus
|
||||
|
||||
@@ -18,7 +18,10 @@ class EPCreate(BaseModel):
|
||||
status: EPStatus = EPStatus.open
|
||||
priority: str = "medium"
|
||||
topic_id: uuid.UUID | None = None
|
||||
workstream_id: uuid.UUID | None = None
|
||||
workplan_id: uuid.UUID | None = Field(
|
||||
default=None,
|
||||
validation_alias=AliasChoices("workplan_id", "workstream_id"),
|
||||
)
|
||||
|
||||
|
||||
class EPUpdate(BaseModel):
|
||||
@@ -29,7 +32,10 @@ class EPUpdate(BaseModel):
|
||||
ep_type: str | None = None
|
||||
status: EPStatus | None = None
|
||||
priority: str | None = None
|
||||
workstream_id: uuid.UUID | None = None
|
||||
workplan_id: uuid.UUID | None = Field(
|
||||
default=None,
|
||||
validation_alias=AliasChoices("workplan_id", "workstream_id"),
|
||||
)
|
||||
|
||||
|
||||
class EPRead(BaseModel):
|
||||
@@ -45,6 +51,10 @@ class EPRead(BaseModel):
|
||||
status: EPStatus
|
||||
priority: str
|
||||
topic_id: uuid.UUID | None = None
|
||||
workstream_id: uuid.UUID | None = None
|
||||
workplan_id: uuid.UUID | None = None
|
||||
created_at: datetime
|
||||
|
||||
@property
|
||||
def workstream_id(self) -> uuid.UUID | None:
|
||||
return self.workplan_id
|
||||
updated_at: datetime
|
||||
|
||||
@@ -2,8 +2,10 @@ import uuid
|
||||
from datetime import date, datetime
|
||||
from typing import Any, Literal
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
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,
|
||||
@@ -11,11 +13,73 @@ from hub_core.schemas.managed_repo import (
|
||||
)
|
||||
|
||||
|
||||
class RepoCreate(CoreRepoCreate):
|
||||
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(BaseModel):
|
||||
|
||||
class RepoUpdate(ClassificationFields):
|
||||
name: str | None = None
|
||||
local_path: str | None = None
|
||||
remote_url: str | None = None
|
||||
@@ -42,7 +106,7 @@ class RepoOnboardResult(BaseModel):
|
||||
stderr: str = ""
|
||||
|
||||
|
||||
class RepoRead(CoreRepoRead):
|
||||
class RepoRead(CoreRepoRead, ClassificationFields):
|
||||
topic_id: uuid.UUID | None = None
|
||||
sbom_source: str | None = None
|
||||
last_sbom_at: datetime | None = None
|
||||
@@ -59,13 +123,17 @@ class DispatchTask(BaseModel):
|
||||
needs_human: bool
|
||||
|
||||
|
||||
class DispatchWorkstream(BaseModel):
|
||||
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
|
||||
@@ -90,13 +158,17 @@ class ScopeIssueDetail(BaseModel):
|
||||
class RepoDispatch(BaseModel):
|
||||
repo_slug: str
|
||||
active_goal: dict[str, Any] | None
|
||||
active_workstreams: list[DispatchWorkstream]
|
||||
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
|
||||
@@ -104,4 +176,4 @@ class RepoScopeHealth(BaseModel):
|
||||
local_path: str | None = None
|
||||
path_available: bool
|
||||
scope_needs_review: bool
|
||||
scope_issue_details: list[ScopeIssueDetail]
|
||||
scope_issue_details: list[ScopeIssueDetail]
|
||||
@@ -2,12 +2,17 @@ import uuid
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from pydantic import AliasChoices, BaseModel, ConfigDict, Field
|
||||
|
||||
from api.schemas.compat import OptionalWorkplanIdCompatMixin
|
||||
|
||||
|
||||
class ProgressEventCreate(BaseModel):
|
||||
topic_id: uuid.UUID | None = None
|
||||
workstream_id: uuid.UUID | None = None
|
||||
workplan_id: uuid.UUID | None = Field(
|
||||
default=None,
|
||||
validation_alias=AliasChoices("workplan_id", "workstream_id"),
|
||||
)
|
||||
task_id: uuid.UUID | None = None
|
||||
decision_id: uuid.UUID | None = None
|
||||
event_type: str
|
||||
@@ -17,11 +22,10 @@ class ProgressEventCreate(BaseModel):
|
||||
session_id: str | None = None
|
||||
|
||||
|
||||
class ProgressEventRead(BaseModel):
|
||||
class ProgressEventRead(OptionalWorkplanIdCompatMixin, BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
id: uuid.UUID
|
||||
topic_id: uuid.UUID | None = None
|
||||
workstream_id: uuid.UUID | None = None
|
||||
task_id: uuid.UUID | None = None
|
||||
decision_id: uuid.UUID | None = None
|
||||
event_type: str
|
||||
@@ -29,4 +33,4 @@ class ProgressEventRead(BaseModel):
|
||||
detail: dict[str, Any] | None = None
|
||||
author: str | None = None
|
||||
session_id: str | None = None
|
||||
created_at: datetime
|
||||
created_at: datetime
|
||||
@@ -5,6 +5,7 @@ from typing import Self
|
||||
from pydantic import BaseModel, ConfigDict, field_validator, model_validator
|
||||
|
||||
from api.models.task import TaskPriority, TaskStatus
|
||||
from api.schemas.compat import WorkplanIdCompatMixin, WorkplanIdCreateMixin
|
||||
from api.task_status import normalize_task_status
|
||||
|
||||
|
||||
@@ -17,8 +18,7 @@ class TaskStatusMixin(BaseModel):
|
||||
return normalize_task_status(value)
|
||||
|
||||
|
||||
class TaskCreate(TaskStatusMixin):
|
||||
workstream_id: uuid.UUID
|
||||
class TaskCreate(TaskStatusMixin, WorkplanIdCreateMixin):
|
||||
title: str
|
||||
description: str | None = None
|
||||
status: TaskStatus = TaskStatus.todo
|
||||
@@ -96,10 +96,9 @@ class TaskStatusBulkSync(BaseModel):
|
||||
return value
|
||||
|
||||
|
||||
class TaskRead(TaskStatusMixin):
|
||||
class TaskRead(TaskStatusMixin, WorkplanIdCompatMixin):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
id: uuid.UUID
|
||||
workstream_id: uuid.UUID
|
||||
title: str
|
||||
description: str | None = None
|
||||
status: TaskStatus
|
||||
@@ -114,8 +113,7 @@ class TaskRead(TaskStatusMixin):
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class TaskCountRead(TaskStatusMixin):
|
||||
workstream_id: uuid.UUID
|
||||
class TaskCountRead(TaskStatusMixin, WorkplanIdCompatMixin):
|
||||
status: TaskStatus
|
||||
count: int
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from pydantic import AliasChoices, BaseModel, ConfigDict, Field
|
||||
|
||||
from api.models.technical_debt import TDStatus
|
||||
|
||||
@@ -35,7 +35,10 @@ class TDCreate(BaseModel):
|
||||
severity: str = "medium"
|
||||
status: TDStatus = TDStatus.open
|
||||
topic_id: uuid.UUID | None = None
|
||||
workstream_id: uuid.UUID | None = None
|
||||
workplan_id: uuid.UUID | None = Field(
|
||||
default=None,
|
||||
validation_alias=AliasChoices("workplan_id", "workstream_id"),
|
||||
)
|
||||
|
||||
|
||||
class TDUpdate(BaseModel):
|
||||
@@ -45,7 +48,10 @@ class TDUpdate(BaseModel):
|
||||
debt_type: str | None = None
|
||||
severity: str | None = None
|
||||
status: TDStatus | None = None
|
||||
workstream_id: uuid.UUID | None = None
|
||||
workplan_id: uuid.UUID | None = Field(
|
||||
default=None,
|
||||
validation_alias=AliasChoices("workplan_id", "workstream_id"),
|
||||
)
|
||||
|
||||
|
||||
class TDRead(BaseModel):
|
||||
@@ -61,7 +67,11 @@ class TDRead(BaseModel):
|
||||
severity: str
|
||||
status: TDStatus
|
||||
topic_id: uuid.UUID | None = None
|
||||
workstream_id: uuid.UUID | None = None
|
||||
workplan_id: uuid.UUID | None = None
|
||||
created_at: datetime
|
||||
|
||||
@property
|
||||
def workstream_id(self) -> uuid.UUID | None:
|
||||
return self.workplan_id
|
||||
updated_at: datetime
|
||||
notes: list[TDNoteRead] = []
|
||||
|
||||
@@ -2,14 +2,19 @@ import uuid
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, computed_field
|
||||
from pydantic import AliasChoices, BaseModel, ConfigDict, Field, computed_field
|
||||
|
||||
from api.schemas.compat import OptionalWorkplanIdCompatMixin
|
||||
|
||||
|
||||
class TokenEventCreate(BaseModel):
|
||||
tokens_in: int
|
||||
tokens_out: int
|
||||
task_id: uuid.UUID | None = None
|
||||
workstream_id: uuid.UUID | None = None
|
||||
workplan_id: uuid.UUID | None = Field(
|
||||
default=None,
|
||||
validation_alias=AliasChoices("workplan_id", "workstream_id"),
|
||||
)
|
||||
repo_id: uuid.UUID | None = None
|
||||
session_id: str | None = None
|
||||
model: str | None = None
|
||||
@@ -32,14 +37,13 @@ class TokenEventCreate(BaseModel):
|
||||
raw_metadata: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class TokenEventRead(BaseModel):
|
||||
class TokenEventRead(OptionalWorkplanIdCompatMixin, BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: uuid.UUID
|
||||
tokens_in: int
|
||||
tokens_out: int
|
||||
task_id: uuid.UUID | None = None
|
||||
workstream_id: uuid.UUID | None = None
|
||||
repo_id: uuid.UUID | None = None
|
||||
session_id: str | None = None
|
||||
model: str | None = None
|
||||
@@ -90,7 +94,10 @@ class TokenEventPatch(BaseModel):
|
||||
tokens_in: int | None = None
|
||||
tokens_out: int | None = None
|
||||
task_id: uuid.UUID | None = None
|
||||
workstream_id: uuid.UUID | None = None
|
||||
workplan_id: uuid.UUID | None = Field(
|
||||
default=None,
|
||||
validation_alias=AliasChoices("workplan_id", "workstream_id"),
|
||||
)
|
||||
repo_id: uuid.UUID | None = None
|
||||
session_id: str | None = None
|
||||
note: str | None = None
|
||||
|
||||
107
api/schemas/workplan.py
Normal file
107
api/schemas/workplan.py
Normal file
@@ -0,0 +1,107 @@
|
||||
import uuid
|
||||
from datetime import date, datetime
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, field_validator
|
||||
|
||||
from api.schemas.workplan_dependency import WorkplanDepStub
|
||||
from api.workplan_status import normalize_workplan_status
|
||||
|
||||
WorkplanStatus = Literal[
|
||||
"proposed",
|
||||
"ready",
|
||||
"active",
|
||||
"blocked",
|
||||
"backlog",
|
||||
"finished",
|
||||
"archived",
|
||||
]
|
||||
ExecutionState = Literal["manual", "queued", "scheduled", "launching", "paused", "completed", "cancelled"]
|
||||
LaunchMode = Literal["manual", "queued", "scheduled", "immediate"]
|
||||
ConcurrencyMode = Literal["sequential", "parallel"]
|
||||
|
||||
|
||||
class WorkplanStatusMixin(BaseModel):
|
||||
@field_validator("status", mode="before", check_fields=False)
|
||||
@classmethod
|
||||
def _normalise_status(cls, value):
|
||||
return normalize_workplan_status(value)
|
||||
|
||||
|
||||
class WorkplanCreate(WorkplanStatusMixin):
|
||||
repo_id: uuid.UUID
|
||||
topic_id: uuid.UUID | None = None
|
||||
slug: str
|
||||
title: str
|
||||
description: str | None = None
|
||||
status: WorkplanStatus = "active"
|
||||
owner: str | None = None
|
||||
due_date: date | None = None
|
||||
planning_priority: str | None = None
|
||||
planning_order: int | None = None
|
||||
execution_state: ExecutionState = "manual"
|
||||
launch_mode: LaunchMode = "manual"
|
||||
concurrency_mode: ConcurrencyMode = "sequential"
|
||||
queue_rank: int | None = None
|
||||
execution_group: str | None = None
|
||||
scheduled_for: datetime | None = None
|
||||
repo_goal_id: uuid.UUID | None = None
|
||||
|
||||
|
||||
class WorkplanUpdate(WorkplanStatusMixin):
|
||||
title: str | None = None
|
||||
description: str | None = None
|
||||
status: WorkplanStatus | None = None
|
||||
owner: str | None = None
|
||||
due_date: date | None = None
|
||||
planning_priority: str | None = None
|
||||
planning_order: int | None = None
|
||||
execution_state: ExecutionState | None = None
|
||||
launch_mode: LaunchMode | None = None
|
||||
concurrency_mode: ConcurrencyMode | None = None
|
||||
queue_rank: int | None = None
|
||||
execution_group: str | None = None
|
||||
scheduled_for: datetime | None = None
|
||||
topic_id: uuid.UUID | None = None
|
||||
repo_id: uuid.UUID | None = None
|
||||
repo_goal_id: uuid.UUID | None = None
|
||||
|
||||
|
||||
class WorkplanRead(WorkplanStatusMixin):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
id: uuid.UUID
|
||||
repo_id: uuid.UUID
|
||||
topic_id: uuid.UUID | None = None
|
||||
repo_goal_id: uuid.UUID | None = None
|
||||
slug: str
|
||||
title: str
|
||||
description: str | None = None
|
||||
status: WorkplanStatus
|
||||
owner: str | None = None
|
||||
due_date: date | None = None
|
||||
planning_priority: str | None = None
|
||||
planning_order: int | None = None
|
||||
execution_state: ExecutionState = "manual"
|
||||
launch_mode: LaunchMode = "manual"
|
||||
concurrency_mode: ConcurrencyMode = "sequential"
|
||||
queue_rank: int | None = None
|
||||
execution_group: str | None = None
|
||||
scheduled_for: datetime | None = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class WorkplanWithTaskCounts(WorkplanRead):
|
||||
tasks_total: int = 0
|
||||
tasks_wait: int = 0
|
||||
tasks_todo: int = 0
|
||||
tasks_progress: int = 0
|
||||
tasks_done: int = 0
|
||||
tasks_cancel: int = 0
|
||||
|
||||
|
||||
class WorkplanWithDeps(WorkplanWithTaskCounts):
|
||||
"""WorkplanWithTaskCounts enriched with dependency graph edges."""
|
||||
depends_on: list[WorkplanDepStub] = []
|
||||
blocks: list[WorkplanDepStub] = []
|
||||
blocked_reasons: list[dict] = []
|
||||
63
api/schemas/workplan_dependency.py
Normal file
63
api/schemas/workplan_dependency.py
Normal file
@@ -0,0 +1,63 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import AliasChoices, BaseModel, ConfigDict, Field, computed_field
|
||||
|
||||
|
||||
class WorkplanDependencyCreate(BaseModel):
|
||||
to_workplan_id: uuid.UUID | None = Field(
|
||||
default=None,
|
||||
validation_alias=AliasChoices("to_workplan_id", "to_workstream_id"),
|
||||
)
|
||||
to_task_id: uuid.UUID | None = None
|
||||
relationship_type: str = "blocks"
|
||||
description: str | None = None
|
||||
|
||||
|
||||
class WorkplanDependencyRead(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
id: uuid.UUID
|
||||
from_workplan_id: uuid.UUID
|
||||
to_workplan_id: uuid.UUID | None = None
|
||||
to_task_id: uuid.UUID | None = None
|
||||
relationship_type: str
|
||||
description: str | None = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class WorkplanDepStub(BaseModel):
|
||||
"""Minimal projection of the other end of a dependency edge."""
|
||||
dep_id: uuid.UUID
|
||||
target_type: str = "workplan"
|
||||
relationship_type: str = "blocks"
|
||||
workplan_id: uuid.UUID | None = Field(
|
||||
default=None,
|
||||
validation_alias=AliasChoices("workplan_id", "workstream_id"),
|
||||
)
|
||||
workplan_slug: str | None = Field(
|
||||
default=None,
|
||||
validation_alias=AliasChoices("workplan_slug", "workstream_slug"),
|
||||
)
|
||||
workplan_title: str | None = Field(
|
||||
default=None,
|
||||
validation_alias=AliasChoices("workplan_title", "workstream_title"),
|
||||
)
|
||||
task_id: uuid.UUID | None = None
|
||||
task_title: str | None = None
|
||||
description: str | None = None
|
||||
|
||||
@computed_field # type: ignore[prop-decorator]
|
||||
@property
|
||||
def workstream_id(self) -> uuid.UUID | None:
|
||||
return self.workplan_id
|
||||
|
||||
@computed_field # type: ignore[prop-decorator]
|
||||
@property
|
||||
def workstream_slug(self) -> str | None:
|
||||
return self.workplan_slug
|
||||
|
||||
@computed_field # type: ignore[prop-decorator]
|
||||
@property
|
||||
def workstream_title(self) -> str | None:
|
||||
return self.workplan_title
|
||||
@@ -1,106 +1,41 @@
|
||||
import uuid
|
||||
from datetime import date, datetime
|
||||
from typing import Literal
|
||||
"""Legacy aliases — prefer ``api.schemas.workplan``."""
|
||||
from api.schemas.workplan import (
|
||||
ConcurrencyMode,
|
||||
ExecutionState,
|
||||
LaunchMode,
|
||||
WorkplanCreate,
|
||||
WorkplanRead,
|
||||
WorkplanStatus,
|
||||
WorkplanStatusMixin,
|
||||
WorkplanUpdate,
|
||||
WorkplanWithDeps,
|
||||
WorkplanWithTaskCounts,
|
||||
)
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, field_validator
|
||||
WorkstreamStatus = WorkplanStatus
|
||||
WorkstreamStatusMixin = WorkplanStatusMixin
|
||||
WorkstreamCreate = WorkplanCreate
|
||||
WorkstreamUpdate = WorkplanUpdate
|
||||
WorkstreamRead = WorkplanRead
|
||||
WorkstreamWithTaskCounts = WorkplanWithTaskCounts
|
||||
WorkstreamWithDeps = WorkplanWithDeps
|
||||
|
||||
from api.schemas.workstream_dependency import WorkstreamDepStub
|
||||
from api.workplan_status import normalize_workstream_status
|
||||
|
||||
WorkstreamStatus = Literal[
|
||||
"proposed",
|
||||
"ready",
|
||||
"active",
|
||||
"blocked",
|
||||
"backlog",
|
||||
"finished",
|
||||
"archived",
|
||||
]
|
||||
ExecutionState = Literal["manual", "queued", "scheduled", "launching", "paused", "completed", "cancelled"]
|
||||
LaunchMode = Literal["manual", "queued", "scheduled", "immediate"]
|
||||
ConcurrencyMode = Literal["sequential", "parallel"]
|
||||
|
||||
|
||||
class WorkstreamStatusMixin(BaseModel):
|
||||
@field_validator("status", mode="before", check_fields=False)
|
||||
@classmethod
|
||||
def _normalise_status(cls, value):
|
||||
return normalize_workstream_status(value)
|
||||
|
||||
|
||||
class WorkstreamCreate(WorkstreamStatusMixin):
|
||||
topic_id: uuid.UUID
|
||||
slug: str
|
||||
title: str
|
||||
description: str | None = None
|
||||
status: WorkstreamStatus = "active"
|
||||
owner: str | None = None
|
||||
due_date: date | None = None
|
||||
planning_priority: str | None = None
|
||||
planning_order: int | None = None
|
||||
execution_state: ExecutionState = "manual"
|
||||
launch_mode: LaunchMode = "manual"
|
||||
concurrency_mode: ConcurrencyMode = "sequential"
|
||||
queue_rank: int | None = None
|
||||
execution_group: str | None = None
|
||||
scheduled_for: datetime | None = None
|
||||
repo_id: uuid.UUID | None = None # GEMS primary: the owning repository
|
||||
repo_goal_id: uuid.UUID | None = None
|
||||
|
||||
|
||||
class WorkstreamUpdate(WorkstreamStatusMixin):
|
||||
title: str | None = None
|
||||
description: str | None = None
|
||||
status: WorkstreamStatus | None = None
|
||||
owner: str | None = None
|
||||
due_date: date | None = None
|
||||
planning_priority: str | None = None
|
||||
planning_order: int | None = None
|
||||
execution_state: ExecutionState | None = None
|
||||
launch_mode: LaunchMode | None = None
|
||||
concurrency_mode: ConcurrencyMode | None = None
|
||||
queue_rank: int | None = None
|
||||
execution_group: str | None = None
|
||||
scheduled_for: datetime | None = None
|
||||
repo_id: uuid.UUID | None = None
|
||||
repo_goal_id: uuid.UUID | None = None
|
||||
|
||||
|
||||
class WorkstreamRead(WorkstreamStatusMixin):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
id: uuid.UUID
|
||||
topic_id: uuid.UUID
|
||||
repo_id: uuid.UUID | None = None
|
||||
repo_goal_id: uuid.UUID | None = None
|
||||
slug: str
|
||||
title: str
|
||||
description: str | None = None
|
||||
status: WorkstreamStatus
|
||||
owner: str | None = None
|
||||
due_date: date | None = None
|
||||
planning_priority: str | None = None
|
||||
planning_order: int | None = None
|
||||
execution_state: ExecutionState = "manual"
|
||||
launch_mode: LaunchMode = "manual"
|
||||
concurrency_mode: ConcurrencyMode = "sequential"
|
||||
queue_rank: int | None = None
|
||||
execution_group: str | None = None
|
||||
scheduled_for: datetime | None = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class WorkstreamWithTaskCounts(WorkstreamRead):
|
||||
tasks_total: int = 0
|
||||
tasks_wait: int = 0
|
||||
tasks_todo: int = 0
|
||||
tasks_progress: int = 0
|
||||
tasks_done: int = 0
|
||||
tasks_cancel: int = 0
|
||||
|
||||
|
||||
class WorkstreamWithDeps(WorkstreamWithTaskCounts):
|
||||
"""WorkstreamWithTaskCounts enriched with dependency graph edges."""
|
||||
depends_on: list[WorkstreamDepStub] = []
|
||||
blocks: list[WorkstreamDepStub] = []
|
||||
blocked_reasons: list[dict] = []
|
||||
__all__ = [
|
||||
"WorkstreamStatus",
|
||||
"WorkstreamStatusMixin",
|
||||
"WorkstreamCreate",
|
||||
"WorkstreamUpdate",
|
||||
"WorkstreamRead",
|
||||
"WorkstreamWithTaskCounts",
|
||||
"WorkstreamWithDeps",
|
||||
"WorkplanStatus",
|
||||
"WorkplanStatusMixin",
|
||||
"WorkplanCreate",
|
||||
"WorkplanUpdate",
|
||||
"WorkplanRead",
|
||||
"WorkplanWithTaskCounts",
|
||||
"WorkplanWithDeps",
|
||||
"ExecutionState",
|
||||
"LaunchMode",
|
||||
"ConcurrencyMode",
|
||||
]
|
||||
@@ -1,36 +1,19 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
"""Legacy aliases — prefer ``api.schemas.workplan_dependency``."""
|
||||
from api.schemas.workplan_dependency import (
|
||||
WorkplanDepStub,
|
||||
WorkplanDependencyCreate,
|
||||
WorkplanDependencyRead,
|
||||
)
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
WorkstreamDependencyCreate = WorkplanDependencyCreate
|
||||
WorkstreamDependencyRead = WorkplanDependencyRead
|
||||
WorkstreamDepStub = WorkplanDepStub
|
||||
|
||||
|
||||
class WorkstreamDependencyCreate(BaseModel):
|
||||
to_workstream_id: uuid.UUID | None = None
|
||||
to_task_id: uuid.UUID | None = None
|
||||
relationship_type: str = "blocks"
|
||||
description: str | None = None
|
||||
|
||||
|
||||
class WorkstreamDependencyRead(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
id: uuid.UUID
|
||||
from_workstream_id: uuid.UUID
|
||||
to_workstream_id: uuid.UUID | None = None
|
||||
to_task_id: uuid.UUID | None = None
|
||||
relationship_type: str
|
||||
description: str | None = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class WorkstreamDepStub(BaseModel):
|
||||
"""Minimal projection of the other end of a dependency edge."""
|
||||
dep_id: uuid.UUID
|
||||
target_type: str = "workstream"
|
||||
relationship_type: str = "blocks"
|
||||
workstream_id: uuid.UUID | None = None
|
||||
workstream_slug: str | None = None
|
||||
workstream_title: str | None = None
|
||||
task_id: uuid.UUID | None = None
|
||||
task_title: str | None = None
|
||||
description: str | None = None
|
||||
__all__ = [
|
||||
"WorkstreamDependencyCreate",
|
||||
"WorkstreamDependencyRead",
|
||||
"WorkstreamDepStub",
|
||||
"WorkplanDependencyCreate",
|
||||
"WorkplanDependencyRead",
|
||||
"WorkplanDepStub",
|
||||
]
|
||||
Reference in New Issue
Block a user