Milestone 414 steps 1–3: a rule is global or belongs to one project, and retrieval honours which; #3191 read-scope fix #158

Merged
bvandeusen merged 5 commits from dev into main 2026-09-15 12:49:19 -04:00
Owner

Milestone 414 "A rule is global or belongs to one project — and retrieval honours which", steps 1–3, plus #3191.

What ships

  • #4074, rule retrieval honours scope (188e78b): the prompt, pre-tool, write-path and preference-slot rule searches surface global rules plus the bound project's own, never another project's. An unbound session gets global rules only. search(content_type="rule") scopes to its project_id, or searches the whole rulebook without one. A shared project's rules reach its collaborators through access.can_read_project.
  • #4052, retire subscriptions and suppressions (0bcd4b5, b6751e4). Breaking.
    • Schema and data: migration 0101 drops project_rulebook_subscriptions, project_rule_suppressions and project_topic_suppressions, and strips the retired choices from stored inception records.
    • Removed everywhere: the subscribe and suppress operations are gone from services, MCP tools, REST routes and the UI.
    • New-project setup asks two questions (design system, starter Systems).
    • Backup v15. Older archives still restore.
    • Rule listings: get_applicable_rules returns a project's own rules plus the global rules tagged to an area it works in.
    • Guidance: the using-scribe skill and docs say global vs project.
  • #4063, move a rule between scopes (4e4020c): move_rule (MCP), POST /api/rules/<id>/move (REST), and a "Where this rule applies" picker in the rule editor and the project rules tab. It keeps the rule's id, history, areas and relations, and refuses a title clash in the target topic with a clear message.
  • #3191, read-scope fix (0bf7406): rules_due_for_verification and rule_history now work with a read-only key. Every registered MCP tool must now be declared read or write, checked by a test that can be shown to fail.

Plugin 2026.09.15.1626.

CI

Green on dev: runs 6897, 6899, 6900 and 6901 (typecheck, plugin hooks, lint, unit, integration).

