Lessons point at rules (milestone 440 steps 1–2) + the away-from-home usage readout #189

Merged
bvandeusen merged 5 commits from dev into main 2026-10-01 13:48:01 -04:00
Owner

What this ships

Milestone 440 "Lessons point at rules — a rule is found through the situations that keep proving it"

  • #4630 "The link" (41e4fba, c839397). Migration 0111 adds lesson_rule_links, with three states: suggested, confirmed, rejected.
    • create_lesson and update_lesson take rule_ids.
    • New tool judge_lesson_link, and PUT /api/lessons/<id>/rules/<rule_id>.
    • get_lesson shows the lesson's rules, and get_rule shows the rule's lessons.
    • Backup v18.
  • #4631 "Judged when written" (f7d8dc2, 95be251). Migration 0112 adds lesson_no_rule. "No rule fits" is now a recorded answer, given with its reason as no_rule="why".
    • An unanswered create returns rule_candidates.
    • Every lesson payload carries rule_judgment.
    • list_lessons(unjudged=true) and GET /api/lessons?unjudged=1 list the open ones.
    • Backup v19.
    • Guidance is in the using-scribe skill and the create_lesson and create_rule docstrings. Plugin 2026.10.01.1724.

#3735 "Step 8 — acceptance: a lesson is recalled on a project it was not written on" (2e4c2d9)

  • usage_for_notes adds surfaced_away_count and pulled_away_count.
  • The usage chip shows them.

Verification

CI on dev passed every job on the head commit 95be251 (run 708). That covers the real-Postgres integration tests for links, no-rule answers, the unjudged list, cascades and ACL.

Deploy notes

  • Two migrations run on startup: 0111, then 0112. Both are additive, with no backfill.
  • Running sessions need /plugin and /reload-plugins to pick up plugin 2026.10.01.1724.

🤖 Generated with Claude Code

