docs(6209): twelve more test files say which behaviour they pin
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 14s
CI & Build / TypeScript typecheck (push) Successful in 52s
CI & Build / integration (push) Successful in 1m12s
CI & Build / Python tests (push) Successful in 1m53s
CI & Build / Build & push image (push) Successful in 12s

shape ledger, MCP projects/search/rulebooks tools, backup, chunking,
plugin context, derived mirror, rule band, shape form gate, plugin
version mint and note usage tests drop issue, milestone and rule numbers,
dates and one-install figures from docstrings, comments and messages.
Three shape-form-gate test names that told the discovery story now name
the property: a registry module's helpers are not service functions, the
divergence gate silences only a family contradiction, and callable helpers
beside a callable canon still prompt. The rulebook tool-count comment that
logged each milestone's additions is replaced by the reason it is a count.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-21 21:24:50 -04:00
co-authored by Claude Opus 5.5
parent 91c96a8b8a
commit d41e72d26c
12 changed files with 223 additions and 265 deletions
+22 -23
View File
@@ -1,9 +1,9 @@
"""chunk_document — the document shape a record is embedded as (#280). """chunk_document — the document shape a record is embedded as.
The model window is 512 tokens and fastembed truncates silently, so before The model window is 512 tokens and fastembed truncates silently, so without
chunking, everything past ~400 words of a record was PERMANENTLY invisible to chunking everything past ~400 words of a record would be invisible to
semantic search. These tests pin the two halves of the fix's contract: short semantic search. These tests pin the two halves of the contract: short
records keep the exact historical shape (the corpus's sharpest vectors are records keep the single-document shape (the corpus's sharpest vectors are
untouched), and long records lose NOTHING — every line of the body lands in untouched), and long records lose NOTHING — every line of the body lands in
some chunk, each chunk inside the window budget, each carrying the title as some chunk, each chunk inside the window budget, each carrying the title as
its topical anchor. its topical anchor.
@@ -29,7 +29,7 @@ def _long_section(tag: str, paragraphs: int = 6, sentence: str = None) -> str:
def test_a_short_record_yields_exactly_the_historical_shape(): def test_a_short_record_yields_exactly_the_historical_shape():
"""Snippets and reference notes are the sharpest records in the corpus """Snippets and reference notes are the sharpest records in the corpus
(#2485) precisely because of this shape — chunking must not touch them.""" precisely because of this shape — chunking must not touch them."""
assert chunk_document("A title", "A short body") == [ assert chunk_document("A title", "A short body") == [
embedding_text("A title", "A short body") embedding_text("A title", "A short body")
] ]
@@ -54,8 +54,8 @@ def test_a_record_exactly_at_budget_stays_whole():
def test_every_line_of_a_long_body_lands_in_some_chunk(): def test_every_line_of_a_long_body_lands_in_some_chunk():
"""The point of #280. Before chunking, a 2,000-word dev-log's last three """The point of chunking: every part of a 2,000-word dev-log can
quarters could not influence retrieval at all. Nothing may be dropped.""" influence retrieval. Nothing may be dropped."""
sections = [ sections = [
f"## Topic {i}\n\n{_long_section(f'topic-{i}')}" for i in range(8) f"## Topic {i}\n\n{_long_section(f'topic-{i}')}" for i in range(8)
] ]
@@ -139,12 +139,11 @@ def test_a_monster_single_paragraph_is_hard_split_not_dropped():
assert total_words == 2000 assert total_words == 2000
# --- no chunk is a heading alone (#4784) ------------------------------------- # --- no chunk is a heading alone ---------------------------------------------
# #
# Found by judging live auto-inject menus: "## Work log — 2026-08-21", # A heading or a one-line preamble as a whole chunk ranks at the top of vague
# "## Findings worth carrying forward" and "Two dev→main PRs this session." # queries — text about nothing embeds close to everything. Each case below is
# were each a whole chunk, and took ranks 1–3 on vague queries — text about # one of those shapes.
# nothing embeds close to everything. Each case below is one of those shapes.
_HEADING = re.compile(r"^#{1,6}\s") _HEADING = re.compile(r"^#{1,6}\s")
@@ -220,7 +219,7 @@ def test_a_continuation_piece_repeats_the_heading_it_sits_under():
assert "## Child" in chunk assert "## Child" in chunk
# --- the read path: best chunk wins (#280 step 4) ---------------------------- # --- the read path: best chunk wins ------------------------------------------
async def test_search_collapses_chunk_rows_to_best_chunk_per_note(): async def test_search_collapses_chunk_rows_to_best_chunk_per_note():
@@ -233,7 +232,7 @@ async def test_search_collapses_chunk_rows_to_best_chunk_per_note():
note_a, note_b = MagicMock(id=1), MagicMock(id=2) note_a, note_b = MagicMock(id=1), MagicMock(id=2)
# Rows are (Note, distance, chunk_index, chunk_text) — the chunk columns # Rows are (Note, distance, chunk_index, chunk_text) — the chunk columns
# ride along so the collapse can report WHICH passage won (#4243). # ride along so the collapse can report WHICH passage won.
rows = [ rows = [
(note_a, 0.10, 3, "the passage that actually matched"), (note_a, 0.10, 3, "the passage that actually matched"),
(note_b, 0.20, 0, "b's best"), (note_b, 0.20, 0, "b's best"),
@@ -259,19 +258,19 @@ async def test_search_collapses_chunk_rows_to_best_chunk_per_note():
# And the winning chunk is reported, not merely used for scoring. Without # And the winning chunk is reported, not merely used for scoring. Without
# this a caller can only preview the head of the body — a span this query # this a caller can only preview the head of the body — a span this query
# has already ranked lower than the one that won (#4243). # has already ranked lower than the one that won.
assert report["best_chunk"][1] == { assert report["best_chunk"][1] == {
"index": 3, "text": "the passage that actually matched", "index": 3, "text": "the passage that actually matched",
} }
assert report["best_chunk"][2]["index"] == 0 assert report["best_chunk"][2]["index"] == 0
# --- the write path: one row per chunk (#280 step 3) ------------------------- # --- the write path: one row per chunk ---------------------------------------
def _session_ctx(log_rows=()): def _session_ctx(log_rows=()):
"""A session stand-in. `log_rows` answers the work-log read a task's """A session stand-in. `log_rows` answers the work-log read a task's
document now needs (#4251) — answered explicitly rather than left to document needs — answered explicitly rather than left to
autovivify, because `list(result.all())` on a bare MagicMock raises and is autovivify, because `list(result.all())` on a bare MagicMock raises and is
swallowed, which would quietly make every one of these a no-log test.""" swallowed, which would quietly make every one of these a no-log test."""
from unittest.mock import AsyncMock, MagicMock from unittest.mock import AsyncMock, MagicMock
@@ -318,9 +317,9 @@ async def test_upsert_stores_one_versioned_row_per_chunk():
async def test_a_tasks_work_logs_reach_the_rows_it_is_stored_as(): async def test_a_tasks_work_logs_reach_the_rows_it_is_stored_as():
"""The wiring half of #4251: the shaper is pure and tested next door, so """The wiring half: the shaper is pure and tested next door, so what is
what is pinned here is that the WRITER actually asks for the logs and pinned here is that the WRITER actually asks for the logs and embeds what
embeds what comes back. A log that is written, readable (#4241) and absent comes back. A log that is written, readable and absent
from the index is the same half-surface one layer down.""" from the index is the same half-surface one layer down."""
import datetime import datetime
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, patch
@@ -380,7 +379,7 @@ async def test_backfill_reembeds_notes_with_a_stale_chunker_version():
session, ctx = _session_ctx() session, ctx = _session_ctx()
# The opening scan, in order: ids already at the current version, every # The opening scan, in order: ids already at the current version, every
# note id, vectors older than their note, tasks logged since their vectors. # note id, vectors older than their note, tasks logged since their vectors.
# IDS ONLY — the text is re-read per note at embed time (#4262). # IDS ONLY — the text is re-read per note at embed time.
session.execute = AsyncMock(side_effect=[ session.execute = AsyncMock(side_effect=[
_ids(1), # note 1 is current _ids(1), # note 1 is current
_ids(1, 2), # the corpus _ids(1, 2), # the corpus
@@ -402,7 +401,7 @@ async def test_backfill_reembeds_notes_with_a_stale_chunker_version():
def test_a_row_is_current_only_in_the_live_models_space(): def test_a_row_is_current_only_in_the_live_models_space():
"""#4132: the column is a width, not an identity, so a same-width model """The column is a width, not an identity, so a same-width model
swap writes a second geometry with no error. The backfill's "current" swap writes a second geometry with no error. The backfill's "current"
predicate has to name BOTH halves of the stamp, on every embedding table.""" predicate has to name BOTH halves of the stamp, on every embedding table."""
from scribe.models.embedding import ( from scribe.models.embedding import (
+17 -17
View File
@@ -12,19 +12,19 @@ The failure is silent and the wrong way round, because every read PREFERS the
mirror — deliberately, since parsing markdown to answer what an index can mirror — deliberately, since parsing markdown to answer what an index can
answer is how a hot path rots. answer is how a hot path rots.
- A SNIPPET went on reporting its old repo/path/symbol to the location - A SNIPPET goes on reporting its old repo/path/symbol to the location
reverse lookup and to prior-art recall while displaying its new body: a reverse lookup and to prior-art recall while displaying its new body: a
record surfaced with full authority and wrong, which the drift-check record surfaced with full authority and wrong, which the drift-check
docstring calls worse than having no record at all (#3128). docstring calls worse than having no record at all.
- A LESSON is worse. Its mirror holds the TRIGGER, and the trigger is the - A LESSON is worse. Its mirror holds the TRIGGER, and the trigger is the
entire retrieval story — a stale one keeps the lesson firing for the entire retrieval story — a stale one keeps the lesson firing for the
situation it used to name while it displays the one it now names. situation it used to name while it displays the one it now names.
#3128's fix was correct and did not generalise: it tested one constant, so A fix that tests one constant does not generalise: the next kind arrives with
milestone 385's lesson arrived with the same design and none of the protection. the same design and none of the protection. The registry tests below assert
The registry tests below assert the PROPERTY — every kind with a derived mirror the PROPERTY — every kind with a derived mirror is registered — so a third
is registered — so a third kind fails here rather than shipping quiet (#3734). kind fails here rather than shipping quiet.
""" """
import inspect import inspect
@@ -114,30 +114,30 @@ async def test_a_title_change_reaches_the_mirror_too():
def test_every_kind_with_a_derived_mirror_is_registered(): def test_every_kind_with_a_derived_mirror_is_registered():
"""The load-bearing one. Before #3734 this was a single `if` naming """The load-bearing one. A single `if` naming one kind leaves the next
snippets, and the kind added next was simply not covered.""" kind uncovered."""
from scribe.services.lessons import LESSON_NOTE_TYPE from scribe.services.lessons import LESSON_NOTE_TYPE
from scribe.services.notes import _mirror_recomposers from scribe.services.notes import _mirror_recomposers
from scribe.services.snippets import SNIPPET_NOTE_TYPE from scribe.services.snippets import SNIPPET_NOTE_TYPE
table = _mirror_recomposers() table = _mirror_recomposers()
assert SNIPPET_NOTE_TYPE in table, "the #3128 fix was lost" assert SNIPPET_NOTE_TYPE in table, "snippets lost their mirror recomposer"
assert LESSON_NOTE_TYPE in table, ( assert LESSON_NOTE_TYPE in table, (
"a lesson's `data` holds its trigger and `lesson_trigger` prefers it, " "a lesson's `data` holds its trigger and `lesson_trigger` prefers it, "
"so a body edit through the generic door would leave the lesson being " "so a body edit through the generic door would leave the lesson being "
"retrieved for a situation it no longer names (#3734)" "retrieved for a situation it no longer names"
) )
def test_the_dispatch_is_a_lookup_not_a_named_kind(): def test_the_dispatch_is_a_lookup_not_a_named_kind():
"""Asserted on structure (rule 167). A lookup extends in one line in one """Asserted on structure. A lookup extends in one line in one
place; naming a kind inline is the shape that left lessons uncovered.""" place; naming a kind inline is the shape that left lessons uncovered."""
from scribe.services import notes as notes_module from scribe.services import notes as notes_module
src = inspect.getsource(notes_module.update_note) src = inspect.getsource(notes_module.update_note)
assert "_mirror_recomposers()" in src assert "_mirror_recomposers()" in src
assert "SNIPPET_NOTE_TYPE" not in src, ( assert "SNIPPET_NOTE_TYPE" not in src, (
"update_note names one kind again — that is the shape #3734 replaced" "update_note names one kind inline — the dispatch must be a lookup"
) )
@@ -184,7 +184,7 @@ async def test_a_body_write_moves_a_lessons_trigger_with_it():
async def test_a_subject_containing_an_em_dash_still_splits(): async def test_a_subject_containing_an_em_dash_still_splits():
"""Why `untrigger_title` is given the trigger instead of splitting on the """Why `untrigger_title` is given the trigger instead of splitting on the
separator: a subject may legitimately contain one. The title here is an separator: a subject may legitimately contain one. The title here is an
UN-MIGRATED one, still carrying its trigger (milestone 427), because that UN-MIGRATED one, still carrying its trigger, because that
is the row the inverse still has to read correctly.""" is the row the inverse still has to read correctly."""
from scribe.services.embeddings import trigger_title from scribe.services.embeddings import trigger_title
from scribe.services.lessons import TRIGGER_KEY from scribe.services.lessons import TRIGGER_KEY
@@ -259,8 +259,8 @@ async def test_a_lesson_title_change_reaches_the_mirror():
], ],
) )
def test_untrigger_title_inverts_trigger_title(subject, trigger): def test_untrigger_title_inverts_trigger_title(subject, trigger):
"""#3207's shape: the join had three copies before it was consolidated, so """The join's inverse lives beside it rather than in whichever caller
its inverse lives beside it rather than in whichever caller wanted it.""" wanted it."""
from scribe.services.embeddings import trigger_title, untrigger_title from scribe.services.embeddings import trigger_title, untrigger_title
title = trigger_title(subject, trigger) title = trigger_title(subject, trigger)
@@ -289,12 +289,12 @@ def test_the_trigger_separator_is_spelled_in_exactly_one_place():
] ]
assert not offenders, ( assert not offenders, (
f"the trigger separator is spelled inline in {offenders} — use " f"the trigger separator is spelled inline in {offenders} — use "
f"TRIGGER_SEP, trigger_title or untrigger_title (#3207)" f"TRIGGER_SEP, trigger_title or untrigger_title"
) )
def test_the_mirror_guards_can_fail(): def test_the_mirror_guards_can_fail():
"""Rule 167: shown turning red once.""" """Shown turning red once."""
from scribe.services.lessons import TRIGGER_KEY, recompose_data from scribe.services.lessons import TRIGGER_KEY, recompose_data
bare = fake_lesson(title="just a title", body="no composed lines", data=None) bare = fake_lesson(title="just a title", body="no composed lines", data=None)
+16 -18
View File
@@ -17,7 +17,7 @@ pytestmark = pytest.mark.usefixtures("_bind_user")
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def _no_systems(): def _no_systems():
"""enter_project now surfaces the project's Systems as the tagging """enter_project now surfaces the project's Systems as the tagging
vocabulary (#2546). These are tool-layer unit tests with no database, so vocabulary. These are tool-layer unit tests with no database, so
the lookup is stubbed to the common case — a project with none. The the lookup is stubbed to the common case — a project with none. The
populated shape is asserted in its own test below. populated shape is asserted in its own test below.
""" """
@@ -40,7 +40,7 @@ def _no_platforms():
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def _no_coverage(): def _no_coverage():
"""enter_project also reads the pattern-coverage cache (#2692) — same """enter_project also reads the pattern-coverage cache — same
deal: no database here, stub the common case (nothing computed). The deal: no database here, stub the common case (nothing computed). The
populated line is asserted in tests/test_pattern_coverage.py. populated line is asserted in tests/test_pattern_coverage.py.
""" """
@@ -51,7 +51,7 @@ def _no_coverage():
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def _no_family(): def _no_family():
"""enter_project reads the family-canon readout (milestone 463 step 6) — """enter_project reads the family-canon readout —
no database here, so the common case is stubbed: nothing to answer. The no database here, so the common case is stubbed: nothing to answer. The
populated key is asserted in its own test below.""" populated key is asserted in its own test below."""
with patch("scribe.mcp.tools.projects.family_adoption_svc.family_readout", with patch("scribe.mcp.tools.projects.family_adoption_svc.family_readout",
@@ -61,7 +61,7 @@ def _no_family():
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def _no_background_seed(): def _no_background_seed():
"""enter_project now fire-and-forgets a coverage self-seed (#2802). These """enter_project fire-and-forgets a coverage self-seed. These
are no-database unit tests, so the spawn is stubbed out; the firing shape are no-database unit tests, so the spawn is stubbed out; the firing shape
has its own test below.""" has its own test below."""
with patch("scribe.mcp.tools.projects.spawn") as mock: with patch("scribe.mcp.tools.projects.spawn") as mock:
@@ -71,7 +71,7 @@ def _no_background_seed():
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def _no_bootstrap(): def _no_bootstrap():
"""With _no_systems stubbing an empty vocabulary, every test here reaches """With _no_systems stubbing an empty vocabulary, every test here reaches
the zero-Systems branch, whose bootstrap ask (#2683) counts the project's the zero-Systems branch, whose bootstrap ask counts the project's
records — a database read. Stub the common case (young project, no ask); records — a database read. Stub the common case (young project, no ask);
the firing shape has its own test below. the firing shape has its own test below.
""" """
@@ -117,7 +117,7 @@ async def test_get_project_enriches_with_milestone_summary():
async def test_get_project_includes_applicable_rules_and_project_rules(): async def test_get_project_includes_applicable_rules_and_project_rules():
"""The augmented get_project response includes applicable_rules and """The augmented get_project response includes applicable_rules and
project_rules pulled from services/rulebooks.get_applicable_rules, and project_rules pulled from services/rulebooks.get_applicable_rules, and
nothing about subscriptions (milestone 414). nothing about subscriptions.
""" """
p = fake_project(id=3, title="Fabled Assistant") p = fake_project(id=3, title="Fabled Assistant")
milestone_summary = [] milestone_summary = []
@@ -191,7 +191,7 @@ async def test_update_project_raises_when_not_found():
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_enter_project_composes_full_context(): async def test_enter_project_composes_full_context():
"""enter_project pulls project + rule titles + milestone summary + open """enter_project pulls project + rule titles + milestone summary + open
tasks in one composed call, each in its brief handshake form (#4045).""" tasks in one composed call, each in its brief handshake form."""
p = fake_project(id=5, title="P") p = fake_project(id=5, title="P")
applicable_payload = { applicable_payload = {
"rules": [{"id": 1, "title": "r1", "statement": "s", "rules": [{"id": 1, "title": "r1", "statement": "s",
@@ -226,7 +226,7 @@ async def test_enter_project_composes_full_context():
assert out["project"] == {"id": 5, "title": "P", "status": "active", "goal": ""} assert out["project"] == {"id": 5, "title": "P", "status": "active", "goal": ""}
assert out["milestone_summary"] == milestone_summary assert out["milestone_summary"] == milestone_summary
# Rules arrive in full by retrieval; the handshake lists the project's own # Rules arrive in full by retrieval; the handshake lists the project's own
# by id and title (#4045), and nothing about subscriptions (milestone 414). # by id and title, and nothing about subscriptions.
assert out["project_rules"] == [{"id": 99, "title": "pr1"}] assert out["project_rules"] == [{"id": 99, "title": "pr1"}]
for gone in ("applicable_rules", "applicable_rules_truncated", "subscribed_rulebooks", for gone in ("applicable_rules", "applicable_rules_truncated", "subscribed_rulebooks",
"suppressed_rules", "suppressed_topics", "recent_notes"): "suppressed_rules", "suppressed_topics", "recent_notes"):
@@ -244,16 +244,14 @@ async def test_enter_project_composes_full_context():
# create-the-System instruction acts on. # create-the-System instruction acts on.
assert out["systems"] == [] assert out["systems"] == []
# Young project, no bootstrap ask -> the key is ABSENT, not null: it exists # Young project, no bootstrap ask -> the key is ABSENT, not null: it exists
# to be acted on, and a key that usually says null gets skipped (#2483). # to be acted on, and a key that usually says null gets skipped.
assert "systems_bootstrap" not in out assert "systems_bootstrap" not in out
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_enter_project_surfaces_the_systems_vocabulary(): async def test_enter_project_surfaces_the_systems_vocabulary():
"""The tagging instruction is only executable if the vocabulary is in """The tagging instruction is only executable if the vocabulary is in
front of the agent when it writes. It never was, and tagging stopped three front of the agent when it writes. Trimmed to id/name/first-line: it rides on every session
days after the feature landed — one System, nothing tagged since July 28
(#2546's audit). Trimmed to id/name/first-line: it rides on every session
start, and the full charter is get_system's job.""" start, and the full charter is get_system's job."""
p = fake_project(id=5) p = fake_project(id=5)
sys1 = MagicMock() sys1 = MagicMock()
@@ -298,7 +296,7 @@ def _enter_project_stubs(p):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_enter_project_carries_the_bootstrap_ask_when_it_fires(): async def test_enter_project_carries_the_bootstrap_ask_when_it_fires():
"""The arrival-moment half of #2683: a mature zero-Systems project greets """The arrival-moment half of the bootstrap ask: a mature zero-Systems project greets
the session with the concrete bootstrap ask, before it is deep in a task — the session with the concrete bootstrap ask, before it is deep in a task —
the moment minting a starter vocabulary is cheapest.""" the moment minting a starter vocabulary is cheapest."""
import contextlib import contextlib
@@ -341,7 +339,7 @@ async def test_enter_project_never_asks_bootstrap_once_a_vocabulary_exists(
async def test_enter_project_fires_the_coverage_seed_on_the_owner( async def test_enter_project_fires_the_coverage_seed_on_the_owner(
_no_background_seed, _no_background_seed,
): ):
"""The arrival self-seed (#2802): entering spawns refresh_if_stale on the """The arrival self-seed: entering spawns refresh_if_stale on the
project OWNER's id with the cache read the enter already did — fire and project OWNER's id with the cache read the enter already did — fire and
forget, so a missing ledger seeds itself without the UI button, and the forget, so a missing ledger seeds itself without the UI button, and the
enter stays fast.""" enter stays fast."""
@@ -393,7 +391,7 @@ async def test_enter_project_hands_back_the_design_system_when_the_project_has_o
assert out["design_system"]["token_count"] == 95 assert out["design_system"]["token_count"] == 95
assert out["design_system"]["inherits_from"] == ["House"] assert out["design_system"]["inherits_from"] == ["House"]
# Summary only: the guidance is one call away (#4045). # Summary only: the guidance is one call away.
assert "guidance" not in out["design_system"] assert "guidance" not in out["design_system"]
assert out["design_system"]["guidance_call"] == "get_design_system(9) → resolved_guidance" assert out["design_system"]["guidance_call"] == "get_design_system(9) → resolved_guidance"
assert ctx.await_args.args == (7, 9) # caller's id, the project's system assert ctx.await_args.args == (7, 9) # caller's id, the project's system
@@ -418,7 +416,7 @@ def test_enter_project_registered_in_register():
assert "enter_project" in mcp.names assert "enter_project" in mcp.names
# --- milestone 297: the inception doors --------------------------------------- # --- the inception doors -----------------------------------------------------
@pytest.mark.asyncio @pytest.mark.asyncio
@@ -491,7 +489,7 @@ def test_inception_routes_and_tool_are_registered():
tool = mcp._tool_manager.get_tool("create_project") tool = mcp._tool_manager.get_tool("create_project")
for name in ("design_system_id", "seed_systems"): for name in ("design_system_id", "seed_systems"):
assert name in tool.parameters.get("properties", {}), name assert name in tool.parameters.get("properties", {}), name
# Rules left inception with subscriptions (milestone 414). # Inception carries no rulebook subscriptions.
assert "subscribe_rulebooks" not in tool.parameters.get("properties", {}) assert "subscribe_rulebooks" not in tool.parameters.get("properties", {})
@@ -500,7 +498,7 @@ def test_inception_routes_and_tool_are_registered():
async def test_enter_project_carries_the_family_line_only_when_it_asks_something(_no_family): async def test_enter_project_carries_the_family_line_only_when_it_asks_something(_no_family):
"""The `family` key is attached when the project has family canon to """The `family` key is attached when the project has family canon to
answer, and absent otherwise — a key that usually says null trains answer, and absent otherwise — a key that usually says null trains
readers to skip it (#2483).""" readers to skip it."""
line = {"line": "family canon on this project's platforms: 2 unassessed", line = {"line": "family canon on this project's platforms: 2 unassessed",
"unassessed": 2, "owed": 0, "needs_recheck": 0, "unassessed": 2, "owed": 0, "needs_recheck": 0,
"calls": {"unassessed": 'list_family_adoptions(project_id=5, status="unassessed")'}} "calls": {"unassessed": 'list_family_adoptions(project_id=5, status="unassessed")'}}
+10 -21
View File
@@ -49,8 +49,7 @@ async def test_get_rulebook_raises_when_not_found():
await get_rulebook(rulebook_id=999) await get_rulebook(rulebook_id=999)
# _plain_detail moved to tests/helpers on its second copy (#2825's own # _plain_detail lives in tests/helpers: two stubs for one seam drift apart quietly, and a
# reason for existing): two stubs for one seam drift apart quietly, and a
# test stubbing it slightly differently asserts something slightly different # test stubbing it slightly differently asserts something slightly different
# than it appears to. # than it appears to.
@@ -199,16 +198,7 @@ def test_register_attaches_every_tool():
mcp = FakeMCP() mcp = FakeMCP()
register(mcp) register(mcp)
# 26 through milestone 307, +2 for the staleness sweep (milestone 312), # Pinned as a count so a tool added or dropped is a deliberate edit here.
# +1 for a rule's edit history (milestone 323), +2 for preferences
# (milestone 399).
# 28 since milestone 394 took list_always_on_rules and the two
# always-on exclusion tools with the tier they served.
# 22 since milestone 414 retired subscriptions and suppressions: the two
# subscribe tools and the four suppress/unsuppress tools. 23 with move_rule
# (milestone 414 step 3), the way a rule changes home. 24 with
# rule_outcome (milestone 419 step 1), the way a rule says what it
# actually changed.
assert len(mcp.names) == 24 assert len(mcp.names) == 24
# spot-check a few names # spot-check a few names
assert "list_rulebooks" in mcp.names assert "list_rulebooks" in mcp.names
@@ -221,12 +211,12 @@ def test_register_attaches_every_tool():
assert "update_preference" in mcp.names assert "update_preference" in mcp.names
assert "create_project_rule" in mcp.names assert "create_project_rule" in mcp.names
assert "move_rule" in mcp.names assert "move_rule" in mcp.names
# milestone 312: the sweep, and the stamp that answers it # the sweep, and the stamp that answers it
assert "rules_due_for_verification" in mcp.names assert "rules_due_for_verification" in mcp.names
assert "mark_rule_verified" in mcp.names assert "mark_rule_verified" in mcp.names
# milestone 323: what a rule used to say # what a rule used to say
assert "rule_history" in mcp.names assert "rule_history" in mcp.names
# milestone 414: a rule's home is its scope; nothing subscribes or mutes. # a rule's home is its scope; nothing subscribes or mutes.
for gone in ("subscribe_project_to_rulebook", "unsubscribe_project_from_rulebook", for gone in ("subscribe_project_to_rulebook", "unsubscribe_project_from_rulebook",
"suppress_rule_for_project", "unsuppress_rule_for_project", "suppress_rule_for_project", "unsuppress_rule_for_project",
"suppress_topic_for_project", "unsuppress_topic_for_project"): "suppress_topic_for_project", "unsuppress_topic_for_project"):
@@ -292,7 +282,7 @@ async def test_create_project_rule_uses_explicit_title_when_given():
assert kwargs["title"] == "no auto-docstrings" assert kwargs["title"] == "no auto-docstrings"
# ── Typed edges between rules (milestone 307) ─────────────────────────── # ── Typed edges between rules ───────────────────────────────────────────
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_relate_rules_forwards_the_kind_and_the_why(): async def test_relate_rules_forwards_the_kind_and_the_why():
@@ -336,7 +326,7 @@ async def test_unrelate_rules_raises_when_the_edge_is_gone():
await unrelate_rules(relation_id=99) await unrelate_rules(relation_id=99)
# ── rule_history (milestone 323 step 3) ──────────────────────────────── # ── rule_history ───────────────────────────────────────────────────────
def _fake_version(**over): def _fake_version(**over):
"""A RuleVersion-shaped stand-in. A real model instance rather than a """A RuleVersion-shaped stand-in. A real model instance rather than a
@@ -397,9 +387,8 @@ async def test_rule_history_opens_one_version_in_full():
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_rule_history_says_an_empty_history_is_ordinary(): async def test_rule_history_says_an_empty_history_is_ordinary():
"""Most rules have never been reworded, and nothing was written before """Most rules have never been reworded. Without this line an empty list
milestone 323. Without this line an empty list reads as a lost history or reads as a lost history or a broken tool."""
a broken tool."""
with patch( with patch(
"scribe.mcp.tools.rulebooks.rulebooks_svc.list_rule_versions", "scribe.mcp.tools.rulebooks.rulebooks_svc.list_rule_versions",
AsyncMock(return_value=[]), AsyncMock(return_value=[]),
@@ -482,7 +471,7 @@ async def test_move_rule_on_someone_elses_rule_is_not_found():
await move_rule(rule_id=94, project_id=3) await move_rule(rule_id=94, project_id=3)
# ── rule_outcome: what a rule actually changed (#4212, milestone 419) ───── # ── rule_outcome: what a rule actually changed ───────────────────────────
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_rule_outcome_records_an_application(): async def test_rule_outcome_records_an_application():
+16 -18
View File
@@ -52,7 +52,7 @@ async def test_fable_search_returns_repackaged_results():
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_search_shows_the_passage_that_matched_not_the_opening(): async def test_search_shows_the_passage_that_matched_not_the_opening():
"""The point of #4243. A record can rank on its sixth paragraph; showing """A record can rank on its sixth paragraph; showing
its first is showing the caller a span the search already judged less its first is showing the caller a span the search already judged less
relevant, and letting them decide from it.""" relevant, and letting them decide from it."""
_user_id_ctx.set(7) _user_id_ctx.set(7)
@@ -131,7 +131,7 @@ async def test_fable_search_content_type_filters_at_service_layer():
specific kinds arrived — it is BROAD here (any non-task, snippets and specific kinds arrived — it is BROAD here (any non-task, snippets and
lessons included) while the web facet of the same name is narrow. Pinning lessons included) while the web facet of the same name is narrow. Pinning
it stops a later tidy-up from silently removing snippets from every caller it stops a later tidy-up from silently removing snippets from every caller
that already asks this way (#4250).""" that already asks this way."""
_user_id_ctx.set(7) _user_id_ctx.set(7)
mock_search = AsyncMock(return_value=[]) mock_search = AsyncMock(return_value=[])
with patch("scribe.mcp.tools.search.semantic_search_notes", mock_search): with patch("scribe.mcp.tools.search.semantic_search_notes", mock_search):
@@ -149,7 +149,7 @@ async def test_fable_search_content_type_filters_at_service_layer():
assert mock_search.call_args.kwargs["is_task"] is None assert mock_search.call_args.kwargs["is_task"] is None
# --- the specific kinds: the engine already supported them (#4250) ----------- # --- the specific kinds the engine supports ----------------------------------
@pytest.mark.asyncio @pytest.mark.asyncio
@@ -181,9 +181,8 @@ async def test_each_specific_kind_reaches_the_engine_filter_it_names(
def test_the_vocabulary_is_derived_from_the_facet_table_not_recopied(): def test_the_vocabulary_is_derived_from_the_facet_table_not_recopied():
"""#3161's property, held at this door too: adding a kind to `_FACETS` is """Adding a kind to `_FACETS` is one edit, at this door too. A hand-kept
one edit. A hand-kept list here is exactly how the agent's search came to list here lets the agent's search offer fewer kinds than the web's."""
offer two kinds while the web's offered nine."""
from scribe.services.knowledge import FACET_TYPES, content_type_filters from scribe.services.knowledge import FACET_TYPES, content_type_filters
for facet in FACET_TYPES: for facet in FACET_TYPES:
@@ -191,7 +190,7 @@ def test_the_vocabulary_is_derived_from_the_facet_table_not_recopied():
assert "is_task" in filters, facet assert "is_task" in filters, facet
# The guard can fail: a name absent from the table is refused, so this is # The guard can fail: a name absent from the table is refused, so this is
# membership in the table and not "every string works" (#167). # membership in the table and not "every string works".
with pytest.raises(ValueError): with pytest.raises(ValueError):
content_type_filters("a-kind-that-is-not-in-the-facet-table") content_type_filters("a-kind-that-is-not-in-the-facet-table")
@@ -215,7 +214,7 @@ async def test_an_unknown_content_type_is_refused_rather_than_returning_nothing(
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_the_docstring_names_every_kind_the_tool_accepts(): async def test_the_docstring_names_every_kind_the_tool_accepts():
"""The docstring IS the agent-facing contract (#2846) — a filter an agent """The docstring IS the agent-facing contract — a filter an agent
has not been told about is unreachable however well it is wired.""" has not been told about is unreachable however well it is wired."""
from scribe.mcp.tools.search import search as search_tool from scribe.mcp.tools.search import search as search_tool
from scribe.services.knowledge import FACET_TYPES from scribe.services.knowledge import FACET_TYPES
@@ -229,12 +228,12 @@ async def test_the_docstring_names_every_kind_the_tool_accepts():
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_an_explicit_search_reaches_a_lesson_from_any_project(): async def test_an_explicit_search_reaches_a_lesson_from_any_project():
"""The wiring half of milestone 385 step 3. """An explicit search reaches lessons in every project.
A lesson records an insight that transfers, so a project filter that hid it A lesson records an insight that transfers, so a project filter that hid it
would hide it precisely on the project that has not learned it yet. This is would hide it precisely on the project that has not learned it yet. This is
the EXPLICIT search — the operator asked — so it opts in; the unasked-for the EXPLICIT search — the operator asked — so it opts in; the unasked-for
injection arms decide their own budget separately (step 5). injection arms decide their own budget separately.
Asserted on the kwarg rather than on results, because what can regress here Asserted on the kwarg rather than on results, because what can regress here
is the wiring: the service grew the capability and a call site that never is the wiring: the service grew the capability and a call site that never
@@ -269,7 +268,7 @@ async def test_fable_search_limit_is_clamped():
(0, {"everywhere": True}), (0, {"everywhere": True}),
]) ])
async def test_rule_search_scopes_to_the_project_it_is_given(project_id, scope): async def test_rule_search_scopes_to_the_project_it_is_given(project_id, scope):
"""With a project: global rules plus that project's (milestone 414). """With a project: global rules plus that project's.
Without one: every rule, because an unscoped "is there a rule about this" Without one: every rule, because an unscoped "is there a rule about this"
is asking the whole rulebook — unlike a hook, which speaks unasked.""" is asking the whole rulebook — unlike a hook, which speaks unasked."""
_user_id_ctx.set(7) _user_id_ctx.set(7)
@@ -293,14 +292,13 @@ def test_a_milestone_is_embedded_by_what_it_is_for_then_its_plan():
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_milestone_search_is_its_own_shape_and_scopes_to_the_project(): async def test_milestone_search_is_its_own_shape_and_scopes_to_the_project():
"""milestone 415: 'is there already a plan for this?' has a tool.""" """'Is there already a plan for this?' has a tool."""
from unittest.mock import MagicMock from unittest.mock import MagicMock
_user_id_ctx.set(7) _user_id_ctx.set(7)
# `body` is a REAL string, not left to MagicMock's attribute autovivication: # `body` is a REAL string, not left to MagicMock's attribute autovivication:
# the result now reads it, and a mock body would make `matched` a mock # the result now reads it, and a mock body would make `matched` a mock
# object and `body_length` zero while the assertion still looked green # object and `body_length` zero while the assertion still looked green.
# (lesson #2833).
ms = MagicMock(id=339, title="M3 — Metadata", description="works, editions", ms = MagicMock(id=339, title="M3 — Metadata", description="works, editions",
status="active", project_id=30, status="active", project_id=30,
body="Step 4 — the metadata editions table.") body="Step 4 — the metadata editions table.")
@@ -323,7 +321,7 @@ async def test_milestone_search_is_its_own_shape_and_scopes_to_the_project():
assert out["results"] == [{ assert out["results"] == [{
"id": 339, "title": "M3 — Metadata", "description": "works, editions", "id": 339, "title": "M3 — Metadata", "description": "works, editions",
# The plan body stays out; the passage that matched comes along, and # The plan body stays out; the passage that matched comes along, and
# says which of the two it is (#4243). # says which of the two it is.
"matched": "Step 4 — the metadata editions table.", "matched": "Step 4 — the metadata editions table.",
"matched_is": "matched_passage", "matched_is": "matched_passage",
"body_length": len("Step 4 — the metadata editions table."), "body_length": len("Step 4 — the metadata editions table."),
@@ -332,7 +330,7 @@ async def test_milestone_search_is_its_own_shape_and_scopes_to_the_project():
}] }]
# --- systems: the charter as an answer, not a filter (#4251) ----------------- # --- systems: the charter as an answer, not a filter -------------------------
@pytest.mark.asyncio @pytest.mark.asyncio
@@ -340,7 +338,7 @@ async def test_system_search_is_its_own_shape_and_carries_the_matched_passage():
"""A System's charter runs to several hundred words and a result shows its """A System's charter runs to several hundred words and a result shows its
NAME — so a match on the paragraph that actually decides where a record NAME — so a match on the paragraph that actually decides where a record
belongs would be previewed by two words that cannot. The passage comes belongs would be previewed by two words that cannot. The passage comes
along, marked as the passage (#4243).""" along, marked as the passage."""
_user_id_ctx.set(7) _user_id_ctx.set(7)
charter = ( charter = (
"Opening sentence about the area in general. " "Opening sentence about the area in general. "
@@ -349,7 +347,7 @@ async def test_system_search_is_its_own_shape_and_carries_the_matched_passage():
) )
fake = MagicMock(id=3, project_id=2, description=charter) fake = MagicMock(id=3, project_id=2, description=charter)
# `name` is MagicMock's own constructor kwarg — passed in, it names the # `name` is MagicMock's own constructor kwarg — passed in, it names the
# mock and leaves `.name` a mock object, which is note #2833's whole point. # mock and leaves `.name` a mock object.
fake.name = "Retrieval & recall" fake.name = "Retrieval & recall"
async def _system_search(uid, q, **kwargs): async def _system_search(uid, q, **kwargs):
+19 -23
View File
@@ -1,4 +1,4 @@
"""Tests for the usage signal (#2085) — was a surfaced record ever pulled? """Tests for the usage signal — was a surfaced record ever pulled?
Covers the payload shaping and the two contracts that make this safe to leave in Covers the payload shaping and the two contracts that make this safe to leave in
the hot path: telemetry never raises, and telemetry never blocks. Plus the thing the hot path: telemetry never raises, and telemetry never blocks. Plus the thing
@@ -90,8 +90,8 @@ async def test_usage_for_notes_splits_counts_by_event():
from datetime import datetime, timezone from datetime import datetime, timezone
ts = datetime(2026, 7, 28, tzinfo=timezone.utc) ts = datetime(2026, 7, 28, tzinfo=timezone.utc)
# Rows are (note_id, event, count, last_at, ambient) since #2477 split the # Rows are (note_id, event, count, last_at, ambient): ranked and ambient
# readout. Ranked and ambient surfacings arrive as separate groups. # surfacings arrive as separate groups.
rows = [ rows = [
(3, "surfaced", 9, ts, False), (3, "surfaced", 9, ts, False),
(3, "surfaced", 40, ts, True), (3, "surfaced", 40, ts, True),
@@ -111,7 +111,7 @@ async def test_usage_for_notes_splits_counts_by_event():
out = await usage_for_notes([3]) out = await usage_for_notes([3])
# The dead-weight reading ("surfaced often, never pulled") is only valid # The dead-weight reading ("surfaced often, never pulled") is only valid
# over surfacings that were CHOICES. 40 enter_project appearances must not # over surfacings that were CHOICES. 40 enter_project appearances must not
# make a record look popular — they sit in ambient_count (#2477). # make a record look popular — they sit in ambient_count.
assert out[3]["surfaced_count"] == 9 assert out[3]["surfaced_count"] == 9
assert out[3]["ambient_count"] == 40 assert out[3]["ambient_count"] == 40
assert out[3]["pull_count"] == 2 assert out[3]["pull_count"] == 2
@@ -120,7 +120,7 @@ async def test_usage_for_notes_splits_counts_by_event():
async def test_usage_for_notes_counts_what_happened_away_from_home(): async def test_usage_for_notes_counts_what_happened_away_from_home():
"""Surfaced or opened on a project other than the record's own is the """Surfaced or opened on a project other than the record's own is the
evidence a record transferred (#3735). It is a SUBSET of the totals, read evidence a record transferred. It is a SUBSET of the totals, read
from the second aggregate, and it must land on the right counter.""" from the second aggregate, and it must land on the right counter."""
from datetime import datetime, timezone from datetime import datetime, timezone
@@ -167,7 +167,7 @@ async def test_no_ids_short_circuits_without_a_query():
@pytest.mark.parametrize( @pytest.mark.parametrize(
"lookups, expected_source", "lookups, expected_source",
[ [
# A hit AT the exact file is the sync class (#2708); a hit from the # A hit AT the exact file is the sync class; a hit from the
# directory query is the reuse-shaped place arm. Both are un-scored, # directory query is the reuse-shaped place arm. Both are un-scored,
# and both must leave a usage trace under their own name. # and both must leave a usage trace under their own name.
([(1, "hit"), (2, "empty")], "write_path_sync"), ([(1, "hit"), (2, "empty")], "write_path_sync"),
@@ -177,9 +177,8 @@ async def test_no_ids_short_circuits_without_a_query():
) )
async def test_unscored_location_arms_are_recorded(lookups, expected_source): async def test_unscored_location_arms_are_recorded(lookups, expected_source):
"""The location arms carry no score, so they have no home in retrieval_logs """The location arms carry no score, so they have no home in retrieval_logs
— they surfaced snippets while leaving no trace anywhere. That was the — without a usage row they would surface snippets while leaving no trace
blocker #2082 recorded against this task; this is the assertion that it's anywhere. Asserted per class."""
closed, per class."""
from scribe.services import plugin_context from scribe.services import plugin_context
here = [{"id": 42, "title": "helper", "user_id": 1, "note_type": "snippet"}] here = [{"id": 42, "title": "helper", "user_id": 1, "note_type": "snippet"}]
@@ -237,15 +236,13 @@ async def test_auto_inject_records_what_survived_the_margin_gate():
def test_every_getter_that_can_be_surfaced_also_records_a_pull(): def test_every_getter_that_can_be_surfaced_also_records_a_pull():
"""Rule #33 contract check — and the one that would have caught #2245. """A contract check between the two halves of the usage pair.
`surfaced` and `pulled` only mean something as a PAIR: the rate between them `surfaced` and `pulled` only mean something as a PAIR: the rate between them
is what #1038 and #2085 gate on. That pair is only closed if the tool which is what pull-through is read from. That pair is only closed if every tool
OPENS a record reports it. `get_note` did, `get_snippet` did, `get_task` did which OPENS a record reports it — and auto-inject ranks kind-blind over a
NOT — and auto-inject ranks kind-blind over a corpus that is overwhelmingly corpus that is mostly tasks and issues, so one getter that does not report
tasks and issues, so the gap sat exactly where the volume is: every surfaced counts the menu's dominant kind as never-pulled.
task counted as never-pulled, dragging measured pull-through toward zero for
the menu's own dominant kind.
Asserted by source inspection rather than by calling the tools, because the Asserted by source inspection rather than by calling the tools, because the
failure is a MISSING call — which no behavioural test of the tool's return failure is a MISSING call — which no behavioural test of the tool's return
@@ -272,12 +269,11 @@ def test_every_getter_that_can_be_surfaced_also_records_a_pull():
# --- persistence (integration) -------------------------------------------- # --- persistence (integration) --------------------------------------------
# Everything above mocks _schedule or the session — deliberately, for the hot # Everything above mocks _schedule or the session — deliberately, for the hot
# path. But that left the two functions that actually touch the database # path. Without these, the two functions that actually touch the database
# (_insert_events and usage_for_notes' real SQL) running against real Postgres # (_insert_events and usage_for_notes' real SQL) would run against real
# nowhere, which is how the deployed instance reported zero for every counter # Postgres nowhere — all-green mocked units over a dead real path. These two
# while surfacing demonstrably fired (#2663): all-green mocked units over a # run in the CI integration lane and split the chain so a failure names its
# dead real path, the #2109 shape. These two run in the CI integration lane # half.
# and split the chain so a failure names its half.
async def _purge(note_id: int) -> None: async def _purge(note_id: int) -> None:
@@ -309,7 +305,7 @@ async def test_insert_and_readout_roundtrip_on_real_postgres(_dispose_engine):
"source": "mcp_get_snippet"}, "source": "mcp_get_snippet"},
]) ])
out = await usage_for_notes([nid]) out = await usage_for_notes([nid])
# write_path_place is a ranked choice; enter_project is ambient (#2477). # write_path_place is a ranked choice; enter_project is ambient.
assert out[nid]["surfaced_count"] == 1 assert out[nid]["surfaced_count"] == 1
assert out[nid]["ambient_count"] == 1 assert out[nid]["ambient_count"] == 1
assert out[nid]["pull_count"] == 1 assert out[nid]["pull_count"] == 1
+13 -18
View File
@@ -1,6 +1,6 @@
"""The plugin's version is MINTED, and CI is the control that it moved. """The plugin's version is MINTED, and CI is the control that it moved.
WHAT THIS IS ABOUT (milestone 334 step 3). `plugin/` ships straight from this WHAT THIS IS ABOUT. `plugin/` ships straight from this
git repo — no build step, so no moment at which CI could stamp a version in. git repo — no build step, so no moment at which CI could stamp a version in.
The value is therefore minted by a script before the commit, and CI's job is The value is therefore minted by a script before the commit, and CI's job is
not to produce it but to prove it moved when it had to. not to produce it but to prove it moved when it had to.
@@ -11,15 +11,15 @@ from different clocks, on purpose:
server image name from COMMIT time, ordering key from BUILD time server image name from COMMIT time, ordering key from BUILD time
plugin one value, from MINT time plugin one value, from MINT time
#3127 §2 prescribes commit time so two lanes building one source report one The server uses commit time so two lanes building one source report one
string. The plugin has one lane and no build, so that reason does not reach string. The plugin has one lane and no build, so that reason does not reach
it. "Let's make these consistent" is the obvious tidy-up and it breaks it. "Let's make these consistent" is the obvious tidy-up and it breaks
whichever artifact loses — which is why the difference is pinned here rather whichever artifact loses — which is why the difference is pinned here rather
than only explained in a comment. than only explained in a comment.
The trade mint time makes — you cannot recompute the value from history, only The trade mint time makes — you cannot recompute the value from history, only
verify it moved — is acceptable ONLY because #3325 established that the verify it moved — is acceptable ONLY because the installer's refresh test
installer's refresh test is `===` with no ordering anywhere. Where a is `===` with no ordering anywhere. Where a
comparator orders, an unreproducible version would be unverifiable too. comparator orders, an unreproducible version would be unverifiable too.
""" """
import ast import ast
@@ -57,7 +57,7 @@ def fake_manifest(version: str = "2026.09.01.2252") -> str:
@pytest.mark.parametrize("when,expected", [ @pytest.mark.parametrize("when,expected", [
# THE midnight case, which #3127 checklist 10 names by hand. An unpadded # THE midnight case. An unpadded
# `%-H%M` renders this hour as `0` and silently shortens the string. # `%-H%M` renders this hour as `0` and silently shortens the string.
(datetime(2026, 1, 5, 0, 0, tzinfo=timezone.utc), "2026.01.05.0000"), (datetime(2026, 1, 5, 0, 0, tzinfo=timezone.utc), "2026.01.05.0000"),
(datetime(2026, 1, 5, 0, 9, tzinfo=timezone.utc), "2026.01.05.0009"), (datetime(2026, 1, 5, 0, 9, tzinfo=timezone.utc), "2026.01.05.0009"),
@@ -150,7 +150,7 @@ def test_the_rewrite_refuses_TWO_version_lines():
def test_the_mint_script_is_version_relevant(): def test_the_mint_script_is_version_relevant():
"""#3127 §3's asymmetry. A change to how the version is COMPUTED is """The deriver's asymmetry. A change to how the version is COMPUTED is
compared against nothing — leave the deriver out of the set and a format compared against nothing — leave the deriver out of the set and a format
change never forces a re-mint, so the manifest keeps a value in the old change never forces a re-mint, so the manifest keeps a value in the old
format indefinitely and nothing says so.""" format indefinitely and nothing says so."""
@@ -189,7 +189,7 @@ def run_check(here: str, there: str | None, changed_paths: list[str], monkeypatc
def test_content_changed_and_the_version_did_not_FAILS(monkeypatch): def test_content_changed_and_the_version_did_not_FAILS(monkeypatch):
"""#2209, exactly. The headline, and the only reason the check exists.""" """The headline, and the only reason the check exists."""
failures = run_check( failures = run_check(
"2026.09.01.2252", "2026.09.01.2252", "2026.09.01.2252", "2026.09.01.2252",
["plugin/hooks/scribe_session_context.sh"], monkeypatch, ["plugin/hooks/scribe_session_context.sh"], monkeypatch,
@@ -209,7 +209,7 @@ def test_content_changed_and_the_version_moved_PASSES(monkeypatch):
def test_a_version_that_is_not_the_canonical_shape_FAILS(monkeypatch): def test_a_version_that_is_not_the_canonical_shape_FAILS(monkeypatch):
"""`K4` returns the manifest string verbatim, so a malformed value is not """`K4` returns the manifest string verbatim, so a malformed value is not
rejected by the installer — it either sorts as an ordinary string or, when rejected by the installer — it either sorts as an ordinary string or, when
unreadable, forces a reinstall every session. Neither is loud (#3325).""" unreadable, forces a reinstall every session. Neither is loud."""
failures = run_check("0.1.48", "0.1.47", [], monkeypatch) failures = run_check("0.1.48", "0.1.47", [], monkeypatch)
assert len(failures) == 1 assert len(failures) == 1
assert "not YYYY.MM.DD.HHMM" in failures[0] assert "not YYYY.MM.DD.HHMM" in failures[0]
@@ -218,7 +218,7 @@ def test_a_version_that_is_not_the_canonical_shape_FAILS(monkeypatch):
@pytest.mark.parametrize("bad", ["2026.9.1.2252", "2026.09.01.252", "2026.09.01"]) @pytest.mark.parametrize("bad", ["2026.9.1.2252", "2026.09.01.252", "2026.09.01"])
def test_an_UNPADDED_or_short_version_FAILS(bad, monkeypatch): def test_an_UNPADDED_or_short_version_FAILS(bad, monkeypatch):
"""The padding is the contract, not cosmetics — one shape for every version """The padding is the contract, not cosmetics — one shape for every version
in the family (#3127 checklist 10).""" in the family."""
assert run_check(bad, "2026.08.30.1200", [], monkeypatch) != [] assert run_check(bad, "2026.08.30.1200", [], monkeypatch) != []
@@ -250,7 +250,7 @@ def test_a_version_that_moved_with_no_content_change_is_NOT_a_failure(monkeypatc
def test_a_failed_diff_FAILS_rather_than_passing_quietly(monkeypatch): def test_a_failed_diff_FAILS_rather_than_passing_quietly(monkeypatch):
"""A check that cannot run must not report the same thing as a check that """A check that cannot run must not report the same thing as a check that
passed — #2663's lesson, and the reason this file's siblings exist. passed.
`rev-parse` is stubbed to SUCCEED so only the diff fails. Failing every git `rev-parse` is stubbed to SUCCEED so only the diff fails. Failing every git
call would trip the base-branch guard first and this would pass while call would trip the base-branch guard first and this would pass while
@@ -280,13 +280,8 @@ def test_the_shipped_manifest_carries_a_minted_version():
def test_the_session_context_hook_still_reads_the_version_field(): def test_the_session_context_hook_still_reads_the_version_field():
"""The marker #2220 asked for. The value's SHAPE changed, not the field or """The hook reads `.version` out of the manifest rather than deriving the
its reader — if this had to move, the derivation went somewhere it should string some other way — if this has to move, the derivation went
not have. somewhere it should not have."""
The READER changed once, in #4107: `jq -r '.version'` became
`scribe_json_pick ... '.version'` when the hooks stopped depending on jq.
What this pins is unchanged — that the hook still reads `.version` out of
the manifest rather than deriving the string some other way."""
hook = (check_plugin.HOOKS_DIR / "scribe_session_context.sh").read_text() hook = (check_plugin.HOOKS_DIR / "scribe_session_context.sh").read_text()
assert re.search(r"scribe_json_pick[^\n]*'\.version'", hook) assert re.search(r"scribe_json_pick[^\n]*'\.version'", hook)
+19 -24
View File
@@ -1,22 +1,20 @@
"""An act surfaces a SET of rules, and rank decides how loudly (#3851). """An act surfaces a SET of rules, and rank decides how loudly.
WHY THIS EXISTS WHY THIS EXISTS
`RULEHINT_LIMIT` was 1. That was right while retrieval merely SUPPLEMENTED a This arm is the whole delivery of rules to an act, and one command can be
33-rule resident set — one salient rule beside everything already loaded. It governed by several rules at once — a `git push` by where to push, what not
stops being right the moment milestone 394 removes residency, because then to push, who polls CI and what to wait for — each of which alone permits the
this arm is the whole delivery, and `git push origin dev` is governed by mistake the others catch. One slot cannot serve that.
rules 1, 2, 9 and 140 at once, each of which alone permits the mistake the
others catch.
Two instruments, and they answer different questions: Two instruments, and they answer different questions:
- `_rule_band` decides HOW MANY. A fixed k fills its slots whether or not - `_rule_band` decides HOW MANY. A fixed k fills its slots whether or not
anything deserves them; a band keeps only what scored close to the top, anything deserves them; a band keeps only what scored close to the top,
so one clearly-relevant rule still shows one. so one clearly-relevant rule still shows one.
- `compact` decides HOW LOUD. Measured at #3851: a full line is ~143 tokens - `compact` decides HOW LOUD. A full line is ~143 tokens once the trigger
once the trigger is rendered, so five of them cost ~646 before every Bash is rendered, so five of them cost ~646 before every Bash call.
call. Top-full-plus-references costs ~198. Top-full-plus-references costs ~198.
WHAT THIS PINS WHAT THIS PINS
@@ -96,7 +94,7 @@ def test_a_flat_cluster_surfaces_together():
def test_the_band_is_computed_from_scores_not_from_the_ledger(): def test_the_band_is_computed_from_scores_not_from_the_ledger():
"""A seen top hit still anchors the band (#3750 x #3851). """A seen top hit still anchors the band.
`_rule_band` never learns what the session has seen — dedup happens after `_rule_band` never learns what the session has seen — dedup happens after
it, in the arms. Pinned here because the tempting "reorder so a fresh rule it, in the arms. Pinned here because the tempting "reorder so a fresh rule
@@ -124,7 +122,7 @@ def test_the_leading_line_carries_the_trigger(seen):
def test_a_later_line_cites_its_rule_without_quoting_the_trigger(seen): def test_a_later_line_cites_its_rule_without_quoting_the_trigger(seen):
"""Rank > 0 is a reference: identity and pointer, no trigger. """Rank > 0 is a reference: identity and pointer, no trigger.
The trigger is the expensive half — 300-400 characters after #3855 — and The trigger is the expensive half — often 300-400 characters — and
the leading line has already demonstrated the shape. Both assertions the leading line has already demonstrated the shape. Both assertions
matter: dropping the trigger is the saving, and keeping `get_rule(id)` is matter: dropping the trigger is the saving, and keeping `get_rule(id)` is
what makes the saving safe, because a cited rule the reader cannot pull is what makes the saving safe, because a cited rule the reader cannot pull is
@@ -140,12 +138,11 @@ def test_a_later_line_cites_its_rule_without_quoting_the_trigger(seen):
def test_shortening_a_line_does_not_decide_what_it_says_about_holding(): def test_shortening_a_line_does_not_decide_what_it_says_about_holding():
"""A reference still tells a repeat from a first surfacing (#3750 x #3851). """A reference still tells a repeat from a first surfacing.
This is the regression the first cut of #3851 actually shipped: the A compact branch that drops the tail along with the trigger makes a rule
compact branch dropped the tail along with the trigger, so a rule the the session had already been told read exactly like one it had not. The
session had already been told read exactly like one it had not. #3750's two are different claims — a repeat is rendered
whole argument is that the two are different claims — a repeat is rendered
precisely because the session may no longer HOLD what it was told — and precisely because the session may no longer HOLD what it was told — and
the tail is the entire difference a reader can act on. the tail is the entire difference a reader can act on.
@@ -153,10 +150,8 @@ def test_shortening_a_line_does_not_decide_what_it_says_about_holding():
a fact about its rank; what the session holds is a fact about the ledger; a fact about its rank; what the session holds is a fact about the ledger;
and neither may be allowed to answer the other's question. and neither may be allowed to answer the other's question.
THREE STATES SINCE #4100, so this checks three. The axis grew and the test THREE STATES, so this checks three. Pinning only two would leave the
grew with it — pinning only two would leave the compact branch free to compact branch free to collapse the middle state into either neighbour.
collapse the new middle state into either neighbour, which is the same
regression this was written for with one more place to hide.
""" """
rule = fake_rule(id=4, title="dev is home", when_to_apply=_TRIGGER) rule = fake_rule(id=4, title="dev is home", when_to_apply=_TRIGGER)
fresh = _rule_hint_line(rule, where="here", seen=False, compact=True) fresh = _rule_hint_line(rule, where="here", seen=False, compact=True)
@@ -182,8 +177,8 @@ def test_a_compact_line_is_materially_shorter_than_a_full_one():
if a later edit puts the trigger back into the compact branch. if a later edit puts the trigger back into the compact branch.
Measured against a REALISTIC trigger, because that is where the saving Measured against a REALISTIC trigger, because that is where the saving
lives: the rules this arm carries run 300-400 characters of trigger after lives: the rules this arm carries run 300-400 characters of trigger, and
#3855, and a toy one-line trigger would make this pass on a compact branch a toy one-line trigger would make this pass on a compact branch
that had stopped saving anything. that had stopped saving anything.
""" """
long_trigger = ( long_trigger = (
@@ -200,7 +195,7 @@ def test_a_compact_line_is_materially_shorter_than_a_full_one():
def test_a_preference_keeps_its_noun_when_compact(): def test_a_preference_keeps_its_noun_when_compact():
"""Force survives the shortening (#3849). """Force survives the shortening.
`kind` and `compact` are independent axes. A preference rendered as a `kind` and `compact` are independent axes. A preference rendered as a
reference must still not read as a rule — the noun is the whole of the reference must still not read as a rule — the noun is the whole of the
+17 -22
View File
@@ -51,7 +51,7 @@ def _exportable_note(**over):
def test_note_rows_carry_the_verification_trio(): def test_note_rows_carry_the_verification_trio():
"""Operator judgment — somebody checked this fact, and this is when — """Operator judgment — somebody checked this fact, and this is when —
which nothing downstream can recompute (milestone 317).""" which nothing downstream can recompute."""
[row] = backup._note_rows([_exportable_note( [row] = backup._note_rows([_exportable_note(
verify_with="curl the AMO docs", verify_with="curl the AMO docs",
expires_when="AMO starts allowing re-signing", expires_when="AMO starts allowing re-signing",
@@ -75,10 +75,10 @@ def test_a_never_checked_note_exports_a_null_stamp_and_restores_as_one():
def test_the_record_type_and_kind_survive_the_export(): def test_the_record_type_and_kind_survive_the_export():
"""#3182's headline. Without these two columns a restore reported success """Without these two columns a restore reports success and hands back a
and handed back a corpus where all 90 snippets and 3 processes were plain corpus where every snippet and process is a plain note and every issue
notes and all 435 issues and the spike were `work` — the entire vocabulary and spike is `work` — the record vocabulary gone, with nothing to notice
milestone 312 and #3128 were about, gone, with nothing to notice it by.""" it by."""
[snippet] = backup._note_rows([_exportable_note(note_type="snippet")]) [snippet] = backup._note_rows([_exportable_note(note_type="snippet")])
[issue] = backup._note_rows([_exportable_note(task_kind="issue", status="done")]) [issue] = backup._note_rows([_exportable_note(task_kind="issue", status="done")])
assert snippet["note_type"] == "snippet" assert snippet["note_type"] == "snippet"
@@ -105,7 +105,7 @@ def test_provenance_and_lifecycle_travel():
def test_a_milestone_carries_its_plan(): def test_a_milestone_carries_its_plan():
"""A milestone IS the plan (0066); `body` is its design and intent and """A milestone IS the plan (0066); `body` is its design and intent and
`description` is only the one-line summary. Dropping it restored every `description` is only the one-line summary. Dropping it restored every
plan as a title with no reasoning behind it (#3182).""" plan as a title with no reasoning behind it."""
m = SimpleNamespace( m = SimpleNamespace(
id=1, user_id=7, project_id=2, title="t", description="d", id=1, user_id=7, project_id=2, title="t", description="d",
body="## Goal\n\nthe actual plan", status="active", order_index=0, body="## Goal\n\nthe actual plan", status="active", order_index=0,
@@ -117,7 +117,7 @@ def test_a_milestone_carries_its_plan():
def test_a_repo_binding_carries_the_branch_its_ledger_follows(): def test_a_repo_binding_carries_the_branch_its_ledger_follows():
"""#2873. Without `ref` a restored binding silently falls back to the """Without `ref` a restored binding silently falls back to the
default branch and the shape ledger starts accounting for a different default branch and the shape ledger starts accounting for a different
tree — a wrong answer that looks like a working one.""" tree — a wrong answer that looks like a working one."""
b = SimpleNamespace(user_id=7, project_id=2, repo_key="Scribe", ref="dev") b = SimpleNamespace(user_id=7, project_id=2, repo_key="Scribe", ref="dev")
@@ -265,7 +265,7 @@ def _stand_in(model):
@pytest.mark.parametrize("table", sorted(_column_guard_targets())) @pytest.mark.parametrize("table", sorted(_column_guard_targets()))
def test_every_column_is_exported_or_declared_excluded(table): def test_every_column_is_exported_or_declared_excluded(table):
"""THE COLUMN GUARD (#3182) — _NOT_INCLUDED's shape, one level down. """THE COLUMN GUARD — _NOT_INCLUDED's shape, one level down.
The table guard below catches a whole table going missing. It cannot catch The table guard below catches a whole table going missing. It cannot catch
a COLUMN going missing from a table it already considers covered, which is a COLUMN going missing from a table it already considers covered, which is
@@ -379,7 +379,7 @@ def _everything_maps(row: dict) -> "backup._Maps":
@pytest.mark.parametrize("table", sorted(_import_guard_targets())) @pytest.mark.parametrize("table", sorted(_import_guard_targets()))
def test_every_exported_column_is_imported_or_declared_excluded(table): def test_every_exported_column_is_imported_or_declared_excluded(table):
"""THE COLUMN GUARD, THE OTHER WAY (#4197). """THE COLUMN GUARD, THE OTHER WAY.
The export guard above makes a dropped column unexpressible on the way The export guard above makes a dropped column unexpressible on the way
OUT. Nothing watched the way back IN, and that is the worse half: an OUT. Nothing watched the way back IN, and that is the worse half: an
@@ -435,9 +435,6 @@ def test_an_unmappable_project_degrades_a_usage_event_and_skips_a_shape_event():
the project is optional by design, null already means "not reported", and the project is optional by design, null already means "not reported", and
dropping the row would lose a real pull and deflate the very pull-through dropping the row would lose a real pull and deflate the very pull-through
the table exists to report. the table exists to report.
Until #4197 this was asserted by reading the source of `_restore_v2` with
`inspect.getsource`, because there was no unit to call. Now there is.
""" """
maps = backup._Maps() maps = backup._Maps()
maps.notes[5] = 55 maps.notes[5] = 55
@@ -467,8 +464,7 @@ def test_a_shapes_divergence_pointer_is_remapped_not_carried():
"""`diverges_from` is a FK to notes.id, like `snippet_id`. Carrying the """`diverges_from` is a FK to notes.id, like `snippet_id`. Carrying the
source id would point at whatever snippet took that number in the source id would point at whatever snippet took that number in the
destination — wrong rather than missing, and nothing downstream could destination — wrong rather than missing, and nothing downstream could
tell. It was dropped entirely until #4197; restoring it raw would have tell. Restoring it raw would be worse than dropping it."""
been the worse fix."""
maps = backup._Maps() maps = backup._Maps()
maps.projects[1] = 11 maps.projects[1] = 11
maps.notes[4] = 44 maps.notes[4] = 44
@@ -507,14 +503,14 @@ def test_the_column_guard_covers_every_table_with_a_row_helper():
def test_not_included_lists_the_known_gaps(): def test_not_included_lists_the_known_gaps():
# The deferred tables must be surfaced explicitly, not silently dropped. # The deferred tables must be surfaced explicitly, not silently dropped.
# forge_connections is excluded as CREDENTIALS (api_keys reasoning): a # forge_connections is excluded as CREDENTIALS (api_keys reasoning): a
# backup that carries forge tokens is a token-exfiltration file (#2778). # backup that carries forge tokens is a token-exfiltration file.
for table in ("groups", "project_shares", "note_shares", "api_keys", for table in ("groups", "project_shares", "note_shares", "api_keys",
"note_embeddings", "retrieval_logs", "forge_connections"): "note_embeddings", "retrieval_logs", "forge_connections"):
assert table in backup._NOT_INCLUDED assert table in backup._NOT_INCLUDED
def test_every_table_is_either_backed_up_or_explicitly_excluded(): def test_every_table_is_either_backed_up_or_explicitly_excluded():
"""THE GUARD (#2293), and the only shape of test that catches an ABSENCE. """THE GUARD, and the only shape of test that catches an ABSENCE.
A new table gets a model and a migration — both fail loudly if wrong — and A new table gets a model and a migration — both fail loudly if wrong — and
then silently never gets a backup section. No error, no warning, and a then silently never gets a backup section. No error, no warning, and a
@@ -599,11 +595,11 @@ async def test_export_full_backup_contains_every_declared_section():
"system_usage_events", "system_usage_events",
# v23: proposals and judgments about a rule's moments. # v23: proposals and judgments about a rule's moments.
"rule_moment_judgments", "rule_moment_judgments",
# v25: family canon (milestone 463). # v25: family canon.
"platforms", "project_platforms", "family_ideas", "platforms", "project_platforms", "family_ideas",
"family_idea_platforms", "family_idea_references", "family_idea_platforms", "family_idea_references",
"family_adoptions", "family_decisions", "family_adoptions", "family_decisions",
# v26: the Profile page (#5879). # v26: the Profile page.
"user_profiles"): "user_profiles"):
assert key in out, f"missing export section: {key}" assert key in out, f"missing export section: {key}"
assert out[key] == [] assert out[key] == []
@@ -625,7 +621,7 @@ def test_supersession_rows_serialise_the_pair():
def test_rule_rows_carry_the_verification_fields(): def test_rule_rows_carry_the_verification_fields():
"""A rule's check must survive a backup. """A rule's check must survive a backup.
`verify_with`/`expires_when`/`verified_at` (milestone 312) say whether a `verify_with`/`expires_when`/`verified_at` say whether a
rule is a fact that can go false and when it was last confirmed. A backup rule is a fact that can go false and when it was last confirmed. A backup
that drops them restores a rulebook that has forgotten which of its rules that drops them restores a rulebook that has forgotten which of its rules
can rot — the exact blindness the fields were added to end. can rot — the exact blindness the fields were added to end.
@@ -647,9 +643,8 @@ def test_rule_rows_carry_the_verification_fields():
assert out["verify_with"] == "cat some/file" assert out["verify_with"] == "cat some/file"
assert out["expires_when"] == "the file grows a shell" assert out["expires_when"] == "the file grows a shell"
assert out["verified_at"] == checked.isoformat() assert out["verified_at"] == checked.isoformat()
# Provenance was exported from 0088 onward but silently dropped on the way # Provenance is exported here; the restore side remaps it through
# back IN until milestone 312. Export side asserted here; the restore side # note_id_map.
# remaps it through note_id_map.
assert out["arose_from_id"] == 99 assert out["arose_from_id"] == 99
+21 -24
View File
@@ -88,7 +88,7 @@ async def test_build_autoinject_hint_titles_only_with_margin_gate():
# Telemetry fired for BOTH retrievals this path runs: the scored menu and # Telemetry fired for BOTH retrievals this path runs: the scored menu and
# the reuse-slot query competing against it. The slot's query used to be # the reuse-slot query competing against it. The slot's query used to be
# the one unlogged retrieval on this path — the hit it displaced was in # the one unlogged retrieval on this path — the hit it displaced was in
# retrieval_logs, the query that displaced it was not (#2463). # retrieval_logs, and the query that displaced it must be too.
sources = [c.kwargs["source"] for c in rec.call_args_list] sources = [c.kwargs["source"] for c in rec.call_args_list]
assert sources == ["auto_inject", "reuse_slot", "lesson_slot"] assert sources == ["auto_inject", "reuse_slot", "lesson_slot"]
@@ -127,7 +127,7 @@ async def test_build_session_context_includes_project_when_scoped():
# No design system on the project -> no design block at all. An install with # No design system on the project -> no design block at all. An install with
# none is the ordinary case, not a degraded one. # none is the ordinary case, not a degraded one.
assert "## Design system" not in out["context"] assert "## Design system" not in out["context"]
# Live state only (decision #4027): how to work with Scribe is the # Live state only: how to work with Scribe is the
# using-scribe skill's to say. A restated reflex here is a copy that drifts. # using-scribe skill's to say. A restated reflex here is a copy that drifts.
assert 'content_type="rule"' not in out["context"] assert 'content_type="rule"' not in out["context"]
assert "Reflex:" not in out["context"] assert "Reflex:" not in out["context"]
@@ -140,8 +140,8 @@ def test_a_short_goal_is_shown_whole():
def test_a_long_goal_is_cut_at_a_word_and_says_where_the_rest_is(): def test_a_long_goal_is_cut_at_a_word_and_says_where_the_rest_is():
"""#4036: a raw slice ended mid-word with nothing marking the cut, so a """A raw slice ends mid-word with nothing marking the cut, and a reader
reader took half a sentence for the whole goal.""" takes half a sentence for the whole goal."""
from scribe.services.plugin_context import _GOAL_CHARS, _goal_line from scribe.services.plugin_context import _GOAL_CHARS, _goal_line
goal = "make the record reach the next session\nso a solution is recalled " * 10 goal = "make the record reach the next session\nso a solution is recalled " * 10
line = _goal_line(goal, 2) line = _goal_line(goal, 2)
@@ -190,7 +190,7 @@ async def test_build_session_context_pushes_the_projects_design_system():
assert "resolve_design_system(9)" in ctx assert "resolve_design_system(9)" in ctx
assert "get_design_system_stylesheet(9)" in ctx assert "get_design_system_stylesheet(9)" in ctx
# The prose pointer names the call that has the MERGED guidance, not # The prose pointer names the call that has the MERGED guidance, not
# enter_project, which carries only the summary now (#4045). # enter_project, which carries only the summary.
assert "`get_design_system(9)` → `resolved_guidance`" in ctx assert "`get_design_system(9)` → `resolved_guidance`" in ctx
@@ -230,8 +230,8 @@ async def test_build_session_context_unbound_repo_emits_bind_hint():
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_a_project_id_that_does_not_resolve_is_reported_not_dropped(): async def test_a_project_id_that_does_not_resolve_is_reported_not_dropped():
"""A `.scribe` marker names a project directly (#4085), so for the first """A `.scribe` marker names a project directly, so a caller arrives
time a caller arrives holding a pointer it BELIEVES in. If the id is for holding a pointer it BELIEVES in. If the id is for
another instance, or names a deleted project, the session has to be told — another instance, or names a deleted project, the session has to be told —
it cannot infer it from an absence. it cannot infer it from an absence.
@@ -306,20 +306,18 @@ async def test_build_process_manifest_truncates_long_preview():
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_build_session_context_caps_length(): async def test_build_session_context_caps_length():
"""The cap still binds, and now has to be provoked rather than tripped. """The cap binds, and has to be provoked rather than tripped.
This used to hand the block 200 long rule titles, because the preload made Nothing this function assembles on its own is ordinarily big enough to
overflow the easy case. Milestone 394 removed that block, so nothing this overflow — which is a reason to drive the cap directly, not a reason to
function assembles on its own is big enough any more — which is a reason drop it. The project and design
to drive the cap directly, not a reason to drop it. The project and design
blocks are still unbounded in principle, and the hook passes this text blocks are still unbounded in principle, and the hook passes this text
through verbatim. through verbatim.
Patching the cap rather than manufacturing 9,000 characters keeps the test Patching the cap rather than manufacturing 9,000 characters keeps the test
about the TRUNCATION PATH — that it cuts, and that it says it cut — which about the TRUNCATION PATH — that it cuts, and that it says it cut — which
is the part a reader depends on. The unbound-repo hint supplies the text: is the part a reader depends on. The unbound-repo hint supplies the text:
since milestone 410 the block carries live state only, and a bare session the block carries live state only, and a bare session is a one-liner.
is a one-liner.
""" """
from scribe.services import plugin_context as pc from scribe.services import plugin_context as pc
@@ -333,7 +331,7 @@ async def test_build_session_context_caps_length():
) )
# --- the reuse slot (#2246) -------------------------------------------------- # --- the reuse slot ---------------------------------------------------------
_CFG = {"enabled": True, "threshold": 0.55, "top_k": 3} _CFG = {"enabled": True, "threshold": 0.55, "top_k": 3}
@@ -348,7 +346,7 @@ def _asked_for_reuse(calls: list[dict]) -> bool:
async def _autoinject(main_hits, reuse_hits, cfg=None, lesson_hits=None): async def _autoinject(main_hits, reuse_hits, cfg=None, lesson_hits=None):
"""Run build_autoinject_hint with each semantic query stubbed by the kinds """Run build_autoinject_hint with each semantic query stubbed by the kinds
it asks for: the unscoped pool, the reserved reuse query, and the reserved it asks for: the unscoped pool, the reserved reuse query, and the reserved
lesson query (milestone 385 step 5). lesson query.
ROUTED ON THE REQUESTED KINDS, not on call order, and that is the point of ROUTED ON THE REQUESTED KINDS, not on call order, and that is the point of
the helper. Order-keyed stubbing was fine while there was one reserved the helper. Order-keyed stubbing was fine while there was one reserved
@@ -458,7 +456,7 @@ async def test_a_process_counts_as_reuse_too():
assert not _asked_for_reuse(calls2) assert not _asked_for_reuse(calls2)
# --- write-path widened beyond snippets (#2246, the mirror half) ------------- # --- write-path prior art beyond snippets ------------------------------------
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_write_path_semantic_arm_asks_for_experience_not_just_snippets(): async def test_write_path_semantic_arm_asks_for_experience_not_just_snippets():
@@ -517,7 +515,7 @@ async def test_write_path_labels_a_non_snippet_hit_with_its_kind():
) )
ctx = out["context"] ctx = out["context"]
# The issue says what it is — and, since #4154, where it stands: a piece of # The issue says what it is — and where it stands: a piece of
# prior art offered as "already tried" reads differently when it is still open. # prior art offered as "already tried" reads differently when it is still open.
assert "· issue (todo)]" in ctx assert "· issue (todo)]" in ctx
assert '[similar 0.72] "debounce helper"' in ctx # the snippet does not assert '[similar 0.72] "debounce helper"' in ctx # the snippet does not
@@ -525,19 +523,18 @@ async def test_write_path_labels_a_non_snippet_hit_with_its_kind():
assert "get_task(id)" in ctx and "get_snippet(id)" in ctx assert "get_task(id)" in ctx and "get_snippet(id)" in ctx
# ── The config stand-in cannot fall behind the real one (#4214) ─────────── # ── The config stand-in cannot fall behind the real one ───────────────────
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_the_config_stand_in_carries_every_key_the_real_one_does(): async def test_the_config_stand_in_carries_every_key_the_real_one_does():
"""THE GUARD `writepath_cfg`'s DOCSTRING ALREADY CLAIMED AND DID NOT HAVE. """THE GUARD `writepath_cfg`'s DOCSTRING CLAIMS.
Three arms read their numbers out of that dict inside a fail-open Three arms read their numbers out of that dict inside a fail-open
`except`, so a missing key does not raise where anyone can see it — the `except`, so a missing key does not raise where anyone can see it — the
arm silently becomes a no-op, which is indistinguishable from the arm arm silently becomes a no-op, which is indistinguishable from the arm
working and finding nothing. The helper derives its SURFACE keys from the working and finding nothing. The helper derives its SURFACE keys from the
registry to prevent exactly that, and then #4214 added a key that is registry to prevent exactly that, and a key that is deliberately not a
deliberately not a surface: the whole derivation missed it, ten tests went surface slips past the derivation unless the key sets are compared.
red at once, and the diagnosis cost a CI round.
Asserting the KEY SETS match, not the values: the stand-in exists to let a Asserting the KEY SETS match, not the values: the stand-in exists to let a
test set different numbers. test set different numbers.
@@ -559,7 +556,7 @@ async def test_the_config_stand_in_carries_every_key_the_real_one_does():
) )
# ─── the passage that matched travels onto the menu (#4243, #4250) ─────────── # ─── the passage that matched travels onto the menu ──────────────────────────
@pytest.mark.asyncio @pytest.mark.asyncio
+36 -39
View File
@@ -1,16 +1,15 @@
"""A canon may only be urged on a shape that could plausibly BE it (#4204). """A canon may only be urged on a shape that could plausibly BE it.
WHAT WAS WRONG. `dominant_canon` is a base rate: it answers "what is most WHY. `dominant_canon` is a base rate: it answers "what is most common in this
common in this directory" and never "is this that". With `kind` carrying only directory" and never "is this that". With `kind` carrying only `css | sym`, a
`css | sym`, a frozen dataclass, a module constant, a sync predicate, a class frozen dataclass, a module constant, a sync predicate, a class and an async
and an async service function were all siblings, so the prior was the entire service function are all siblings, so the prior is the entire argument — it
argument — it told a registry module of pure helpers to build from the would tell a registry module of pure helpers to build from an
`async_session` service canon. Upstream, the auto-stamp had no resemblance `async_session` service canon. An auto-stamp without a resemblance floor
floor at all (`elif sid in resembles`), so 0.69 asserted as confidently as asserts 0.69 as confidently as 0.95.
0.95, and one payload-level score was applied to every symbol in the file.
The two halves compound: loose stamping manufactures the density that the The two halves compound: loose stamping manufactures the density that the
divergence check then reads as authority. Both are fixed by the same divergence check then reads as authority. Both are held by the same
primitive, and it is tested here on REAL signatures taken from the ledger — primitive, and it is tested here on REAL signatures taken from the ledger —
Python, Go, TypeScript and Vue — rather than invented ones, because a Python, Go, TypeScript and Vue — rather than invented ones, because a
classifier that only works on the examples its author imagined is the failure classifier that only works on the examples its author imagined is the failure
@@ -75,9 +74,9 @@ def test_css_is_its_own_form_regardless_of_signature() -> None:
assert shape_form("", "css") == "css" assert shape_form("", "css") == "css"
def test_the_five_shapes_that_started_this_are_not_service_functions() -> None: def test_a_registry_modules_helpers_are_not_service_functions() -> None:
"""The concrete case. #2860 is an `async def` service unit; none of these """The concrete case. The canon is an `async def` service unit; none of
is one, and all five were told to build from it.""" these is one."""
canon = shape_form("async def create_note(user_id: int, ...):", "sym") canon = shape_form("async def create_note(user_id: int, ...):", "sym")
assert canon == "async-fn" assert canon == "async-fn"
for sig in ( for sig in (
@@ -90,27 +89,25 @@ def test_the_five_shapes_that_started_this_are_not_service_functions() -> None:
assert not forms_agree(shape_form(sig, "sym"), canon), sig assert not forms_agree(shape_form(sig, "sym"), canon), sig
def test_how_many_of_the_five_the_divergence_gate_actually_silences() -> None: def test_the_divergence_gate_silences_only_a_family_contradiction() -> None:
"""HONEST ACCOUNTING, and it is deliberately an assertion rather than a """HONEST ACCOUNTING, and it is deliberately an assertion rather than a
comment so it fails the day the answer changes. comment so it fails the day the answer changes.
Divergence gates at FAMILY level and only on contradiction, because a form Divergence gates at FAMILY level and only on contradiction, because a form
mismatch is the PREMISE of a divergence prompt, not an objection to it — mismatch is the PREMISE of a divergence prompt, not an objection to it —
see `shape_family`. So of #4204's five false prompts this silences one: see `shape_family`. So of these five this silences one: `Point` is a type
`Point` is a type and the canon is a callable. The four `def` helpers are and the canon is a callable. The four `def` helpers are callables like the
callables like the canon and still ask. canon and still ask.
That is not a shortcoming of the gate, it is the limit of the signature: That is not a shortcoming of the gate, it is the limit of the signature:
at this level those four are indistinguishable from #2793's acceptance at this level those four are indistinguishable from the divergence
case, where a sync `confirmDanger` beside an async confirm helper SHOULD readout's acceptance case, where a sync `confirmDanger` beside an async
be flagged. confirm helper SHOULD be flagged.
CORRECTED 2026-09-21 (#4208). This used to say separating them needs Widening `kind` does not separate them: it buys the silence by bucketing
"widen `kind` or a comparison of meaning", offering the two as `fn` apart from `async-fn`, which takes the async canon out of a sync
alternatives. Widening `kind` does not separate them: it buys the silence candidate's denominator and silences the acceptance case by the identical
by bucketing `fn` apart from `async-fn`, which takes the async canon out mechanism, one layer down. Whatever separates these four
of a sync candidate's denominator and silences #2793's acceptance case by
the identical mechanism, one layer down. Whatever separates these four
has to distinguish a helper that does the canon's JOB from one that does has to distinguish a helper that does the canon's JOB from one that does
not, and no signature carries that. A comparison of meaning is the only not, and no signature carries that. A comparison of meaning is the only
lever, not one of two.""" lever, not one of two."""
@@ -130,10 +127,10 @@ def test_how_many_of_the_five_the_divergence_gate_actually_silences() -> None:
def test_a_sync_function_beside_an_async_canon_is_still_a_fair_question() -> None: def test_a_sync_function_beside_an_async_canon_is_still_a_fair_question() -> None:
"""#2793's acceptance case, which the first version of this change broke: """The divergence readout's acceptance case: gating divergence on
gating divergence on `forms_agree` silenced a hand-rolled sync confirm in `forms_agree` would silence a hand-rolled sync confirm in a directory
a directory where an async confirm helper is canon — the exact prompt the where an async confirm helper is canon — the exact prompt the readout
milestone exists to produce.""" exists to produce."""
canon = shape_form("async function onTrash() {", "sym") canon = shape_form("async function onTrash() {", "sym")
mine = shape_form("function confirmDanger() {", "sym") mine = shape_form("function confirmDanger() {", "sym")
assert canon == "async-fn" and mine == "fn" assert canon == "async-fn" and mine == "fn"
@@ -185,8 +182,8 @@ def test_a_canon_whose_instances_agree_asserts_that_form() -> None:
def test_a_canon_whose_instances_disagree_asserts_nothing() -> None: def test_a_canon_whose_instances_disagree_asserts_nothing() -> None:
"""Portal's #3283 exactly: one snippet recorded as canon for a class, a """One snippet recorded as canon for a class, a plain def, an async def
plain def, an async def and a dozen tests. That spread is the SYMPTOM of and a dozen tests. That spread is the SYMPTOM of
loose stamping, so reading it as "no opinion" makes the two fixes loose stamping, so reading it as "no opinion" makes the two fixes
cooperate — a poisoned canon falls silent instead of flagging others.""" cooperate — a poisoned canon falls silent instead of flagging others."""
rows = [_Row("class A:"), _Row("def b():"), _Row("async def c():")] rows = [_Row("class A:"), _Row("def b():"), _Row("async def c():")]
@@ -334,7 +331,7 @@ def test_the_resemblance_floor_is_above_the_retrieval_floors() -> None:
assert _RESEMBLE_MIN >= 0.80 assert _RESEMBLE_MIN >= 0.80
# ── The denominator, not the gate (#4208) ───────────────────────────────── # ── The denominator, not the gate ─────────────────────────────────────────
# #
# `dominant_canon` is a base rate, and a base rate is only a statement about # `dominant_canon` is a base rate, and a base rate is only a statement about
# something if its denominator is a population somebody asked a question # something if its denominator is a population somebody asked a question
@@ -369,7 +366,7 @@ def test_a_callable_is_not_counted_against_a_dataclass() -> None:
def test_the_async_canon_stays_in_a_sync_candidates_denominator() -> None: def test_the_async_canon_stays_in_a_sync_candidates_denominator() -> None:
"""THE ACCEPTANCE CASE OF #2793, and the reason this narrows by family """THE DIVERGENCE READOUT'S ACCEPTANCE CASE, and the reason this narrows by family
rather than by form. rather than by form.
A hand-rolled sync `confirmDanger` in a directory where an async confirm A hand-rolled sync `confirmDanger` in a directory where an async confirm
@@ -421,13 +418,13 @@ def test_a_family_passed_where_a_form_belongs_would_be_a_silent_no_op() -> None:
assert by_form != by_family assert by_form != by_family
def test_the_four_survivors_still_prompt() -> None: def test_callable_helpers_beside_a_callable_canon_still_prompt() -> None:
"""HONEST ACCOUNTING, matching the sibling test above. """HONEST ACCOUNTING, matching the sibling test above.
The narrowed denominator does not silence #4204's four `def` helpers, and The narrowed denominator does not silence the four `def` helpers, and
was never going to: they are callables, the canon is a callable, so cannot: they are callables, the canon is a callable, so nothing is
nothing is excluded from their count. What changes is that the count is excluded from their count. What it buys is that the count is over
now over comparable things. Asserted so the claim cannot quietly rot into comparable things. Asserted so the claim cannot quietly rot into
"this fixed it". "this fixed it".
""" """
rows = [_Row(SERVICE) for _ in range(6)] + [_Row("class Point:", snippet_id=9)] rows = [_Row(SERVICE) for _ in range(6)] + [_Row("class Point:", snippet_id=9)]
+17 -18
View File
@@ -1,9 +1,8 @@
"""The shape ledger's schema contract (#2787, milestone 294, note 2786). """The shape ledger's schema contract.
Step 1 pins the model: identity, the classification vocabulary, and the The model: identity, the classification vocabulary, and the token-free
token-free serialisation. The sync pass (step 2) and the classification serialisation; then the pure halves of the sync pass and the classification
surface (step 3) grow their tests here; DB-backed behavior lands in the surface. DB-backed behaviour is in the integration lane.
integration lane once there is behavior to exercise.
""" """
import pytest import pytest
@@ -47,7 +46,7 @@ def test_snippet_reference_survives_snippet_deletion_as_null():
def test_status_queries_have_an_index(): def test_status_queries_have_an_index():
"""list_shapes(status=unclassified) is THE todo query (step 3) — it must """list_shapes(status=unclassified) is THE todo query — it must
not degrade into a project-wide scan as ledgers reach thousands of rows.""" not degrade into a project-wide scan as ledgers reach thousands of rows."""
names = {ix.name for ix in CodeShape.__table__.indexes} names = {ix.name for ix in CodeShape.__table__.indexes}
assert "ix_code_shapes_project_status" in names assert "ix_code_shapes_project_status" in names
@@ -181,7 +180,7 @@ def test_signature_similarity_blanks_the_names():
def test_a_model_with_a_different_mixin_list_is_proposed_against_the_model_canon(): def test_a_model_with_a_different_mixin_list_is_proposed_against_the_model_canon():
"""The band 0.8 cut (#4306). Measured over judged instances: a model class """The band 0.8 cut. Measured over judged instances: a model class
whose mixins differ from the canon's scores ~0.795 against it — a true whose mixins differ from the canon's scores ~0.795 against it — a true
instance, and the proposal is read before it is confirmed, so the floor instance, and the proposal is read before it is confirmed, so the floor
must admit it.""" must admit it."""
@@ -324,7 +323,7 @@ def test_derive_groups_copy_before_name_with_floors():
("d.css", "css", "btn", "s1"), ("e.css", "css", "btn", "s2"), ("f.css", "css", "btn", "s3"), ("d.css", "css", "btn", "s1"), ("e.css", "css", "btn", "s2"), ("f.css", "css", "btn", "s3"),
("g.py", "sym", "main", "s4"), ("h.py", "sym", "main", "s5"), # only 2 files → no name group ("g.py", "sym", "main", "s4"), ("h.py", "sym", "main", "s5"), # only 2 files → no name group
("i.py", "sym", "one", "s6"), ("i.py", "sym", "one", "s6"),
# CSS (note 2917): identical bodies under different names are NOT a # CSS: identical bodies under different names are NOT a
# copy — two meanings sharing the style system's look; the same # copy — two meanings sharing the style system's look; the same
# class in two files IS a family (the name floor is 2 for css). # class in two files IS a family (the name floor is 2 for css).
("j.css", "css", "muted", "same"), ("k.css", "css", "pin-auto", "same"), ("j.css", "css", "muted", "same"), ("k.css", "css", "pin-auto", "same"),
@@ -344,7 +343,7 @@ def test_derive_groups_copy_before_name_with_floors():
def test_derive_new_summary_counts_copies_first_seen_since_the_previous_refresh(): def test_derive_new_summary_counts_copies_first_seen_since_the_previous_refresh():
"""#2899: the arrival-moment drift signal — derive-grouped rows created """The arrival-moment drift signal — derive-grouped rows created
after the previous refresh's stamp, newest first, judged rows and a after the previous refresh's stamp, newest first, judged rows and a
first seed (since=None) never count.""" first seed (since=None) never count."""
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
@@ -376,8 +375,8 @@ def test_derive_new_summary_counts_copies_first_seen_since_the_previous_refresh(
def test_proposal_summary_ranks_body_identical_groups_first_and_sees_scoped_rows(): def test_proposal_summary_ranks_body_identical_groups_first_and_sees_scoped_rows():
"""#2872: dup groups (the real copies) outrank name groups (usually """Dup groups (the real copies) outrank name groups (usually
convention), wider spread first; #2869: scoped rows are in the readout.""" convention), wider spread first; scoped rows are in the readout."""
from scribe.models.code_shape import CodeShape from scribe.models.code_shape import CodeShape
from scribe.services.shape_ledger import proposal_summary from scribe.services.shape_ledger import proposal_summary
@@ -396,7 +395,7 @@ def test_proposal_summary_ranks_body_identical_groups_first_and_sees_scoped_rows
row("v/J.vue", "closed-msg", "dup:abc", status="exempt"), row("v/J.vue", "closed-msg", "dup:abc", status="exempt"),
] ]
out = proposal_summary(rows) out = proposal_summary(rows)
# Milestone 302: with consumer paths in hand, each css group says what # With consumer paths in hand, each css group says what
# renders it — distinct files across the members; absent otherwise. # renders it — distinct files across the members; absent otherwise.
assert "consumers" not in out["derive_groups"][0] assert "consumers" not in out["derive_groups"][0]
for i, r in enumerate(rows): # unsaved rows have no id; give them one for i, r in enumerate(rows): # unsaved rows have no id; give them one
@@ -429,7 +428,7 @@ def test_proposer_tools_are_mounted():
assert mcp._tool_manager.get_tool("confirm_shape_proposals") is not None assert mcp._tool_manager.get_tool("confirm_shape_proposals") is not None
tool = mcp._tool_manager.get_tool("list_shapes") tool = mcp._tool_manager.get_tool("list_shapes")
assert "proposal" in tool.parameters.get("properties", {}) assert "proposal" in tool.parameters.get("properties", {})
# #2868: the audit surfaces — compact pages and the sweep form. # The audit surfaces — compact pages and the sweep form.
assert "compact" in tool.parameters.get("properties", {}) assert "compact" in tool.parameters.get("properties", {})
rule = mcp._tool_manager.get_tool("classify_shapes_by_rule") rule = mcp._tool_manager.get_tool("classify_shapes_by_rule")
assert rule is not None assert rule is not None
@@ -437,7 +436,7 @@ def test_proposer_tools_are_mounted():
assert name in rule.parameters.get("properties", {}), name assert name in rule.parameters.get("properties", {}), name
# --- #2868: the bulk surfaces (pure) ----------------------------------------- # --- the bulk surfaces (pure) -----------------------------------------------
def test_compact_row_carries_identity_standing_and_the_proposers_word_only(): def test_compact_row_carries_identity_standing_and_the_proposers_word_only():
@@ -461,7 +460,7 @@ def test_compact_row_carries_identity_standing_and_the_proposers_word_only():
def test_resolve_consumers_prefers_the_own_file_and_fans_out_for_shared_names(): def test_resolve_consumers_prefers_the_own_file_and_fans_out_for_shared_names():
"""Milestone 302: a class named in a template resolves to that file's """A class named in a template resolves to that file's
OWN row when it defines the class (a scoped rule, consumed by its own OWN row when it defines the class (a scoped rule, consumed by its own
markup); otherwise to every other definition of the name — one shared markup); otherwise to every other definition of the name — one shared
sheet, or all of several (the map fans out rather than guessing).""" sheet, or all of several (the map fans out rather than guessing)."""
@@ -490,7 +489,7 @@ def test_resolve_consumers_prefers_the_own_file_and_fans_out_for_shared_names():
def test_resolve_consumers_credits_every_row_a_prefix_could_have_built(): def test_resolve_consumers_credits_every_row_a_prefix_could_have_built():
"""#2970: `status-${s}` names a class the map cannot pin down, so the """`status-${s}` names a class the map cannot pin down, so the
prefix reference `status-*` credits every row whose symbol starts with prefix reference `status-*` credits every row whose symbol starts with
the head — each under the same own-file-else-fan-out rule. Crediting all the head — each under the same own-file-else-fan-out rule. Crediting all
of them is the honest reading: the alternative is calling live rules of them is the honest reading: the alternative is calling live rules
@@ -533,7 +532,7 @@ def test_consumer_edges_table_cascades_with_the_shape():
def test_uses_edges_table_and_validation(): def test_uses_edges_table_and_validation():
"""#2870: consumption is its own relation — a table that cascades with """Consumption is its own relation — a table that cascades with
both ends, and `uses` on a classification must be a list of ids.""" both ends, and `uses` on a classification must be a list of ids."""
from scribe.models import Base from scribe.models import Base
from scribe.models.code_shape import USE_BASES, CodeShapeUse from scribe.models.code_shape import USE_BASES, CodeShapeUse
@@ -562,7 +561,7 @@ def test_reference_canons_names_every_used_canon_not_just_the_best():
def test_reason_codes_are_a_fixed_catalogue_and_validated(): def test_reason_codes_are_a_fixed_catalogue_and_validated():
"""#2874: an optional index beside the prose reason; unknown codes are a """An optional index beside the prose reason; unknown codes are a
structural error (the batch applies nothing).""" structural error (the batch applies nothing)."""
from scribe.models.code_shape import REASON_CODES from scribe.models.code_shape import REASON_CODES
from scribe.services.shape_ledger import validate_classifications from scribe.services.shape_ledger import validate_classifications