Project rule + topic suppressions #54

Merged
bvandeusen merged 1 commits from dev into main 2026-06-01 08:01:59 -04:00
Owner

Lets a project mute individual rules or whole topics from rulebooks it subscribes to, without unsubscribing the rulebook. Companion to the prior rules-consolidation work (S1–S4 in PR #53) and implements Scribe task #187.

Use case

A project subscribes to a broad rulebook (e.g. FabledSword family, or a future "FabledSword design system") but wants to opt out of one or two specific rules — or a whole topic — that doesn't apply. Without this, the operator's only options were "don't subscribe at all" (loses every rule in the rulebook) or "fork the rulebook" (maintenance nightmare).

Changes

Schema (migration 0060): two new pure many-to-many association tables — project_rule_suppressions(project_id, rule_id) and project_topic_suppressions(project_id, topic_id). PKs on both columns; FKs CASCADE. No deleted_at — pure associations, hard-deleted on project trash.

MCP tools (4): suppress_rule_for_project, unsuppress_rule_for_project, suppress_topic_for_project, unsuppress_topic_for_project. All idempotent; verify project ownership AND that the rule/topic belongs to a rulebook the user owns. Project-scoped rules (Rule.project_id) are deliberately not suppressible — delete them instead.

REST: POST/DELETE /api/projects/<pid>/suppressions/rules/<rid> and POST/DELETE /api/projects/<pid>/suppressions/topics/<tid>. 204 on success.

Service-layer projection: get_applicable_rules runs two extra queries (cheap — bounded by suppression count) and now emits:

  • suppressed_rules — detail objects {id, title, topic_id, topic_title, rulebook_id, rulebook_title} (deviation from the plan's bare ID-array spec; UI needed titles, bundling avoids a follow-up lookup)
  • suppressed_topics — detail objects {id, title, rulebook_id, rulebook_title}
  • The main rules list is filtered SQL-side, so truncated reflects the post-suppression count
  • Each rule in rules now also carries topic_id and rulebook_id columns for the UI's per-row affordance

These two new fields propagate through get_project, enter_project, get_task (plan), and start_planning so Claude sees what's muted on a project from any of the standard handshakes.

Trash cascade: project deletion hard-deletes suppression rows for that project. FK CASCADE would only fire on a final purge; the soft-delete path keeps the project row alive, so we clean up explicitly here to ensure restore doesn't bring stale mutes back.

Frontend (ProjectRulesTab.vue):

  • "× skip" affordance reveals on hover for each rule row and each topic header
  • Collapsed "Suppressed (N)" section at the bottom with per-item "↻ re-enable" buttons
  • groupByRulebookAndTopic refactored to return id-bearing structs (was string-keyed records — needed for the per-topic suppress click)

Tests

  • 4 new MCP-tool tests (signature pass-through for each suppress/unsuppress)
  • Register count assertion bumped 18 → 22
  • Routes signature loop extended; new structural tests for handler-callable + association-tables-declared
  • get_applicable_rules shape tests rewritten: execute count went 3 → 5, rules tuple grew 5 → 7 columns, new test covers suppressed_rules/suppressed_topics content
  • Trash project-cascade test: execute count 5 → 7 (added two DELETEs on suppression tables)

Deploy

Migration 0060 runs automatically on container start. Tested on dev: migration applied cleanly; MCP suppression tools work end-to-end; UI affordance reveals on hover and the Suppressed section toggles correctly.

What this does NOT do

  • Supersede semantics deferred — a project rule cannot yet replace a rulebook rule (only mute it). The stated use case was pure ignore; replacement can land later if it ever bites.
  • Project-scoped rules untouchableRule.project_id rules can't be suppressed (delete them instead). Asymmetric on purpose: suppression is meant for rules you don't own.

🤖 Generated with Claude Code

Lets a project mute individual rules or whole topics from rulebooks it subscribes to, without unsubscribing the rulebook. Companion to the prior rules-consolidation work (S1–S4 in PR #53) and implements Scribe task #187. ## Use case A project subscribes to a broad rulebook (e.g. FabledSword family, or a future "FabledSword design system") but wants to opt out of one or two specific rules — or a whole topic — that doesn't apply. Without this, the operator's only options were "don't subscribe at all" (loses every rule in the rulebook) or "fork the rulebook" (maintenance nightmare). ## Changes **Schema (migration 0060):** two new pure many-to-many association tables — `project_rule_suppressions(project_id, rule_id)` and `project_topic_suppressions(project_id, topic_id)`. PKs on both columns; FKs CASCADE. No `deleted_at` — pure associations, hard-deleted on project trash. **MCP tools (4):** `suppress_rule_for_project`, `unsuppress_rule_for_project`, `suppress_topic_for_project`, `unsuppress_topic_for_project`. All idempotent; verify project ownership AND that the rule/topic belongs to a rulebook the user owns. Project-scoped rules (`Rule.project_id`) are deliberately not suppressible — delete them instead. **REST:** `POST/DELETE /api/projects/<pid>/suppressions/rules/<rid>` and `POST/DELETE /api/projects/<pid>/suppressions/topics/<tid>`. 204 on success. **Service-layer projection:** `get_applicable_rules` runs two extra queries (cheap — bounded by suppression count) and now emits: - `suppressed_rules` — detail objects `{id, title, topic_id, topic_title, rulebook_id, rulebook_title}` (deviation from the plan's bare ID-array spec; UI needed titles, bundling avoids a follow-up lookup) - `suppressed_topics` — detail objects `{id, title, rulebook_id, rulebook_title}` - The main `rules` list is filtered SQL-side, so `truncated` reflects the post-suppression count - Each rule in `rules` now also carries `topic_id` and `rulebook_id` columns for the UI's per-row affordance These two new fields propagate through `get_project`, `enter_project`, `get_task` (plan), and `start_planning` so Claude sees what's muted on a project from any of the standard handshakes. **Trash cascade:** project deletion hard-deletes suppression rows for that project. FK CASCADE would only fire on a final purge; the soft-delete path keeps the project row alive, so we clean up explicitly here to ensure restore doesn't bring stale mutes back. **Frontend (`ProjectRulesTab.vue`):** - "× skip" affordance reveals on hover for each rule row and each topic header - Collapsed "Suppressed (N)" section at the bottom with per-item "↻ re-enable" buttons - `groupByRulebookAndTopic` refactored to return id-bearing structs (was string-keyed records — needed for the per-topic suppress click) ## Tests - 4 new MCP-tool tests (signature pass-through for each suppress/unsuppress) - Register count assertion bumped 18 → 22 - Routes signature loop extended; new structural tests for handler-callable + association-tables-declared - `get_applicable_rules` shape tests rewritten: execute count went 3 → 5, rules tuple grew 5 → 7 columns, new test covers `suppressed_rules`/`suppressed_topics` content - Trash project-cascade test: execute count 5 → 7 (added two DELETEs on suppression tables) ## Deploy Migration 0060 runs automatically on container start. Tested on dev: ✅ migration applied cleanly; ✅ MCP suppression tools work end-to-end; ✅ UI affordance reveals on hover and the Suppressed section toggles correctly. ## What this does NOT do - **Supersede semantics deferred** — a project rule cannot yet *replace* a rulebook rule (only mute it). The stated use case was pure ignore; replacement can land later if it ever bites. - **Project-scoped rules untouchable** — `Rule.project_id` rules can't be suppressed (delete them instead). Asymmetric on purpose: suppression is meant for rules you don't own. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
bvandeusen added 1 commit 2026-06-01 08:01:53 -04:00
feat(rules): project rule + topic suppressions
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 39s
CI & Build / Python tests (push) Successful in 43s
CI & Build / Build & push image (push) Successful in 1m18s
7861607fb8
Lets a project mute individual rules or whole topics from rulebooks it
subscribes to, without unsubscribing the rulebook. Two new association
tables (migration 0060), 4 MCP tools (suppress/unsuppress × rule/topic),
4 REST endpoints, and an inline "× skip" affordance plus collapsed
"Suppressed (N)" section in the project's Rules tab.

get_applicable_rules now emits suppressed_rules and suppressed_topics
(detail objects with rulebook/topic context, not just IDs) so the UI
can render the suppressed list without a follow-up lookup. The main
rules projection grew topic_id and rulebook_id columns for the per-row
suppress affordance.

Project deletion cascades the suppression rows via hard DELETE — they
are pure associations with no soft-delete column, and restoring a
deleted project should start fresh, not inherit stale mutes.

Project-scoped rules (Rule.project_id) are deliberately not suppressible
— delete them with delete_rule instead.

Implements plan-task #187.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
bvandeusen merged commit 0e980ee4b0 into main 2026-06-01 08:01:59 -04:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: bvandeusen/FabledScribe#54