"""task_kind gains 'spike' — the investigation, not the change
(milestone 312 step 5)

Revision ID: 0091
Revises: 0090
Create Date: 2026-08-27

A spike is a task shape the others cannot hold. `work` ships a change;
`issue` fixes something broken. A spike is time-boxed and its output is
KNOWLEDGE — it succeeds by producing an answer, and nothing ships at the
end of it. "Find out whether the runner can be given a bash shell" is not
work, and filing it as work makes a finished investigation look like an
abandoned change.

It is the record a failed check asks for. Milestone 312 gave rules a
`verify_with`; when one of those fails, the rule is wrong and the next move
is often to go and find out what replaced it. `notes.arose_from_id` already
exists (0065), so that constraint -> spike link needs no further schema.

Rule 36: `task_kind` is gated by a CHECK whitelist, so the value and the
widened constraint land in the SAME migration — DROP then ADD, exactly as
0065 did when it introduced 'issue'. Adding the value and constraining it
later leaves a window where the database accepts anything.

'plan' stays in the list though it is retired (plans are milestones since
0066): historical plan-tasks still carry it, and dropping it from the
whitelist would make old rows unwritable.
"""
from alembic import op

revision = "0091"
down_revision = "0090"
branch_labels = None
depends_on = None

# One tuple so the upgrade and the downgrade cannot disagree about what the
# list was on either side of this migration.
_KINDS_AFTER = ("work", "plan", "issue", "spike")
_KINDS_BEFORE = ("work", "plan", "issue")


# Restated rather than imported from 0088, which has the same helper. A
# migration is a snapshot: it must keep working when the code around it has
# moved on, so it never imports from live modules or from its siblings. Six
# duplicated lines are the price of that, and the cheap half of the bargain.
def _in_list(values: tuple[str, ...]) -> str:
    return "task_kind IN (" + ", ".join(f"'{v}'" for v in values) + ")"


def upgrade() -> None:
    op.drop_constraint("notes_task_kind_check", "notes", type_="check")
    op.create_check_constraint(
        "notes_task_kind_check", "notes", _in_list(_KINDS_AFTER),
    )


def downgrade() -> None:
    # Any row already filed as a spike would violate the narrowed constraint,
    # so they are demoted to 'work' first. Lossy and deliberately so: the
    # alternative is a downgrade that fails on real data, which is worse than
    # a downgrade that says what it did.
    op.execute("UPDATE notes SET task_kind = 'work' WHERE task_kind = 'spike'")
    op.drop_constraint("notes_task_kind_check", "notes", type_="check")
    op.create_check_constraint(
        "notes_task_kind_check", "notes", _in_list(_KINDS_BEFORE),
    )
