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)
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.
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)
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>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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)andproject_topic_suppressions(project_id, topic_id). PKs on both columns; FKs CASCADE. Nodeleted_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>andPOST/DELETE /api/projects/<pid>/suppressions/topics/<tid>. 204 on success.Service-layer projection:
get_applicable_rulesruns 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}ruleslist is filtered SQL-side, sotruncatedreflects the post-suppression countrulesnow also carriestopic_idandrulebook_idcolumns for the UI's per-row affordanceThese two new fields propagate through
get_project,enter_project,get_task(plan), andstart_planningso 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):groupByRulebookAndTopicrefactored to return id-bearing structs (was string-keyed records — needed for the per-topic suppress click)Tests
get_applicable_rulesshape tests rewritten: execute count went 3 → 5, rules tuple grew 5 → 7 columns, new test coverssuppressed_rules/suppressed_topicscontentDeploy
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
Rule.project_idrules can't be suppressed (delete them instead). Asymmetric on purpose: suppression is meant for rules you don't own.🤖 Generated with Claude Code