diff --git a/tests/conftest.py b/tests/conftest.py index a94ca320..112580a6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,8 +7,8 @@ in the environment before importing the app. For unit tests of pure functions no database is needed at all. -The fixtures below are the ONE definition of three things that used to be -copied into a dozen test modules each (#2825). They are deliberately not +The fixtures below are the ONE definition of things a dozen test modules +each need. They are deliberately not autouse: a module opts in with ``pytestmark = pytest.mark.usefixtures("")`` (or a test names the fixture as a parameter), so a unit test that never touches the engine or the @@ -80,7 +80,7 @@ def _no_embedding(): """ from unittest.mock import MagicMock - # Milestones embed too since milestone 415; a plan created in a test would + # Milestones embed too; a plan created in a test would # otherwise detach the same model-loading task. with patch("scribe.services.notes.embed_note", MagicMock()), \ patch("scribe.services.milestones.embed_milestone", MagicMock()): @@ -89,7 +89,7 @@ def _no_embedding(): @pytest.fixture def _no_supersession(): - """Stub the auto-inject menu's "which lines are superseded?" lookup (#278). + """Stub the auto-inject menu's "which lines are superseded?" lookup. That is a real database call on a path the plugin-context tests exercise without one. Stubbed to "nothing superseded" — the ordinary state — rather @@ -104,7 +104,7 @@ def _no_supersession(): @pytest.fixture(autouse=True) def _no_reply_shape_log(): - """Stub the reply-shape delivery row (milestone 500 step 3). + """Stub the reply-shape delivery row. Autouse for _no_system_labels' reason: every turn's retrieval, every moment request and every Scribe tool that reaches a shaped moment writes @@ -118,7 +118,7 @@ def _no_reply_shape_log(): @pytest.fixture(autouse=True) def _no_system_labels(): - """Stub the menu's "which System is each line about?" lookup (#4364). + """Stub the menu's "which System is each line about?" lookup. Autouse because every test that renders an injected menu reaches it, and it is a real database call on a path those tests run without one. Stubbed @@ -132,7 +132,7 @@ def _no_system_labels(): @pytest.fixture(autouse=True) def _no_rulings_arm(): - """Stub the rulings arm both PreToolUse builders run (milestone 444). + """Stub the rulings arm both PreToolUse builders run. Autouse for _no_system_labels' reason: every write-path and tool-rule test with a project in scope reaches it, and its first act is reading the @@ -147,8 +147,7 @@ def _no_rulings_arm(): @pytest.fixture(autouse=True) def _no_system_usage_readout(): - """Stub the per-System usage lookup the Systems list and get_system carry - (#4769). + """Stub the per-System usage lookup the Systems list and get_system carry. Autouse for _no_rulings_arm's reason: every route and tool test that reads a System now reaches a GROUP BY on system_usage_events. Stubbed to @@ -168,7 +167,7 @@ def _no_system_usage_readout(): @pytest.fixture(autouse=True) def _no_task_log_arm(): """Stub the task-log read arm that get_task / list_tasks / get_milestone - grew in #4241. + carry. Autouse for the reason _no_rule_arm is: those three tools now read work logs, and the reads go through the access layer to Postgres. Every unit @@ -193,7 +192,7 @@ def _no_task_log_arm(): @pytest.fixture(autouse=True) def _no_lesson_rule_links(request): - """Stub the lesson↔rule link decorations (milestone 440). + """Stub the lesson↔rule link decorations. Every door that returns a lesson or a rule now attaches its links, and the read is a real database call — so every unit test that opens either would @@ -201,16 +200,16 @@ def _no_lesson_rule_links(request): Both decorations are fail-open, so the cost would be a slow failed connect per test rather than a failure, which is worse: it would never be noticed. - The candidate search a new lesson runs (#4631) is stubbed to "nothing + The candidate search a new lesson runs is stubbed to "nothing resembles it" for the same reason: it embeds, and the model load would otherwise ride along with every unit test that creates a lesson. The - co-surfacing recorder (#4637) is stubbed to "no proposal": every hook + co-surfacing recorder is stubbed to "no proposal": every hook builder test that renders a lesson beside a rule would otherwise reach - for the database to count the pair. And the via-lesson rule step (#4633) + for the database to count the pair. And the via-lesson rule step is stubbed to "no lesson brought a rule": all three rule arms run it, and its first act is a database read. tests/test_rule_via_lesson.py binds the real function at import time, before this patch runs. The convergence - check a "no rule fits" answer runs (#4634) is stubbed to "no group" for + check a "no rule fits" answer runs is stubbed to "no group" for the same reason; tests/test_lesson_convergence.py binds the real one. Skipped for integration tests, which exercise the real links against @@ -231,7 +230,7 @@ def _no_lesson_rule_links(request): @pytest.fixture(autouse=True) def _no_family_triggers(request): - """Stub family canon's write-time triggers (milestone 463 step 3). + """Stub family canon's write-time triggers. create_note, create_task, create_snippet, the updates that change a body, and update_milestone(status="done") now look for a pattern carried between @@ -255,7 +254,7 @@ def _no_family_triggers(request): @pytest.fixture(autouse=True) def _no_moment_delivery(request): - """Stub the moment lookup the mapped MCP tools attach (milestone 458). + """Stub the moment lookup the mapped MCP tools attach. update_task, the create_* record tools and the milestone tools now hand back the rules mounted on the moment they mark, and finding those is two @@ -276,7 +275,7 @@ def _no_moment_delivery(request): @pytest.fixture(autouse=True) def _no_rule_arm(): - """Stub the write-path hint's standing-RULES arm (milestone 307). + """Stub the write-path hint's standing-RULES arm. Autouse, and deliberately so. The arm calls semantic_search_rules, which loads the embedding model — so every unrelated plugin-context test that @@ -297,7 +296,7 @@ def _no_rule_arm(): @pytest.fixture(autouse=True) def _no_rule_overlap(): - """Stub the rule/preference create path's overlap check (#4134). + """Stub the rule/preference create path's overlap check. The same reason as _no_rule_arm, one door over: every create_rule / create_project_rule / create_preference now asks semantic_search_rules @@ -314,7 +313,7 @@ def _no_rule_overlap(): @pytest.fixture(autouse=True) def _default_gate_bars(): - """Read the create gate's bars as their defaults, not from settings (#4385). + """Read the create gate's bars as their defaults, not from settings. Every create through the note gate now asks the user's settings for its similarity bars, which is a database read on a path the gate's unit tests diff --git a/tests/helpers.py b/tests/helpers.py index cbd032ad..bcd8908c 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -1,8 +1,7 @@ """Shared test helpers — the plain functions tests call, as opposed to the fixtures in conftest.py. -Each of these was copied into several test modules before #2825 consolidated -them; a module imports what it needs with ``from tests.helpers import ...``. +Each of these serves several test modules; a module imports what it needs with ``from tests.helpers import ...``. """ from __future__ import annotations @@ -17,9 +16,7 @@ async def drive_update_note(note, **kwargs): The patch stack is the point: update_note reaches for a version snapshot, an embedding refresh and a project reactivation on its way out, none of - which a unit test has. Written twice — once for the snippet mirror - (#3128) and once for the verification fields (#3182/317) — before being - consolidated here. + which a unit test has. Returns whatever update_note returned; assert on the `note` you passed in. """ @@ -43,12 +40,11 @@ def tool_doc(module: str, name: str) -> str: Flattened because these are hard-wrapped at ~76 characters, so any phrase worth asserting on is liable to straddle a line break — a property of the - formatter, not of the guidance. The disambiguator guard (#3123) learned - that on its own first run, matching raw text and reporting a phrase absent - that was plainly there. + formatter, not of the guidance. Matching raw text reports a phrase absent + that is plainly there. Used by every test that pins the docstring CONTRACT rather than its - wording. The tool docstring is the agent-facing contract (rule 119), so + wording. The tool docstring is the agent-facing contract, so these guards exist to catch it being tidied down to a parameter list. """ import importlib @@ -63,8 +59,7 @@ def compiled_sql(element, dialect=None) -> str: """A SQLAlchemy clause or statement rendered as literal SQL text. For asserting on the shape of a predicate without a database — which is how - the visibility clauses and the knowledge facets are both tested. Was a - private copy in each of those modules before #3128 needed a third. + the visibility clauses and the knowledge facets are both tested. Pass `dialect` when the assertion is about something only one backend renders — a Postgres row-lock mode, say. The generic dialect is enough for @@ -93,9 +88,8 @@ def session_returning(note): """``make_mock_session`` whose every ``execute()`` yields `note` (or None). The commonest shape above the bare session: a service that loads one - record by id and acts on it. Two files had spelled this out identically - before a third was about to (#3207 — derive before the third copy), and - it belongs beside `make_mock_session` for the same reason that one does. + record by id and acts on it, and it belongs beside `make_mock_session` + for the same reason that one does. For a service making SEVERAL different reads, set ``session.execute.side_effect`` to a list of results instead. @@ -134,7 +128,7 @@ def fake_record(**attrs) -> MagicMock: """A MagicMock record with REAL values on the attributes named, and a ``to_dict()`` that mirrors them. - The hazard this exists for (note 2109): an auto-created MagicMock attribute + The hazard this exists for: an auto-created MagicMock attribute is truthy and has a repr — so a bare MagicMock handed to the product reads as trashed, shared, a task, and owned by a MagicMock. Name every attribute the code under test will read; the per-model ``fake_*`` builders below @@ -180,10 +174,10 @@ def fake_note(**attrs) -> MagicMock: "note_type": "note", "is_task": False, "task_kind": "work", "status": "todo" if is_task else None, "data": None, "deleted_at": None, - # Milestone 317: a truthy mock here reads as "this note carries a + # A truthy mock here reads as "this note carries a # check", which trips the guard on records that may not have one. "verify_with": None, "expires_when": None, "verified_at": None, - # Milestone 381: a truthy mock would read as a live claim. + # A truthy mock would read as a live claim. "claimed_by": None, "claimed_at": None, "claim_touched_at": None, "claim_session": None, }, attrs) @@ -196,7 +190,7 @@ def fake_task(**attrs) -> MagicMock: "tags": [], "parent_id": None, "project_id": None, "is_task": True, "task_kind": "work", "user_id": 7, "deleted_at": None, "verify_with": None, "expires_when": None, "verified_at": None, - # Milestone 381: a truthy mock would read as a live claim. + # A truthy mock would read as a live claim. "claimed_by": None, "claimed_at": None, "claim_touched_at": None, "claim_session": None, }, attrs) @@ -219,9 +213,8 @@ def fake_lesson(**attrs) -> MagicMock: """A stand-in lesson: a note whose `note_type` is what makes it one. The title is the subject alone and the trigger lives in `data`, because - that is the record the product creates (milestone 427) — the trigger joins - the title only in the embedded document. A default whose title carried the - trigger would be testing a row only an un-migrated database holds. + that is the record the product creates — the trigger joins the title only + in the embedded document. The check fields and `arose_from_id` are explicitly None for the reason `fake_snippet`'s `data` is: `update_note` reads `verify_with` and @@ -310,8 +303,7 @@ def plain_rule_detail(): the TOOL forwarded the right arguments, so the seam is stubbed the same way the create/update calls themselves already are. - Consolidated here on its second copy, per this module's own reason for - existing (#2825) — two stubs for one seam drift apart quietly, and a test + Shared here because two stubs for one seam drift apart quietly, and a test stubbing the seam slightly differently is a test asserting something slightly different than it appears to. """ @@ -356,8 +348,7 @@ def http_sink(reply: bytes = b'{"context":"","note_ids":[]}', (one dict per request, in order). Lets the shell be tested end to end — the extraction, the encoding, the URL — without a Scribe instance. - Three test modules each carried their own ``_Sink`` handler before #2904 - consolidated them here; pass ``reply`` for the body the hook should see. + Pass ``reply`` for the body the hook should see. ``by_path`` is for a hook that calls more than one route: each request is answered with the reply for its path (``reply`` when none is listed), and @@ -406,10 +397,10 @@ def http_sink(reply: bytes = b'{"context":"","note_ids":[]}', def writepath_cfg(**over): - """A complete `get_writepath_config` stand-in, built from the registry (#4102). + """A complete `get_writepath_config` stand-in, built from the registry. - DERIVED, NOT LITERAL, and the reason is a failure mode this file already - warned about in prose without being able to prevent: the write-path hint + DERIVED, NOT LITERAL, because of a failure mode prose cannot prevent: the + write-path hint drives three arms, each of which reads its numbers out of the config dict inside a fail-open `except`. A dict missing one key does not raise where a reader would see it — the arm silently becomes a no-op, which is @@ -417,9 +408,9 @@ def writepath_cfg(**over): So the keys come from `retrieval_surfaces.SURFACES`. A seventh surface, or a rename, changes this helper for free and cannot quietly disable an arm in - ten hand-written dicts that each looked complete on the day they were typed. + hand-written dicts that each looked complete on the day they were typed. - NOT EVERY KEY IS A SURFACE, and that gap already bit once (#4214). The + NOT EVERY KEY IS A SURFACE. The checkpoint bar has no entry in SURFACES on purpose — everything in that table is a floor/budget pair belonging to one QUERY, and the checkpoint runs none, it re-reads hits the rule arms already produced. Adding it there @@ -458,11 +449,7 @@ def need_tools(*tools): A skip rather than a stub on purpose. Stubbing `jq` would test the stub — these tests exist precisely because the shell's behaviour is the thing in - question (#2932), so anything short of the real tool proves nothing. - - Consolidated 2026-09-21 from three byte-identical copies in - test_contract_around_the_change, test_hook_json_reader and - test_session_slippage_readout. + question, so anything short of the real tool proves nothing. """ import shutil @@ -482,14 +469,11 @@ async def rule_row(rule_id: int): map, which can hold a value the database never accepted — a column the write never reached, a default the ORM supplied rather than the schema. Opening a new session forces a real read and is the only way `verify_with` - being NULL rather than "" is distinguishable at all (milestone 312). + being NULL rather than "" is distinguishable at all. Import is lazy because this module is imported by unit tests that have no database and must not pay for one — the same reason `plugin_config` defers its service imports. - - Consolidated 2026-09-21 from two byte-identical copies in - test_integration_rule_move and test_integration_rule_verification. """ from scribe.models import async_session from scribe.models.rulebook import Rule @@ -519,7 +503,7 @@ def skill_text(name: str) -> str: A skill keeps what matters on most turns in SKILL.md and moves what matters at one moment into sibling files it links (Agent Skills progressive - disclosure, #4398). Both are the skill's own statement, so a test asking + disclosure). Both are the skill's own statement, so a test asking "does this skill still say X" reads them together rather than pinning X to whichever file it happens to sit in today. """ diff --git a/tests/test_guidance_ownership.py b/tests/test_guidance_ownership.py index 46ae84be..e639a10b 100644 --- a/tests/test_guidance_ownership.py +++ b/tests/test_guidance_ownership.py @@ -1,17 +1,16 @@ """Every piece of agent guidance has exactly one owner that states it. -WHY THIS EXISTS (milestone 410, decision #4027) +WHY THIS EXISTS -Scribe's guidance to agents was written up to five times over: the MCP +Scribe's guidance to agents reaches a session through five surfaces: the MCP `_INSTRUCTIONS`, the plugin's static session context, the live session context -the server builds, the `using-scribe` skill, and tool docstrings. An earlier -design (#2494) made that deliberate — insurance against any one surface failing -silently — and the copies drifted apart instead (#2497, #4022). +the server builds, the skills, and tool docstrings. A practice stated in full +on several of them drifts apart, copy by copy. -Decision #4027 replaced the redundancy with ownership: the server orients with -a short index, the skills hold the depth, tool docstrings hold each tool's -contract, and each client adapter holds only its own timing and conventions. -This module is the registry that decision is enforced from. +So each practice has one owner: the server orients with a short index, the +skills hold the depth, tool docstrings hold each tool's contract, and each +client adapter holds only its own timing and conventions. This module is the +registry that ownership is enforced from. WHAT IT PINS @@ -27,7 +26,7 @@ WHAT IT PINS surface every MCP client receives, so each reflex it indexes keeps its `index` markers there — a one-line pointer, not a copy. Only session-start practices carry `index`: judging, reporting and the missed-rule route fire - mid-work, and #4389 took them out of the index (the skills and the in-band + mid-work, so they stay out of the index (the skills and the in-band responses carry them at the moment they apply). WHAT IT CANNOT SEE @@ -90,7 +89,7 @@ def delivered_surfaces() -> dict[str, str]: - `commands` — the Claude Code adapter's slash commands - `live` — the live session context the server builds - `shapes` — the reply shapes the server delivers at their moments, - in the full form a session first sees (milestone 500) + in the full form a session first sees """ server = (ROOT / "src/scribe/mcp/server.py").read_text() match = re.search(r'_INSTRUCTIONS = """(.*?)"""', server, re.S) @@ -103,7 +102,7 @@ def delivered_surfaces() -> dict[str, str]: "live": _live_session_context_source(), "shapes": _delivered_shapes(), } - # A skill is SKILL.md plus the reference files it links (#4398): one + # A skill is SKILL.md plus the reference files it links: one # owner, however many files it is split across. for skill in sorted((ROOT / "plugin/skills").glob("*/SKILL.md")): surfaces[f"skill:{skill.parent.name}"] = skill_text(skill.parent.name) @@ -144,9 +143,8 @@ TOPICS: tuple[Topic, ...] = ( index=("lessons inform",), # Stated twice on purpose, at the two moments it is needed: the # skill while deciding what a record IS, and create_rule while a - # proposal is actually being written — which is where the - # mis-routing #3733 recorded happens, and the caller there has not - # necessarily read the skill. + # proposal is actually being written — which is where mis-routing + # happens, and the caller there has not necessarily read the skill. shared_with=("docstrings",)), Topic("a lesson grows each time it proves itself", U, ("update_lesson", "re-keyed", "grows each time"), @@ -182,7 +180,7 @@ TOPICS: tuple[Topic, ...] = ( Topic("a retrieved rule outranks a default habit", U, ("outranks a default habit",), "a retrieved rule outranks a default habit"), Topic("log on completion and on a problem", U, ("hit or discover a problem",), "hit or discover a problem"), - # Milestone 381 step 4: the half of a hand-off only the model can do. The + # The half of a hand-off only the model can do. The # release is mechanical (SessionEnd hook); saying what happened is not. Topic("hand off before the context stops existing", U, ("hand off", "claims it for this session"), @@ -196,7 +194,7 @@ TOPICS: tuple[Topic, ...] = ( Topic("where a new rule goes, and its trigger", U, ("create_project_rule", "when_to_apply"), "whichever home it gets"), Topic("a rule vs the other entities", U, ("standing instruction",), "first ask whether it's a rule at all"), - # Moments (milestone 458 step 8). Delivery at a moment and its in-session + # Moments. Delivery at a moment and its in-session # corrections are owned by using-scribe's moments.md; the index carries one # clause, and a new rule's moments are asked where its home and trigger are. Topic("rules mounted on a moment arrive by lookup; correct a misfire in-session", U, @@ -212,9 +210,8 @@ TOPICS: tuple[Topic, ...] = ( Topic("a missed when is mounted or mapped, not reworded", U, ("update_rule(moments=",), "first ask whether it missed a when or a what"), - # Milestone 416 step 9: the tuning tools shipped in #4102/#4104 and were - # named on NO instruction surface — measured, `retrieval_tuning_history` - # returned zero events. Machinery with no route to it. + # The tuning tools need a route on an instruction surface; machinery + # nothing names goes unused. Topic("a missed rule is a trigger to fix before a floor to move", U, ("retrieval_telemetry", "tune_retrieval", "retrieval_surfaces"), "take it to the record first and the dial second"), @@ -222,9 +219,8 @@ TOPICS: tuple[Topic, ...] = ( ("what_might_apply",), "ask what already covers that moment"), Topic("reference notes update in place; dev-logs don't", U, ("reference note",), "state updates in place; chronicles don't"), - # Milestone 444: an operator's ruling was kept only as a quote in a work - # log, beside a session's reading of it that contradicted it, and three - # later sessions built on the reading. No index marker: both fire + # A ruling kept only as a quote in a work log sits beside a session's + # reading of it, and later sessions build on the reading. No index marker: both fire # mid-work — when the operator decides, and when code is read as intent. Topic("an operator's ruling lives on the System it governs", U, ("rulings", "update_system", "the operator decided it"), @@ -238,8 +234,8 @@ TOPICS: tuple[Topic, ...] = ( # ── process arcs — owned by their skills ── Topic("plan in a milestone, steps created together", "skill:writing-plans", ("start_planning", "{{ref:"), "a milestone earns its place when the work has an arc", index=("start_planning",)), - # Milestone 415: sessions opened a second plan beside the roadmap milestone - # that already covered the work, because nothing told them to look. + # Without this, a session opens a second plan beside the roadmap milestone + # that already covers the work. Topic("find the existing plan before making one", "skill:writing-plans", ('content_type="milestone"', "unplanned_milestones", "existing_milestone"), "when an active milestone already covers the work, the plan is that milestone", @@ -247,14 +243,14 @@ TOPICS: tuple[Topic, ...] = ( Topic("reuse recorded shapes; record at first build", "skill:reusing-code", ("create_snippet", "when_to_use", "first build", "second copy"), "prior art offered beside a write is not noise", index=("create_snippet",)), - # Milestone 439: the writer judges what it wrote, at the end of the turn, + # The writer judges what it wrote, at the end of the turn, # asked by the plugin's Stop hook — the moment the knowledge exists. Topic("judge what you wrote before the turn ends", "skill:reusing-code", ("before the turn ends", "classify_shapes", 'kind: "file"'), "you are the one who knows what the code you just wrote is"), Topic("report back where the work stands", "skill:reporting-back", ("reporting-back", "placement"), "take the placement from the record"), - # Milestone 500: the default shapes are server product, delivered at their + # The default shapes are server product, delivered at their # moments with the operator's preferences mounted beside them — so the # rule that a preference wins is stated where the shape arrives. Topic("a preference beside a delivered shape is what the operator asked for", "shapes", @@ -263,8 +259,8 @@ TOPICS: tuple[Topic, ...] = ( Topic("every reply leads with its conclusion and stays short", "shapes", ("conclusion first", "the shortest reply that carries the answer", "what can go"), "length is work handed back to the reader"), - # Milestone 409 step 7: the sections were being FILLED rather than chosen, - # so a reply could satisfy every heading and still be unreadable. These + # Sections FILLED rather than chosen let a reply satisfy every heading and + # still be unreadable. These # three are the discipline around the scaffold, not the scaffold itself. Topic("sections are chosen, not filled, and the reply is cut twice", "skill:reporting-back", ("not a form to complete", "what can go"), @@ -275,19 +271,18 @@ TOPICS: tuple[Topic, ...] = ( Topic("a settled decision is acted on, not re-opened", "skill:reporting-back", ("already made",), "reads as contradicting yourself rather than as being careful"), - # Milestone 444: a cap a past session chose rode inside an option as a - # "known cost", the operator approved the option, and the cap read as - # theirs from then on. + # A cap a past session chose, riding inside an option as a "known cost", + # reads as the operator's once they approve the option. Topic("an option says whose call each carried assumption was", "skill:reporting-back", ("past session chose this", "conflict"), "an unlabelled one gets approved as if it had been asked"), - # Milestone 409 step 8: `placement` rides a WRITE, so a task the reply only - # cites arrived with nothing vouching for it — which is how a step finished - # four days earlier was reported as the open one (#4154). + # `placement` rides a WRITE, so a task the reply only cites arrives with + # nothing vouching for it — a finished step can be reported as the open + # one. Topic("a record you only cite still gets read", "skill:reporting-back", ("next_step", "only mention"), "a record you only mention is a record to read"), - # Milestone 463 step 6: family canon — when an idea is evaluated, how a + # Family canon — when an idea is evaluated, how a # project answers one, what a reason is, and the conflict order's reflex. # The criteria, outcomes and grounds themselves are product text on the # tools that enforce them; the skill owns when and how. @@ -298,7 +293,7 @@ TOPICS: tuple[Topic, ...] = ( index=("family-canon",)), # ── per-tool contracts and in-band behaviour — owned by the server ── Topic("closing a task cues the report", "docstrings", ("report_back",), "reporting this to the operator?"), - # The agent is the judge (#4208). Two topics, not one, because they fire + # The agent is the judge. Two topics, not one, because they fire # at different moments and say different things: using-scribe owns the # authority — what you may decide and that deciding means READING, not # thresholding — while reporting-back owns the shape a report takes when @@ -368,8 +363,8 @@ def test_every_topic_is_stated_by_its_owner(): gaps = owner_gaps(TOPICS, delivered_surfaces()) assert not gaps, ( "these topics are no longer stated in full by their owner:\n " - + "\n ".join(gaps) + "\nEvery guidance topic has one owner (decision " - "#4027). If the text moved on purpose, move the topic's owner here in the " + + "\n ".join(gaps) + "\nEvery guidance topic has one owner. " + "If the text moved on purpose, move the topic's owner here in the " "same commit; if it was reworded, update its markers and statement." ) @@ -378,7 +373,7 @@ def test_no_topic_is_stated_in_full_off_its_owner(): found = copies(TOPICS, delivered_surfaces()) assert not found, ( "guidance stated in full on a surface that doesn't own it:\n " - + "\n ".join(found) + "\nOne owner per topic (decision #4027): replace " + + "\n ".join(found) + "\nOne owner per topic: replace " "the copy with a one-line pointer to the owner. If both places genuinely " "need it at different moments, declare it in `shared_with` with the reason." ) @@ -405,7 +400,7 @@ def test_topic_keys_are_unique(): def test_the_ownership_guards_can_fail(): - """Rule 167: each guard is shown turning red once.""" + """Each guard is shown turning red once.""" surfaces = { "instructions": "use the widget", "docstrings": "the widget owner statement lives here", @@ -424,9 +419,9 @@ def test_the_ownership_guards_can_fail(): assert index_gaps((topic,), surfaces) == ["'widget': ['not in the index']"] -# ── The skills are client-neutral (milestone 410 step 2) ──────────────── +# ── The skills are client-neutral ─────────────────────────────────────── # -# Agent Skills is an open format read by dozens of clients (#4023), so the +# Agent Skills is an open format read by dozens of clients, so the # skills folder is the part of every client package that is shared verbatim. # Anything only one client understands belongs in that client's adapter — # for Claude Code, the plugin's static context, hooks and commands — or the @@ -465,7 +460,7 @@ def test_the_skills_name_no_particular_client(): offenders = {path: hits for path, hits in offenders.items() if hits} assert not offenders, ( f"skills that name one client: {offenders}. Skills are shared by every " - f"client package (decision #4027); say the universal thing in the skill " + f"client package; say the universal thing in the skill " f"and put the client's own name for it in that client's adapter." ) diff --git a/tests/test_instruction_surfaces_agree.py b/tests/test_instruction_surfaces_agree.py index 47ab15f2..8c1d1249 100644 --- a/tests/test_instruction_surfaces_agree.py +++ b/tests/test_instruction_surfaces_agree.py @@ -1,29 +1,11 @@ """What the instruction surfaces must say that one owner per topic cannot enforce. -WHY THIS FILE IS SMALLER THAN IT WAS - -Until milestone 410 this file pinned REDUNDANCY: every session-start surface — -the MCP `_INSTRUCTIONS`, the plugin's static context, the `using-scribe` skill — -had to restate how a rule reaches a session, what an empty session means, the -Systems reflex and the snippet-recording triggers. That was the design #2494 -chose after #2497 (two surfaces disagreeing about who loads the rules) and -#2198 (every hook silently inert): say it everywhere, so no single failure -loses it. - -Decision #4027 replaced that with ownership, and tests/test_guidance_ownership.py -now enforces it: every topic stated in full by exactly one owner, with a -one-line pointer in the server's index. The tests that required every surface -to repeat itself were retired there, because they enforced the very duplication -that drifted (#4022). What they protected is still protected: - - "state how to ask for a rule" / "say an empty session is not an empty - rulebook" → the rules topic: owned by using-scribe, indexed in - `_INSTRUCTIONS` with both phrases; - - the Systems reflex and the snippet-recording triggers → their topics' - markers, required on their owners; - - "never name the SessionStart push without stating the ask" (#2497's exact - shape) → retired outright. Since milestone 394 the push carries no rules, - so naming it can no longer imply rules were handled, and requiring every - surface that mentions it to restate the ask would force a copy. +tests/test_guidance_ownership.py enforces who states each topic: every topic +stated in full by exactly one owner, with a one-line pointer in the server's +index. How to ask for a rule and what an empty session means are the rules +topic, owned by using-scribe and indexed in `_INSTRUCTIONS`; the Systems +reflex and the snippet-recording triggers are their topics' markers, required +on their owners. WHAT STAYS HERE @@ -46,7 +28,7 @@ def _all_surfaces() -> list[tuple[str, str]]: what the hook does is not the same act as telling an agent it need not pull. The boundary is "does a session read this", which is skills (loaded by description match, and each reference file a skill links is read on its - own, so it is its own surface — #4398), the hook-injected static context, and the MCP server's + own, so it is its own surface), the hook-injected static context, and the MCP server's own instructions. """ found = [(str(p.relative_to(ROOT)), p.read_text()) @@ -68,10 +50,9 @@ def _instructions_text() -> str: # Claude Code injects only the first ~2,048 characters of an MCP server's -# instructions and silently cuts the rest mid-word (#2562: observed live — -# the previous 20k-char version delivered ~10% of itself, and none of the -# Systems tagging guidance ever reached a session). The budget is set below -# the cap, not at it: the field runs ~600–1,600 characters (spike #4389), and +# instructions and silently cuts the rest mid-word, so guidance past the cap +# never reaches a session. The budget is set below the cap, not at it: the +# field runs ~600–1,600 characters, and # a block pressed against 2,048 is being used for depth that belongs in a # skill. INSTRUCTIONS_BUDGET = 1600 @@ -81,18 +62,17 @@ def test_instructions_fit_the_fold(): text = _instructions_text() assert len(text) <= INSTRUCTIONS_BUDGET, ( f"_INSTRUCTIONS is {len(text)} chars; Claude Code injects only ~2,048 " - f"and silently cuts the rest (#2562). This block is an INDEX — state " - f"the topic in full on its owner (a skill or the tool's docstring, " - f"decision #4027) and give it at most a line here; see the comment " + f"and silently cuts the rest. This block is an INDEX — state " + f"the topic in full on its owner (a skill or the tool's docstring) " + f"and give it at most a line here; see the comment " f"above _INSTRUCTIONS." ) # ── force: a surface that says rules bind must say what does not ──────── # -# Added with the preference kind (milestone 399). Before it, "rules bind" was -# the whole truth and every surface said so flatly. It is now half of one, and -# the half that is missing is the dangerous half to omit: a session reading +# With a preference kind beside rules, "rules bind" is half of the truth, and +# the missing half is the dangerous half to omit: a session reading # only "rules bind" and then receiving a preference has been told, by the most # authoritative surface it has, to treat it as binding. # @@ -107,7 +87,7 @@ def test_instructions_fit_the_fold(): # PINNED ON THE CLAIM, NOT THE WORD "bind". A bare substring also matches # `bind_repo`, `list_repo_bindings` and the DNS-rebinding comment in # server.py — so it would one day fail a skill that mentions repo binding and -# has nothing to do with force, which is rule 167's named failure: a guard +# has nothing to do with force — a guard # raising a false alarm about the very thing it protects. These phrases are # the ones that actually assert bindingness to a reader. BINDING_CLAIMS = ( @@ -144,7 +124,7 @@ def test_a_surface_claiming_rules_bind_also_names_what_does_not(): # whole corpus; a session that meets a third line shape with no name for it # falls back on the nearest one it has, which is the binding one. # -# The currency half is the claim #3896 was written around: a preference is the +# The currency half is the central claim: a preference is the # one record a session updates ITSELF, without asking. Stated only in the # skill, it reaches a session that loaded the skill — which is exactly the # session least likely to need telling. In the index it reaches everyone. @@ -168,12 +148,12 @@ def test_the_index_names_all_three_strengths(): f"client receives, and a record kind it does not name is one the session " f"has to guess the force of — which it does by reaching for the kind it " f"does know. Keep it to a clause; the full statement lives on " - f"using-scribe (decision #4027)." + f"using-scribe." ) def test_the_strength_guard_can_fail(): - """Rule 167. The guard has to be able to bite, and it bites on omission — + """The guard has to be able to bite, and it bites on omission — the failure that actually happens, when a kind is added to the product and the index is left describing the corpus that came before it.""" text = "rules bind; preferences guide and you keep them current." @@ -182,14 +162,13 @@ def test_the_strength_guard_can_fail(): assert missing == ["that lessons inform"] -# ── Reporting back (milestone 409 step 3) ────────────────────────────── +# ── Reporting back ───────────────────────────────────────────────────── # # The reporting-back skill carries the shapes, but a skill only helps if it # fires. The reflex that sends a session to it is stated in using-scribe, and # the in-band cue on update_task (`placement`, `report_back`) is the half that -# fires on its own in every client. The server's index carried a REPORT line -# until #4389: reporting is a mid-work stance, not a session-start practice. The -# static context was a second plugin-side copy until milestone 410. +# fires on its own in every client. The server's index carries no REPORT line: +# reporting is a mid-work stance, not a session-start practice. REPORT_REFLEX = "report back in a shape the operator can read" REPORT_SURFACES = ( @@ -211,8 +190,8 @@ def test_the_reporting_reflex_is_stated_in_using_scribe(): def test_the_reporting_reflex_takes_placement_from_the_record(): - """The failure milestone 409 began from: a placement reconstructed from - memory reads exactly like a real one when it is wrong.""" + """A placement reconstructed from memory reads exactly like a real one + when it is wrong.""" for path in REPORT_SURFACES: text = " ".join(path.read_text().split()) assert "`placement`" in text, f"{path.relative_to(ROOT)} no longer points at the placement block" diff --git a/tests/test_integration_shape_classify.py b/tests/test_integration_shape_classify.py index 314f44cd..5e3a5a9b 100644 --- a/tests/test_integration_shape_classify.py +++ b/tests/test_integration_shape_classify.py @@ -1,4 +1,4 @@ -"""Real-Postgres integration tests for shape classification (#2789). +"""Real-Postgres integration tests for shape classification. What mocks can't prove: the all-or-nothing batch against real rows, the write-ACL gate, the todo query's filters, and the consumer map riding @@ -96,7 +96,7 @@ async def test_classify_applies_judgments_and_reports_unmatched(seeded): @pytest.mark.integration async def test_a_bad_batch_applies_nothing(seeded): - """All-or-nothing (#2709's lesson): a caller must never learn later that + """All-or-nothing: a caller must never learn later that half a batch silently happened.""" owner, pid, sid = seeded["owner"], seeded["pid"], seeded["snippet"] with pytest.raises(ValueError) as err: @@ -131,7 +131,7 @@ async def test_classification_is_write_gated_and_listing_read_gated(seeded): @pytest.mark.integration async def test_rule_form_sweeps_unclassified_rows_only_and_applies_whole(seeded): - """#2868: one judgment over a directory + glob; judged rows are left + """One judgment over a directory + glob; judged rows are left alone unless include_judged; the same gates as the row form.""" owner, other, pid, sid = seeded["owner"], seeded["other"], seeded["pid"], seeded["snippet"] await classify_shapes(owner, pid, [ @@ -171,7 +171,7 @@ async def test_rule_form_sweeps_unclassified_rows_only_and_applies_whole(seeded) @pytest.mark.integration async def test_sync_stamps_scoped_rows_and_unstamps_when_they_become_reachable(seeded): - """#2869: by-construction one-offs arrive `scoped` (mechanical), count as + """By-construction one-offs arrive `scoped` (mechanical), count as accounted, are reached by the sweep's default, and go back to the todo if a later tree makes them ordinary. A judgment overrides the stamp.""" from scribe.services.coverage import ArchiveShape @@ -203,7 +203,7 @@ async def test_sync_stamps_scoped_rows_and_unstamps_when_they_become_reachable(s @pytest.mark.integration async def test_uses_edges_are_the_consumer_map(seeded): - """#2870: a shape keeps ONE snippet_id (what it is) and any number of + """A shape keeps ONE snippet_id (what it is) and any number of uses edges (what it calls); the snippet's consumer map lists them, list_shapes(uses=N) finds them, and a sweep can write them.""" from scribe.services import snippets as snippets_svc @@ -296,7 +296,7 @@ async def test_get_snippet_carries_the_structured_consumer_map(seeded): @pytest.mark.integration async def test_sync_refiles_rows_whose_snippet_was_purged(seeded): - """The SET NULL companion (#2787): a judgment whose target is hard-deleted + """The SET NULL companion: a judgment whose target is hard-deleted rejoins the todo on the next sync instead of dangling target-less.""" owner, pid, sid = seeded["owner"], seeded["pid"], seeded["snippet"] await classify_shapes(owner, pid, [ @@ -318,12 +318,12 @@ async def test_sync_refiles_rows_whose_snippet_was_purged(seeded): assert row.snippet_id is None -# --- #2791: the write-path feed lands hook evidence as rows ------------------- +# --- the write-path feed lands hook evidence as rows -------------------------- @pytest.mark.integration async def test_write_path_evidence_is_a_proposal_never_a_verdict(seeded): - """Milestone 439. Pulled + referenced → every named shape of the + """Pulled + referenced → every named shape of the snippet's kind CARRIES the snippet as a proposal (basis "reference"), and its status is untouched: the agent that wrote it says what it is. A judgment is never touched, not even by a proposal. The outsider @@ -465,7 +465,7 @@ async def test_recent_pulls_reads_the_usage_stream(seeded): assert sid + 2000 not in pulls -# --- #2792: the mechanical proposer against real rows ------------------------- +# --- the mechanical proposer against real rows ------------------------------- def _quiet_semantic(): @@ -594,7 +594,7 @@ async def test_derive_groups_land_on_rows_and_in_the_summary(seeded): summary = proposal_summary(await live_rows(pid)) assert summary["proposed"] == 1 - # #2872: the body-identical group (a real copy) outranks the bigger name + # The body-identical group (a real copy) outranks the bigger name # group (usually convention), even at size 2 vs 3. order = [g["group"] for g in summary["derive_groups"]] assert order[0].startswith("dup:") and order[1] == "name:css:card" @@ -605,7 +605,7 @@ async def test_derive_groups_land_on_rows_and_in_the_summary(seeded): # One of the css copies gets judged → the group shrinks on the next pass # but stays a family: a class in two files is already a recipe living in - # two places (css name floor 2, note 2917). Judge the second and it's gone. + # two places (css name floor 2). Judge the second and it's gone. await classify_shapes(owner, pid, [ {"path": "b/z.css", "symbol": "card", "status": "exempt", "reason": "print sheet"}, ]) @@ -623,8 +623,8 @@ async def test_derive_groups_land_on_rows_and_in_the_summary(seeded): @pytest.mark.integration async def test_write_time_derive_names_the_family_or_the_canon_for_a_name(seeded): - """#2900: against real rows — a name in a family → the family (other - files, count; for CSS a NAME family, never a body one — note 2917); a + """Against real rows — a name in a family → the family (other files, + count; for CSS a NAME family, never a body one); a name whose canonical row lives elsewhere → that canon; a judged row at the path, the canon's own file, or an unknown name → silence.""" from scribe.services.shape_ledger import apply_derive_groups, write_time_derive @@ -667,7 +667,7 @@ async def test_write_time_derive_names_the_family_or_the_canon_for_a_name(seeded @pytest.mark.integration async def test_derive_new_names_the_copy_that_joined_a_family_since_the_stamp(seeded): - """#2899: the first sync seeds one `slug`; a later sync adds an identical + """The first sync seeds one `slug`; a later sync adds an identical copy. Against the stamp between them, derive_new counts ONLY the newcomer — the drift since the last refresh, not the whole family.""" from datetime import datetime, timezone @@ -700,7 +700,7 @@ async def test_derive_new_names_the_copy_that_joined_a_family_since_the_stamp(se @pytest.mark.integration async def test_transition_and_prefix_references_clear_the_unused_css_flag(seeded): - """#2970 end to end: the two class forms a template never spells out — + """End to end: the two class forms a template never spells out — a transition `name=` and a concatenated name — travel from the real extractor through resolution into the flag, so rules they reach stop being reported as unused. Only a rule nothing can reach stays listed.""" @@ -752,7 +752,7 @@ async def test_transition_and_prefix_references_clear_the_unused_css_flag(seeded @pytest.mark.integration async def test_consumer_map_syncs_edges_from_template_references(seeded): - """Milestone 302: the consumer edges follow the archive — own-file + """The consumer edges follow the archive — own-file resolution for a scoped class, fan-out to the shared sheet for a class a template does not define, counts refreshed and stale edges removed on the next sync, and a vanished row's edges gone with it.""" @@ -812,7 +812,7 @@ async def test_consumer_map_syncs_edges_from_template_references(seeded): assert rows[("v/B.vue", "error-msg")] not in edges -# --- #2793: the divergence readout against real rows ------------------------- +# --- the divergence readout against real rows ------------------------------- @pytest.mark.integration @@ -833,7 +833,7 @@ async def test_a_second_confirm_dialog_is_detected_and_named(seeded): from scribe.services import snippets as snippets_svc owner, pid = seeded["owner"], seeded["pid"] - # The confirm helper is TS canon: since #2871 a sym basis only proposes + # The confirm helper is TS canon: a sym basis only proposes # within the shape's language family, so the fixture's Python snippet # says nothing about these Vue bodies — the canon must be one of theirs. canon = await snippets_svc.create_snippet( @@ -906,10 +906,9 @@ async def test_a_second_confirm_dialog_is_detected_and_named(seeded): @pytest.mark.integration async def test_weak_stamps_elect_no_canon_and_their_flags_are_withdrawn(seeded): - """#4608, the poisoned-ledger case as it stood on two projects: a - directory's "dominant canon" was elected entirely by hook stamps written - at 0.68-0.72, before the 0.80 floor, so every new shape there was told it - diverged from a YAML CI snippet. The same fixture as the acceptance case + """The poisoned-ledger case: a directory's "dominant canon" elected + entirely by hook stamps written below the 0.80 floor would tell every new + shape there it diverged from an unrelated snippet. The same fixture as the acceptance case above, with one difference — the four instances are weak hook stamps rather than an audit — and the prompt must fall silent: at refresh (the standing flag is withdrawn) and at write time. The stamps themselves are @@ -984,14 +983,14 @@ async def test_weak_stamps_elect_no_canon_and_their_flags_are_withdrawn(seeded): @pytest.mark.integration async def test_a_semantic_miss_does_not_silence_the_prompt(seeded): - """#4208, reversed on measurement: a semantic miss is not evidence. + """A semantic miss is not evidence. THE FIXTURE IS THE ACCEPTANCE CASE ABOVE, DELIBERATELY. A hand-rolled `confirmDanger` beside an async confirm canon is structurally identical to a registry helper beside an async service canon, and the arm's miss was - meant to tell them apart. Measured live, true instances of a canon rarely - clear the arm's floor either, so a miss fires on both — silencing on - it silenced #2793's own case. Here the arm has read the body, found + meant to tell them apart. True instances of a canon rarely clear the arm's + floor either, so a miss fires on both — silencing on it would silence the + case the readout exists for. Here the arm has read the body, found nothing, and the prompt is still RAISED. Asserted on the row, not a flag count: `since` sits a second early to @@ -1087,9 +1086,9 @@ async def _two_generations(seeded, tag: str): @pytest.mark.integration async def test_the_capped_semantic_pass_reaches_the_new_shape_first(seeded): - """Measured live: after the #4208 version bump, 1,533 rows competed for 150 - semantic checks in row order and the six new shapes — the only ones the - divergence check acts on — were all left unread and flagged. With room for + """After a version bump, every row competes for a capped number of + semantic checks; in row order the new shapes — the only ones the + divergence check acts on — are left unread and flagged. With room for ONE check, the new shape must be the one that gets it.""" from unittest.mock import AsyncMock, patch @@ -1155,7 +1154,7 @@ async def test_history_records_what_was_used_when_and_drift_asks_for_a_recheck(s assert await shape_history(other, pid, "src/app.py") == {} -# --- milestone 439: the writer judges what it wrote, before the sync has seen it +# --- the writer judges what it wrote, before the sync has seen it @pytest.mark.integration async def test_a_shape_written_this_turn_is_judged_before_the_sync_sees_it(seeded): @@ -1235,7 +1234,7 @@ async def test_unjudged_is_what_nobody_read(seeded): @pytest.mark.integration async def test_a_whole_file_snippet_makes_its_file_row_canonical(seeded): - """Milestone 439: a snippet recorded at a component's path, with no + """A snippet recorded at a component's path, with no symbol, is the canon for that file row — the claim a whole-file record makes and a definition row never could. It still covers no definition inside the file.""" diff --git a/tests/test_pattern_coverage.py b/tests/test_pattern_coverage.py index 19c1edef..32d72947 100644 --- a/tests/test_pattern_coverage.py +++ b/tests/test_pattern_coverage.py @@ -1,4 +1,4 @@ -"""Pattern-library coverage (#2692) — the extractor that mirrors the hook, +"""Pattern-library coverage — the extractor that mirrors the hook, the shape/record matcher, and the end-to-end measurement on real Postgres. The extractor here and the hook's awk program (scribe_prior_art.sh ARM 1) @@ -66,11 +66,9 @@ EXTRACTION_VECTORS = [ []), ("dedup-within-file", "def f():\n pass\ndef f():\n pass\n", [("sym", "f")]), - # --- comment and string spans (#4222) ------------------------------------ + # --- comment and string spans -------------------------------------------- # Prose is not code. A wrapped docstring line beginning "class AND the" - # announced a shape called `AND` to a live session; `with` and `nobody` - # out of one module docstring in scripts/check_dangling_styles.py reached - # persisted, judged `code_shapes` rows. + # would otherwise announce a shape called `AND`. ("docstring-prose", 'def real_one():\n """Its own text is about the\n' ' class AND the to_dict, and is a def bar():\n' @@ -81,18 +79,15 @@ EXTRACTION_VECTORS = [ ("unterminated-docstring", 'def before():\n """oops, never closed\n\ndef after():\n pass\n', [("sym", "before"), ("sym", "after")]), - # The CSS half of the same defect (#2990): a wrapped comment line that + # The CSS half of the same rule: a wrapped comment line that # happens to begin with a dotted token reads as a selector. ("css-comment-selector", "/* A real base rule, not just descendants: the check reads a\n" " .ghost, class that only ever appears as an ancestor */\n.check { }\n", [("css", "check")]), - # The rest of #2990's verify list, which this scan satisfies as written: - # a comment holding a WHOLE rule defines nothing, and an unterminated - # comment does not swallow the file. The phantom it was filed for — - # `.editor-body` out of a wrapped comment in TaskEditorView.vue, a - # persisted row with a `used_by` count of zero inflating unused_css — is - # among the twenty-two this removes. + # A comment holding a WHOLE rule defines nothing, and an unterminated + # comment does not swallow the file. A phantom class out of a comment + # would persist with a `used_by` count of zero and inflate unused_css. ("comment-containing-a-whole-rule", "/* .ghost {\n color: red;\n } */\n.real { color: blue; }\n", [("css", "real")]), @@ -195,7 +190,7 @@ def test_shapes_from_archive_strips_the_wrapper_and_gates_files(): assert shapes_from_archive(_tarball(TREE)) == TREE_SHAPES -# --- unit: template class references — the CSS consumer map (milestone 302) -- +# --- unit: template class references — the CSS consumer map ---------------- def test_class_references_reads_vue_static_and_dynamic_forms_only(): @@ -229,7 +224,7 @@ def test_class_references_reads_vue_static_and_dynamic_forms_only(): def test_class_references_reads_framework_transition_names(): """A transition `name=` is a class reference: the framework applies `.toast-enter-active` and friends at runtime, so a stylesheet that - defines them is consumed even though no template spells one out (#2970). + defines them is consumed even though no template spells one out. Every spelling of the tag counts; a bound `:name` stays unknowable.""" refs = class_references( "a/T.vue", @@ -251,7 +246,7 @@ def test_class_references_reads_framework_transition_names(): def test_class_references_reads_a_concatenated_name_as_a_prefix(): """`status-${s}` cannot be resolved to one class, but its static head is real information: it is emitted as the prefix reference `status-*` so - the rows it could have built are not reported unused (#2970). A head too + the rows it could have built are not reported unused. A head too short to mean anything, or a bare separator, says nothing.""" vue = ( '
' @@ -301,7 +296,7 @@ def test_scan_archive_returns_definitions_and_references_from_one_walk(): scan = scan_archive(_tarball(tree)) assert isinstance(scan, ArchiveScan) assert [(d.path, d.kind, d.name) for d in scan.definitions] == TREE_SHAPES + [ - ("web/Card.vue", "file", "Card"), # a component is a unit (milestone 439) + ("web/Card.vue", "file", "Card"), # a component is a unit ("web/Card.vue", "css", "card"), ] # Only files whose markup names a class appear; the .py/.css files don't. @@ -309,7 +304,7 @@ def test_scan_archive_returns_definitions_and_references_from_one_walk(): assert shapes_from_archive(_tarball(tree)) == [(d.path, d.kind, d.name) for d in scan.definitions] -# --- unit: the covering predicate (lives with the ledger since #2788) -------- +# --- unit: the covering predicate (lives with the ledger) -------------------- def test_location_covers_by_exact_path_dir_prefix_and_css_dot(): @@ -341,7 +336,7 @@ def test_largest_gaps_ranks_by_unclassified_and_drops_clean_dirs(): def test_type_import_specifiers_are_not_definitions(): - """#2904: `import { type Foo, bar }` is the same two words as `type Foo =` + """`import { type Foo, bar }` is the same two words as `type Foo =` and defines nothing; only a `type` line with a declaration after the name counts (TS alias, Go/Rust type).""" from scribe.services.coverage import extract_shapes @@ -375,7 +370,7 @@ def test_coverage_line_is_evidence_carrying_and_labeled_estimate(): def test_bind_repo_tool_takes_a_ref(): - """#2873: the binding names the branch the ledger follows.""" + """The binding names the branch the ledger follows.""" from scribe.mcp.server import build_mcp_server tool = build_mcp_server()._tool_manager.get_tool("bind_repo") assert "ref" in tool.parameters.get("properties", {}) @@ -412,7 +407,7 @@ def _forge(tar_bytes: bytes): def _selector(tar_bytes: bytes): - """The keyring shape compute_coverage consumes since #2778 — one owner + """The keyring shape compute_coverage consumes — one owner keyring holding the mocked Gitea adapter.""" from scribe.services.forge import ForgeSelector @@ -482,10 +477,10 @@ async def test_coverage_measures_the_tree_exactly_and_caches(seeded): assert coverage["largest_gaps"] == [ {"dir": "src", "unclassified": 2, "total": 3} ] - # #2899: a first computation has no previous stamp — nothing is "new". + # A first computation has no previous stamp — nothing is "new". assert coverage["derive_new"] == {"count": 0, "examples": []} - # The walk fed the LEDGER (#2788): every extracted shape has a row, the + # The walk fed the LEDGER: every extracted shape has a row, the # snippet reference locations are mechanically stamped canonical WITH # their snippet id, and the rest sit in the todo state. async with async_session() as s: @@ -516,7 +511,7 @@ async def test_coverage_measures_the_tree_exactly_and_caches(seeded): @pytest.mark.integration async def test_ledger_keeps_judgments_and_stamps_vanished_shapes(seeded): - """The two survival rules (#2788): an agent's classification outlives + """The two survival rules: an agent's classification outlives recompute, and a shape that leaves the tree is stamped vanished — kept for history, dropped from the readout.""" from datetime import datetime, timezone @@ -593,9 +588,8 @@ async def test_enter_project_surfaces_the_line_only_once_computed(seeded): @pytest.mark.integration async def test_explicit_refresh_names_its_failures(seeded): - """refresh_for_caller (#2802) raises fixable errors instead of silence: - an agent mid-task must learn WHY nothing measured — 'None' is exactly the - stranding the button-only path caused.""" + """refresh_for_caller raises fixable errors instead of silence: an agent + mid-task must learn WHY nothing measured, and 'None' would strand it.""" from scribe.models import async_session from scribe.services.coverage import refresh_for_caller @@ -617,8 +611,8 @@ async def test_explicit_refresh_names_its_failures(seeded): @pytest.mark.integration async def test_background_seed_is_quiet_without_a_forge(seeded): - """refresh_if_stale (#2802) must exit silently for a forge-less owner — - rule #115's baseline — and treat a fresh cache as nothing-to-do.""" + """refresh_if_stale must exit silently for a forge-less owner — the + baseline install — and treat a fresh cache as nothing-to-do.""" from scribe.services.coverage import refresh_coverage, refresh_if_stale uid, pid = seeded["uid"], seeded["pid"] @@ -651,7 +645,7 @@ async def test_unservable_binding_measures_nothing(seeded): assert await compute_coverage(uid, other_pid, selector=_selector(_tarball(TREE))) is None -# --- #2792: fingerprints and the proposer's readout -------------------------- +# --- fingerprints and the proposer's readout -------------------------------- def test_extract_definitions_fingerprints_each_block(): @@ -684,14 +678,14 @@ def test_extract_definitions_fingerprints_each_block(): # And the identity view is unchanged for the hook mirror. from scribe.services.coverage import extract_shapes assert extract_shapes(text) == [(d.kind, d.name) for d in extract_definitions(text)] - # A CSS rule's fingerprint is its declarations (#2872): the same body + # A CSS rule's fingerprint is its declarations: the same body # under another selector is the same shape to the derive grouping. css = ".closed-msg {\n text-align: center;\n padding: 0.5rem 0;\n}\n.error-block {\n text-align: center;\n padding: 0.5rem 0;\n}\n.other {\n text-align: left;\n}\n" d = {x.name: x for x in extract_definitions(css)} assert d["closed-msg"].body_sha == d["error-block"].body_sha != d["other"].body_sha - # One-line rules hash their own declarations — never the empty string - # (first deploy grouped 68 unrelated one-liners as one copy) — and a - # SINGLE declaration is not a shape (#2903): it keeps its selector in the + # One-line rules hash their own declarations — never the empty string, + # which would group every unrelated one-liner as one copy — and a + # SINGLE declaration is not a shape: it keeps its selector in the # hash, so `.a { color: red }` groups only with another `.a`, never with # `.b { color: red }`. Two declarations and up stay selector-agnostic. one = ".a { color: red; }\n\n.b { color: red; }\n\n.c { color: blue; }\n\n.a {\n color: red;\n}\n" @@ -718,7 +712,7 @@ def test_coverage_line_names_the_proposers_standing(): assert "; 90 unclassified (40 proposed, 2 derive groups), largest: src" in line line = coverage_line({**base, "proposed": 0, "derive_groups": [{"group": "a"}]}) assert "(1 derive group)" in line - # Milestone 302: a css top copy says what renders it; unused classes + # A css top copy says what renders it; unused classes # join the standing block only when measured (None = no evidence). line = coverage_line({**base, "unclassified": 0, "proposed": 0, "derive_groups": [ {"group": "name:css:error-msg", "label": ".error-msg", "files": 6, @@ -729,7 +723,7 @@ def test_coverage_line_names_the_proposers_standing(): {"group": "name:css:x", "label": ".x", "files": 2, "consumers": {"count": 1, "paths": ["a.vue"]}}], "unused_css": None}) assert "top copy .x ×2 files · used by 1 template" in line and "unused" not in line - # #2874: the next action on the line — biggest canon queue, widest copy. + # The next action on the line — biggest canon queue, widest copy. line = coverage_line({ **base, "proposed": 40, "top_canon": {"snippet_id": 2844, "count": 78}, "derive_groups": [{"group": "dup:abc", "label": "closed-msg (identical body)", "files": 3}], @@ -738,7 +732,7 @@ def test_coverage_line_names_the_proposers_standing(): def test_coverage_line_shows_standing_work_even_with_nothing_unclassified(): - """#2899: since the scoped bucket a ledger can be fully accounted and + """A ledger can be fully accounted and still carry derive groups / proposals / divergence — the line names them as `standing:` instead of hiding them behind the todo count, and names the drift since the previous refresh first-copy-first.""" @@ -789,7 +783,7 @@ def test_coverage_line_names_divergence_and_recheck(): def test_scoped_definitions_are_vue_script_setup_and_scoped_style_only(): - """#2869: one-offs by construction — every sym in a .vue and every css + """One-offs by construction — every sym in a .vue and every css rule inside