CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / TypeScript typecheck (push) Successful in 59s
CI & Build / integration (push) Successful in 1m32s
CI & Build / Python tests (push) Successful in 2m33s
CI & Build / Build & push image (push) Successful in 26s
Record, milestone, phase and plan citations go from every migration header; the "end before start" fix (0043) and the suppressed-count column (0095) state the condition they prevent rather than the incident. 0053 and 0055 pointed at design specs under docs/superpowers that are not in the repo; those pointers are removed. Kept: alembic's generated "Create Date:" headers (template metadata, not rationale), 0121's ARM_BORN justification beside the constant, and 0051's illustrative "update task 42" format example. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
91 lines
3.9 KiB
Python
91 lines
3.9 KiB
Python
"""rules gain a `kind` — a preference is a rule that does not bind
|
|
|
|
Revision ID: 0098
|
|
Revises: 0097
|
|
Create Date: 2026-09-10
|
|
|
|
A rule says what must be followed. There was no way to record the other
|
|
thing the operator kept writing rules for: **how they want work done**.
|
|
Several rules in a mature rulebook are not really rules — pace this kind of
|
|
debugging, hand off an action with its reason, end a finding with an offer.
|
|
Ignoring one of those does not break anything or cross a boundary; it costs
|
|
consistency. They were written as rules because a rule was the only record
|
|
that is global, keyed to a situation, and delivered when that situation
|
|
arrives.
|
|
|
|
So: `kind`. `rule` binds. `preference` describes how this person wants it
|
|
done, and — the part that makes it its own kind rather than a softer label —
|
|
**it is expected to change as the work teaches it.** The agent updates a
|
|
preference in the ordinary course of working, where a rule waits for its
|
|
author.
|
|
|
|
WHY A COLUMN AND NOT A TABLE. Everything a preference needs already exists on
|
|
`rules` and nowhere else: `when_to_apply` as a real column, a
|
|
trigger-dominated embedding document, ownership-scoped search that is
|
|
deliberately not filtered to one project, three retrieval arms with per-arm
|
|
telemetry, typed relations, and versioning. The two differ in exactly one
|
|
dimension — force — and one dimension is a field.
|
|
|
|
The drift machinery is the decisive part. `rule_versions` already snapshots
|
|
every write, and `rule_relations.overrides` already models "this supersedes
|
|
that for its scope". A separate table would have rebuilt both, and moving
|
|
existing rows into it would have changed their ids — silently invalidating
|
|
every record in the corpus that cites a rule by number.
|
|
|
|
DEFAULTS TO `rule`, so this migration changes NOTHING about what binds. An
|
|
install upgrades and every existing row keeps the force it had. That is the
|
|
same reasoning 0088 used for `tier`, and it is the reason both are safe to
|
|
apply without reading the data first.
|
|
|
|
The CHECK is created with the column (there is no prior constraint,
|
|
so the pair is created together — a value added to it LATER does DROP + ADD
|
|
in one migration).
|
|
|
|
`rule_versions` GETS THE COLUMN TOO, and that half is not bookkeeping.
|
|
`record_if_changed` decides whether an edit is worth a snapshot by comparing
|
|
the fields a version carries; a field a version does not carry is a field
|
|
whose change records NO HISTORY AT ALL. Without this, turning a rule into a
|
|
preference — the single most consequential edit either kind can undergo,
|
|
because it is the moment something stops binding — would leave the history
|
|
silent. Nullable and no CHECK there, matching `tier`: a version is a record
|
|
of what was, and a constraint on it would refuse to store a kind later
|
|
dropped from the live whitelist.
|
|
|
|
Downgrade drops all three. Nothing reads `kind` for correctness; a rule that
|
|
was a preference simply becomes a rule again, which is the safe direction.
|
|
"""
|
|
import sqlalchemy as sa
|
|
from alembic import op
|
|
|
|
|
|
revision = "0098"
|
|
down_revision = "0097"
|
|
branch_labels = None
|
|
depends_on = None
|
|
|
|
# Kept in one place so upgrade and the CHECK agree by construction — 0088's
|
|
# idiom, for the same reason.
|
|
_KINDS = ("rule", "preference")
|
|
|
|
|
|
def _in_list(column: str, values: tuple[str, ...]) -> str:
|
|
return f"{column} IN (" + ", ".join(f"'{v}'" for v in values) + ")"
|
|
|
|
|
|
def upgrade() -> None:
|
|
op.add_column(
|
|
"rules",
|
|
sa.Column("kind", sa.Text(), nullable=False, server_default="rule"),
|
|
)
|
|
op.create_check_constraint("ck_rules_kind", "rules", _in_list("kind", _KINDS))
|
|
# Nullable, no CHECK — see the module docstring. A version written before
|
|
# this migration genuinely does not know, and NULL there means "not
|
|
# recorded", never "was a rule".
|
|
op.add_column("rule_versions", sa.Column("kind", sa.Text(), nullable=True))
|
|
|
|
|
|
def downgrade() -> None:
|
|
op.drop_column("rule_versions", "kind")
|
|
op.drop_constraint("ck_rules_kind", "rules", type_="check")
|
|
op.drop_column("rules", "kind")
|