Commit Graph
2 Commits
Author SHA1 Message Date
bvandeusenandClaude Opus 5 22bb6d7a1a fix(tests): move #4196's usage guards to the seam they now describe (#4230)
CI & Build / integration (push) Successful in 45s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / Python tests (push) Successful in 1m34s
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 10s
CI & Build / Build & push image (push) Successful in 33s
CI 7223 went red on seven tests in test_lesson_usage_surface.py. None of them
caught a regression. Every property they pin still holds; they pinned it at the
old LOCATION: they patched `usage_for_notes` on the door modules and read the
route source for `usage_for_notes(` and `empty_usage()`. 62f3a48 moved all
seven doors onto `note_usage.attach_usage`, so the name is gone from the doors.

This is rule 167's case: a guard that fails on code you believe is correct has
usually moved out from under its property. So the guards are relocated, not
deleted. Each one still has an assertion that can fail:

- MCP list / once-per-page / read-before-pull: patch `usage_for_notes` on
  `note_usage`, the module every door now reads it through. The ordering test
  still fails if a pull is recorded before the count is read.
- REST list: calls `attach_usage(items)` exactly once, and does NOT hand-roll
  `it["usage"] =`, which is how a row could lose its zero-filled key again.
  The seam's own zero-fill and single aggregate are asserted in
  test_usage_attach_seam.py.
- REST detail: `attach_usage(` still precedes `record_pulled(`.
- Detail view: the advice moved into utils/deadWeight.ts, so assert BOTH that
  the view reads DEAD_WEIGHT_ADVICE.lesson AND that the table's lesson entry
  still points at `when_to_apply`, so the guard cannot pass by pointing at an
  entry that has stopped saying the right thing.

Missed before pushing because I grepped src/ for callers of the old name and
not tests/ for patchers of it. A test that patches a name off a module is a
caller too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
2026-09-21 22:03:05 -04:00
bvandeusenandClaude Opus 5 edbc31f8ca feat(lessons): the kind whose whole question is "is this trigger right" was the one kind that could not see its own counts (#4196)
CI & Build / Python lint (push) Successful in 8s
CI & Build / Plugin hooks (push) Successful in 17s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / integration (push) Successful in 57s
CI & Build / Python tests (push) Successful in 1m41s
CI & Build / Build & push image (push) Successful in 36s
The lesson slot has recorded surfaced-vs-opened since it shipped. Nothing
showed it. `get_lesson`'s REST door attached `usage` to the payload and no
view rendered it; the listing did not attach it at all, and neither MCP door
did.

#4196 asks when a lesson that keeps getting followed should become a rule, and
names the trap in the same breath: raw frequency cannot separate "this should
bind" from "this trigger is too broad", and the second is the commoner reading
by a wide margin. Surfaced-AND-opened can separate them. Neither question is
answerable by a reader who cannot see the numbers, which is why this is the
first step and not the threshold.

NO THRESHOLD IS PROPOSED HERE, deliberately. The corpus today is 10 lessons
with 7 recorded surfacings and 3 opens, over about fifteen hours of usage
data. A promotion rule fitted to that would be fitting noise — #3311's failure,
and the warning lesson #4228 was written to carry. `UsageBadge` already
declines to render a verdict under three surfacings for the same reason. So
#4196 stays open: its subject, the promotion path, is still unbuilt. What
lands is the evidence it needs.

  - REST `GET /api/lessons` and MCP `list_lessons` attach `usage` to every
    row, from one aggregate per page rather than a per-row read, which would
    be N+1 by construction. Every row carries the key zero-filled, so "never
    surfaced" is a state a reader can see rather than a missing field they
    have to interpret.
  - MCP `get_lesson` attaches it too, and reads it BEFORE recording its own
    pull. That door records a pull on every open — it has to, or the kind sits
    permanently at zero — which makes the order load-bearing in a way it is
    not for a kind that only counts. The REST detail door already ordered it
    this way; the two now agree about what the number means.
  - `LessonDetailView` renders `UsageBadge` (snippet #3460) rather than
    re-spelling the chip, with the advice keyed to this kind: a lesson that is
    repeatedly offered and never opened is usually keyed to a situation nobody
    is in, so it points at re-keying `when_to_apply`, not at deleting the
    claim.

Guards, in the two styles this pair of doors already uses: the MCP side driven
behaviourally through mocks, including the call ORDER for `get_lesson`; the
REST side on structure like its siblings in test_lesson_rest_door.py, because
the route is decorated and returns a Quart response. Rule 167's falsifier is
included.

KNOWN GAP, not fixed here: `KnowledgeView` is the only lesson LIST in the UI
and it reads `/knowledge`, not `/lessons` — so the REST listing change reaches
`frontend/src/api/lessons.ts::listLessons`, which currently has no consumer.
The agent-facing listing does reach a reader today.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
2026-09-21 02:33:33 -04:00