docs(6209): coverage, guidance-ownership, shape, warnings tests and shared fixtures
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 52s
CI & Build / integration (push) Successful in 1m12s
CI & Build / Python tests (push) Successful in 1m56s
CI & Build / Build & push image (push) Successful in 13s

test_pattern_coverage, test_guidance_ownership, test_integration_shape_classify,
test_instruction_surfaces_agree, test_retrieval_warnings, helpers.py and
conftest.py state the property each test or fixture holds. The issue and
milestone numbers, consolidation dates and one-install measurements that
recorded how each was found are dropped; fixture dates and ids stay.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-21 21:21:52 -04:00
co-authored by Claude Opus 5.5
parent 8518afb54d
commit 91c96a8b8a
7 changed files with 199 additions and 256 deletions
+19 -20
View File
@@ -7,8 +7,8 @@ in the environment before importing the app.
For unit tests of pure functions no database is needed at all. 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 The fixtures below are the ONE definition of things a dozen test modules
copied into a dozen test modules each (#2825). They are deliberately not each need. They are deliberately not
autouse: a module opts in with autouse: a module opts in with
``pytestmark = pytest.mark.usefixtures("<name>")`` (or a test names the ``pytestmark = pytest.mark.usefixtures("<name>")`` (or a test names the
fixture as a parameter), so a unit test that never touches the engine or 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 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. # otherwise detach the same model-loading task.
with patch("scribe.services.notes.embed_note", MagicMock()), \ with patch("scribe.services.notes.embed_note", MagicMock()), \
patch("scribe.services.milestones.embed_milestone", MagicMock()): patch("scribe.services.milestones.embed_milestone", MagicMock()):
@@ -89,7 +89,7 @@ def _no_embedding():
@pytest.fixture @pytest.fixture
def _no_supersession(): 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 That is a real database call on a path the plugin-context tests exercise
without one. Stubbed to "nothing superseded" — the ordinary state — rather without one. Stubbed to "nothing superseded" — the ordinary state — rather
@@ -104,7 +104,7 @@ def _no_supersession():
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def _no_reply_shape_log(): 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 Autouse for _no_system_labels' reason: every turn's retrieval, every
moment request and every Scribe tool that reaches a shaped moment writes 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) @pytest.fixture(autouse=True)
def _no_system_labels(): 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 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 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) @pytest.fixture(autouse=True)
def _no_rulings_arm(): 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 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 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) @pytest.fixture(autouse=True)
def _no_system_usage_readout(): def _no_system_usage_readout():
"""Stub the per-System usage lookup the Systems list and get_system carry """Stub the per-System usage lookup the Systems list and get_system carry.
(#4769).
Autouse for _no_rulings_arm's reason: every route and tool test that 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 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) @pytest.fixture(autouse=True)
def _no_task_log_arm(): def _no_task_log_arm():
"""Stub the task-log read arm that get_task / list_tasks / get_milestone """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 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 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) @pytest.fixture(autouse=True)
def _no_lesson_rule_links(request): 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 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 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 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. 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 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 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 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 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 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 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. the same reason; tests/test_lesson_convergence.py binds the real one.
Skipped for integration tests, which exercise the real links against Skipped for integration tests, which exercise the real links against
@@ -231,7 +230,7 @@ def _no_lesson_rule_links(request):
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def _no_family_triggers(request): 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, create_note, create_task, create_snippet, the updates that change a body,
and update_milestone(status="done") now look for a pattern carried between 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) @pytest.fixture(autouse=True)
def _no_moment_delivery(request): 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 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 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) @pytest.fixture(autouse=True)
def _no_rule_arm(): 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 Autouse, and deliberately so. The arm calls semantic_search_rules, which
loads the embedding model — so every unrelated plugin-context test that loads the embedding model — so every unrelated plugin-context test that
@@ -297,7 +296,7 @@ def _no_rule_arm():
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def _no_rule_overlap(): 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 / The same reason as _no_rule_arm, one door over: every create_rule /
create_project_rule / create_preference now asks semantic_search_rules create_project_rule / create_preference now asks semantic_search_rules
@@ -314,7 +313,7 @@ def _no_rule_overlap():
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def _default_gate_bars(): 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 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 similarity bars, which is a database read on a path the gate's unit tests
+24 -40
View File
@@ -1,8 +1,7 @@
"""Shared test helpers — the plain functions tests call, as opposed to the """Shared test helpers — the plain functions tests call, as opposed to the
fixtures in conftest.py. fixtures in conftest.py.
Each of these was copied into several test modules before #2825 consolidated Each of these serves several test modules; a module imports what it needs with ``from tests.helpers import ...``.
them; a module imports what it needs with ``from tests.helpers import ...``.
""" """
from __future__ import annotations 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, 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 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 which a unit test has.
(#3128) and once for the verification fields (#3182/317) — before being
consolidated here.
Returns whatever update_note returned; assert on the `note` you passed in. 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 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 worth asserting on is liable to straddle a line break — a property of the
formatter, not of the guidance. The disambiguator guard (#3123) learned formatter, not of the guidance. Matching raw text reports a phrase absent
that on its own first run, matching raw text and reporting a phrase absent that is plainly there.
that was plainly there.
Used by every test that pins the docstring CONTRACT rather than its 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. these guards exist to catch it being tidied down to a parameter list.
""" """
import importlib import importlib
@@ -63,8 +59,7 @@ def compiled_sql(element, dialect=None) -> str:
"""A SQLAlchemy clause or statement rendered as literal SQL text. """A SQLAlchemy clause or statement rendered as literal SQL text.
For asserting on the shape of a predicate without a database — which is how 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 the visibility clauses and the knowledge facets are both tested.
private copy in each of those modules before #3128 needed a third.
Pass `dialect` when the assertion is about something only one backend Pass `dialect` when the assertion is about something only one backend
renders — a Postgres row-lock mode, say. The generic dialect is enough for 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). """``make_mock_session`` whose every ``execute()`` yields `note` (or None).
The commonest shape above the bare session: a service that loads one 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 record by id and acts on it, and it belongs beside `make_mock_session`
before a third was about to (#3207 — derive before the third copy), and for the same reason that one does.
it belongs beside `make_mock_session` for the same reason that one does.
For a service making SEVERAL different reads, set For a service making SEVERAL different reads, set
``session.execute.side_effect`` to a list of results instead. ``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 """A MagicMock record with REAL values on the attributes named, and a
``to_dict()`` that mirrors them. ``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 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 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 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", "note_type": "note", "is_task": False, "task_kind": "work",
"status": "todo" if is_task else None, "status": "todo" if is_task else None,
"data": None, "deleted_at": 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. # check", which trips the guard on records that may not have one.
"verify_with": None, "expires_when": None, "verified_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, "claimed_by": None, "claimed_at": None, "claim_touched_at": None,
"claim_session": None, "claim_session": None,
}, attrs) }, attrs)
@@ -196,7 +190,7 @@ def fake_task(**attrs) -> MagicMock:
"tags": [], "parent_id": None, "project_id": None, "is_task": True, "tags": [], "parent_id": None, "project_id": None, "is_task": True,
"task_kind": "work", "user_id": 7, "deleted_at": None, "task_kind": "work", "user_id": 7, "deleted_at": None,
"verify_with": None, "expires_when": None, "verified_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, "claimed_by": None, "claimed_at": None, "claim_touched_at": None,
"claim_session": None, "claim_session": None,
}, attrs) }, attrs)
@@ -219,9 +213,8 @@ def fake_lesson(**attrs) -> MagicMock:
"""A stand-in lesson: a note whose `note_type` is what makes it one. """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 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 that is the record the product creates — the trigger joins the title only
the title only in the embedded document. A default whose title carried the in the embedded document.
trigger would be testing a row only an un-migrated database holds.
The check fields and `arose_from_id` are explicitly None for the reason The check fields and `arose_from_id` are explicitly None for the reason
`fake_snippet`'s `data` is: `update_note` reads `verify_with` and `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 the TOOL forwarded the right arguments, so the seam is stubbed the same
way the create/update calls themselves already are. way the create/update calls themselves already are.
Consolidated here on its second copy, per this module's own reason for Shared here because two stubs for one seam drift apart quietly, and a test
existing (#2825) — two stubs for one seam drift apart quietly, and a test
stubbing the seam slightly differently is a test asserting something stubbing the seam slightly differently is a test asserting something
slightly different than it appears to. 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 — (one dict per request, in order). Lets the shell be tested end to end —
the extraction, the encoding, the URL — without a Scribe instance. the extraction, the encoding, the URL — without a Scribe instance.
Three test modules each carried their own ``_Sink`` handler before #2904 Pass ``reply`` for the body the hook should see.
consolidated them here; pass ``reply`` for the body the hook should see.
``by_path`` is for a hook that calls more than one route: each request is ``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 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): 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 DERIVED, NOT LITERAL, because of a failure mode prose cannot prevent: the
warned about in prose without being able to prevent: the write-path hint write-path hint
drives three arms, each of which reads its numbers out of the config dict 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 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 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 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 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 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 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 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 — 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 these tests exist precisely because the shell's behaviour is the thing in
question (#2932), so anything short of the real tool proves nothing. question, 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.
""" """
import shutil 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 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. 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` 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 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 database and must not pay for one — the same reason `plugin_config` defers
its service imports. 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 import async_session
from scribe.models.rulebook import Rule 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 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 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 "does this skill still say X" reads them together rather than pinning X to
whichever file it happens to sit in today. whichever file it happens to sit in today.
""" """
+39 -44
View File
@@ -1,17 +1,16 @@
"""Every piece of agent guidance has exactly one owner that states it. """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 `_INSTRUCTIONS`, the plugin's static session context, the live session context
the server builds, the `using-scribe` skill, and tool docstrings. An earlier the server builds, the skills, and tool docstrings. A practice stated in full
design (#2494) made that deliberate — insurance against any one surface failing on several of them drifts apart, copy by copy.
silently — and the copies drifted apart instead (#2497, #4022).
Decision #4027 replaced the redundancy with ownership: the server orients with So each practice has one owner: the server orients with a short index, the
a short index, the skills hold the depth, tool docstrings hold each tool's skills hold the depth, tool docstrings hold each tool's contract, and each
contract, and each client adapter holds only its own timing and conventions. client adapter holds only its own timing and conventions. This module is the
This module is the registry that decision is enforced from. registry that ownership is enforced from.
WHAT IT PINS WHAT IT PINS
@@ -27,7 +26,7 @@ WHAT IT PINS
surface every MCP client receives, so each reflex it indexes keeps its 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 `index` markers there — a one-line pointer, not a copy. Only session-start
practices carry `index`: judging, reporting and the missed-rule route fire 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). responses carry them at the moment they apply).
WHAT IT CANNOT SEE WHAT IT CANNOT SEE
@@ -90,7 +89,7 @@ def delivered_surfaces() -> dict[str, str]:
- `commands` — the Claude Code adapter's slash commands - `commands` — the Claude Code adapter's slash commands
- `live` — the live session context the server builds - `live` — the live session context the server builds
- `shapes` — the reply shapes the server delivers at their moments, - `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() server = (ROOT / "src/scribe/mcp/server.py").read_text()
match = re.search(r'_INSTRUCTIONS = """(.*?)"""', server, re.S) match = re.search(r'_INSTRUCTIONS = """(.*?)"""', server, re.S)
@@ -103,7 +102,7 @@ def delivered_surfaces() -> dict[str, str]:
"live": _live_session_context_source(), "live": _live_session_context_source(),
"shapes": _delivered_shapes(), "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. # owner, however many files it is split across.
for skill in sorted((ROOT / "plugin/skills").glob("*/SKILL.md")): for skill in sorted((ROOT / "plugin/skills").glob("*/SKILL.md")):
surfaces[f"skill:{skill.parent.name}"] = skill_text(skill.parent.name) surfaces[f"skill:{skill.parent.name}"] = skill_text(skill.parent.name)
@@ -144,9 +143,8 @@ TOPICS: tuple[Topic, ...] = (
index=("lessons inform",), index=("lessons inform",),
# Stated twice on purpose, at the two moments it is needed: the # Stated twice on purpose, at the two moments it is needed: the
# skill while deciding what a record IS, and create_rule while a # skill while deciding what a record IS, and create_rule while a
# proposal is actually being written — which is where the # proposal is actually being written — which is where mis-routing
# mis-routing #3733 recorded happens, and the caller there has not # happens, and the caller there has not necessarily read the skill.
# necessarily read the skill.
shared_with=("docstrings",)), shared_with=("docstrings",)),
Topic("a lesson grows each time it proves itself", U, Topic("a lesson grows each time it proves itself", U,
("update_lesson", "re-keyed", "grows each time"), ("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",), Topic("a retrieved rule outranks a default habit", U, ("outranks a default habit",),
"a retrieved rule 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"), 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. # release is mechanical (SessionEnd hook); saying what happened is not.
Topic("hand off before the context stops existing", U, Topic("hand off before the context stops existing", U,
("hand off", "claims it for this session"), ("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"), Topic("where a new rule goes, and its trigger", U, ("create_project_rule", "when_to_apply"),
"whichever home it gets"), "whichever home it gets"),
Topic("a rule vs the other entities", U, ("standing instruction",), "first ask whether it's a rule at all"), 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 # 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. # 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, 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, Topic("a missed when is mounted or mapped, not reworded", U,
("update_rule(moments=",), ("update_rule(moments=",),
"first ask whether it missed a when or a what"), "first ask whether it missed a when or a what"),
# Milestone 416 step 9: the tuning tools shipped in #4102/#4104 and were # The tuning tools need a route on an instruction surface; machinery
# named on NO instruction surface — measured, `retrieval_tuning_history` # nothing names goes unused.
# returned zero events. Machinery with no route to it.
Topic("a missed rule is a trigger to fix before a floor to move", U, Topic("a missed rule is a trigger to fix before a floor to move", U,
("retrieval_telemetry", "tune_retrieval", "retrieval_surfaces"), ("retrieval_telemetry", "tune_retrieval", "retrieval_surfaces"),
"take it to the record first and the dial second"), "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"), ("what_might_apply",), "ask what already covers that moment"),
Topic("reference notes update in place; dev-logs don't", U, ("reference note",), Topic("reference notes update in place; dev-logs don't", U, ("reference note",),
"state updates in place; chronicles don't"), "state updates in place; chronicles don't"),
# Milestone 444: an operator's ruling was kept only as a quote in a work # A ruling kept only as a quote in a work log sits beside a session's
# log, beside a session's reading of it that contradicted it, and three # reading of it, and later sessions build on the reading. No index marker: both fire
# later sessions built on the reading. No index marker: both fire
# mid-work — when the operator decides, and when code is read as intent. # 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, Topic("an operator's ruling lives on the System it governs", U,
("rulings", "update_system", "the operator decided it"), ("rulings", "update_system", "the operator decided it"),
@@ -238,8 +234,8 @@ TOPICS: tuple[Topic, ...] = (
# ── process arcs — owned by their skills ── # ── process arcs — owned by their skills ──
Topic("plan in a milestone, steps created together", "skill:writing-plans", ("start_planning", "{{ref:"), 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",)), "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 # Without this, a session opens a second plan beside the roadmap milestone
# that already covered the work, because nothing told them to look. # that already covers the work.
Topic("find the existing plan before making one", "skill:writing-plans", Topic("find the existing plan before making one", "skill:writing-plans",
('content_type="milestone"', "unplanned_milestones", "existing_milestone"), ('content_type="milestone"', "unplanned_milestones", "existing_milestone"),
"when an active milestone already covers the work, the plan is that 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", Topic("reuse recorded shapes; record at first build", "skill:reusing-code",
("create_snippet", "when_to_use", "first build", "second copy"), ("create_snippet", "when_to_use", "first build", "second copy"),
"prior art offered beside a write is not noise", index=("create_snippet",)), "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. # asked by the plugin's Stop hook — the moment the knowledge exists.
Topic("judge what you wrote before the turn ends", "skill:reusing-code", Topic("judge what you wrote before the turn ends", "skill:reusing-code",
("before the turn ends", "classify_shapes", 'kind: "file"'), ("before the turn ends", "classify_shapes", 'kind: "file"'),
"you are the one who knows what the code you just wrote is"), "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"), Topic("report back where the work stands", "skill:reporting-back", ("reporting-back", "placement"),
"take the placement from the record"), "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 # moments with the operator's preferences mounted beside them — so the
# rule that a preference wins is stated where the shape arrives. # 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", 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", Topic("every reply leads with its conclusion and stays short", "shapes",
("conclusion first", "the shortest reply that carries the answer", "what can go"), ("conclusion first", "the shortest reply that carries the answer", "what can go"),
"length is work handed back to the reader"), "length is work handed back to the reader"),
# Milestone 409 step 7: the sections were being FILLED rather than chosen, # Sections FILLED rather than chosen let a reply satisfy every heading and
# so a reply could satisfy every heading and still be unreadable. These # still be unreadable. These
# three are the discipline around the scaffold, not the scaffold itself. # three are the discipline around the scaffold, not the scaffold itself.
Topic("sections are chosen, not filled, and the reply is cut twice", Topic("sections are chosen, not filled, and the reply is cut twice",
"skill:reporting-back", ("not a form to complete", "what can go"), "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", Topic("a settled decision is acted on, not re-opened",
"skill:reporting-back", ("already made",), "skill:reporting-back", ("already made",),
"reads as contradicting yourself rather than as being careful"), "reads as contradicting yourself rather than as being careful"),
# Milestone 444: a cap a past session chose rode inside an option as a # A cap a past session chose, riding inside an option as a "known cost",
# "known cost", the operator approved the option, and the cap read as # reads as the operator's once they approve the option.
# theirs from then on.
Topic("an option says whose call each carried assumption was", Topic("an option says whose call each carried assumption was",
"skill:reporting-back", ("past session chose this", "conflict"), "skill:reporting-back", ("past session chose this", "conflict"),
"an unlabelled one gets approved as if it had been asked"), "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 # `placement` rides a WRITE, so a task the reply only cites arrives with
# cites arrived with nothing vouching for it — which is how a step finished # nothing vouching for it — a finished step can be reported as the open
# four days earlier was reported as the open one (#4154). # one.
Topic("a record you only cite still gets read", Topic("a record you only cite still gets read",
"skill:reporting-back", ("next_step", "only mention"), "skill:reporting-back", ("next_step", "only mention"),
"a record you only mention is a record to read"), "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. # project answers one, what a reason is, and the conflict order's reflex.
# The criteria, outcomes and grounds themselves are product text on the # The criteria, outcomes and grounds themselves are product text on the
# tools that enforce them; the skill owns when and how. # tools that enforce them; the skill owns when and how.
@@ -298,7 +293,7 @@ TOPICS: tuple[Topic, ...] = (
index=("family-canon",)), index=("family-canon",)),
# ── per-tool contracts and in-band behaviour — owned by the server ── # ── 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?"), 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 # at different moments and say different things: using-scribe owns the
# authority — what you may decide and that deciding means READING, not # authority — what you may decide and that deciding means READING, not
# thresholding — while reporting-back owns the shape a report takes when # 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()) gaps = owner_gaps(TOPICS, delivered_surfaces())
assert not gaps, ( assert not gaps, (
"these topics are no longer stated in full by their owner:\n " "these topics are no longer stated in full by their owner:\n "
+ "\n ".join(gaps) + "\nEvery guidance topic has one owner (decision " + "\n ".join(gaps) + "\nEvery guidance topic has one owner. "
"#4027). If the text moved on purpose, move the topic's owner here in the " "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." "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()) found = copies(TOPICS, delivered_surfaces())
assert not found, ( assert not found, (
"guidance stated in full on a surface that doesn't own it:\n " "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 " "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." "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(): def test_the_ownership_guards_can_fail():
"""Rule 167: each guard is shown turning red once.""" """Each guard is shown turning red once."""
surfaces = { surfaces = {
"instructions": "use the widget", "instructions": "use the widget",
"docstrings": "the widget owner statement lives here", "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']"] 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. # skills folder is the part of every client package that is shared verbatim.
# Anything only one client understands belongs in that client's adapter — # Anything only one client understands belongs in that client's adapter —
# for Claude Code, the plugin's static context, hooks and commands — or the # 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} offenders = {path: hits for path, hits in offenders.items() if hits}
assert not offenders, ( assert not offenders, (
f"skills that name one client: {offenders}. Skills are shared by every " 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." f"and put the client's own name for it in that client's adapter."
) )
+24 -45
View File
@@ -1,29 +1,11 @@
"""What the instruction surfaces must say that one owner per topic cannot enforce. """What the instruction surfaces must say that one owner per topic cannot enforce.
WHY THIS FILE IS SMALLER THAN IT WAS 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
Until milestone 410 this file pinned REDUNDANCY: every session-start surface — index. How to ask for a rule and what an empty session means are the rules
the MCP `_INSTRUCTIONS`, the plugin's static context, the `using-scribe` skill — topic, owned by using-scribe and indexed in `_INSTRUCTIONS`; the Systems
had to restate how a rule reaches a session, what an empty session means, the reflex and the snippet-recording triggers are their topics' markers, required
Systems reflex and the snippet-recording triggers. That was the design #2494 on their owners.
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.
WHAT STAYS HERE 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. 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 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 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. own instructions.
""" """
found = [(str(p.relative_to(ROOT)), p.read_text()) 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 # 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 — # instructions and silently cuts the rest mid-word, so guidance past the cap
# the previous 20k-char version delivered ~10% of itself, and none of the # never reaches a session. The budget is set below the cap, not at it: the
# Systems tagging guidance ever reached a session). The budget is set below # field runs ~600–1,600 characters, and
# the cap, not at it: the field runs ~600–1,600 characters (spike #4389), and
# a block pressed against 2,048 is being used for depth that belongs in a # a block pressed against 2,048 is being used for depth that belongs in a
# skill. # skill.
INSTRUCTIONS_BUDGET = 1600 INSTRUCTIONS_BUDGET = 1600
@@ -81,18 +62,17 @@ def test_instructions_fit_the_fold():
text = _instructions_text() text = _instructions_text()
assert len(text) <= INSTRUCTIONS_BUDGET, ( assert len(text) <= INSTRUCTIONS_BUDGET, (
f"_INSTRUCTIONS is {len(text)} chars; Claude Code injects only ~2,048 " 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"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"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 give it at most a line here; see the comment "
f"above _INSTRUCTIONS." f"above _INSTRUCTIONS."
) )
# ── force: a surface that says rules bind must say what does not ──────── # ── force: a surface that says rules bind must say what does not ────────
# #
# Added with the preference kind (milestone 399). Before it, "rules bind" was # With a preference kind beside rules, "rules bind" is half of the truth, and
# the whole truth and every surface said so flatly. It is now half of one, and # the missing half is the dangerous half to omit: a session reading
# the half that is missing is the dangerous half to omit: a session reading
# only "rules bind" and then receiving a preference has been told, by the most # only "rules bind" and then receiving a preference has been told, by the most
# authoritative surface it has, to treat it as binding. # 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 # PINNED ON THE CLAIM, NOT THE WORD "bind". A bare substring also matches
# `bind_repo`, `list_repo_bindings` and the DNS-rebinding comment in # `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 # 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 # raising a false alarm about the very thing it protects. These phrases are
# the ones that actually assert bindingness to a reader. # the ones that actually assert bindingness to a reader.
BINDING_CLAIMS = ( 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 # 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. # 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 # 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 # 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. # 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"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"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"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(): 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 failure that actually happens, when a kind is added to the product and
the index is left describing the corpus that came before it.""" the index is left describing the corpus that came before it."""
text = "rules bind; preferences guide and you keep them current." 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"] 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 # 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 # 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 # 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 # fires on its own in every client. The server's index carries no REPORT line:
# until #4389: reporting is a mid-work stance, not a session-start practice. The # reporting is a mid-work stance, not a session-start practice.
# static context was a second plugin-side copy until milestone 410.
REPORT_REFLEX = "report back in a shape the operator can read" REPORT_REFLEX = "report back in a shape the operator can read"
REPORT_SURFACES = ( 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(): def test_the_reporting_reflex_takes_placement_from_the_record():
"""The failure milestone 409 began from: a placement reconstructed from """A placement reconstructed from memory reads exactly like a real one
memory reads exactly like a real one when it is wrong.""" when it is wrong."""
for path in REPORT_SURFACES: for path in REPORT_SURFACES:
text = " ".join(path.read_text().split()) text = " ".join(path.read_text().split())
assert "`placement`" in text, f"{path.relative_to(ROOT)} no longer points at the placement block" assert "`placement`" in text, f"{path.relative_to(ROOT)} no longer points at the placement block"
+30 -31
View File
@@ -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 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 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 @pytest.mark.integration
async def test_a_bad_batch_applies_nothing(seeded): 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.""" half a batch silently happened."""
owner, pid, sid = seeded["owner"], seeded["pid"], seeded["snippet"] owner, pid, sid = seeded["owner"], seeded["pid"], seeded["snippet"]
with pytest.raises(ValueError) as err: 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 @pytest.mark.integration
async def test_rule_form_sweeps_unclassified_rows_only_and_applies_whole(seeded): 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.""" alone unless include_judged; the same gates as the row form."""
owner, other, pid, sid = seeded["owner"], seeded["other"], seeded["pid"], seeded["snippet"] owner, other, pid, sid = seeded["owner"], seeded["other"], seeded["pid"], seeded["snippet"]
await classify_shapes(owner, pid, [ 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 @pytest.mark.integration
async def test_sync_stamps_scoped_rows_and_unstamps_when_they_become_reachable(seeded): 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 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.""" if a later tree makes them ordinary. A judgment overrides the stamp."""
from scribe.services.coverage import ArchiveShape 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 @pytest.mark.integration
async def test_uses_edges_are_the_consumer_map(seeded): 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, uses edges (what it calls); the snippet's consumer map lists them,
list_shapes(uses=N) finds them, and a sweep can write them.""" list_shapes(uses=N) finds them, and a sweep can write them."""
from scribe.services import snippets as snippets_svc 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 @pytest.mark.integration
async def test_sync_refiles_rows_whose_snippet_was_purged(seeded): 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.""" rejoins the todo on the next sync instead of dangling target-less."""
owner, pid, sid = seeded["owner"], seeded["pid"], seeded["snippet"] owner, pid, sid = seeded["owner"], seeded["pid"], seeded["snippet"]
await classify_shapes(owner, pid, [ 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 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 @pytest.mark.integration
async def test_write_path_evidence_is_a_proposal_never_a_verdict(seeded): 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"), 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. 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 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 assert sid + 2000 not in pulls
# --- #2792: the mechanical proposer against real rows ------------------------- # --- the mechanical proposer against real rows -------------------------------
def _quiet_semantic(): 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)) summary = proposal_summary(await live_rows(pid))
assert summary["proposed"] == 1 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. # group (usually convention), even at size 2 vs 3.
order = [g["group"] for g in summary["derive_groups"]] order = [g["group"] for g in summary["derive_groups"]]
assert order[0].startswith("dup:") and order[1] == "name:css:card" 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 # 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 # 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, [ await classify_shapes(owner, pid, [
{"path": "b/z.css", "symbol": "card", "status": "exempt", "reason": "print sheet"}, {"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 @pytest.mark.integration
async def test_write_time_derive_names_the_family_or_the_canon_for_a_name(seeded): 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 """Against real rows — a name in a family → the family (other files,
files, count; for CSS a NAME family, never a body one — note 2917); a count; for CSS a NAME family, never a body one); a
name whose canonical row lives elsewhere → that canon; a judged row at name whose canonical row lives elsewhere → that canon; a judged row at
the path, the canon's own file, or an unknown name → silence.""" the path, the canon's own file, or an unknown name → silence."""
from scribe.services.shape_ledger import apply_derive_groups, write_time_derive 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 @pytest.mark.integration
async def test_derive_new_names_the_copy_that_joined_a_family_since_the_stamp(seeded): 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 copy. Against the stamp between them, derive_new counts ONLY the
newcomer — the drift since the last refresh, not the whole family.""" newcomer — the drift since the last refresh, not the whole family."""
from datetime import datetime, timezone 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 @pytest.mark.integration
async def test_transition_and_prefix_references_clear_the_unused_css_flag(seeded): 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 a transition `name=` and a concatenated name — travel from the real
extractor through resolution into the flag, so rules they reach stop extractor through resolution into the flag, so rules they reach stop
being reported as unused. Only a rule nothing can reach stays listed.""" 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 @pytest.mark.integration
async def test_consumer_map_syncs_edges_from_template_references(seeded): 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 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 template does not define, counts refreshed and stale edges removed on
the next sync, and a vanished row's edges gone with it.""" 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 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 @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 from scribe.services import snippets as snippets_svc
owner, pid = seeded["owner"], seeded["pid"] 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 # 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. # says nothing about these Vue bodies — the canon must be one of theirs.
canon = await snippets_svc.create_snippet( 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 @pytest.mark.integration
async def test_weak_stamps_elect_no_canon_and_their_flags_are_withdrawn(seeded): 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 """The poisoned-ledger case: a directory's "dominant canon" elected
directory's "dominant canon" was elected entirely by hook stamps written entirely by hook stamps written below the 0.80 floor would tell every new
at 0.68-0.72, before the 0.80 floor, so every new shape there was told it shape there it diverged from an unrelated snippet. The same fixture as the acceptance case
diverged from a YAML CI snippet. The same fixture as the acceptance case
above, with one difference — the four instances are weak hook stamps above, with one difference — the four instances are weak hook stamps
rather than an audit — and the prompt must fall silent: at refresh (the 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 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 @pytest.mark.integration
async def test_a_semantic_miss_does_not_silence_the_prompt(seeded): 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 THE FIXTURE IS THE ACCEPTANCE CASE ABOVE, DELIBERATELY. A hand-rolled
`confirmDanger` beside an async confirm canon is structurally identical to `confirmDanger` beside an async confirm canon is structurally identical to
a registry helper beside an async service canon, and the arm's miss was 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 meant to tell them apart. True instances of a canon rarely clear the arm's
clear the arm's floor either, so a miss fires on both — silencing on floor either, so a miss fires on both — silencing on it would silence the
it silenced #2793's own case. Here the arm has read the body, found case the readout exists for. Here the arm has read the body, found
nothing, and the prompt is still RAISED. nothing, and the prompt is still RAISED.
Asserted on the row, not a flag count: `since` sits a second early to 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 @pytest.mark.integration
async def test_the_capped_semantic_pass_reaches_the_new_shape_first(seeded): 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 """After a version bump, every row competes for a capped number of
semantic checks in row order and the six new shapes — the only ones the semantic checks; in row order the new shapes — the only ones the
divergence check acts on — were all left unread and flagged. With room for divergence check acts on — are left unread and flagged. With room for
ONE check, the new shape must be the one that gets it.""" ONE check, the new shape must be the one that gets it."""
from unittest.mock import AsyncMock, patch 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") == {} 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 @pytest.mark.integration
async def test_a_shape_written_this_turn_is_judged_before_the_sync_sees_it(seeded): 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 @pytest.mark.integration
async def test_a_whole_file_snippet_makes_its_file_row_canonical(seeded): 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 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 makes and a definition row never could. It still covers no definition
inside the file.""" inside the file."""
+39 -45
View File
@@ -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 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) 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", ("dedup-within-file", "def f():\n pass\ndef f():\n pass\n",
[("sym", "f")]), [("sym", "f")]),
# --- comment and string spans (#4222) ------------------------------------ # --- comment and string spans --------------------------------------------
# Prose is not code. A wrapped docstring line beginning "class AND the" # Prose is not code. A wrapped docstring line beginning "class AND the"
# announced a shape called `AND` to a live session; `with` and `nobody` # would otherwise announce a shape called `AND`.
# out of one module docstring in scripts/check_dangling_styles.py reached
# persisted, judged `code_shapes` rows.
("docstring-prose", ("docstring-prose",
'def real_one():\n """Its own text is about the\n' 'def real_one():\n """Its own text is about the\n'
' class AND the to_dict, and is a def bar():\n' ' class AND the to_dict, and is a def bar():\n'
@@ -81,18 +79,15 @@ EXTRACTION_VECTORS = [
("unterminated-docstring", ("unterminated-docstring",
'def before():\n """oops, never closed\n\ndef after():\n pass\n', 'def before():\n """oops, never closed\n\ndef after():\n pass\n',
[("sym", "before"), ("sym", "after")]), [("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. # happens to begin with a dotted token reads as a selector.
("css-comment-selector", ("css-comment-selector",
"/* A real base rule, not just descendants: the check reads a\n" "/* A real base rule, not just descendants: the check reads a\n"
" .ghost, class that only ever appears as an ancestor */\n.check { }\n", " .ghost, class that only ever appears as an ancestor */\n.check { }\n",
[("css", "check")]), [("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
# a comment holding a WHOLE rule defines nothing, and an unterminated # comment does not swallow the file. A phantom class out of a comment
# comment does not swallow the file. The phantom it was filed for — # would persist with a `used_by` count of zero and inflate unused_css.
# `.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.
("comment-containing-a-whole-rule", ("comment-containing-a-whole-rule",
"/* .ghost {\n color: red;\n } */\n.real { color: blue; }\n", "/* .ghost {\n color: red;\n } */\n.real { color: blue; }\n",
[("css", "real")]), [("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 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(): 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(): def test_class_references_reads_framework_transition_names():
"""A transition `name=` is a class reference: the framework applies """A transition `name=` is a class reference: the framework applies
`.toast-enter-active` and friends at runtime, so a stylesheet that `.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.""" Every spelling of the tag counts; a bound `:name` stays unknowable."""
refs = class_references( refs = class_references(
"a/T.vue", "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(): def test_class_references_reads_a_concatenated_name_as_a_prefix():
"""`status-${s}` cannot be resolved to one class, but its static head is """`status-${s}` cannot be resolved to one class, but its static head is
real information: it is emitted as the prefix reference `status-*` so 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.""" short to mean anything, or a bare separator, says nothing."""
vue = ( vue = (
'<div :class="`status-${task.status}`" />' '<div :class="`status-${task.status}`" />'
@@ -301,7 +296,7 @@ def test_scan_archive_returns_definitions_and_references_from_one_walk():
scan = scan_archive(_tarball(tree)) scan = scan_archive(_tarball(tree))
assert isinstance(scan, ArchiveScan) assert isinstance(scan, ArchiveScan)
assert [(d.path, d.kind, d.name) for d in scan.definitions] == TREE_SHAPES + [ 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"), ("web/Card.vue", "css", "card"),
] ]
# Only files whose markup names a class appear; the .py/.css files don't. # 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] 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(): 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(): 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 and defines nothing; only a `type` line with a declaration after the
name counts (TS alias, Go/Rust type).""" name counts (TS alias, Go/Rust type)."""
from scribe.services.coverage import extract_shapes 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(): 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 from scribe.mcp.server import build_mcp_server
tool = build_mcp_server()._tool_manager.get_tool("bind_repo") tool = build_mcp_server()._tool_manager.get_tool("bind_repo")
assert "ref" in tool.parameters.get("properties", {}) assert "ref" in tool.parameters.get("properties", {})
@@ -412,7 +407,7 @@ def _forge(tar_bytes: bytes):
def _selector(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.""" keyring holding the mocked Gitea adapter."""
from scribe.services.forge import ForgeSelector 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"] == [ assert coverage["largest_gaps"] == [
{"dir": "src", "unclassified": 2, "total": 3} {"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": []} 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 # snippet reference locations are mechanically stamped canonical WITH
# their snippet id, and the rest sit in the todo state. # their snippet id, and the rest sit in the todo state.
async with async_session() as s: async with async_session() as s:
@@ -516,7 +511,7 @@ async def test_coverage_measures_the_tree_exactly_and_caches(seeded):
@pytest.mark.integration @pytest.mark.integration
async def test_ledger_keeps_judgments_and_stamps_vanished_shapes(seeded): 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 recompute, and a shape that leaves the tree is stamped vanished — kept
for history, dropped from the readout.""" for history, dropped from the readout."""
from datetime import datetime, timezone from datetime import datetime, timezone
@@ -593,9 +588,8 @@ async def test_enter_project_surfaces_the_line_only_once_computed(seeded):
@pytest.mark.integration @pytest.mark.integration
async def test_explicit_refresh_names_its_failures(seeded): async def test_explicit_refresh_names_its_failures(seeded):
"""refresh_for_caller (#2802) raises fixable errors instead of silence: """refresh_for_caller raises fixable errors instead of silence: an agent
an agent mid-task must learn WHY nothing measured — 'None' is exactly the mid-task must learn WHY nothing measured, and 'None' would strand it."""
stranding the button-only path caused."""
from scribe.models import async_session from scribe.models import async_session
from scribe.services.coverage import refresh_for_caller from scribe.services.coverage import refresh_for_caller
@@ -617,8 +611,8 @@ async def test_explicit_refresh_names_its_failures(seeded):
@pytest.mark.integration @pytest.mark.integration
async def test_background_seed_is_quiet_without_a_forge(seeded): async def test_background_seed_is_quiet_without_a_forge(seeded):
"""refresh_if_stale (#2802) must exit silently for a forge-less owner — """refresh_if_stale must exit silently for a forge-less owner — the
rule #115's baseline — and treat a fresh cache as nothing-to-do.""" baseline install — and treat a fresh cache as nothing-to-do."""
from scribe.services.coverage import refresh_coverage, refresh_if_stale from scribe.services.coverage import refresh_coverage, refresh_if_stale
uid, pid = seeded["uid"], seeded["pid"] 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 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(): 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. # And the identity view is unchanged for the hook mirror.
from scribe.services.coverage import extract_shapes from scribe.services.coverage import extract_shapes
assert extract_shapes(text) == [(d.kind, d.name) for d in extract_definitions(text)] 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. # 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" 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)} 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 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 # One-line rules hash their own declarations — never the empty string,
# (first deploy grouped 68 unrelated one-liners as one copy) — and a # which would group every unrelated one-liner as one copy — and a
# SINGLE declaration is not a shape (#2903): it keeps its selector in the # SINGLE declaration is not a shape: it keeps its selector in the
# hash, so `.a { color: red }` groups only with another `.a`, never with # hash, so `.a { color: red }` groups only with another `.a`, never with
# `.b { color: red }`. Two declarations and up stay selector-agnostic. # `.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" 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 assert "; 90 unclassified (40 proposed, 2 derive groups), largest: src" in line
line = coverage_line({**base, "proposed": 0, "derive_groups": [{"group": "a"}]}) line = coverage_line({**base, "proposed": 0, "derive_groups": [{"group": "a"}]})
assert "(1 derive group)" in line 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). # join the standing block only when measured (None = no evidence).
line = coverage_line({**base, "unclassified": 0, "proposed": 0, "derive_groups": [ line = coverage_line({**base, "unclassified": 0, "proposed": 0, "derive_groups": [
{"group": "name:css:error-msg", "label": ".error-msg", "files": 6, {"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, {"group": "name:css:x", "label": ".x", "files": 2,
"consumers": {"count": 1, "paths": ["a.vue"]}}], "unused_css": None}) "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 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({ line = coverage_line({
**base, "proposed": 40, "top_canon": {"snippet_id": 2844, "count": 78}, **base, "proposed": 40, "top_canon": {"snippet_id": 2844, "count": 78},
"derive_groups": [{"group": "dup:abc", "label": "closed-msg (identical body)", "files": 3}], "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(): 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 still carry derive groups / proposals / divergence — the line names
them as `standing:` instead of hiding them behind the todo count, and them as `standing:` instead of hiding them behind the todo count, and
names the drift since the previous refresh first-copy-first.""" 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(): 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 <style scoped>; an unscoped <style> block and non-.vue files rule inside <style scoped>; an unscoped <style> block and non-.vue files
stay ordinary.""" stay ordinary."""
from scribe.services.coverage import extract_definitions, scoped_definitions from scribe.services.coverage import extract_definitions, scoped_definitions
@@ -816,10 +810,10 @@ def test_scoped_definitions_are_vue_script_setup_and_scoped_style_only():
def test_svelte_scopes_by_default_and_its_module_script_stays_importable(): def test_svelte_scopes_by_default_and_its_module_script_stays_importable():
"""#4608: Svelte scopes the other way round from Vue — every <style> is """Svelte scopes the other way round from Vue — every <style> is the
the component's own unless it opts out, and only <script module> / component's own unless it opts out, and only <script module> /
<script context="module"> can be imported elsewhere. Without this a <script context="module"> can be imported elsewhere. Without this a
Svelte project's every `.card` and `.title` sat in the human todo.""" Svelte project's every `.card` and `.title` sits in the human todo."""
from scribe.services.coverage import extract_definitions, scoped_definitions from scribe.services.coverage import extract_definitions, scoped_definitions
svelte = ( svelte = (
"<script module lang=\"ts\">\n" "<script module lang=\"ts\">\n"
@@ -847,8 +841,8 @@ def test_svelte_scopes_by_default_and_its_module_script_stays_importable():
def test_the_line_names_the_review_queue_and_the_tool_that_holds_it(): def test_the_line_names_the_review_queue_and_the_tool_that_holds_it():
"""#4608: `stamps_to_review` existed and two projects never called it, """`stamps_to_review` goes uncalled unless something on arrival says
because nothing on arrival said there was anything to review.""" there is anything to review."""
from scribe.services.coverage import coverage_line from scribe.services.coverage import coverage_line
base = {"accounted": 10, "total": 10, "counts": {"exempt": 10}, base = {"accounted": 10, "total": 10, "counts": {"exempt": 10},
"unclassified": 0, "computed_at": "2026-09-30T00:00:00"} "unclassified": 0, "computed_at": "2026-09-30T00:00:00"}
@@ -860,7 +854,7 @@ def test_the_line_names_the_review_queue_and_the_tool_that_holds_it():
@pytest.mark.integration @pytest.mark.integration
async def test_binding_ref_is_the_branch_the_ledger_follows(seeded): async def test_binding_ref_is_the_branch_the_ledger_follows(seeded):
"""#2873: a binding that names a ref is read at that ref (not the forge's """A binding that names a ref is read at that ref (not the forge's
default branch); "" clears it; None on a re-bind leaves it standing.""" default branch); "" clears it; None on a re-bind leaves it standing."""
from scribe.services.coverage import compute_coverage from scribe.services.coverage import compute_coverage
from scribe.services.repo_bindings import bindings_for_project, set_binding from scribe.services.repo_bindings import bindings_for_project, set_binding
@@ -894,14 +888,14 @@ async def test_binding_ref_is_the_branch_the_ledger_follows(seeded):
("web/button.css", ".btn {\n color: red;\n}\n", False), ("web/button.css", ".btn {\n color: red;\n}\n", False),
]) ])
def test_a_file_is_a_unit_when_it_renders_and_nothing_inside_carries_its_name(path, text, unit): def test_a_file_is_a_unit_when_it_renders_and_nothing_inside_carries_its_name(path, text, unit):
"""Milestone 439: one structural test, not a framework list.""" """One structural test, not a framework list."""
from scribe.services.coverage import class_references, extract_definitions, is_file_unit from scribe.services.coverage import class_references, extract_definitions, is_file_unit
assert is_file_unit(path, text, extract_definitions(text), class_references(path, text)) is unit assert is_file_unit(path, text, extract_definitions(text), class_references(path, text)) is unit
def test_the_line_says_whether_writers_judged_what_they_wrote(): def test_the_line_says_whether_writers_judged_what_they_wrote():
"""Milestone 439: the write-time figure — silent until the question has """The write-time figure — silent until the question has
been put, then turns checked, asked, and asks walked past.""" been put, then turns checked, asked, and asks walked past."""
from scribe.services.coverage import coverage_line from scribe.services.coverage import coverage_line
base = {"accounted": 10, "total": 10, "counts": {"exempt": 10}, base = {"accounted": 10, "total": 10, "counts": {"exempt": 10},
+24 -31
View File
@@ -1,4 +1,4 @@
"""`retrieval_telemetry` says what is wrong, rather than implying it (#3431). """`retrieval_telemetry` says what is wrong, rather than implying it.
WHY THIS IS TESTED AS ARITHMETIC AND NOT THROUGH THE DATABASE. The warnings WHY THIS IS TESTED AS ARITHMETIC AND NOT THROUGH THE DATABASE. The warnings
are a pure function of the blocks the readout already built — that is the are a pure function of the blocks the readout already built — that is the
@@ -8,12 +8,12 @@ the wrong shape. Every case below is a shape that once produced, or would
produce, a wrong reading: produce, a wrong reading:
* an arm that never declines, which is either a missing floor or a missing * an arm that never declines, which is either a missing floor or a missing
LOG (#3497 — the rule arms recorded only their hits, so their decline LOG (an arm that records only its hits has a structurally zero decline
count was structurally zero and the obvious warning would have sent a count, and the obvious warning would send a reader to move a threshold
reader to move a threshold that was never involved); that was never involved);
* a search that never declines, which is a search working correctly; * a search that never declines, which is a search working correctly;
* a quiet window, where firing every check would describe an empty database * a quiet window, where firing every check would describe an empty database
rather than a broken one (rule 115); rather than a broken one;
* a floor the band is sitting on, which is the case where tuning changes * a floor the band is sitting on, which is the case where tuning changes
volume while looking like it changes quality. volume while looking like it changes quality.
@@ -76,7 +76,7 @@ def test_the_warning_carries_the_numbers_that_produced_it() -> None:
def test_a_quiet_window_flags_nothing() -> None: def test_a_quiet_window_flags_nothing() -> None:
"""Three calls returning nothing is an afternoon, not a defect (rule 115).""" """Three calls returning nothing is an afternoon, not a defect."""
assert codes(warn({"auto_inject": src(calls=3, zero_result_calls=0)})) == set() assert codes(warn({"auto_inject": src(calls=3, zero_result_calls=0)})) == set()
@@ -98,7 +98,7 @@ def test_an_arm_that_does_decline_is_not_flagged() -> None:
def test_an_arm_not_known_to_log_unconditionally_is_exempt(monkeypatch) -> None: def test_an_arm_not_known_to_log_unconditionally_is_exempt(monkeypatch) -> None:
"""#3497: a structurally-zero decline count is a logging bug, not a floor. """A structurally-zero decline count is a logging bug, not a floor.
Flagging it would report a ranking problem and send the reader to a Flagging it would report a ranking problem and send the reader to a
threshold that was never involved. threshold that was never involved.
@@ -183,7 +183,7 @@ def test_records_shown_and_never_opened_are_reported_per_corpus() -> None:
def test_each_corpus_is_sent_only_to_a_tool_that_takes_it() -> None: def test_each_corpus_is_sent_only_to_a_tool_that_takes_it() -> None:
"""#4798: one remedy for both corpora sent the rules reader to """One remedy for both corpora would send the rules reader to
`menus_to_review`, which refuses every source but the notes menu. Tied to `menus_to_review`, which refuses every source but the notes menu. Tied to
`REVIEWABLE` itself, so it goes red in either direction: the rules text `REVIEWABLE` itself, so it goes red in either direction: the rules text
naming the tool while no rule arm is reviewable, or a rule arm becoming naming the tool while no rule arm is reviewable, or a rule arm becoming
@@ -231,7 +231,7 @@ def test_an_unregistered_source_gets_no_other_verdict() -> None:
def test_every_registered_source_stays_quiet_when_healthy() -> None: def test_every_registered_source_stays_quiet_when_healthy() -> None:
"""The negative control for the whole suite (rule 167). """The negative control for the whole suite.
A guard that cannot pass cleanly is not a guard — if a healthy window A guard that cannot pass cleanly is not a guard — if a healthy window
produced warnings, every assertion above would be meaningless. produced warnings, every assertion above would be meaningless.
@@ -256,7 +256,7 @@ def test_a_point_that_emitted_is_not_reported() -> None:
def test_a_deliberately_quiet_point_is_never_reported() -> None: def test_a_deliberately_quiet_point_is_never_reported() -> None:
"""A justified silence must not read as a gap (#2475).""" """A justified silence must not read as a gap."""
quiet = {p["source"] for p in _silent_surfaces({}, {}, {}, active=True)} quiet = {p["source"] for p in _silent_surfaces({}, {}, {}, active=True)}
for web_only in ("rest_search", "browse_search", "rest_note", "rest_rule"): for web_only in ("rest_search", "browse_search", "rest_note", "rest_rule"):
assert web_only not in quiet assert web_only not in quiet
@@ -264,7 +264,7 @@ def test_a_deliberately_quiet_point_is_never_reported() -> None:
def test_an_inactive_window_reports_no_silence_at_all() -> None: def test_an_inactive_window_reports_no_silence_at_all() -> None:
"""On a fresh install every point is silent; the list would be the """On a fresh install every point is silent; the list would be the
registry printed back (rule 115).""" registry printed back."""
assert _silent_surfaces({}, {}, {}, active=False) == [] assert _silent_surfaces({}, {}, {}, active=False) == []
@@ -292,14 +292,13 @@ def test_a_setting_falls_back_rather_than_raising(raw, fallback, want) -> None:
assert _num(raw, fallback) == want assert _num(raw, fallback) == want
# ── read_and_unacted / outcomes_never_recorded (#4213, milestone 419) ───── # ── read_and_unacted / outcomes_never_recorded ──────────────────────────
# #
# The pair exists because ZERO OUTCOMES IS AMBIGUOUS, and getting that wrong # The pair exists because ZERO OUTCOMES IS AMBIGUOUS: a window with no outcome
# would have been this milestone's own failure mode in miniature: a window # rows cannot tell "every rule was ignored" from "nothing reports outcomes
# with no outcome rows cannot tell "every rule was ignored" from "nothing # yet". Reporting the first when the truth is the second manufactures a
# reports outcomes yet". Reporting the first when the truth is the second # finding out of an unwired feature — a statistic that cannot vary, read as a
# manufactures a finding out of an unwired feature — #3311, where a statistic # fact about the corpus.
# that could not vary was read as a fact about the corpus.
def ru(**kw) -> dict: def ru(**kw) -> dict:
base = { base = {
@@ -358,7 +357,7 @@ def test_every_opened_rule_acted_on_reports_nothing() -> None:
def test_no_rules_opened_at_all_reports_neither() -> None: def test_no_rules_opened_at_all_reports_neither() -> None:
"""Silence is not a finding. A window where nothing was opened has nothing """Silence is not a finding. A window where nothing was opened has nothing
to say about outcomes, and saying it anyway would put a warning on every to say about outcomes, and saying it anyway would put a warning on every
fresh install (rule 115).""" fresh install."""
ws = warn({}, rule_usage=ru(distinct_rules_surfaced=12)) ws = warn({}, rule_usage=ru(distinct_rules_surfaced=12))
assert "read_and_unacted" not in codes(ws) assert "read_and_unacted" not in codes(ws)
assert "outcomes_never_recorded" not in codes(ws) assert "outcomes_never_recorded" not in codes(ws)
@@ -367,7 +366,7 @@ def test_no_rules_opened_at_all_reports_neither() -> None:
def test_an_absent_rule_usage_block_is_not_a_finding() -> None: def test_an_absent_rule_usage_block_is_not_a_finding() -> None:
"""A failed rule-usage read leaves the keys missing or zero. Neither may """A failed rule-usage read leaves the keys missing or zero. Neither may
become a warning, because a warning computed over rows that could not be become a warning, because a warning computed over rows that could not be
loaded describes the outage, not the corpus (#2663).""" loaded describes the outage, not the corpus."""
assert "read_and_unacted" not in codes(warn({}, rule_usage={})) assert "read_and_unacted" not in codes(warn({}, rule_usage={}))
assert "outcomes_never_recorded" not in codes(warn({}, rule_usage={})) assert "outcomes_never_recorded" not in codes(warn({}, rule_usage={}))
assert "outcomes_never_recorded" not in codes( assert "outcomes_never_recorded" not in codes(
@@ -375,7 +374,7 @@ def test_an_absent_rule_usage_block_is_not_a_finding() -> None:
) )
# ── floor_moved_mid_window (#4225) ──────────────────────────────────────── # ── floor_moved_mid_window ────────────────────────────────────────────────
# #
# WHY THE BAND CHECK IS SUSPENDED RATHER THAN SOFTENED. # WHY THE BAND CHECK IS SUSPENDED RATHER THAN SOFTENED.
# #
@@ -481,16 +480,10 @@ def test_a_quiet_arm_is_not_suspended_either_way() -> None:
# floor row, "no move recorded" and "no move" are different statements, and # floor row, "no move recorded" and "no move" are different statements, and
# the first was standing in for the second. # the first was standing in for the second.
# #
# It showed up the day this instance's ledger was seeded. Its earliest event # The case: an arm whose first floor row is dated inside the window. Without
# of any kind is 2026-09-17; `write_path_rule`'s only floor row is a release # this check the readout prints its band as a negative distance above its
# baseline written 2026-09-21; a 30-day window opens 2026-08-22. The readout # floor — the impossible number the section above exists to prevent — with
# printed that arm's band as "-0.0208 above its floor" — the impossible # the suspension silent, because there is nothing on record for it to notice.
# negative the section above exists to prevent — with the suspension silent,
# because there was nothing on record for it to notice.
#
# The distinction is not academic for #4261: every window for the next month
# opens before that date, and measuring a change against an instrument that
# reports a number it cannot support is the #4225 trap one level up.
GAP_FROM = "2026-09-21T11:58:00+00:00" GAP_FROM = "2026-09-21T11:58:00+00:00"