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
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:
+22
-23
@@ -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
|
||||
chunking, everything past ~400 words of a record was PERMANENTLY invisible to
|
||||
semantic search. These tests pin the two halves of the fix's contract: short
|
||||
records keep the exact historical shape (the corpus's sharpest vectors are
|
||||
The model window is 512 tokens and fastembed truncates silently, so without
|
||||
chunking everything past ~400 words of a record would be invisible to
|
||||
semantic search. These tests pin the two halves of the contract: short
|
||||
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
|
||||
some chunk, each chunk inside the window budget, each carrying the title as
|
||||
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():
|
||||
"""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") == [
|
||||
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():
|
||||
"""The point of #280. Before chunking, a 2,000-word dev-log's last three
|
||||
quarters could not influence retrieval at all. Nothing may be dropped."""
|
||||
"""The point of chunking: every part of a 2,000-word dev-log can
|
||||
influence retrieval. Nothing may be dropped."""
|
||||
sections = [
|
||||
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
|
||||
|
||||
|
||||
# --- 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",
|
||||
# "## Findings worth carrying forward" and "Two dev→main PRs this session."
|
||||
# were each a whole chunk, and took ranks 1–3 on vague queries — text about
|
||||
# nothing embeds close to everything. Each case below is one of those shapes.
|
||||
# A heading or a one-line preamble as a whole chunk ranks at the top of vague
|
||||
# queries — text about nothing embeds close to everything. Each case below is
|
||||
# one of those shapes.
|
||||
|
||||
_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
|
||||
|
||||
|
||||
# --- 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():
|
||||
@@ -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)
|
||||
# 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 = [
|
||||
(note_a, 0.10, 3, "the passage that actually matched"),
|
||||
(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
|
||||
# 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] == {
|
||||
"index": 3, "text": "the passage that actually matched",
|
||||
}
|
||||
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=()):
|
||||
"""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
|
||||
swallowed, which would quietly make every one of these a no-log test."""
|
||||
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():
|
||||
"""The wiring half of #4251: the shaper is pure and tested next door, so
|
||||
what is pinned here is that the WRITER actually asks for the logs and
|
||||
embeds what comes back. A log that is written, readable (#4241) and absent
|
||||
"""The wiring half: the shaper is pure and tested next door, so what is
|
||||
pinned here is that the WRITER actually asks for the logs and embeds what
|
||||
comes back. A log that is written, readable and absent
|
||||
from the index is the same half-surface one layer down."""
|
||||
import datetime
|
||||
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()
|
||||
# The opening scan, in order: ids already at the current version, every
|
||||
# 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=[
|
||||
_ids(1), # note 1 is current
|
||||
_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():
|
||||
"""#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"
|
||||
predicate has to name BOTH halves of the stamp, on every embedding table."""
|
||||
from scribe.models.embedding import (
|
||||
|
||||
@@ -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
|
||||
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
|
||||
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
|
||||
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.
|
||||
|
||||
#3128's fix was correct and did not generalise: it tested one constant, so
|
||||
milestone 385's lesson arrived with the same design and none of the protection.
|
||||
The registry tests below assert the PROPERTY — every kind with a derived mirror
|
||||
is registered — so a third kind fails here rather than shipping quiet (#3734).
|
||||
A fix that tests one constant does not generalise: the next kind arrives with
|
||||
the same design and none of the protection. The registry tests below assert
|
||||
the PROPERTY — every kind with a derived mirror is registered — so a third
|
||||
kind fails here rather than shipping quiet.
|
||||
"""
|
||||
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():
|
||||
"""The load-bearing one. Before #3734 this was a single `if` naming
|
||||
snippets, and the kind added next was simply not covered."""
|
||||
"""The load-bearing one. A single `if` naming one kind leaves the next
|
||||
kind uncovered."""
|
||||
from scribe.services.lessons import LESSON_NOTE_TYPE
|
||||
from scribe.services.notes import _mirror_recomposers
|
||||
from scribe.services.snippets import SNIPPET_NOTE_TYPE
|
||||
|
||||
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, (
|
||||
"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 "
|
||||
"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():
|
||||
"""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."""
|
||||
from scribe.services import notes as notes_module
|
||||
|
||||
src = inspect.getsource(notes_module.update_note)
|
||||
assert "_mirror_recomposers()" 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():
|
||||
"""Why `untrigger_title` is given the trigger instead of splitting on the
|
||||
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."""
|
||||
from scribe.services.embeddings import trigger_title
|
||||
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):
|
||||
"""#3207's shape: the join had three copies before it was consolidated, so
|
||||
its inverse lives beside it rather than in whichever caller wanted it."""
|
||||
"""The join's inverse lives beside it rather than in whichever caller
|
||||
wanted it."""
|
||||
from scribe.services.embeddings import trigger_title, untrigger_title
|
||||
|
||||
title = trigger_title(subject, trigger)
|
||||
@@ -289,12 +289,12 @@ def test_the_trigger_separator_is_spelled_in_exactly_one_place():
|
||||
]
|
||||
assert not offenders, (
|
||||
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():
|
||||
"""Rule 167: shown turning red once."""
|
||||
"""Shown turning red once."""
|
||||
from scribe.services.lessons import TRIGGER_KEY, recompose_data
|
||||
|
||||
bare = fake_lesson(title="just a title", body="no composed lines", data=None)
|
||||
|
||||
@@ -17,7 +17,7 @@ pytestmark = pytest.mark.usefixtures("_bind_user")
|
||||
@pytest.fixture(autouse=True)
|
||||
def _no_systems():
|
||||
"""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
|
||||
populated shape is asserted in its own test below.
|
||||
"""
|
||||
@@ -40,7 +40,7 @@ def _no_platforms():
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
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
|
||||
populated line is asserted in tests/test_pattern_coverage.py.
|
||||
"""
|
||||
@@ -51,7 +51,7 @@ def _no_coverage():
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
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
|
||||
populated key is asserted in its own test below."""
|
||||
with patch("scribe.mcp.tools.projects.family_adoption_svc.family_readout",
|
||||
@@ -61,7 +61,7 @@ def _no_family():
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
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
|
||||
has its own test below."""
|
||||
with patch("scribe.mcp.tools.projects.spawn") as mock:
|
||||
@@ -71,7 +71,7 @@ def _no_background_seed():
|
||||
@pytest.fixture(autouse=True)
|
||||
def _no_bootstrap():
|
||||
"""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);
|
||||
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():
|
||||
"""The augmented get_project response includes 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")
|
||||
milestone_summary = []
|
||||
@@ -191,7 +191,7 @@ async def test_update_project_raises_when_not_found():
|
||||
@pytest.mark.asyncio
|
||||
async def test_enter_project_composes_full_context():
|
||||
"""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")
|
||||
applicable_payload = {
|
||||
"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["milestone_summary"] == milestone_summary
|
||||
# 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"}]
|
||||
for gone in ("applicable_rules", "applicable_rules_truncated", "subscribed_rulebooks",
|
||||
"suppressed_rules", "suppressed_topics", "recent_notes"):
|
||||
@@ -244,16 +244,14 @@ async def test_enter_project_composes_full_context():
|
||||
# create-the-System instruction acts on.
|
||||
assert out["systems"] == []
|
||||
# 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
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_enter_project_surfaces_the_systems_vocabulary():
|
||||
"""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
|
||||
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
|
||||
front of the agent when it writes. Trimmed to id/name/first-line: it rides on every session
|
||||
start, and the full charter is get_system's job."""
|
||||
p = fake_project(id=5)
|
||||
sys1 = MagicMock()
|
||||
@@ -298,7 +296,7 @@ def _enter_project_stubs(p):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
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 moment minting a starter vocabulary is cheapest."""
|
||||
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(
|
||||
_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
|
||||
forget, so a missing ledger seeds itself without the UI button, and the
|
||||
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"]["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 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
|
||||
@@ -418,7 +416,7 @@ def test_enter_project_registered_in_register():
|
||||
assert "enter_project" in mcp.names
|
||||
|
||||
|
||||
# --- milestone 297: the inception doors ---------------------------------------
|
||||
# --- the inception doors -----------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -491,7 +489,7 @@ def test_inception_routes_and_tool_are_registered():
|
||||
tool = mcp._tool_manager.get_tool("create_project")
|
||||
for name in ("design_system_id", "seed_systems"):
|
||||
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", {})
|
||||
|
||||
|
||||
@@ -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):
|
||||
"""The `family` key is attached when the project has family canon to
|
||||
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",
|
||||
"unassessed": 2, "owed": 0, "needs_recheck": 0,
|
||||
"calls": {"unassessed": 'list_family_adoptions(project_id=5, status="unassessed")'}}
|
||||
|
||||
@@ -49,8 +49,7 @@ async def test_get_rulebook_raises_when_not_found():
|
||||
await get_rulebook(rulebook_id=999)
|
||||
|
||||
|
||||
# _plain_detail moved to tests/helpers on its second copy (#2825's own
|
||||
# reason for existing): two stubs for one seam drift apart quietly, and a
|
||||
# _plain_detail lives in tests/helpers: two stubs for one seam drift apart quietly, and a
|
||||
# test stubbing it slightly differently asserts something slightly different
|
||||
# than it appears to.
|
||||
|
||||
@@ -199,16 +198,7 @@ def test_register_attaches_every_tool():
|
||||
mcp = FakeMCP()
|
||||
|
||||
register(mcp)
|
||||
# 26 through milestone 307, +2 for the staleness sweep (milestone 312),
|
||||
# +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.
|
||||
# Pinned as a count so a tool added or dropped is a deliberate edit here.
|
||||
assert len(mcp.names) == 24
|
||||
# spot-check a few names
|
||||
assert "list_rulebooks" in mcp.names
|
||||
@@ -221,12 +211,12 @@ def test_register_attaches_every_tool():
|
||||
assert "update_preference" in mcp.names
|
||||
assert "create_project_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 "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
|
||||
# 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",
|
||||
"suppress_rule_for_project", "unsuppress_rule_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"
|
||||
|
||||
|
||||
# ── Typed edges between rules (milestone 307) ───────────────────────────
|
||||
# ── Typed edges between rules ───────────────────────────────────────────
|
||||
|
||||
@pytest.mark.asyncio
|
||||
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)
|
||||
|
||||
|
||||
# ── rule_history (milestone 323 step 3) ────────────────────────────────
|
||||
# ── rule_history ───────────────────────────────────────────────────────
|
||||
|
||||
def _fake_version(**over):
|
||||
"""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
|
||||
async def test_rule_history_says_an_empty_history_is_ordinary():
|
||||
"""Most rules have never been reworded, and nothing was written before
|
||||
milestone 323. Without this line an empty list reads as a lost history or
|
||||
a broken tool."""
|
||||
"""Most rules have never been reworded. Without this line an empty list
|
||||
reads as a lost history or a broken tool."""
|
||||
with patch(
|
||||
"scribe.mcp.tools.rulebooks.rulebooks_svc.list_rule_versions",
|
||||
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)
|
||||
|
||||
|
||||
# ── rule_outcome: what a rule actually changed (#4212, milestone 419) ─────
|
||||
# ── rule_outcome: what a rule actually changed ───────────────────────────
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rule_outcome_records_an_application():
|
||||
|
||||
@@ -52,7 +52,7 @@ async def test_fable_search_returns_repackaged_results():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
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
|
||||
relevant, and letting them decide from it."""
|
||||
_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
|
||||
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
|
||||
that already asks this way (#4250)."""
|
||||
that already asks this way."""
|
||||
_user_id_ctx.set(7)
|
||||
mock_search = AsyncMock(return_value=[])
|
||||
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
|
||||
|
||||
|
||||
# --- the specific kinds: the engine already supported them (#4250) -----------
|
||||
# --- the specific kinds the engine supports ----------------------------------
|
||||
|
||||
|
||||
@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():
|
||||
"""#3161's property, held at this door too: adding a kind to `_FACETS` is
|
||||
one edit. A hand-kept list here is exactly how the agent's search came to
|
||||
offer two kinds while the web's offered nine."""
|
||||
"""Adding a kind to `_FACETS` is one edit, at this door too. A hand-kept
|
||||
list here lets the agent's search offer fewer kinds than the web's."""
|
||||
from scribe.services.knowledge import FACET_TYPES, content_type_filters
|
||||
|
||||
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
|
||||
|
||||
# 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):
|
||||
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
|
||||
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."""
|
||||
from scribe.mcp.tools.search import search as search_tool
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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}),
|
||||
])
|
||||
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"
|
||||
is asking the whole rulebook — unlike a hook, which speaks unasked."""
|
||||
_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
|
||||
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
|
||||
|
||||
_user_id_ctx.set(7)
|
||||
# `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
|
||||
# object and `body_length` zero while the assertion still looked green
|
||||
# (lesson #2833).
|
||||
# object and `body_length` zero while the assertion still looked green.
|
||||
ms = MagicMock(id=339, title="M3 — Metadata", description="works, editions",
|
||||
status="active", project_id=30,
|
||||
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"] == [{
|
||||
"id": 339, "title": "M3 — Metadata", "description": "works, editions",
|
||||
# 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_is": "matched_passage",
|
||||
"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
|
||||
@@ -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
|
||||
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
|
||||
along, marked as the passage (#4243)."""
|
||||
along, marked as the passage."""
|
||||
_user_id_ctx.set(7)
|
||||
charter = (
|
||||
"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)
|
||||
# `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"
|
||||
|
||||
async def _system_search(uid, q, **kwargs):
|
||||
|
||||
+19
-23
@@ -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
|
||||
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
|
||||
|
||||
ts = datetime(2026, 7, 28, tzinfo=timezone.utc)
|
||||
# Rows are (note_id, event, count, last_at, ambient) since #2477 split the
|
||||
# readout. Ranked and ambient surfacings arrive as separate groups.
|
||||
# Rows are (note_id, event, count, last_at, ambient): ranked and ambient
|
||||
# surfacings arrive as separate groups.
|
||||
rows = [
|
||||
(3, "surfaced", 9, ts, False),
|
||||
(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])
|
||||
# The dead-weight reading ("surfaced often, never pulled") is only valid
|
||||
# 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]["ambient_count"] == 40
|
||||
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():
|
||||
"""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 datetime import datetime, timezone
|
||||
|
||||
@@ -167,7 +167,7 @@ async def test_no_ids_short_circuits_without_a_query():
|
||||
@pytest.mark.parametrize(
|
||||
"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,
|
||||
# and both must leave a usage trace under their own name.
|
||||
([(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):
|
||||
"""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
|
||||
blocker #2082 recorded against this task; this is the assertion that it's
|
||||
closed, per class."""
|
||||
— without a usage row they would surface snippets while leaving no trace
|
||||
anywhere. Asserted per class."""
|
||||
from scribe.services import plugin_context
|
||||
|
||||
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():
|
||||
"""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
|
||||
is what #1038 and #2085 gate on. That pair is only closed if the tool which
|
||||
OPENS a record reports it. `get_note` did, `get_snippet` did, `get_task` did
|
||||
NOT — and auto-inject ranks kind-blind over a corpus that is overwhelmingly
|
||||
tasks and issues, so the gap sat exactly where the volume is: every surfaced
|
||||
task counted as never-pulled, dragging measured pull-through toward zero for
|
||||
the menu's own dominant kind.
|
||||
is what pull-through is read from. That pair is only closed if every tool
|
||||
which OPENS a record reports it — and auto-inject ranks kind-blind over a
|
||||
corpus that is mostly tasks and issues, so one getter that does not report
|
||||
counts the menu's dominant kind as never-pulled.
|
||||
|
||||
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
|
||||
@@ -272,12 +269,11 @@ def test_every_getter_that_can_be_surfaced_also_records_a_pull():
|
||||
|
||||
# --- persistence (integration) --------------------------------------------
|
||||
# Everything above mocks _schedule or the session — deliberately, for the hot
|
||||
# path. But that left the two functions that actually touch the database
|
||||
# (_insert_events and usage_for_notes' real SQL) running against real Postgres
|
||||
# nowhere, which is how the deployed instance reported zero for every counter
|
||||
# while surfacing demonstrably fired (#2663): all-green mocked units over a
|
||||
# dead real path, the #2109 shape. These two run in the CI integration lane
|
||||
# and split the chain so a failure names its half.
|
||||
# path. Without these, the two functions that actually touch the database
|
||||
# (_insert_events and usage_for_notes' real SQL) would run against real
|
||||
# Postgres nowhere — all-green mocked units over a dead real path. These two
|
||||
# run in the CI integration lane and split the chain so a failure names its
|
||||
# half.
|
||||
|
||||
|
||||
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"},
|
||||
])
|
||||
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]["ambient_count"] == 1
|
||||
assert out[nid]["pull_count"] == 1
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""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.
|
||||
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.
|
||||
@@ -11,15 +11,15 @@ from different clocks, on purpose:
|
||||
server image name from COMMIT time, ordering key from BUILD 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
|
||||
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
|
||||
than only explained in a comment.
|
||||
|
||||
The trade mint time makes — you cannot recompute the value from history, only
|
||||
verify it moved — is acceptable ONLY because #3325 established that the
|
||||
installer's refresh test is `===` with no ordering anywhere. Where a
|
||||
verify it moved — is acceptable ONLY because the installer's refresh test
|
||||
is `===` with no ordering anywhere. Where a
|
||||
comparator orders, an unreproducible version would be unverifiable too.
|
||||
"""
|
||||
import ast
|
||||
@@ -57,7 +57,7 @@ def fake_manifest(version: str = "2026.09.01.2252") -> str:
|
||||
|
||||
|
||||
@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.
|
||||
(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"),
|
||||
@@ -150,7 +150,7 @@ def test_the_rewrite_refuses_TWO_version_lines():
|
||||
|
||||
|
||||
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
|
||||
change never forces a re-mint, so the manifest keeps a value in the old
|
||||
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):
|
||||
"""#2209, exactly. The headline, and the only reason the check exists."""
|
||||
"""The headline, and the only reason the check exists."""
|
||||
failures = run_check(
|
||||
"2026.09.01.2252", "2026.09.01.2252",
|
||||
["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):
|
||||
"""`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
|
||||
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)
|
||||
assert len(failures) == 1
|
||||
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"])
|
||||
def test_an_UNPADDED_or_short_version_FAILS(bad, monkeypatch):
|
||||
"""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) != []
|
||||
|
||||
|
||||
@@ -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):
|
||||
"""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
|
||||
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():
|
||||
"""The marker #2220 asked for. The value's SHAPE changed, not the field or
|
||||
its reader — if this had to move, the derivation went 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."""
|
||||
"""The hook reads `.version` out of the manifest rather than deriving the
|
||||
string some other way — if this has to move, the derivation went
|
||||
somewhere it should not have."""
|
||||
hook = (check_plugin.HOOKS_DIR / "scribe_session_context.sh").read_text()
|
||||
assert re.search(r"scribe_json_pick[^\n]*'\.version'", hook)
|
||||
|
||||
@@ -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
|
||||
|
||||
`RULEHINT_LIMIT` was 1. That was right while retrieval merely SUPPLEMENTED a
|
||||
33-rule resident set — one salient rule beside everything already loaded. It
|
||||
stops being right the moment milestone 394 removes residency, because then
|
||||
this arm is the whole delivery, and `git push origin dev` is governed by
|
||||
rules 1, 2, 9 and 140 at once, each of which alone permits the mistake the
|
||||
others catch.
|
||||
This arm is the whole delivery of rules to an act, and one command can be
|
||||
governed by several rules at once — a `git push` by where to push, what not
|
||||
to push, who polls CI and what to wait for — each of which alone permits the
|
||||
mistake the others catch. One slot cannot serve that.
|
||||
|
||||
Two instruments, and they answer different questions:
|
||||
|
||||
- `_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,
|
||||
so one clearly-relevant rule still shows one.
|
||||
- `compact` decides HOW LOUD. Measured at #3851: a full line is ~143 tokens
|
||||
once the trigger is rendered, so five of them cost ~646 before every Bash
|
||||
call. Top-full-plus-references costs ~198.
|
||||
- `compact` decides HOW LOUD. A full line is ~143 tokens once the trigger
|
||||
is rendered, so five of them cost ~646 before every Bash call.
|
||||
Top-full-plus-references costs ~198.
|
||||
|
||||
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():
|
||||
"""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
|
||||
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):
|
||||
"""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
|
||||
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
|
||||
@@ -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():
|
||||
"""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
|
||||
compact branch dropped the tail along with the trigger, so a rule the
|
||||
session had already been told read exactly like one it had not. #3750's
|
||||
whole argument is that the two are different claims — a repeat is rendered
|
||||
A compact branch that drops the tail along with the trigger makes a rule
|
||||
the session had already been told read exactly like one it had not. The
|
||||
two are different claims — a repeat is rendered
|
||||
precisely because the session may no longer HOLD what it was told — and
|
||||
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;
|
||||
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
|
||||
grew with it — pinning only two would leave the compact branch free to
|
||||
collapse the new middle state into either neighbour, which is the same
|
||||
regression this was written for with one more place to hide.
|
||||
THREE STATES, so this checks three. Pinning only two would leave the
|
||||
compact branch free to collapse the middle state into either neighbour.
|
||||
"""
|
||||
rule = fake_rule(id=4, title="dev is home", when_to_apply=_TRIGGER)
|
||||
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.
|
||||
|
||||
Measured against a REALISTIC trigger, because that is where the saving
|
||||
lives: the rules this arm carries run 300-400 characters of trigger after
|
||||
#3855, and a toy one-line trigger would make this pass on a compact branch
|
||||
lives: the rules this arm carries run 300-400 characters of trigger, and
|
||||
a toy one-line trigger would make this pass on a compact branch
|
||||
that had stopped saving anything.
|
||||
"""
|
||||
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():
|
||||
"""Force survives the shortening (#3849).
|
||||
"""Force survives the shortening.
|
||||
|
||||
`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
|
||||
|
||||
@@ -51,7 +51,7 @@ def _exportable_note(**over):
|
||||
|
||||
def test_note_rows_carry_the_verification_trio():
|
||||
"""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(
|
||||
verify_with="curl the AMO docs",
|
||||
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():
|
||||
"""#3182's headline. Without these two columns a restore reported success
|
||||
and handed back a corpus where all 90 snippets and 3 processes were plain
|
||||
notes and all 435 issues and the spike were `work` — the entire vocabulary
|
||||
milestone 312 and #3128 were about, gone, with nothing to notice it by."""
|
||||
"""Without these two columns a restore reports success and hands back a
|
||||
corpus where every snippet and process is a plain note and every issue
|
||||
and spike is `work` — the record vocabulary gone, with nothing to notice
|
||||
it by."""
|
||||
[snippet] = backup._note_rows([_exportable_note(note_type="snippet")])
|
||||
[issue] = backup._note_rows([_exportable_note(task_kind="issue", status="done")])
|
||||
assert snippet["note_type"] == "snippet"
|
||||
@@ -105,7 +105,7 @@ def test_provenance_and_lifecycle_travel():
|
||||
def test_a_milestone_carries_its_plan():
|
||||
"""A milestone IS the plan (0066); `body` is its design and intent and
|
||||
`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(
|
||||
id=1, user_id=7, project_id=2, title="t", description="d",
|
||||
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():
|
||||
"""#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
|
||||
tree — a wrong answer that looks like a working one."""
|
||||
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()))
|
||||
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
|
||||
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()))
|
||||
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
|
||||
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
|
||||
dropping the row would lose a real pull and deflate the very pull-through
|
||||
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.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
|
||||
source id would point at whatever snippet took that number in the
|
||||
destination — wrong rather than missing, and nothing downstream could
|
||||
tell. It was dropped entirely until #4197; restoring it raw would have
|
||||
been the worse fix."""
|
||||
tell. Restoring it raw would be worse than dropping it."""
|
||||
maps = backup._Maps()
|
||||
maps.projects[1] = 11
|
||||
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():
|
||||
# The deferred tables must be surfaced explicitly, not silently dropped.
|
||||
# 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",
|
||||
"note_embeddings", "retrieval_logs", "forge_connections"):
|
||||
assert table in backup._NOT_INCLUDED
|
||||
|
||||
|
||||
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
|
||||
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",
|
||||
# v23: proposals and judgments about a rule's moments.
|
||||
"rule_moment_judgments",
|
||||
# v25: family canon (milestone 463).
|
||||
# v25: family canon.
|
||||
"platforms", "project_platforms", "family_ideas",
|
||||
"family_idea_platforms", "family_idea_references",
|
||||
"family_adoptions", "family_decisions",
|
||||
# v26: the Profile page (#5879).
|
||||
# v26: the Profile page.
|
||||
"user_profiles"):
|
||||
assert key in out, f"missing export section: {key}"
|
||||
assert out[key] == []
|
||||
@@ -625,7 +621,7 @@ def test_supersession_rows_serialise_the_pair():
|
||||
def test_rule_rows_carry_the_verification_fields():
|
||||
"""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
|
||||
that drops them restores a rulebook that has forgotten which of its rules
|
||||
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["expires_when"] == "the file grows a shell"
|
||||
assert out["verified_at"] == checked.isoformat()
|
||||
# Provenance was exported from 0088 onward but silently dropped on the way
|
||||
# back IN until milestone 312. Export side asserted here; the restore side
|
||||
# remaps it through note_id_map.
|
||||
# Provenance is exported here; the restore side remaps it through
|
||||
# note_id_map.
|
||||
assert out["arose_from_id"] == 99
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
# 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
|
||||
# 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]
|
||||
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
|
||||
# none is the ordinary case, not a degraded one.
|
||||
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.
|
||||
assert 'content_type="rule"' 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():
|
||||
"""#4036: a raw slice ended mid-word with nothing marking the cut, so a
|
||||
reader took half a sentence for the whole goal."""
|
||||
"""A raw slice ends mid-word with nothing marking the cut, and a reader
|
||||
takes half a sentence for the whole goal."""
|
||||
from scribe.services.plugin_context import _GOAL_CHARS, _goal_line
|
||||
goal = "make the record reach the next session\nso a solution is recalled " * 10
|
||||
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 "get_design_system_stylesheet(9)" in ctx
|
||||
# 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
|
||||
|
||||
|
||||
@@ -230,8 +230,8 @@ async def test_build_session_context_unbound_repo_emits_bind_hint():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
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
|
||||
time a caller arrives holding a pointer it BELIEVES in. If the id is for
|
||||
"""A `.scribe` marker names a project directly, so a caller arrives
|
||||
holding a pointer it BELIEVES in. If the id is for
|
||||
another instance, or names a deleted project, the session has to be told —
|
||||
it cannot infer it from an absence.
|
||||
|
||||
@@ -306,20 +306,18 @@ async def test_build_process_manifest_truncates_long_preview():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
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
|
||||
overflow the easy case. Milestone 394 removed that block, so nothing this
|
||||
function assembles on its own is big enough any more — which is a reason
|
||||
to drive the cap directly, not a reason to drop it. The project and design
|
||||
Nothing this function assembles on its own is ordinarily big enough to
|
||||
overflow — which is a reason 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
|
||||
through verbatim.
|
||||
|
||||
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
|
||||
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
|
||||
is a one-liner.
|
||||
the block carries live state only, and a bare session is a one-liner.
|
||||
"""
|
||||
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}
|
||||
|
||||
@@ -348,7 +346,7 @@ def _asked_for_reuse(calls: list[dict]) -> bool:
|
||||
async def _autoinject(main_hits, reuse_hits, cfg=None, lesson_hits=None):
|
||||
"""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
|
||||
lesson query (milestone 385 step 5).
|
||||
lesson query.
|
||||
|
||||
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
|
||||
@@ -458,7 +456,7 @@ async def test_a_process_counts_as_reuse_too():
|
||||
assert not _asked_for_reuse(calls2)
|
||||
|
||||
|
||||
# --- write-path widened beyond snippets (#2246, the mirror half) -------------
|
||||
# --- write-path prior art beyond snippets ------------------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
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"]
|
||||
# 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.
|
||||
assert "· issue (todo)]" in ctx
|
||||
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
|
||||
|
||||
|
||||
# ── The config stand-in cannot fall behind the real one (#4214) ───────────
|
||||
# ── The config stand-in cannot fall behind the real one ───────────────────
|
||||
|
||||
@pytest.mark.asyncio
|
||||
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
|
||||
`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
|
||||
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
|
||||
deliberately not a surface: the whole derivation missed it, ten tests went
|
||||
red at once, and the diagnosis cost a CI round.
|
||||
registry to prevent exactly that, and a key that is deliberately not a
|
||||
surface slips past the derivation unless the key sets are compared.
|
||||
|
||||
Asserting the KEY SETS match, not the values: the stand-in exists to let a
|
||||
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
|
||||
|
||||
@@ -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
|
||||
common in this directory" and never "is this that". With `kind` carrying only
|
||||
`css | sym`, a frozen dataclass, a module constant, a sync predicate, a class
|
||||
and an async service function were all siblings, so the prior was the entire
|
||||
argument — it told a registry module of pure helpers to build from the
|
||||
`async_session` service canon. Upstream, the auto-stamp had no resemblance
|
||||
floor at all (`elif sid in resembles`), so 0.69 asserted as confidently as
|
||||
0.95, and one payload-level score was applied to every symbol in the file.
|
||||
WHY. `dominant_canon` is a base rate: it answers "what is most common in this
|
||||
directory" and never "is this that". With `kind` carrying only `css | sym`, a
|
||||
frozen dataclass, a module constant, a sync predicate, a class and an async
|
||||
service function are all siblings, so the prior is the entire argument — it
|
||||
would tell a registry module of pure helpers to build from an
|
||||
`async_session` service canon. An auto-stamp without a resemblance floor
|
||||
asserts 0.69 as confidently as 0.95.
|
||||
|
||||
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 —
|
||||
Python, Go, TypeScript and Vue — rather than invented ones, because a
|
||||
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"
|
||||
|
||||
|
||||
def test_the_five_shapes_that_started_this_are_not_service_functions() -> None:
|
||||
"""The concrete case. #2860 is an `async def` service unit; none of these
|
||||
is one, and all five were told to build from it."""
|
||||
def test_a_registry_modules_helpers_are_not_service_functions() -> None:
|
||||
"""The concrete case. The canon is an `async def` service unit; none of
|
||||
these is one."""
|
||||
canon = shape_form("async def create_note(user_id: int, ...):", "sym")
|
||||
assert canon == "async-fn"
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
comment so it fails the day the answer changes.
|
||||
|
||||
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 —
|
||||
see `shape_family`. So of #4204's five false prompts this silences one:
|
||||
`Point` is a type and the canon is a callable. The four `def` helpers are
|
||||
callables like the canon and still ask.
|
||||
see `shape_family`. So of these five this silences one: `Point` is a type
|
||||
and the canon is a callable. The four `def` helpers are callables like the
|
||||
canon and still ask.
|
||||
|
||||
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
|
||||
case, where a sync `confirmDanger` beside an async confirm helper SHOULD
|
||||
be flagged.
|
||||
at this level those four are indistinguishable from the divergence
|
||||
readout's acceptance case, where a sync `confirmDanger` beside an async
|
||||
confirm helper SHOULD be flagged.
|
||||
|
||||
CORRECTED 2026-09-21 (#4208). This used to say separating them needs
|
||||
"widen `kind` or a comparison of meaning", offering the two as
|
||||
alternatives. Widening `kind` does not separate them: it buys the silence
|
||||
by bucketing `fn` apart from `async-fn`, which takes the async canon out
|
||||
of a sync candidate's denominator and silences #2793's acceptance case by
|
||||
the identical mechanism, one layer down. Whatever separates these four
|
||||
Widening `kind` does not separate them: it buys the silence by bucketing
|
||||
`fn` apart from `async-fn`, which takes the async canon out of a sync
|
||||
candidate's denominator and silences the 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
|
||||
not, and no signature carries that. A comparison of meaning is the only
|
||||
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:
|
||||
"""#2793's acceptance case, which the first version of this change broke:
|
||||
gating divergence on `forms_agree` silenced a hand-rolled sync confirm in
|
||||
a directory where an async confirm helper is canon — the exact prompt the
|
||||
milestone exists to produce."""
|
||||
"""The divergence readout's acceptance case: gating divergence on
|
||||
`forms_agree` would silence a hand-rolled sync confirm in a directory
|
||||
where an async confirm helper is canon — the exact prompt the readout
|
||||
exists to produce."""
|
||||
canon = shape_form("async function onTrash() {", "sym")
|
||||
mine = shape_form("function confirmDanger() {", "sym")
|
||||
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:
|
||||
"""Portal's #3283 exactly: one snippet recorded as canon for a class, a
|
||||
plain def, an async def and a dozen tests. That spread is the SYMPTOM of
|
||||
"""One snippet recorded as canon for a class, a plain def, an async def
|
||||
and a dozen tests. That spread is the SYMPTOM of
|
||||
loose stamping, so reading it as "no opinion" makes the two fixes
|
||||
cooperate — a poisoned canon falls silent instead of flagging others."""
|
||||
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
|
||||
|
||||
|
||||
# ── 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
|
||||
# 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:
|
||||
"""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.
|
||||
|
||||
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
|
||||
|
||||
|
||||
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.
|
||||
|
||||
The narrowed denominator does not silence #4204's four `def` helpers, and
|
||||
was never going to: they are callables, the canon is a callable, so
|
||||
nothing is excluded from their count. What changes is that the count is
|
||||
now over comparable things. Asserted so the claim cannot quietly rot into
|
||||
The narrowed denominator does not silence the four `def` helpers, and
|
||||
cannot: they are callables, the canon is a callable, so nothing is
|
||||
excluded from their count. What it buys is that the count is over
|
||||
comparable things. Asserted so the claim cannot quietly rot into
|
||||
"this fixed it".
|
||||
"""
|
||||
rows = [_Row(SERVICE) for _ in range(6)] + [_Row("class Point:", snippet_id=9)]
|
||||
|
||||
+17
-18
@@ -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
|
||||
token-free serialisation. The sync pass (step 2) and the classification
|
||||
surface (step 3) grow their tests here; DB-backed behavior lands in the
|
||||
integration lane once there is behavior to exercise.
|
||||
The model: identity, the classification vocabulary, and the token-free
|
||||
serialisation; then the pure halves of the sync pass and the classification
|
||||
surface. DB-backed behaviour is in the integration lane.
|
||||
"""
|
||||
import pytest
|
||||
|
||||
@@ -47,7 +46,7 @@ def test_snippet_reference_survives_snippet_deletion_as_null():
|
||||
|
||||
|
||||
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."""
|
||||
names = {ix.name for ix in CodeShape.__table__.indexes}
|
||||
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():
|
||||
"""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
|
||||
instance, and the proposal is read before it is confirmed, so the floor
|
||||
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"),
|
||||
("g.py", "sym", "main", "s4"), ("h.py", "sym", "main", "s5"), # only 2 files → no name group
|
||||
("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
|
||||
# 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"),
|
||||
@@ -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():
|
||||
"""#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
|
||||
first seed (since=None) never count."""
|
||||
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():
|
||||
"""#2872: dup groups (the real copies) outrank name groups (usually
|
||||
convention), wider spread first; #2869: scoped rows are in the readout."""
|
||||
"""Dup groups (the real copies) outrank name groups (usually
|
||||
convention), wider spread first; scoped rows are in the readout."""
|
||||
from scribe.models.code_shape import CodeShape
|
||||
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"),
|
||||
]
|
||||
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.
|
||||
assert "consumers" not in out["derive_groups"][0]
|
||||
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
|
||||
tool = mcp._tool_manager.get_tool("list_shapes")
|
||||
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", {})
|
||||
rule = mcp._tool_manager.get_tool("classify_shapes_by_rule")
|
||||
assert rule is not None
|
||||
@@ -437,7 +436,7 @@ def test_proposer_tools_are_mounted():
|
||||
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():
|
||||
@@ -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():
|
||||
"""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
|
||||
markup); otherwise to every other definition of the name — one shared
|
||||
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():
|
||||
"""#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
|
||||
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
|
||||
@@ -533,7 +532,7 @@ def test_consumer_edges_table_cascades_with_the_shape():
|
||||
|
||||
|
||||
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."""
|
||||
from scribe.models import Base
|
||||
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():
|
||||
"""#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)."""
|
||||
from scribe.models.code_shape import REASON_CODES
|
||||
from scribe.services.shape_ledger import validate_classifications
|
||||
|
||||
Reference in New Issue
Block a user