"""note_supersessions; drop the never-written notes.consolidated_at

Revision ID: 0076
Revises: 0075
Create Date: 2026-08-07

Step 1 of milestone #278. Structure only — nothing reads or writes the new
table yet, and nothing behaves differently after this runs.

## What the table is for

Old records outrank newer ones on the same subject, because a similarity score
cannot tell time. A note that accurately described how something worked in June
is still accurate ABOUT June; it is just no longer the answer. Nothing recorded
that, so nothing could act on it.

The claim points FORWARD — the newer record names what it overtakes — because
the older one cannot know it has been overtaken. Many-to-many and partial: a
note may supersede parts of several others and be overtaken piecemeal by
several later ones, which is why this is a table rather than a column. Both
directions are queried: `superseded_id` answers "has this been overtaken?" at
ranking time, `superseder_id` answers "what does this replace?" in a record
view. An array column could serve one and not the other.

CASCADE on both sides is safe because trashing is not a delete: `trash_svc`
stamps `deleted_at`, so a trashed note keeps its claims and `restore` brings
them back. The cascade fires only on `purge_trash`, where the row genuinely
goes — and a claim about a row that no longer exists is not actionable.

## What is being dropped, and why now

`notes.consolidated_at` was written by NOTHING — no service, no route, no tool
— while being serialised into every note and task payload as `null`. It cost a
column, a line in every response, and worse: it IMPLIED a capability. A reader
reasonably concludes notes can be consolidated and this records when.

That reading was reasonable precisely because merge/unmerge exists for snippets
and not for notes, so the column looked like the notes-side half of that
feature, modelled and abandoned.

It is dropped rather than repurposed for supersession, and the distinction is
the point (#2483): consolidation folds several records into one survivor and
destroys the originals. Merging two snippets is lossless — one helper, several
call sites. Folding two dev-logs means writing a summary and losing what each
actually said. Supersession is the opposite act: both records survive, and the
older one is merely ranked behind. Smuggling one in under a column named for
the other would have buried that difference in schema.

## Downgrade

Re-adds `consolidated_at` nullable, which is how it lived — so downgrade
restores the shape, not the (nonexistent) data. Drops the table; any recorded
supersession claims are lost, which costs ranking its input and nothing else,
since no note's own content depends on them.
"""
import sqlalchemy as sa
from alembic import op

revision = "0076"
down_revision = "0075"
branch_labels = None
depends_on = None


def upgrade() -> None:
    op.create_table(
        "note_supersessions",
        sa.Column("id", sa.Integer, primary_key=True),
        sa.Column(
            "superseder_id",
            sa.Integer,
            sa.ForeignKey("notes.id", ondelete="CASCADE"),
            nullable=False,
        ),
        sa.Column(
            "superseded_id",
            sa.Integer,
            sa.ForeignKey("notes.id", ondelete="CASCADE"),
            nullable=False,
        ),
        sa.Column(
            "created_at",
            sa.DateTime(timezone=True),
            server_default=sa.text("now()"),
            nullable=False,
        ),
        sa.UniqueConstraint(
            "superseder_id", "superseded_id", name="uq_note_supersessions_pair"
        ),
        # Declaring that a note supersedes ITSELF is meaningless, and under flat
        # demotion it would demote a record on its own authority. Refused in the
        # service too, with a message — this is the backstop that holds when
        # something writes rows directly.
        sa.CheckConstraint(
            "superseder_id <> superseded_id", name="ck_note_supersessions_not_self"
        ),
    )
    op.create_index(
        "ix_note_supersessions_superseder", "note_supersessions", ["superseder_id"]
    )
    op.create_index(
        "ix_note_supersessions_superseded", "note_supersessions", ["superseded_id"]
    )

    op.drop_column("notes", "consolidated_at")


def downgrade() -> None:
    op.add_column(
        "notes",
        sa.Column("consolidated_at", sa.DateTime(timezone=True), nullable=True),
    )
    op.drop_index("ix_note_supersessions_superseded", table_name="note_supersessions")
    op.drop_index("ix_note_supersessions_superseder", table_name="note_supersessions")
    op.drop_table("note_supersessions")
