43a860c3ac
Rules can now belong to either a rulebook topic OR a single project,
enforced by a CHECK constraint (exactly-one of topic_id/project_id).
Adds the create_project_rule MCP tool + REST endpoint, surfaces
project-scoped rules in get_project/get_task/start_planning under a
new project_rules field, and adds a project Rules tab section with an
inline create form so the operator can author project rules from the
UI without rulebook ceremony.
- migration 0059: rules.project_id (FK projects ON DELETE CASCADE),
topic_id now nullable, CHECK ck_rule_topic_xor_project, index on
project_id
- model: Rule gains project_id; to_dict exposes it
- service: create_project_rule with project-ownership guard; list_rules
with project_id filter UNIONs subscription-derived + project-scoped;
get_applicable_rules adds a project_rules field; get_rule / update_rule
/ delete_rule fetch via a shared _fetch_owned_rule that handles both
rulebook and project ownership paths
- trash: project delete cascades to project-scoped rules
- MCP: create_project_rule tool registered; _INSTRUCTIONS mentions both
create_rule and create_project_rule paths
- REST: POST /api/projects/<id>/rules (statement required, title derived
if omitted)
- frontend: Rule type gains nullable topic_id + project_id; createProjectRule
client; ProjectRulesTab.vue gains a "Project rules" section with inline
create form and per-rule expand/delete
- tests: register count → 18; create_project_rule unit tests (required
fields, title derivation, explicit-title pass-through); applicable_rules
shape tests now include project_rules; trash cascade test updated to
expect 5 executions
S1+S2 (always_on flag + Scribe-first prompt) shipped in 658348f.
S4 (enter_project handshake) follows.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
50 lines
1.5 KiB
Python
50 lines
1.5 KiB
Python
"""project-scoped rules
|
|
|
|
Revision ID: 0059
|
|
Revises: 0058
|
|
Create Date: 2026-06-01
|
|
|
|
Rules can now belong to either a rulebook topic (cross-project standard) or
|
|
a single project (project-scoped). Adds `rules.project_id`, makes `topic_id`
|
|
nullable, and adds a CHECK constraint enforcing exactly-one. The previous
|
|
unique constraint on (topic_id, title) still applies because PostgreSQL
|
|
treats NULL as distinct — two project-scoped rules with the same title and
|
|
NULL topic_id remain unique.
|
|
"""
|
|
from alembic import op
|
|
import sqlalchemy as sa
|
|
|
|
|
|
revision = "0059"
|
|
down_revision = "0058"
|
|
branch_labels = None
|
|
depends_on = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
op.add_column(
|
|
"rules",
|
|
sa.Column(
|
|
"project_id",
|
|
sa.BigInteger(),
|
|
sa.ForeignKey("projects.id", ondelete="CASCADE"),
|
|
nullable=True,
|
|
),
|
|
)
|
|
op.alter_column("rules", "topic_id", nullable=True)
|
|
op.create_index("ix_rules_project_id", "rules", ["project_id"])
|
|
op.create_check_constraint(
|
|
"ck_rule_topic_xor_project",
|
|
"rules",
|
|
"(topic_id IS NULL) <> (project_id IS NULL)",
|
|
)
|
|
|
|
|
|
def downgrade() -> None:
|
|
op.drop_constraint("ck_rule_topic_xor_project", "rules", type_="check")
|
|
op.drop_index("ix_rules_project_id", table_name="rules")
|
|
# Any rule with NULL topic_id will block re-tightening. Operator must
|
|
# migrate or delete project-scoped rules before downgrading.
|
|
op.alter_column("rules", "topic_id", nullable=False)
|
|
op.drop_column("rules", "project_id")
|