After deploy

  • /plugin and /reload-plugins.
  • Check in the browser: a project's rules tab, the new-project setup card, and the move picker.
  • Step 4 (#4075): move rule 94 to a global topic, then check in a live session that other projects' rules no longer arrive.

🤖 Generated with Claude Code

https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy

Milestone 414 "A rule is global or belongs to one project — and retrieval honours which", steps 1–3, plus #3191. ## What ships - **#4074, rule retrieval honours scope** (188e78b): the prompt, pre-tool, write-path and preference-slot rule searches surface global rules plus the bound project's own, never another project's. An unbound session gets global rules only. `search(content_type="rule")` scopes to its `project_id`, or searches the whole rulebook without one. A shared project's rules reach its collaborators through `access.can_read_project`. - **#4052, retire subscriptions and suppressions** (0bcd4b5, b6751e4). **Breaking.** - **Schema and data:** migration 0101 drops `project_rulebook_subscriptions`, `project_rule_suppressions` and `project_topic_suppressions`, and strips the retired choices from stored inception records. - **Removed everywhere:** the subscribe and suppress operations are gone from services, MCP tools, REST routes and the UI. - **New-project setup** asks two questions (design system, starter Systems). - **Backup v15.** Older archives still restore. - **Rule listings:** `get_applicable_rules` returns a project's own rules plus the global rules tagged to an area it works in. - **Guidance:** the using-scribe skill and docs say global vs project. - **#4063, move a rule between scopes** (4e4020c): `move_rule` (MCP), `POST /api/rules/<id>/move` (REST), and a "Where this rule applies" picker in the rule editor and the project rules tab. It keeps the rule's id, history, areas and relations, and refuses a title clash in the target topic with a clear message. - **#3191, read-scope fix** (0bf7406): `rules_due_for_verification` and `rule_history` now work with a read-only key. Every registered MCP tool must now be declared read or write, checked by a test that can be shown to fail. Plugin 2026.09.15.1626. ## CI Green on dev: runs 6897, 6899, 6900 and 6901 (typecheck, plugin hooks, lint, unit, integration). ## After deploy - `/plugin` and `/reload-plugins`. - Check in the browser: a project's rules tab, the new-project setup card, and the move picker. - Step 4 (#4075): move rule 94 to a global topic, then check in a live session that other projects' rules no longer arrive. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
bvandeusen added 5 commits 2026-09-15 12:49:13 -04:00
feat(rules): retrieval honours a rule's home — global everywhere, a project's rules only in that project (#4074)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / TypeScript typecheck (push) Successful in 54s
CI & Build / integration (push) Successful in 59s
CI & Build / Python tests (push) Successful in 1m37s
CI & Build / Build & push image (push) Successful in 31s
188e78bbcd
semantic_search_rules searched every rule the user owned, and every hook arm
called it without a project, so each project's rules were injected into every
other project's sessions and a project rule meant nothing a session could feel.

The search now takes a scope: global rules by default (an unbound session, or a
caller that forgets to say), global plus project N when given project_id (N's
rules only if the caller can read that project, through access.can_read_project),
and every owned rule with everywhere=True. The four hook arms and the report
preference lookup pass the session's project; an explicit
search(content_type="rule") scopes to its project_id, or asks the whole rulebook
without one.

Milestone 414 step 1. Guarded by an AST walk that every hook call site passes
project_id, and an integration test on real Postgres that a rule is reached only
from its home.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
feat(rules)!: retire rulebook subscriptions and per-project suppressions (#4052)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / integration (push) Successful in 49s
CI & Build / TypeScript typecheck (push) Successful in 57s
CI & Build / Python tests (push) Failing after 1m3s
CI & Build / Build & push image (push) Skipped
0bcd4b5540
A rule's home is its scope now: a rule in a rulebook topic is global, a rule on
a project applies to that project, and retrieval reads that directly (#4074).
A subscription had stopped changing anything a session received; a suppression
muted rules from a subscription. Operator, 2026-09-15: "we have global and
project scoped rules, we don't need the subscriptions now."

What goes, whole (rule 22):
- Migration 0101 drops project_rulebook_subscriptions, project_rule_suppressions
  and project_topic_suppressions, and strips subscribe_rulebooks (and 394's
  leftover exclude_always_on_rulebooks) from stored inception choices.
- Service, MCP and REST: subscribe/unsubscribe and the four suppress/unsuppress
  operations. The Subscribers checklist, the subscribe chips, the skip buttons
  and the Suppressed section in the rules UI.
- Inception asks two questions (design system, seed Systems). create_project and
  decide_project_inception lose subscribe_rulebooks.
- Backup v15 stops exporting the three sections; older archives still restore,
  the keys simply unread. Trash no longer hard-deletes suppression rows.

What changes meaning:
- get_applicable_rules is a project's LISTING: its own rules, plus the global
  rules tagged to an area it works in. Untagged global rules apply everywhere
  and arrive by retrieval, so they are not listed. A co_surfaces partner on a
  different project is not dragged in.
- list_rules(project_id) lists that project's own rules.
- rules_payload drops subscribed_rulebooks and suppressed_*; the handshake's
  brief form is project_rules alone.
- using-scribe's "Where a new rule goes" and inception sections, tool
  docstrings and docs say global vs project. Plugin 2026.09.15.1620.

Milestone 414 step 2.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
test(backup): pin BACKUP_VERSION 15 (#4052)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Successful in 42s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / Python tests (push) Successful in 1m35s
CI & Build / Build & push image (push) Successful in 36s
b6751e4214
The subscription and suppression sections left the payload in 0bcd4b5; the
version moved with them and the pin did not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
feat(rules): move a rule between global and project scope, keeping its id, history, areas and edges (#4063)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 10s
CI & Build / integration (push) Successful in 54s
CI & Build / TypeScript typecheck (push) Successful in 55s
CI & Build / Python tests (push) Successful in 1m41s
CI & Build / Build & push image (push) Successful in 33s
4e4020c040
A rule's home is its reach: a rulebook topic makes it global, a project makes it
that project's. There was no way to change one, so a project rule decided to be
global could only be recreated and the original trashed — losing the id every
record cites, its edit history, its area tags and its relations.

- services.rulebooks.move_rule(rule_id, user_id, topic_id= | project_id=):
  exactly one destination (the model's CHECK), owned by the caller, not the
  rule's current home. A topic already holding a live rule with the same title
  is refused with a message naming that rule, instead of uq_rule_per_topic
  failing the commit. Someone else's rule reads as not found.
- Deliberately NOT done, and said in the docstring: no version (a version is
  what a rule said, milestone 323 decision 4), no duplicate gate (nothing new
  enters the corpus), no re-embed (retrieval reads the home at query time).
- Both doors: MCP move_rule, REST POST /api/rules/<id>/move (rule 33).
- UI: RuleHomePicker, one component in the rule editor (a global rule) and a
  project's rules tab (a project rule), so the two cannot drift on what a
  destination is.
- using-scribe names move_rule under "Where a new rule goes". Plugin
  2026.09.15.1626.

Milestone 414 step 3.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
fix(mcp): two rule reads reach a read-only key, and every tool must now be classified (#3191)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 10s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / integration (push) Successful in 55s
CI & Build / Python tests (push) Successful in 1m40s
CI & Build / Build & push image (push) Successful in 25s
0bf7406f42
rules_due_for_verification (the rule staleness sweep) and rule_history (what a
rule used to say) are pure reads, and a read-scoped API key was refused both:
neither is in _READ_ONLY_TOOLS, and the completeness test that should have
caught it only looked at tools whose NAMES start like a read (get_, list_,
search…). Neither does.

- Both join _READ_ONLY_TOOLS; the comment that pointed at this issue now says
  why they sat unlisted.
- _WRITE_TOOLS declares every writing tool by name. Nothing reads it at
  runtime — default-deny already refuses an unlisted tool — it exists so the
  classification is total.
- test_every_registered_tool_is_classified_exactly_once takes its candidates
  from what build_mcp_server() actually mounts, requires each in exactly one
  of the three sets, and still flags a classified name that is no tool. The
  decision stays explicit; only the candidate set widened.
- test_the_completeness_check_can_fail drops a real tool from its set and
  asserts it is noticed (rule 167).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
bvandeusen merged commit 790abdb68c into main 2026-09-15 12:49:19 -04:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: bvandeusen/FabledScribe#158