b131e2b04c0e56a4fa1086990eae4ecdc02389a3
2
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
95dc25eaab |
feat(lessons): a lesson is readable, writable and browsable by a human (#3734)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 11s
CI & Build / TypeScript typecheck (push) Failing after 31s
CI & Build / integration (push) Successful in 48s
CI & Build / Python tests (push) Successful in 1m33s
CI & Build / Build & push image (push) Skipped
Step 7's actual UI. Before this the frontend had zero lesson code — the kind existed for agents only, which is rule 27 failing. THE EDITOR ASKS FOR THE TRIGGER BY NAME, and leads with it. Three fields — the trigger, the claim, the detail — never one markdown box. That is the design step 1 settled, and the evidence is blunt: the snippet corpus carries a trigger on every record with no guard anywhere, because a service composes the title from a named parameter. What is at 100% is a named structured field, not a writer remembering a convention. The trigger gets the most room, its own explanation, and a save button that refuses without it and says why. The form shows the composed title live, so the writer is agreeing to a document they can read rather than one assembled out of sight. A 409 from the duplicate gate is rendered as the record that already covers the moment, with a link to improve it and an explicit override — not as a failure. THE BROWSE VOCABULARY GAINS THE KIND, which #3161 warned this step not to get wrong: a facet chip, a badge label, and routing to `/lessons/:id` rather than the note editor, which cannot edit a trigger. The badge is neutral alongside snippet and process — a hue would make the softest record in the corpus look like the loudest, next to a rule that actually binds. BOTH DIRECTIONS OF THE PROVENANCE. The detail page resolves `learned_from` to titles rather than bare ids, because "#4181" tells a reader nothing about whether it is worth opening. And `LessonsTaughtPanel` answers the reverse on the record's own page — the direction the task body calls the one that gets forgotten. It has no author to type it, which is exactly why it tends never to get built. A component, not markup in the task editor, so the same panel mounts on any record a lesson can cite instead of being written a second time (#3207). Silent when empty: most records taught no lesson, and a panel that says "None yet" everywhere is one people learn to skip. GLOBAL-BY-DEFAULT IS MADE LEGIBLE. A lesson meeting you on a project it was not written on reads as a bug unless the page says otherwise, so the origin line says it as a property of the kind rather than as an apology. Design system tokens throughout; no new raw hex. `--fs-error` rather than `--fs-danger` — 31 uses against 1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy |
||
|
|
d36d68a20f |
feat(lessons): the REST door a human can actually reach (#3734)
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Successful in 48s
CI & Build / TypeScript typecheck (push) Successful in 52s
CI & Build / Python tests (push) Successful in 1m33s
CI & Build / Build & push image (push) Successful in 23s
Step 7, part two. Milestone 385 built the lesson kind through the MCP tools, which is the agent's surface. The Vue app speaks REST, so a lesson was a record a person could not create, read, edit or retire — rule 27 failing at the door rather than in the view. `/api/lessons` now offers list, create, read, update and trash, plus `/api/lessons/taught-by/<id>` — the reverse of `learned_from`, which the task body calls the direction that gets forgotten and arguably the more useful one: a reader opening an old issue wants to know what was learned from it, and until now the relation was only navigable from the lesson's side. `lessons_taught_by` reads `data[taught_by]` through `path_exists`, the same jsonpath dialect the snippet location lookup uses, so both reverse lookups hit the GIN index (0070) the same way rather than scanning bodies. Share-aware via `readable_notes_clause`: it renders beside a record the caller can already see, so a lesson shared with them belongs there exactly as their own does. THE TRIGGER IS REFUSED WHEN EMPTY, at create and at update. This is the one place the door is not a thin wrapper, and it is deliberate: the service will store a triggerless lesson quite happily — it saves, reads correctly in every listing, and never surfaces. There is nothing to notice afterwards, because it looks exactly like a lesson that works. Better to refuse it than to hand back a record that looks finished. The refusal says why, so the next reader does not take it for a nag and delete it. `lesson_to_dict` moves into the service and the MCP tool's `_to_dict` becomes an alias for it. Both doors now return one shape — a payload spelled once per door answers the two of them differently the first time a field is added — and both compose through `services/lessons.py`, so a lesson written from the web ranks identically to one written by an agent. The document IS what ranks, so that parity is the whole reason the door is thin. The dedup gate matches the MCP path: two lessons under one trigger compete in a single ranked list for one reserved slot, so a duplicate here displaces rather than merely clutters. NOT DONE YET: this is the door, not the UI. #3734 stays in_progress until the Vue views, the router entries, the Knowledge browse badge and the both-ways sources panel exist — rule 27 is about the operator being able to touch it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy |