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>
84 lines
3.7 KiB
Python
84 lines
3.7 KiB
Python
"""add rule_usage_events — was a surfaced rule ever read?
|
|
|
|
Revision ID: 0094
|
|
Revises: 0093
|
|
Create Date: 2026-09-02
|
|
|
|
The sibling `note_usage_events` has had since 0071, and the third rule-side
|
|
table to arrive after `rule_embeddings` and `rule_versions` — each one added
|
|
because the rule side kept inheriting machinery built for notes and getting
|
|
the weaker version of it.
|
|
|
|
WHAT IT MEASURES. A standing-rule arm that returns something on every call
|
|
and clears its threshold every time is either a perfectly tuned surface or a
|
|
bar it cannot fail to clear, and `retrieval_logs`
|
|
cannot tell them apart: it records what the ranker scored, never whether the
|
|
hint was any use.
|
|
|
|
WHY NOT A rule_id COLUMN ON note_usage_events. The row shares no note-specific
|
|
fields and the aggregate readout is the same shape, which is the strongest case
|
|
for sharing there is. What decides against it is identity at
|
|
RESTORE: `note_usage_events`'s importer maps `note_id` through `note_id_map`
|
|
and drops what does not resolve. A rule id parked in that column would come
|
|
back from a backup silently reattached to whatever note took that number —
|
|
telemetry not merely lost but wrong, and wrong in a way nothing downstream
|
|
could detect. `rule_versions` made the same call for the same reason.
|
|
|
|
FK-free on `rule_id` and `user_id`, matching note_usage_events, retrieval_logs
|
|
and app_logs — and deliberately unlike `rule_versions`, which does carry FKs.
|
|
The difference is what the row is for: a version belongs to a rule's history
|
|
and dies with it; telemetry outlives the row it describes. Deleting a rule must
|
|
not erase the evidence that it was surfaced forty times and opened never, since
|
|
that evidence is exactly the case for having deleted it.
|
|
|
|
No CHECK on `event`, matching the note twin. DROP + ADD governs adding a value
|
|
to a column that is already gated; it does not require gating one that never was,
|
|
and a two-member enum whose members are written by two functions in one module
|
|
is not where that discipline earns its cost.
|
|
|
|
Downgrade drops the table outright. The data is purely observational — nothing
|
|
reads it for correctness, so losing it costs history and no behaviour.
|
|
"""
|
|
from alembic import op
|
|
import sqlalchemy as sa
|
|
|
|
|
|
revision = "0094"
|
|
down_revision = "0093"
|
|
branch_labels = None
|
|
depends_on = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
op.create_table(
|
|
"rule_usage_events",
|
|
# BigInteger throughout where the note twin uses Integer: rules.id is
|
|
# BigInteger, so rule_id must be, and a high-churn append-only table is
|
|
# a poor place to discover an id ceiling.
|
|
sa.Column("id", sa.BigInteger(), primary_key=True),
|
|
sa.Column(
|
|
"created_at",
|
|
sa.DateTime(timezone=True),
|
|
nullable=False,
|
|
server_default=sa.text("now()"),
|
|
),
|
|
sa.Column("user_id", sa.BigInteger(), nullable=True),
|
|
sa.Column("rule_id", sa.BigInteger(), nullable=False),
|
|
sa.Column("event", sa.Text(), nullable=False),
|
|
sa.Column("source", sa.Text(), nullable=False),
|
|
)
|
|
# Every readout is "these rule ids, split by event", so the composite is the
|
|
# one that actually gets used; the others serve pruning and per-user views.
|
|
op.create_index(
|
|
"ix_rule_usage_rule_event", "rule_usage_events", ["rule_id", "event"]
|
|
)
|
|
op.create_index("ix_rule_usage_created_at", "rule_usage_events", ["created_at"])
|
|
op.create_index("ix_rule_usage_user_id", "rule_usage_events", ["user_id"])
|
|
|
|
|
|
def downgrade() -> None:
|
|
op.drop_index("ix_rule_usage_user_id", table_name="rule_usage_events")
|
|
op.drop_index("ix_rule_usage_created_at", table_name="rule_usage_events")
|
|
op.drop_index("ix_rule_usage_rule_event", table_name="rule_usage_events")
|
|
op.drop_table("rule_usage_events")
|