feat(rules): project-scoped rules (S3)
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>
This commit is contained in:
@@ -81,8 +81,18 @@ class Rule(Base, SoftDeleteMixin):
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
|
||||
topic_id: Mapped[int] = mapped_column(
|
||||
BigInteger, ForeignKey("rulebook_topics.id", ondelete="CASCADE")
|
||||
# Exactly one of topic_id / project_id is set — enforced by CHECK
|
||||
# constraint ck_rule_topic_xor_project (migration 0059).
|
||||
topic_id: Mapped[int | None] = mapped_column(
|
||||
BigInteger,
|
||||
ForeignKey("rulebook_topics.id", ondelete="CASCADE"),
|
||||
nullable=True,
|
||||
)
|
||||
project_id: Mapped[int | None] = mapped_column(
|
||||
BigInteger,
|
||||
ForeignKey("projects.id", ondelete="CASCADE"),
|
||||
nullable=True,
|
||||
index=True,
|
||||
)
|
||||
title: Mapped[str] = mapped_column(Text)
|
||||
statement: Mapped[str] = mapped_column(Text)
|
||||
@@ -102,6 +112,7 @@ class Rule(Base, SoftDeleteMixin):
|
||||
return {
|
||||
"id": self.id,
|
||||
"topic_id": self.topic_id,
|
||||
"project_id": self.project_id,
|
||||
"title": self.title,
|
||||
"statement": self.statement,
|
||||
"why": self.why or "",
|
||||
|
||||
Reference in New Issue
Block a user