"""The `lesson` kind — its trigger, its title, and its place in the vocabulary. WHY THIS EXISTS (milestone 385 step 2, #3729) A lesson is a note that must be findable by WHEN IT APPLIES rather than by what it is about. That is a document-shape fact: the trigger has to reach the embedded text, and decision #4157 put it in `notes.data` with a mirror in the title and the head of the body — the shape snippets already use. THE ONE THAT MATTERS MOST `test_one_join_builds_every_trigger_title`. Three kinds now rank on a `{subject} — {trigger}` title: rules, snippets and lessons. That join had three implementations before this step and would have had four; #3207 records what that costs. The guard is behavioural rather than `assert a is b`, because the three are reached through different public names and a test that compared identities would pass on a re-implementation that merely re-exported. WHAT IS DELIBERATELY NOT TESTED HERE That the database accepts `note_type='lesson'`. `note_type` carries no CHECK — only `task_kind` does — so there is nothing to assert against in a unit test, and asserting the absence would pin the schema's current shape rather than the behaviour that matters. The real guard is in the integration lane, where a lesson is written and read back: it holds whether or not a constraint exists, and goes red the day one is added without this value. """ from types import SimpleNamespace from scribe.services import knowledge as knowledge_svc from scribe.services import lessons as lessons_svc from scribe.services import snippets as snippets_svc from scribe.services.embeddings import trigger_title def _note(data=None, body=""): return SimpleNamespace(data=data, body=body) def test_the_trigger_is_read_from_the_indexed_mirror(): note = _note(data={"when_to_apply": "a stack trace, and it is still broken"}) assert lessons_svc.lesson_trigger(note) == "a stack trace, and it is still broken" def test_the_body_answers_when_the_mirror_is_absent(): """Not dead code. A row written before the mirror existed is still readable, and an absent mirror has to degrade to the right answer rather than to silence — the discipline `snippet_fields` follows for the same reason.""" note = _note(body="**When to apply:** about to reach for a second hypothesis\n\nbody") assert lessons_svc.lesson_trigger(note) == "about to reach for a second hypothesis" def test_the_mirror_wins_when_both_are_present(): note = _note( data={"when_to_apply": "from the mirror"}, body="**When to apply:** from the body", ) assert lessons_svc.lesson_trigger(note) == "from the mirror" def test_no_trigger_reads_as_empty_rather_than_raising(): """A lesson with no trigger is unreachable, not broken. Whatever refuses to write one belongs on the write path; a reader's job is to say so plainly.""" assert lessons_svc.lesson_trigger(_note()) == "" assert lessons_svc.lesson_trigger(_note(data={}, body="no trigger line here")) == "" def test_one_join_builds_every_trigger_title(): """THE GUARD. Rules, snippets and lessons rank on the same title shape. Behavioural on purpose — see the module docstring. Each of the three is called through the name its own callers use, so a fourth hand-rolled copy fails here even though it would look correct in isolation. """ subject, trigger = "Pace hard debugging", "a stack trace and it is still broken" expected = f"{subject} — {trigger}" assert trigger_title(subject, trigger) == expected assert lessons_svc.compose_title(subject, trigger) == expected assert snippets_svc.compose_title(subject, trigger) == expected def test_a_subject_with_no_trigger_degrades_to_the_subject(): """It still embeds, just less sharply — an argument for backfilling triggers, not for padding the title with whatever text is to hand.""" assert trigger_title("debounce", "") == "debounce" assert lessons_svc.compose_title(" debounce ") == "debounce" assert trigger_title("", "when it applies") == "when it applies" def test_the_kind_is_in_the_browse_vocabulary(): """A kind the browse surface does not know is a record nobody can filter to. Asserted through the public table rather than a literal, because the door's validation, the counts and both dialects of the type filter are all generated from it (#3161) — so this is the one place that decides. """ assert lessons_svc.LESSON_NOTE_TYPE in knowledge_svc.FACET_TYPES assert lessons_svc.LESSON_NOTE_TYPE in knowledge_svc.NON_TASK_FACETS def test_the_global_kinds_list_names_the_lesson_and_nothing_else(): """`embeddings.GLOBAL_NOTE_TYPES` spells "lesson" as a literal because `services.lessons` imports `trigger_title` from that module, so importing it back would close a cycle (milestone 385 step 3). The copy is pinned here instead — the one thing a literal costs is that it can drift, and this is what stops it. """ from scribe.services.embeddings import GLOBAL_NOTE_TYPES assert GLOBAL_NOTE_TYPES == (lessons_svc.LESSON_NOTE_TYPE,) def test_a_lesson_is_not_a_task_on_either_arm_of_the_filter(): """The cell left empty on purpose (#3163). `is_task` IS `status is not None`, so a lesson that acquired a status would become a task and start appearing in open-work listings — the one way filling a field in by accident changes what the record IS. """ assert knowledge_svc.facet_is_task(lessons_svc.LESSON_NOTE_TYPE) is False lesson = SimpleNamespace(note_type="lesson", is_task=False, task_kind="work") assert knowledge_svc.matches_facet(lesson, "lesson") is True assert knowledge_svc.matches_facet(lesson, "task") is False # And it does not answer to another kind's facet. assert knowledge_svc.matches_facet(lesson, "note") is False