feat: persist repository rename identity
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a049a4-ee9f-78e1-9d66-2cb0f9bea3e3
This commit is contained in:
parent
13f7a10018
commit
8988a093f2
10 changed files with 1454 additions and 4 deletions
|
|
@ -43,6 +43,11 @@ from api.models.legacy_meter import LegacyInterface, LegacyInterfaceUsageBucket
|
|||
from api.models.write_idempotency_key import WriteIdempotencyKey
|
||||
from api.models.work_record_identifier_alias import WorkRecordIdentifierAlias
|
||||
from api.models.review_contract import ReviewContract, ReviewReceipt
|
||||
from api.models.repository_rename import (
|
||||
RepositoryForgeIdentity,
|
||||
RepositoryRenameOperation,
|
||||
RepositorySlug,
|
||||
)
|
||||
from api.models.suggestion import (
|
||||
Suggestion,
|
||||
SuggestionNote,
|
||||
|
|
@ -85,5 +90,6 @@ __all__ = [
|
|||
"WriteIdempotencyKey",
|
||||
"WorkRecordIdentifierAlias",
|
||||
"ReviewContract", "ReviewReceipt",
|
||||
"RepositoryForgeIdentity", "RepositoryRenameOperation", "RepositorySlug",
|
||||
"Suggestion", "SuggestionNote", "SuggestionRelevanceBump", "SuggestionStage",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -53,6 +53,21 @@ class ManagedRepo(Base, TimestampMixin):
|
|||
"RepoGoal", back_populates="repo", lazy="selectin"
|
||||
)
|
||||
|
||||
forge_identity: Mapped["RepositoryForgeIdentity | None"] = relationship( # noqa: F821
|
||||
"RepositoryForgeIdentity",
|
||||
back_populates="repo",
|
||||
lazy="selectin",
|
||||
uselist=False,
|
||||
)
|
||||
|
||||
slug_records: Mapped[list["RepositorySlug"]] = relationship( # noqa: F821
|
||||
"RepositorySlug", back_populates="repo", lazy="selectin"
|
||||
)
|
||||
|
||||
rename_operations: Mapped[list["RepositoryRenameOperation"]] = relationship( # noqa: F821
|
||||
"RepositoryRenameOperation", back_populates="repo", lazy="selectin"
|
||||
)
|
||||
|
||||
@property
|
||||
def domain_slug(self) -> str:
|
||||
return self.domain.slug if self.domain is not None else ""
|
||||
|
|
|
|||
292
api/models/repository_rename.py
Normal file
292
api/models/repository_rename.py
Normal file
|
|
@ -0,0 +1,292 @@
|
|||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import (
|
||||
BigInteger,
|
||||
Boolean,
|
||||
CheckConstraint,
|
||||
DateTime,
|
||||
ForeignKey,
|
||||
ForeignKeyConstraint,
|
||||
Index,
|
||||
String,
|
||||
Text,
|
||||
UniqueConstraint,
|
||||
text,
|
||||
)
|
||||
from sqlalchemy.dialects.postgresql import JSONB, UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from api.models.base import Base, TimestampMixin, new_uuid
|
||||
|
||||
|
||||
FORGE_IDENTITY_STATES = ("unverified", "verified")
|
||||
REPOSITORY_SLUG_KINDS = ("canonical", "alias")
|
||||
REPOSITORY_RENAME_PHASES = (
|
||||
"draft",
|
||||
"preflighted",
|
||||
"forge-renamed",
|
||||
"statehub-rebound",
|
||||
"source-synced",
|
||||
"consumers-verified",
|
||||
"completed",
|
||||
"rollback-preflight",
|
||||
"rolled-back",
|
||||
)
|
||||
TERMINAL_REPOSITORY_RENAME_PHASES = ("completed", "rolled-back")
|
||||
|
||||
|
||||
class RepositoryForgeIdentity(Base, TimestampMixin):
|
||||
"""One explicitly verified (or explicitly unverified) Forge identity per repo."""
|
||||
|
||||
__tablename__ = "repository_forge_identities"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("repo_id", name="uq_repository_forge_identities_repo_id"),
|
||||
UniqueConstraint(
|
||||
"id",
|
||||
"repo_id",
|
||||
"verification_state",
|
||||
name="uq_repository_forge_identity_verified_ref",
|
||||
),
|
||||
UniqueConstraint(
|
||||
"provider",
|
||||
"forge_instance",
|
||||
"forge_owner",
|
||||
"forge_repository_id",
|
||||
name="uq_repository_forge_identity_tuple",
|
||||
),
|
||||
CheckConstraint(
|
||||
"verification_state IN ('unverified', 'verified')",
|
||||
name="ck_repository_forge_identity_state",
|
||||
),
|
||||
CheckConstraint(
|
||||
"forge_repository_id IS NULL OR forge_repository_id > 0",
|
||||
name="ck_repository_forge_id_positive",
|
||||
),
|
||||
CheckConstraint(
|
||||
"verification_state != 'verified' OR "
|
||||
"(provider IS NOT NULL AND forge_instance IS NOT NULL "
|
||||
"AND forge_owner IS NOT NULL AND forge_repository_id IS NOT NULL "
|
||||
"AND verified_at IS NOT NULL AND verified_by IS NOT NULL)",
|
||||
name="ck_repository_forge_verified_complete",
|
||||
),
|
||||
CheckConstraint(
|
||||
"verification_state = 'verified' OR "
|
||||
"(verified_at IS NULL AND verified_by IS NULL)",
|
||||
name="ck_repository_forge_unverified_has_no_attestation",
|
||||
),
|
||||
)
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True), primary_key=True, default=new_uuid
|
||||
)
|
||||
repo_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True),
|
||||
ForeignKey("managed_repos.id", ondelete="RESTRICT"),
|
||||
nullable=False,
|
||||
)
|
||||
provider: Mapped[str | None] = mapped_column(String(40), nullable=True)
|
||||
forge_instance: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
||||
forge_owner: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
||||
forge_repository_id: Mapped[int | None] = mapped_column(BigInteger, nullable=True)
|
||||
verification_state: Mapped[str] = mapped_column(
|
||||
String(20), nullable=False, default="unverified", server_default="unverified"
|
||||
)
|
||||
verified_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True
|
||||
)
|
||||
verified_by: Mapped[str | None] = mapped_column(String(160), nullable=True)
|
||||
verification_evidence: Mapped[dict] = mapped_column(
|
||||
JSONB, nullable=False, default=dict, server_default="{}"
|
||||
)
|
||||
|
||||
repo: Mapped["ManagedRepo"] = relationship( # noqa: F821
|
||||
"ManagedRepo", back_populates="forge_identity", lazy="selectin"
|
||||
)
|
||||
rename_operations: Mapped[list["RepositoryRenameOperation"]] = relationship(
|
||||
"RepositoryRenameOperation",
|
||||
back_populates="forge_identity",
|
||||
lazy="selectin",
|
||||
viewonly=True,
|
||||
)
|
||||
|
||||
|
||||
class RepositoryRenameOperation(Base, TimestampMixin):
|
||||
"""Append-preserving journal for one phased canonical-coordinate change."""
|
||||
|
||||
__tablename__ = "repository_rename_operations"
|
||||
__table_args__ = (
|
||||
CheckConstraint(
|
||||
"phase IN ('draft', 'preflighted', 'forge-renamed', "
|
||||
"'statehub-rebound', 'source-synced', 'consumers-verified', "
|
||||
"'completed', 'rollback-preflight', 'rolled-back')",
|
||||
name="ck_repository_rename_phase",
|
||||
),
|
||||
CheckConstraint(
|
||||
"old_slug != new_slug", name="ck_repository_rename_distinct_slugs"
|
||||
),
|
||||
CheckConstraint(
|
||||
"expected_forge_repository_id > 0",
|
||||
name="ck_repository_rename_forge_id_positive",
|
||||
),
|
||||
CheckConstraint(
|
||||
"forge_identity_state = 'verified'",
|
||||
name="ck_repository_rename_verified_identity",
|
||||
),
|
||||
CheckConstraint(
|
||||
"phase = 'draft' OR preflighted_at IS NOT NULL",
|
||||
name="ck_repository_rename_preflight_timestamp",
|
||||
),
|
||||
CheckConstraint(
|
||||
"phase != 'completed' OR completed_at IS NOT NULL",
|
||||
name="ck_repository_rename_completed_timestamp",
|
||||
),
|
||||
CheckConstraint(
|
||||
"phase != 'rolled-back' OR rolled_back_at IS NOT NULL",
|
||||
name="ck_repository_rename_rollback_timestamp",
|
||||
),
|
||||
Index(
|
||||
"uq_repository_rename_active_repo",
|
||||
"repo_id",
|
||||
unique=True,
|
||||
postgresql_where=text("phase NOT IN ('completed', 'rolled-back')"),
|
||||
),
|
||||
Index(
|
||||
"uq_repository_rename_active_new_slug",
|
||||
"new_slug",
|
||||
unique=True,
|
||||
postgresql_where=text("phase NOT IN ('completed', 'rolled-back')"),
|
||||
),
|
||||
Index(
|
||||
"ix_repository_rename_phase", "phase"
|
||||
),
|
||||
ForeignKeyConstraint(
|
||||
["forge_identity_id", "repo_id", "forge_identity_state"],
|
||||
[
|
||||
"repository_forge_identities.id",
|
||||
"repository_forge_identities.repo_id",
|
||||
"repository_forge_identities.verification_state",
|
||||
],
|
||||
name="fk_repository_rename_verified_identity",
|
||||
ondelete="RESTRICT",
|
||||
),
|
||||
)
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True), primary_key=True, default=new_uuid
|
||||
)
|
||||
repo_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True),
|
||||
ForeignKey("managed_repos.id", ondelete="RESTRICT"),
|
||||
nullable=False,
|
||||
index=True,
|
||||
)
|
||||
forge_identity_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True), nullable=False
|
||||
)
|
||||
forge_identity_state: Mapped[str] = mapped_column(
|
||||
String(20), nullable=False, default="verified", server_default="verified"
|
||||
)
|
||||
expected_provider: Mapped[str] = mapped_column(String(40), nullable=False)
|
||||
expected_forge_instance: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
expected_forge_owner: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
expected_forge_repository_id: Mapped[int] = mapped_column(
|
||||
BigInteger, nullable=False
|
||||
)
|
||||
expected_source_commit: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
expected_default_branch: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
old_slug: Mapped[str] = mapped_column(String(100), nullable=False, index=True)
|
||||
new_slug: Mapped[str] = mapped_column(String(100), nullable=False, index=True)
|
||||
old_coordinates: Mapped[dict] = mapped_column(JSONB, nullable=False)
|
||||
new_coordinates: Mapped[dict] = mapped_column(JSONB, nullable=False)
|
||||
phase: Mapped[str] = mapped_column(
|
||||
String(32), nullable=False, default="draft", server_default="draft"
|
||||
)
|
||||
actor: Mapped[str] = mapped_column(String(160), nullable=False)
|
||||
phase_changed_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False
|
||||
)
|
||||
preflighted_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True
|
||||
)
|
||||
preflight_expires_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True
|
||||
)
|
||||
completed_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True
|
||||
)
|
||||
rolled_back_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True
|
||||
)
|
||||
evidence: Mapped[dict] = mapped_column(
|
||||
JSONB, nullable=False, default=dict, server_default="{}"
|
||||
)
|
||||
error_code: Mapped[str | None] = mapped_column(String(80), nullable=True)
|
||||
error_message: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
error_details: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
|
||||
error_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True
|
||||
)
|
||||
|
||||
repo: Mapped["ManagedRepo"] = relationship( # noqa: F821
|
||||
"ManagedRepo", back_populates="rename_operations", lazy="selectin"
|
||||
)
|
||||
forge_identity: Mapped[RepositoryForgeIdentity] = relationship(
|
||||
"RepositoryForgeIdentity",
|
||||
back_populates="rename_operations",
|
||||
lazy="selectin",
|
||||
viewonly=True,
|
||||
)
|
||||
slugs: Mapped[list["RepositorySlug"]] = relationship(
|
||||
"RepositorySlug", back_populates="source_operation", lazy="selectin"
|
||||
)
|
||||
|
||||
|
||||
class RepositorySlug(Base, TimestampMixin):
|
||||
"""Global uniqueness boundary for current and protected prior slugs."""
|
||||
|
||||
__tablename__ = "repository_slugs"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("slug", name="uq_repository_slugs_slug"),
|
||||
CheckConstraint(
|
||||
"kind IN ('canonical', 'alias')", name="ck_repository_slug_kind"
|
||||
),
|
||||
CheckConstraint(
|
||||
"kind != 'alias' OR protected",
|
||||
name="ck_repository_slug_alias_protected",
|
||||
),
|
||||
Index(
|
||||
"uq_repository_slugs_one_canonical",
|
||||
"repo_id",
|
||||
unique=True,
|
||||
postgresql_where=text("kind = 'canonical'"),
|
||||
),
|
||||
Index("ix_repository_slugs_repo_kind", "repo_id", "kind"),
|
||||
)
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True), primary_key=True, default=new_uuid
|
||||
)
|
||||
repo_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True),
|
||||
ForeignKey("managed_repos.id", ondelete="RESTRICT"),
|
||||
nullable=False,
|
||||
)
|
||||
slug: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
kind: Mapped[str] = mapped_column(String(16), nullable=False)
|
||||
protected: Mapped[bool] = mapped_column(
|
||||
Boolean, nullable=False, default=True, server_default="true"
|
||||
)
|
||||
source_operation_id: Mapped[uuid.UUID | None] = mapped_column(
|
||||
UUID(as_uuid=True),
|
||||
ForeignKey("repository_rename_operations.id", ondelete="RESTRICT"),
|
||||
nullable=True,
|
||||
index=True,
|
||||
)
|
||||
|
||||
repo: Mapped["ManagedRepo"] = relationship( # noqa: F821
|
||||
"ManagedRepo", back_populates="slug_records", lazy="selectin"
|
||||
)
|
||||
source_operation: Mapped[RepositoryRenameOperation | None] = relationship(
|
||||
"RepositoryRenameOperation", back_populates="slugs", lazy="selectin"
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue