1fcfd47ab6ff6d23f249080548dccedfbdf8c2e3
3
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
1d201d2ff7 |
feat(lessons): a lesson can be written, and it keeps every incident that taught it (#3731)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / integration (push) Successful in 1m3s
CI & Build / Python tests (push) Successful in 1m39s
CI & Build / Build & push image (push) Successful in 28s
Milestone 385 step 4 — the write path. ITS OWN TOOL MODULE, not create_note(note_type="lesson"), on the snippet and process precedent and for the reason that precedent exists: a kind whose value depends on one field being filled needs a door that ASKS for that field by name. create_note would take a lesson through a generic body parameter and the trigger — the whole of why a lesson is findable — would be something the writer had to know to include. THE TRIGGER IS REQUIRED, refused rather than flagged. Step 1 left the choice open. Refusing is right for the same reason create_rule makes enforcement the deciding question: a lesson with no trigger is not a weaker lesson, it is a note that will never surface, and nothing downstream can tell the difference — it saves, reads correctly in every listing, and is silently absent from the one moment it was written for. A flag is a warning nobody is present to read; the write path is where the writer still is. The message says SYMPTOM, because "required" alone produces a topic where a situation was wanted. The docstring carries the distinction this milestone exists to fix, in a line a reader can apply: the difference between a lesson and a rule is FORCE, not importance. If ignoring it would be a mistake it is a rule and needs the operator's yes; if ignoring it just means someone re-derives it the slow way it is a lesson, and nobody is bound. CARDINALITY: a LIST, in notes.data under `taught_by`. The founding example generalised three incidents into one claim about failure classes no CI lane can see — generalising across incidents is the shape a good lesson HAS, and arose_from_id holds one, so a single id keeps the first and drops two while reading as complete. It lives in `data` rather than a join table for the reason decision #4157 put the trigger there: a table would settle, for every note kind at once, whether provenance is multi-valued — a question nothing has measured. `arose_from_id` is filled only when there is exactly ONE source, because every surface that renders it renders it as THE origin, and one of three would make those surfaces state something false. THE DUPLICATE GATE, which step 4 asked to check: a lesson is judged at a bar ABOVE the sibling band, not the general 0.90. #2518 measured deliberately parallel variants at 0.92 on a document that is mostly prose about the thing, which is exactly a lesson's shape now — so at 0.90 two genuinely different lessons about one area ("CI cannot see this class of failure") would refuse each other. Its own constant rather than reusing the snippet's: the two are separate facts that coincide today, and this number is inherited from a structurally analogous corpus rather than measured on lessons, of which there are none yet. Follows canon #2846 including the third registration point it names and this change would otherwise have missed: get_lesson is in server._READ_ONLY_TOOLS and the two writers in _WRITE_TOOLS, which test_mcp_auth requires. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy |
||
|
|
1361ed7200 |
feat(lessons): the document shape is the stored record, and it travels (#3730)
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 10s
CI & Build / integration (push) Successful in 51s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / Python tests (push) Successful in 1m31s
CI & Build / Build & push image (push) Successful in 25s
Milestone 385 step 3 — the step where the kind either works or is cosmetic.
THE DOCUMENT, and why there is no `lesson_document()` beside `rule_document()`
in embeddings. The step expected one. The difference is where the sharp shape
LIVES. A rule keeps its trigger in a column and its title is a plain name, so
`{title} — {trigger}` has to be synthesised at embed time and exists nowhere
else. A snippet — the only sharp record in the corpus by #2485's measurement,
0.153 top-to-second against 0.010–0.023 — gets there the other way: its STORED
title is already the join and its stored body already opens with the trigger,
so the ordinary `title\nbody` join IS the sharp document. Step 1 chose the
snippet route and step 2 built it, so `lessons.lesson_document` composes what
is STORED and the generic chunker does the rest.
The consequence the step asked about: `chunk_document` is untouched, so
CHUNKER_VERSION does not move and NOTHING re-embeds. The step's "Re-embed"
section describes a change this design does not make.
THE NARRATIVE stays in the body, departing from the step's instruction to keep
it out. `rule_document` excludes `why` because long dated narrative made
sixteen dev-logs land on the centroid of "development" — but that finding
predates chunking (#280). A body over budget is now split, and every chunk is
prefixed with the title, which for a lesson carries the trigger. The story
occupies its own vectors instead of averaging itself into the trigger's, and
each of those is still anchored to when the lesson applies. A guard asserts
exactly that. Holding the story out would cost the reader the only part that
explains the insight, to buy a sharpness the chunker already provides.
GLOBAL IN THE SEARCH is the real new code: `GLOBAL_NOTE_TYPES` and
`include_global_kinds` on `semantic_search_notes`, widening the PROJECT filter
alone. Off by default, because two callers depend on that filter holding — the
near-duplicate gate compares a record only against its own project on purpose,
and a globally visible kind there would let a lesson block an unrelated note's
create on a project its author never touched. It composes with `note_type`
rather than overriding it, so narrowing to snippets does not quietly acquire
lessons, and it changes nothing about the ACL: `notes_visibility_clause` still
gates every row.
Wired into the explicit MCP search only — the operator asked, and there is no
budget to spend. The unasked-for injection arms are step 5's subject (#3732)
and the legibility of a lesson appearing on a foreign project is step 7's
(#3734), so neither is turned on here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
|
||
|
|
0ab15d7d80 |
feat(lessons): the lesson kind, and one join for every trigger title (#3729)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 10s
CI & Build / integration (push) Successful in 50s
CI & Build / TypeScript typecheck (push) Successful in 54s
CI & Build / Python tests (push) Failing after 1m5s
CI & Build / Build & push image (push) Skipped
Milestone 385 step 2, implementing decision #4157 from the step-1 spike. The kind: `note_type='lesson'`, a note findable by WHEN IT APPLIES rather than by what it is about. The trigger lives in `notes.data.when_to_apply`, mirrored into the title and the head of the body — the shape snippets already use, and the reason nothing re-embeds: chunk_document is untouched, so CHUNKER_VERSION does not move. NO MIGRATION, and the step assumed there would be one. `note_type` carries no CHECK — only `task_kind` does (0056, 0065). Migration 0036 added it as plain Text with a server default and nothing has gated it since, so rule 36 has no whitelist to expand and #3128's failure mode (a value the database refuses) cannot arise for this column. The vocabulary that actually decides what a reader can reach is services.knowledge._FACETS, which since #3161 is one table feeding the door's validation, the counts and both dialects of the type filter — so the kind lands there in a single edit. ONE JOIN, not a fourth copy. `{subject} — {trigger}` had three implementations: rule_document, snippets.compose_title, and this step needed another. #3207 records what that costs, so the join moves to embeddings.trigger_title beside embedding_text and all three delegate. Behaviour is unchanged for rules and snippets; the guard calls each through its own public name, so a re-implementation fails it. The #3163 bill is stated in the service docstring rather than left to be inferred: versions, supersession, trash, the share ACL, tags, project and System tagging, chunked embeddings and the duplicate gate are all inherited; status/task_kind/milestone_id and recurrence are not, and verify_with/expires_when are available but outside the kind's contract. The status cell is the one that matters — `is_task` IS `status is not None`, so a lesson that acquired one would become a task. The integration guard asserts the WRITE rather than the constraint: it holds whether or not note_type is ever gated, and goes red only if it is gated without this value. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy |