diff --git a/alembic/versions/0079_code_shapes_ledger.py b/alembic/versions/0079_code_shapes_ledger.py new file mode 100644 index 0000000..e548bc2 --- /dev/null +++ b/alembic/versions/0079_code_shapes_ledger.py @@ -0,0 +1,66 @@ +"""The shape ledger: code_shapes (#2787, milestone 294) + +Revision ID: 0079 +Revises: 0078 +Create Date: 2026-08-19 + +The accounting half of the pattern system (governing note 2786): the snippet +library records canon (small); this table accounts for EVERY shape the +coverage extractor finds in a bound repo (total). Rows arrive `unclassified` +from the coverage sync (step 2) and gain judgments — canonical / instance / +variant / exempt — from audits, hooks, and the mechanical proposer. +Unclassified IS the todo list. +""" +import sqlalchemy as sa +from alembic import op + +revision = "0079" +down_revision = "0078" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.create_table( + "code_shapes", + sa.Column("id", sa.Integer(), primary_key=True), + sa.Column( + "project_id", + sa.Integer(), + sa.ForeignKey("projects.id", ondelete="CASCADE"), + nullable=False, + ), + sa.Column("repo_key", sa.Text(), nullable=False), + sa.Column("path", sa.Text(), nullable=False), + sa.Column("symbol", sa.Text(), nullable=False), + sa.Column("kind", sa.Text(), nullable=False), + sa.Column("status", sa.Text(), nullable=False, server_default="unclassified"), + sa.Column( + "snippet_id", + sa.BigInteger(), + sa.ForeignKey("notes.id", ondelete="SET NULL"), + nullable=True, + ), + sa.Column("reason", sa.Text(), nullable=True), + sa.Column("classified_by", sa.Text(), nullable=True), + sa.Column("classified_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("first_seen_commit", sa.Text(), nullable=False, server_default=""), + sa.Column("last_seen_commit", sa.Text(), nullable=False, server_default=""), + sa.Column("vanished_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), + sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), + sa.UniqueConstraint( + "project_id", "repo_key", "path", "symbol", "kind", + name="uq_code_shapes_identity", + ), + ) + op.create_index( + "ix_code_shapes_project_status", "code_shapes", ["project_id", "status"] + ) + op.create_index("ix_code_shapes_snippet", "code_shapes", ["snippet_id"]) + + +def downgrade() -> None: + op.drop_index("ix_code_shapes_snippet", table_name="code_shapes") + op.drop_index("ix_code_shapes_project_status", table_name="code_shapes") + op.drop_table("code_shapes") diff --git a/frontend/src/views/ProjectView.vue b/frontend/src/views/ProjectView.vue index 0076373..906ab7e 100644 --- a/frontend/src/views/ProjectView.vue +++ b/frontend/src/views/ProjectView.vue @@ -424,15 +424,17 @@ async function loadNotes() { interface CoverageGap { dir: string; - uncovered: number; + unclassified: number; total: number; } interface Coverage { total: number; - recorded: number; + accounted: number; + unclassified: number; + counts: Record; estimate: boolean; computed_at: string; - repos: { repo: string; ref: string; total: number; recorded: number }[]; + repos: { repo: string; ref: string; total: number; accounted: number }[]; largest_gaps: CoverageGap[]; } @@ -712,32 +714,42 @@ async function confirmDelete() {

- Not measured yet — Refresh compares the bound repo's definitions - against recorded snippets. + Not measured yet — Refresh reads the bound repo's definitions into + the shape ledger and reports how many are classified against canon.

{{ coverageError }}