"""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")