## What this ships **Milestone 440 "Lessons point at rules — a rule is found through the situations that keep proving it"** - **#4630 "The link"** (41e4fba, c839397). Migration 0111 adds `lesson_rule_links`, with three states: suggested, confirmed, rejected. - `create_lesson` and `update_lesson` take `rule_ids`. - New tool `judge_lesson_link`, and `PUT /api/lessons/<id>/rules/<rule_id>`. - `get_lesson` shows the lesson's `rules`, and `get_rule` shows the rule's `lessons`. - Backup v18. - **#4631 "Judged when written"** (f7d8dc2, 95be251). Migration 0112 adds `lesson_no_rule`. "No rule fits" is now a recorded answer, given with its reason as `no_rule="why"`. - An unanswered create returns `rule_candidates`. - Every lesson payload carries `rule_judgment`. - `list_lessons(unjudged=true)` and `GET /api/lessons?unjudged=1` list the open ones. - Backup v19. - Guidance is in the using-scribe skill and the `create_lesson` and `create_rule` docstrings. Plugin 2026.10.01.1724. **#3735 "Step 8 — acceptance: a lesson is recalled on a project it was not written on"** (2e4c2d9) - `usage_for_notes` adds `surfaced_away_count` and `pulled_away_count`. - The usage chip shows them. ## Verification CI on dev passed every job on the head commit 95be251 (run 708). That covers the real-Postgres integration tests for links, no-rule answers, the unjudged list, cascades and ACL. ## Deploy notes - Two migrations run on startup: 0111, then 0112. Both are additive, with no backfill. - Running sessions need `/plugin` and `/reload-plugins` to pick up plugin 2026.10.01.1724. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
bvandeusen added 5 commits 2026-10-01 13:47:58 -04:00
feat(usage): count what happened away from a record's own project — the readout #3735 needs
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / TypeScript typecheck (push) Successful in 54s
CI & Build / integration (push) Successful in 1m6s
CI & Build / Python tests (push) Successful in 1m50s
CI & Build / Build & push image (push) Successful in 38s
2e4c2d9493
Milestone 385 step 8 (#3735 "a lesson is recalled on a project it was not
written on") is defined by opened-on-another-project. The reader's project has
been recorded on every usage event since 0c8e109, but nothing compared it with
the record's own, so the criterion was still unreadable.

- note_usage.usage_for_notes: a second aggregate in the same session joins
  notes and counts surfaced_away_count / pulled_away_count. Counted only where
  both projects are known and differ; ranked surfacings only (#2477). The
  first aggregate is untouched, so events on deleted notes still count.
- empty_usage carries both keys zero-filled; every door that attaches usage
  (list_lessons, get_lesson, snippets, knowledge) gets them through
  attach_usage.
- UsageBadge tooltip says "On other projects: surfaced N×, opened M×" when it
  happened, and nothing when it did not.
- Tests: the mocked split test feeds both aggregates; a unit test for the
  away counters; a real-Postgres test that home, unreported and ambient
  events are all left out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
feat(lessons): a lesson names the rule it is an instance of — lesson_rule_links (milestone 440 step 1, #4630)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 15s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / integration (push) Successful in 54s
CI & Build / Python tests (push) Failing after 1m13s
CI & Build / Build & push image (push) Skipped
41e4fbaba1
The link between a lesson (one concrete situation) and the rule that governs
it, with the operator's soft-then-hard design built into its state:
suggested while evidence accumulates, confirmed or rejected once judged. Only
confirmed will carry a rule in retrieval (#4633); rejected is kept so the pair
is never proposed again.

- models/lesson_rule_link.py + migration 0111: one row per (lesson, rule),
  CASCADE on both ends, indexed both ways, CHECK on state (rule 36), evidence
  JSONB and judged_at.
- services/lesson_rules.py: require_rules (validated before any write, so
  a bad id leaves nothing half-linked), set_lesson_rules (set-semantics;
  a dropped rule becomes rejected, not forgotten), judge_link, and the two
  reads. ACL: write on the lesson (share-aware), ownership of the rule; a
  reader sees only rules they own. Decorations are fail-open (#4286).
- MCP: create_lesson / update_lesson take rule_ids; get/create/update return
  `rules`; new judge_lesson_link tool. REST: the same on /api/lessons plus
  PUT /api/lessons/<id>/rules/<rule_id>. Rules: rule_detail carries `lessons`.
- Backup v18: export (full and user-scoped, both ends in scope), builder,
  importer; both column guards register the table.
- Tests: integration (states, set-semantics, judge, ACL all-or-nothing,
  cascade both ways, CHECK, one row per pair); unit (door wiring, judge
  registered, migration/model state agreement, backup skip and unjudged
  stays unjudged). conftest stubs the decorations for unit tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix(mcp): classify judge_lesson_link as a write tool (#4630)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 11s
CI & Build / integration (push) Successful in 48s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / Python tests (push) Successful in 1m42s
CI & Build / Build & push image (push) Successful in 31s
c8393975c3
The tool-classification guard (test_mcp_auth) failed CI run 705: the new
judge_lesson_link tool sat in no set, so a read key would have been silently
denied it. It changes a link's state, so it belongs in _WRITE_TOOLS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
feat(lessons): judged when written — a new lesson is offered its rules, and "no rule fits" is an answer (milestone 440 step 2, #4631)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 14s
CI & Build / TypeScript typecheck (push) Successful in 52s
CI & Build / integration (push) Successful in 53s
CI & Build / Python tests (push) Failing after 1m15s
CI & Build / Build & push image (push) Skipped
f7d8dc2e55
"Which rule is this lesson an instance of?" now has three recorded answers:
a rule named (a confirmed link, #4630), no rule fits (new), or unjudged.

- Model + migration 0112: lesson_no_rule (lesson_id PK, CASCADE from the
  note; why; judged_at). A table rather than a key in notes.data, because
  that mirror is re-composed from the body on every edit and would erase it.
- Service (lesson_rules): set_no_rule rejects any confirmed link with the
  reason; a confirmation (set_lesson_rules or judge_link) deletes the answer;
  require_one_answer refuses both answers in one call before any write;
  judgments_for_lessons + attach_lesson_rules add rule_judgment (and no_rule)
  to every lesson payload; list_unjudged lists the open ones; rule_candidates
  searches rules with the lesson's claim + trigger at the explicit-search bar,
  None when the search could not run.
- MCP: create_lesson/update_lesson take no_rule; an unanswered create returns
  rule_candidates, rule_judgment and a rule_hint; list_lessons(unjudged=true).
- REST: the same on POST/PATCH /api/lessons and GET ?unjudged=1; create
  returns rule_candidates.
- Backup v19: a lesson_no_rule section, export (full and per-user) and import.
- Guidance: create_lesson docstring, writing-records.md in using-scribe (owner,
  pinned in test_guidance_ownership), create_rule docstring on linking the
  lessons a new rule governs. Plugin version minted.
- Tests: door units, integration for the three states, the rejection reason,
  scoping, cascade; backup registries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix(lessons): build the rule-candidate query with trigger_title (#4631)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 15s
CI & Build / TypeScript typecheck (push) Successful in 1m0s
CI & Build / integration (push) Successful in 1m1s
CI & Build / Python tests (push) Successful in 1m53s
CI & Build / Build & push image (push) Successful in 27s
95be2512ab
CI run 707 failed test_the_trigger_separator_is_spelled_in_exactly_one_place:
rule_candidates joined the lesson's claim and trigger with an inline " — ",
a second spelling of embeddings.TRIGGER_SEP (#3207). It now calls
trigger_title — the same join a rule's own document title is built with,
which was the point of the query shape in the first place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
bvandeusen merged commit 6ba40c9e10 into main 2026-10-01 13:48:01 -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#189