CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / TypeScript typecheck (push) Successful in 13s
CI & Build / integration (push) Successful in 25s
CI & Build / Python tests (push) Successful in 54s
CI & Build / Build & push image (push) Successful in 45s
The accounting half of the pattern system (governing note 2786): the snippet library records canon (small), this table accounts for EVERY extracted shape (total). Identity is (project, repo_key, path, symbol, kind) — kind included because one file can define '.foo' (css) and 'foo' (sym) as distinct shapes. Status vocabulary: canonical / instance / variant / exempt / unclassified, with unclassified as the default and THE todo state; classifications carry who judged (agent|audit|hook|mechanical|import), when, and the why for variants/exemptions. first/last-seen commits + vanished_at keep history instead of deleting it; a rename reads as vanish+new (accepted for v1). snippet_id is SET NULL on snippet deletion so accounting rows outlive their target and rejoin the todo via the step-2 sync, never dangle silently. Backups: v7 carries code_shapes (judgment data, worth moving) — full and per-user export sections, and a restore that keeps a judgment only when its snippet survives the id re-mapping, downgrading to unclassified otherwise. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
99 lines
4.2 KiB
Python
99 lines
4.2 KiB
Python
from datetime import datetime
|
|
|
|
from sqlalchemy import (
|
|
BigInteger,
|
|
DateTime,
|
|
ForeignKey,
|
|
Index,
|
|
Integer,
|
|
Text,
|
|
UniqueConstraint,
|
|
)
|
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
|
|
from scribe.models import Base
|
|
from scribe.models.base import TimestampMixin
|
|
|
|
# The classification vocabulary (note 2786). `unclassified` is the default and
|
|
# THE todo state; every other status is a judgment, stamped with who made it.
|
|
SHAPE_STATUSES = ("canonical", "instance", "variant", "exempt", "unclassified")
|
|
SHAPE_CLASSIFIERS = ("agent", "audit", "hook", "mechanical", "import")
|
|
|
|
|
|
class CodeShape(Base, TimestampMixin):
|
|
"""One extracted code shape and its classification against canon (#2787).
|
|
|
|
The accounting half of the pattern system (governing note 2786): the
|
|
snippet library records CANON (small); this ledger accounts for EVERY
|
|
shape the coverage extractor finds in a bound repo (total). A row's
|
|
status says how the shape relates to canon — it IS a snippet's reference
|
|
(`canonical`), conforms to one (`instance` — snippet_id may point at
|
|
another project's snippet, so family canon counts), departs deliberately
|
|
(`variant`, with the why in `reason`), was judged one-off (`exempt`,
|
|
a recorded judgment rather than silence), or awaits judgment
|
|
(`unclassified` — the todo).
|
|
|
|
Identity is (project, repo_key, path, symbol, kind) — kind is part of it
|
|
because one file can define `.foo` (css) and `foo` (sym) as distinct
|
|
shapes. A rename therefore reads as vanish + new row: accepted for v1,
|
|
because chasing renames needs content identity the extractor doesn't
|
|
have. `vanished_at` keeps the history instead of deleting it.
|
|
|
|
snippet_id is SET NULL on snippet deletion: the classification's target
|
|
is gone but the judgment happened; the sync pass (step 2) re-files such
|
|
rows as unclassified so they rejoin the todo instead of dangling.
|
|
"""
|
|
|
|
__tablename__ = "code_shapes"
|
|
__table_args__ = (
|
|
UniqueConstraint(
|
|
"project_id", "repo_key", "path", "symbol", "kind",
|
|
name="uq_code_shapes_identity",
|
|
),
|
|
Index("ix_code_shapes_project_status", "project_id", "status"),
|
|
Index("ix_code_shapes_snippet", "snippet_id"),
|
|
)
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
project_id: Mapped[int] = mapped_column(
|
|
Integer, ForeignKey("projects.id", ondelete="CASCADE"), nullable=False
|
|
)
|
|
repo_key: Mapped[str] = mapped_column(Text, nullable=False)
|
|
path: Mapped[str] = mapped_column(Text, nullable=False)
|
|
symbol: Mapped[str] = mapped_column(Text, nullable=False)
|
|
kind: Mapped[str] = mapped_column(Text, nullable=False) # "css" | "sym"
|
|
status: Mapped[str] = mapped_column(Text, nullable=False, default="unclassified")
|
|
snippet_id: Mapped[int | None] = mapped_column(
|
|
BigInteger, ForeignKey("notes.id", ondelete="SET NULL"), nullable=True
|
|
)
|
|
reason: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
classified_by: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
classified_at: Mapped[datetime | None] = mapped_column(
|
|
DateTime(timezone=True), nullable=True
|
|
)
|
|
first_seen_commit: Mapped[str] = mapped_column(Text, default="")
|
|
last_seen_commit: Mapped[str] = mapped_column(Text, default="")
|
|
vanished_at: Mapped[datetime | None] = mapped_column(
|
|
DateTime(timezone=True), nullable=True
|
|
)
|
|
|
|
def to_dict(self) -> dict:
|
|
return {
|
|
"id": self.id,
|
|
"project_id": self.project_id,
|
|
"repo_key": self.repo_key,
|
|
"path": self.path,
|
|
"symbol": self.symbol,
|
|
"kind": self.kind,
|
|
"status": self.status,
|
|
"snippet_id": self.snippet_id,
|
|
"reason": self.reason,
|
|
"classified_by": self.classified_by,
|
|
"classified_at": self.classified_at.isoformat() if self.classified_at else None,
|
|
"first_seen_commit": self.first_seen_commit,
|
|
"last_seen_commit": self.last_seen_commit,
|
|
"vanished_at": self.vanished_at.isoformat() if self.vanished_at else None,
|
|
"created_at": self.created_at.isoformat(),
|
|
"updated_at": self.updated_at.isoformat(),
|
|
}
|