Milestone/step labels, record ids, the rescoped-rule date and the 296-call measurement go; each header states what the change protects. "rule 36" citations become the DROP + ADD practice they named. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
66 lines
2.7 KiB
Python
66 lines
2.7 KiB
Python
"""task_kind gains 'spike' — the investigation, not the change
|
|
|
|
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. Rules carry 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.
|
|
|
|
`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),
|
|
)
|