fix(lessons): a lesson's name is one claim on one line (#4797)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / integration (push) Successful in 54s
CI & Build / Python tests (push) Successful in 1m48s
CI & Build / Build & push image (push) Successful in 28s

#4797 "A lesson's what takes a whole narrative, and the narrative becomes
its name". `what` is the title every listing and menu prints. Nothing
enforced its documented "one line", so lessons written with the incident
in `what` printed up to ~1,500 characters as a menu line, buried the
claim, and diluted the trigger in the embedded title. That was 14 of the
39 lessons on this install.

- lessons.require_claim refuses a `what` over WHAT_MAX_CHARS (240) or
  running over several lines. The refusal says the story goes in
  `insight`.
- Both doors call it before writing:
  - MCP create_lesson / update_lesson;
  - REST create / update.
  An update checks only a NEW name, so a lesson stored with a long one can
  still take the edit that repairs it.
- lessons.claim_line is the display half. _menu_name shows an over-long
  stored name as its first sentence marked " …", because a door guard
  does not undo rows already stored and an unrepaired install would keep
  printing them.
- The tool docstrings state the bound.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 23:04:53 -04:00
co-authored by Claude Opus 5.5
parent 9909cd2450
commit d2ac7bf220
5 changed files with 197 additions and 4 deletions
+6
View File
@@ -156,6 +156,10 @@ async def create_lesson_route():
when_to_apply = (data.get("when_to_apply") or "").strip()
if not what:
return jsonify({"error": "what is required"}), 400
try:
lessons_svc.require_claim(what)
except ValueError as exc:
return jsonify({"error": str(exc)}), 400
# The trigger is not optional at this door even though the service will
# store a lesson without one. A lesson with no trigger saves, reads
# correctly in every listing, and never surfaces — there is nothing to
@@ -300,6 +304,8 @@ async def update_lesson_route(lesson_id: int):
linked = None
no_rule = (data.get("no_rule") or "").strip()
try:
if kwargs.get("what"):
lessons_svc.require_claim(kwargs["what"])
if data.get("rule_ids") is not None:
linked = await lesson_rules_svc.require_rules(uid, data["rule_ids"])
lesson_rules_svc.require_one_answer(linked, no_rule)