docs(6209): the remaining test files — lessons, reply moment and shape, retrieval specs and scopes, rule doors and judgments, services, snippets, search scope; five tests renamed to their property
CI & Build / Python lint (push) Successful in 5s
CI & Build / Plugin hooks (push) Successful in 17s
CI & Build / TypeScript typecheck (push) Successful in 57s
CI & Build / integration (push) Successful in 1m19s
CI & Build / Python tests (push) Successful in 2m2s
CI & Build / Build & push image (push) Successful in 15s

Docstrings, section headers and assertion messages drop the record number,
milestone or step that introduced the behaviour, and incident accounts are
restated as the present-tense failure shape. A dead pointer to a design doc
that is not in the repo is removed.

Renamed to the property they check:
  test_the_old_shape_would_be_caught -> test_a_rank_then_scope_query_is_caught
  test_a_ledger_repeat_no_longer_produces_that_case_at_all -> test_a_ledger_repeat_is_counted_not_withheld
  test_the_header_no_longer_promises_once_per_session -> test_the_header_explains_the_seen_marker
  test_the_portal_case_no_longer_stamps_one_canon_onto_a_whole_file -> test_one_canon_is_not_stamped_onto_a_whole_file
  test_the_repos_own_prose_no_longer_defines_with -> test_the_repos_own_prose_does_not_define_with
  test_the_original_regression_create_note_with_content -> test_the_real_server_refuses_content_on_create_note

Fixture and example ids stay: "note 5" in a supersession chain, "rule 156"
in an illustrative question, `#2275` as the hex-versus-citation case,
`#3244` as the example of a bare number, fixed dates in date-filter tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-21 21:44:58 -04:00
co-authored by Claude Opus 5.5
parent 79de8938b9
commit 914f31359c
73 changed files with 203 additions and 214 deletions
+6 -7
View File
@@ -99,14 +99,13 @@ def test_a_sentence_about_a_definition_is_not_a_definition(repo):
assert "in 1 other file(s)" in out
def test_the_repos_own_prose_no_longer_defines_with():
"""Not a fixture — this repo's actual corpus, which is where the defect
was found.
def test_the_repos_own_prose_does_not_define_with():
"""Not a fixture — this repo's actual corpus, the realistic case.
Before the confirmation pass the arm named four files for `with`:
`…record type with no vector…`, `A class with no rules anywhere…` and two
more, all of them prose in docstrings and comments. It now names none,
and none is the correct answer: nothing here defines a symbol called
Without the confirmation pass the arm names files for `with` on prose in
docstrings and comments — `…record type with no vector…`, `A class with
no rules anywhere…`. With it the arm names none, and none is the correct
answer: nothing here defines a symbol called
`with`, and in several of the languages in this tree it could not, because
it is a keyword.
+3 -2
View File
@@ -150,8 +150,9 @@ async def test_a_repeat_is_not_recorded_as_a_fresh_surfacing():
@pytest.mark.asyncio
async def test_the_header_no_longer_promises_once_per_session():
"""A contract stated in the prose is a contract, and this one changed.
async def test_the_header_explains_the_seen_marker():
"""A contract stated in the prose is a contract: the header explains the
`seen` marker and does not promise a record once per session.
Cheap to forget and invisible when wrong: the menu would carry a marker
the header had never explained, and a reader would have to guess.
+3 -3
View File
@@ -1,8 +1,8 @@
"""A lesson's name is one claim on one line (#4797).
"""A lesson's name is one claim on one line.
`what` is the title every listing and menu prints. Written as the incident
instead of the claim, it turned a menu line into 1,416 characters with the
claim somewhere in the middle. Two halves are pinned: the doors refuse a new
instead of the claim, it turns a menu line into a paragraph with the claim
somewhere in the middle. Two halves are pinned: the doors refuse a new
one, and the menu shows one line for a name stored before they did.
"""
from __future__ import annotations
+3 -3
View File
@@ -1,4 +1,4 @@
"""Convergence named at the write (milestone 440, #4634).
"""Convergence named at the write.
When a lesson is answered "no rule fits", the response looks for other
no-rule lessons in the same situation and, once there are enough, names the
@@ -53,7 +53,7 @@ def test_one_incident_written_up_three_times_is_not_a_recurring_situation():
assert links_svc.convergence_group(same) is None
# ── The bar is relative to each lesson's own register (#5193) ──────────────
# ── The bar is relative to each lesson's own register ──────────────────────
def test_too_little_background_says_nothing_stands_out():
@@ -138,7 +138,7 @@ async def test_lessons_that_stand_out_together_are_named():
@pytest.mark.asyncio
async def test_a_large_no_rule_pool_in_one_flat_band_names_nothing():
"""The #5193 failure: every lesson answered "no rule", every score above
"""The flat-band failure: every lesson answered "no rule", every score above
a fixed bar, and a group the size of the pool. Nothing stands out of a
flat band, so nothing is named however large the pool grows."""
a, b, c = _run(no_rule=set(_NOTES), sim=lambda x, y: 0.66 + 0.01 * ((x + y) % 5))
+3 -3
View File
@@ -1,4 +1,4 @@
"""The lesson → rule link at its doors (milestone 440 step 1, #4630).
"""The lesson → rule link at its doors.
The link's own behaviour — states, cascade, ACL — is tested against Postgres in
tests/test_integration_lesson_rule_links.py. These pin the wiring a mock CAN
@@ -112,7 +112,7 @@ def test_verdicts_name_real_states():
def test_the_migration_check_and_the_model_agree_on_the_states():
"""Rule 36's drift, guarded: the CHECK is written from the migration's
"""Model/migration drift, guarded: the CHECK is written from the migration's
tuple and the code from the model's, so they must be the same tuple."""
path = Path(__file__).resolve().parents[1] / "alembic" / "versions" / "0111_lesson_rule_links.py"
spec = importlib.util.spec_from_file_location("m0111", path)
@@ -145,7 +145,7 @@ def test_backup_keeps_an_unjudged_link_unjudged():
assert built.judged_at is None
# ── #4631: judged when written ───────────────────────────────────────────────
# ── judged when written ──────────────────────────────────────────────────────
@pytest.mark.asyncio
+3 -3
View File
@@ -1,4 +1,4 @@
"""Both records show the lesson → rule link in the web UI (milestone 440, #4635).
"""Both records show the lesson → rule link in the web UI.
The frontend has no component test runner, so these read the source — and
each is written against something the backend owns, so a drift on either
@@ -122,7 +122,7 @@ def test_no_page_spells_its_own_write_check():
assert offenders == []
# ── #4658: the web editor records the answer ─────────────────────────────────
# ── the web editor records the answer ────────────────────────────────────────
def _payload_fn(view: str) -> str:
@@ -131,7 +131,7 @@ def _payload_fn(view: str) -> str:
def test_the_editor_sends_the_answer_under_the_names_both_routes_read():
"""Rule 33's contract check: a field the form sends under another name is
"""The contract check: a field the form sends under another name is
dropped without an error, and the lesson saves unjudged."""
from scribe.routes import lessons as routes
+1 -1
View File
@@ -1,4 +1,4 @@
"""Soft links — the counting rules (milestone 440, #4637).
"""Soft links — the counting rules.
What a suggested link's evidence counts is a design decision with a failure
mode on each side: count every arrival and two similar records "prove" a
+6 -6
View File
@@ -1,9 +1,9 @@
"""List tools return rows that say what a record is, never what it says (#4061).
"""List tools return rows that say what a record is, never what it says.
list_tasks once returned every row's to_dict(), body included. A project's todo
list came to 93-165k characters, past what an MCP client accepts as a tool
result, so the list arrived as a file to page through — the failure #4045 fixed
for enter_project, one call over. A list is for choosing which record to open;
A list that returns every row's to_dict(), body included, runs a project's todo
list to 93-165k characters, past what an MCP client accepts as a tool result,
so the list arrives as a file to page through — the same failure enter_project
avoids by carrying a summary. A list is for choosing which record to open;
get_task / get_note read one in full.
"""
import json
@@ -41,7 +41,7 @@ def test_a_task_row_names_what_it_is_and_where_it_sits_but_not_its_body():
assert row["milestone_title"] == "An existing plan is found"
assert {"id", "title", "status", "task_kind", "priority", "milestone_id",
"tags", "updated_at", "project_id"} <= set(row)
# Empty optional fields are left off rather than sent as null (#2483).
# Empty optional fields are left off rather than sent as null.
assert not {"description", "parent_id", "due_date"} & set(row)
assert row["updated_at"] == WHEN.isoformat()
+4 -4
View File
@@ -1,8 +1,8 @@
"""Every semantic search hands on the passage that matched (#4243, #4250).
"""Every semantic search hands on the passage that matched.
Three searches collapse chunk rows to the best one per record, so each of them
KNOWS which passage earned the hit. Each used to drop it, leaving every door to
preview the head of the document instead — a span the search had already scored
KNOWS which passage earned the hit. A search that drops it leaves every door to
preview the head of the document instead — a span the search has already scored
lower, with nothing saying so.
These pin the mechanism (`report["best_chunk"]` from all three searches) and
@@ -26,7 +26,7 @@ from tests.helpers import make_mock_session
def _searching(rows):
"""A patched session whose one query returns `rows` — the shared
make_mock_session (#2834) rather than a third local copy of the
make_mock_session rather than a third local copy of the
__aenter__/__aexit__ dance."""
session = make_mock_session()
result = MagicMock()
+1 -1
View File
@@ -1,4 +1,4 @@
"""Cross-family contracts for the `list_*` MCP tools (#2278, shape 4).
"""Cross-family contracts for the `list_*` MCP tools.
Per-tool tests cover what each list tool does. Nothing covered what the FAMILY
owes its callers, which is where the missing-sibling shape hides: a capability
+1 -1
View File
@@ -110,7 +110,7 @@ async def test_a_real_tools_refusal_reaches_the_caller():
assert "at least one verdict" in str(exc.value)
async def test_the_original_regression_create_note_with_content():
async def test_the_real_server_refuses_content_on_create_note():
"""Against the REAL server: `content=` on create_note
must raise before dispatch — naming the bad argument and listing `body`
among the accepted ones — instead of creating a body-less note."""
+1 -1
View File
@@ -1,4 +1,4 @@
"""The batch create door, and the guessed-id refusal at every create/update door (#4016).
"""The batch create door, and the guessed-id refusal at every create/update door.
What the unit lane can pin: the door refuses a guessed id BEFORE the duplicate
gate or any write, a batch is refused whole, and a malformed record fails with
+3 -3
View File
@@ -181,8 +181,8 @@ async def test_resolve_returns_serialised_tokens_with_their_provenance():
@pytest.mark.asyncio
async def test_update_token_can_clear_supersedes_with_an_empty_list():
"""`[]` means "this token replaces nothing after all" — a real edit. Guarded
on `is not None` so it isn't swallowed as "unchanged", the same trap #2077
recorded for update_snippet."""
on `is not None` so it isn't swallowed as "unchanged", the same trap
update_snippet guards against."""
with patch("scribe.mcp.tools.design_systems.ds_svc") as svc:
svc.update_token = AsyncMock(return_value=_fake_token())
from scribe.mcp.tools.design_systems import update_design_token
@@ -203,7 +203,7 @@ async def test_update_token_leaves_supersedes_alone_when_omitted():
@pytest.mark.asyncio
async def test_get_design_system_carries_the_chain_merged_guidance():
"""enter_project carries only a summary and points here (#4045). The
"""enter_project carries only a summary and points here. The
system's own `guidance` field is only its departures; a session building
UI needs the house style too, so the merged form rides alongside."""
from scribe.mcp.tools.design_systems import get_design_system
+1 -1
View File
@@ -126,7 +126,7 @@ async def test_get_milestone_returns_body_steps_and_rules():
assert out["milestone"]["body"] == "## Goal"
assert out["milestone"]["total"] == 1
assert [(s["id"], s["title"], s["status"]) for s in out["steps"]] == [(9, "step 1", "todo")]
# A step's body is get_task's job (#4061); its placement is not.
# A step's body is get_task's job; its placement is not.
assert "body" not in out["steps"][0]
assert out["steps"][0]["milestone_title"] == m.title
assert out["applicable_rules"] == [{"id": 1, "title": "r"}]
+2 -3
View File
@@ -32,9 +32,8 @@ async def test_start_planning_tool_delegates_to_service():
@pytest.mark.asyncio
async def test_start_planning_returns_the_plan_that_already_covers_it():
"""The FabledLibrarian failure (milestone 415): a session that could not see
the existing plan made a second one. Now it is handed the first, and
nothing is created."""
"""A session that cannot see the existing plan makes a second one. It is
handed the first instead, and nothing is created."""
match = {"duplicate": True, "existing_id": 12}
svc = AsyncMock()
with patch("scribe.mcp.tools.tasks.dedup_svc.plan_gate",
+2 -2
View File
@@ -36,7 +36,7 @@ async def test_create_process_sets_note_type():
async def test_create_process_blocks_a_near_duplicate():
"""The gate matters more for processes than for other kinds: each one becomes
a skill file that auto-surfaces, so two near-identical procedures don't just
bloat the corpus — they compete to be followed (#2250)."""
bloat the corpus — they compete to be followed."""
from scribe.services.dedup import DuplicateMatch
# The real dataclass, not a MagicMock: a mock answers every attribute, so it
@@ -166,7 +166,7 @@ async def test_delete_process_refuses_a_plain_note():
mock_delete.assert_not_awaited()
# ── the moments a procedure is for (milestone 458 step 5) ────────────────
# ── the moments a procedure is for ───────────────────────────────────────
@pytest.mark.asyncio
async def test_create_process_stores_the_moments_it_is_for():
+3 -4
View File
@@ -1,11 +1,10 @@
"""update_task closes a task with a one-line reminder of what the report needs.
The in-band half of milestone 409 step 3: the skill and static context only
This is the in-band half of reporting back: the skill and static context only
exist in the Claude Code plugin, and a tool response reaches every MCP client
at the moment a piece of work closes. Pinned on the response, not the wording.
The operator's own completion-report preferences used to ride here too, from
a fixed-question search (409 step 4). Milestone 500 retired that: they are
The operator's own completion-report preferences do not ride here: they are
mounted on finishing work and arrive in `moment_rules` beside the delivered
reply shape, so this response carries no key of its own for them.
"""
@@ -55,7 +54,7 @@ _OWED = {"idea_id": 9, "idea_title": "Signed APK lane", "project_id": 4,
async def test_closing_names_the_owed_adoptions_filed_while_the_task_was_open():
"""The finishing moment of milestone 463 step 6: owed family work filed
"""The finishing moment of family canon: owed family work filed
into a project during this task comes back for the report to name."""
from scribe.services.family_adoption import OWED_CUE
+1 -1
View File
@@ -141,7 +141,7 @@ async def test_delete_snippet_retires_or_raises():
doomed = MagicMock()
doomed.title = "debounce helper"
# The confirmation names what went (#3273) — after the delete there is
# The confirmation names what went — after the delete there is
# nothing left to look the title up from.
with patch("scribe.services.snippets.delete_snippet", AsyncMock(return_value=True)), \
patch("scribe.services.snippets.get_snippet", AsyncMock(return_value=doomed)):
+5 -5
View File
@@ -37,7 +37,7 @@ async def test_list_tasks_kind_empty_means_no_filter():
@pytest.mark.asyncio
async def test_create_task_passes_spike():
"""The kind a failed rule-check asks for (milestone 312).
"""The kind a failed rule-check asks for.
Time-boxed, and its output is knowledge rather than a change — filing one
as `work` makes a finished investigation look like an abandoned change.
@@ -51,12 +51,12 @@ async def test_create_task_passes_spike():
@pytest.mark.asyncio
async def test_update_task_can_re_file_a_kind():
"""A task's kind must be CORRECTABLE, not write-once (#3129).
"""A task's kind must be CORRECTABLE, not write-once.
What a piece of work turns out to be is often clear only once it is under
way. Before this, `update_task` had no `kind` parameter at all and the
REST route dropped the field the editor was already sending — the save
reported success and reverted on reload.
way. Without a `kind` parameter on `update_task`, and with a REST route that
drops the field the editor sends, the save reports success and reverts on
reload.
"""
mock = AsyncMock(return_value=fake_note(task_kind="spike"))
with patch("scribe.mcp.tools.tasks.notes_svc.update_note", mock):
+1 -1
View File
@@ -1,4 +1,4 @@
"""The DRY close-out measure (#4745).
"""The DRY close-out measure.
Stdlib-only and kept in scripts/ so any project can run a scratch copy of it,
so these tests import it by path rather than as a package.
+1 -1
View File
@@ -1,4 +1,4 @@
"""Which actions reach which moment (milestone 458 step 2) — the pure half.
"""Which actions reach which moment — the pure half.
Matching and resolution take no database: the defaults are code and an
install's mappings arrive as rows, so every case below hands `resolve` the
+3 -3
View File
@@ -1,4 +1,4 @@
"""The moment catalog — the vocabulary rules mount on (milestone 458 step 1).
"""The moment catalog — the vocabulary rules mount on.
What this pins is what a mount depends on: every name is well-formed and
resolvable, an unknown name is refused rather than stored, the catalog speaks
@@ -16,7 +16,7 @@ _NAME = re.compile(r"^[a-z]+\.[a-z]+$")
def test_the_catalog_is_not_empty():
"""The sweeps below are vacuous over an empty catalog (rule 167)."""
"""The sweeps below are vacuous over an empty catalog."""
assert len(moments.MOMENTS) >= 10
@@ -120,7 +120,7 @@ def test_the_tools_are_registered_and_classified():
def test_both_doors_read_one_catalog():
"""Rule 33 parity: the tool and the routes call the same services, so the
"""Door parity: the tool and the routes call the same services, so the
session and the Settings view cannot name different moments or disagree
about what a mapping does."""
from scribe.mcp.tools import moments as tool
+7 -7
View File
@@ -1,9 +1,9 @@
"""A record the operator names by number is looked up, not left to ranking (#4796).
"""A record the operator names by number is looked up, not left to ranking.
"yes go ahead with 4448" says exactly which record is meant, and the prompt
menu could not use it: a number means nothing to an embedding, so the menu
filled with whatever resembled the words around it. The parser cases below are
drawn from real operator prompts, and the half that must NOT be read as a
"yes go ahead with 4448" says exactly which record is meant, and a menu
built by ranking cannot use it: a number means nothing to an embedding, so the
menu fills with whatever resembles the words around it. The parser cases below
are drawn from real operator prompts, and the half that must NOT be read as a
reference matters as much as the half that must: every misread number is a
stranger's record dropped in front of the reader.
"""
@@ -181,8 +181,8 @@ async def test_an_id_the_reader_cannot_open_is_dropped():
@pytest.mark.asyncio
async def test_a_number_in_the_assistants_reply_is_not_named():
"""The query is enriched with the reply before a short prompt is searched
(#4364); only the operator's own words can name a record."""
"""The query is enriched with the reply before a short prompt is searched;
only the operator's own words can name a record."""
rec = _task(4448, "the named one")
out, _, _ = await _hint(
"sounds good", records={4448: rec},
+1 -1
View File
@@ -1,4 +1,4 @@
"""Platforms without a database (milestone 463 step 2).
"""Platforms without a database.
Detection is pure — a catalog and a list of repo paths in, platform ids out —
so it is pinned here against fixture trees. What it writes, and what it must
+2 -2
View File
@@ -1,4 +1,4 @@
"""The PreCompact hook steers the summary and never blocks the compaction (#3680).
"""The PreCompact hook steers the summary and never blocks the compaction.
The mechanism this pins was read out of the installed Claude Code build, not
out of the documentation, which describes a different one. Three facts decide
@@ -49,7 +49,7 @@ EVENT = {"session_id": "s1", "transcript_path": "/tmp/t.jsonl", "cwd": "/repo",
def _run(event: dict, tmpdir: str | None = None) -> subprocess.CompletedProcess:
"""Run the hook with its ledger directory ISOLATED.
The hook reads this session's rule ledgers since #4216, and they live under
The hook reads this session's rule ledgers, and they live under
`$TMPDIR/scribe-priorart`. Without an override these tests would read the
machine's real /tmp: on a developer box mid-session that is not empty, and
the output would depend on what some other session happened to leave
+3 -4
View File
@@ -2,11 +2,10 @@
THE PROBLEM THIS IS ABOUT. The agent has the record open; the operator does
not. `#3244` reads as complete to the writer and as homework to the reader,
who has to look it up to know what their own conversation is about. The
operator's words: *"I don't know what a note, task, or milestone is by its ID
number."*
who has to look it up to know what their own conversation is about. Nobody
knows what a note, task or milestone is by its id number.
PRODUCT, NOT A RULE (rule 119). Every Scribe user hits this, so the fix is in
PRODUCT, NOT A RULE. Every Scribe user hits this, so the fix is in
the surfaces the product ships — the skill that shapes how an agent writes,
and the tool responses that hand a record back. A per-instance rule would fix
it for one operator and leave the behaviour wrong for everyone else.
+5 -6
View File
@@ -1,11 +1,10 @@
"""Tests for task lifecycle timestamps and recurrence logic.
The note stand-ins come from `fake_note`, not a bare MagicMock. These four
predated that helper and broke the moment `update_note` started reading a
column they did not set (milestone 317): a fresh MagicMock is truthy on every
attribute, so `verify_with` read as "this record carries a check" and the
guard refused the write. That is note 2109's lesson, and the reason fake_note
exists — a stand-in has to be able to say NO.
The note stand-ins come from `fake_note`, not a bare MagicMock. A fresh
MagicMock is truthy on every attribute, so once `update_note` reads a column
the stand-in did not set, `verify_with` reads as "this record carries a check"
and the guard refuses the write. That is the reason fake_note exists — a
stand-in has to be able to say NO.
"""
from datetime import date, datetime, timezone
from unittest.mock import AsyncMock, MagicMock, patch
+3 -4
View File
@@ -1,7 +1,6 @@
"""The reply moment: the finished reply, checked before it goes out (milestone
458 step 4, folded in from milestone 456 step 8).
"""The reply moment: the finished reply, checked before it goes out.
The operator's ruling: an unopened rule above the stop bar HOLDS the reply
An unopened rule above the stop bar HOLDS the reply
once, in the server's words — the backstop for whatever the earlier arms
missed — and the rewrite is never held. These pin, with the database stubbed:
- which moments a reply reaches;
@@ -228,7 +227,7 @@ async def test_a_turn_that_closed_nothing_is_not_section_checked():
async def test_a_closing_turn_is_section_checked_and_both_holds_fold_into_one_reason():
"""One end-of-turn request (milestone 500 step 4): the section check and
"""One end-of-turn request: the section check and
the rule hold answer together, in one `reason`."""
body, _hold, check = await _reply_route(
{"reply": "done", "closed": 1, "closed_task_ids": [41, True, "x"], "rewrite": False},
+2 -2
View File
@@ -1,4 +1,4 @@
"""The core reply shape rides every turn (milestone 500 step 3).
"""The core reply shape rides every turn.
Every turn ends in a reply, and the operator's prompt is the last point before
it is written, so the per-turn retrieval carries the core shape ahead of
@@ -119,7 +119,7 @@ def test_the_core_goes_whole_once_then_as_a_reminder_then_whole_after_a_compacti
def test_the_hook_and_the_route_agree_on_the_ledgers_names():
"""Rule 33 across the seam: a renamed field fails silently."""
"""A contract across the seam: a renamed field fails silently."""
from scribe.routes import plugin as routes
hook = HOOK.read_text()
+2 -2
View File
@@ -1,4 +1,4 @@
"""Carrying a floor across a calibration change (#4104).
"""Carrying a floor across a calibration change.
WHAT THIS PINS
@@ -15,7 +15,7 @@ WHAT THIS PINS
mid-backfill would end up with every bar at zero.
4. **Every registry surface can be migrated.** A seventh arm that nobody adds
a re-scorer for is one whose floor silently cannot survive a model change.
5. **A half-migrated corpus is a refusal (#4132).** While any row the surface
5. **A half-migrated corpus is a refusal.** While any row the surface
searches is stamped with another model, a re-score measures a blend of two
geometries — so nothing is sampled until the backfill has finished.
"""
+2 -2
View File
@@ -1,4 +1,4 @@
"""The review pass: judging whether an injected line RELATED (#4772).
"""The review pass: judging whether an injected line RELATED.
Unit tests pin the refusals and the re-run's fidelity to the arm it replays.
Integration tests pin the parts a mock would make true by construction: which
@@ -60,7 +60,7 @@ async def test_the_re_run_searches_the_way_the_arm_does():
a menu nobody was shown. Both searches are RUN and their keywords
compared, so a change to the arm's spec fails here until the review
follows it — and the arm has to have filters for the check to mean
anything (rule 167)."""
anything."""
from scribe.services import retrieval_pipeline as rp
arm_kw: dict = {}
+4 -4
View File
@@ -167,8 +167,8 @@ async def test_injected_menu_labels_the_record_kind():
assert "[snippet]" in by_id[1]
assert "[process]" in by_id[2]
# Task-ness wins over note_type, and an issue says so rather than "task".
# A task also carries its status (#4154): #4 is DONE, and a line that said
# only "[task]" is what let a finished step be cited as an open one.
# A task also carries its status: #4 is DONE, and a line that says only
# "[task]" lets a finished step be cited as an open one.
assert "[issue (todo)]" in by_id[3]
assert "[task (done)]" in by_id[4]
assert "[note]" in by_id[5]
@@ -181,7 +181,7 @@ async def test_injected_menu_labels_the_record_kind():
def test_list_shaped_services_use_the_shared_visibility_clause():
"""Every service that LISTS notes must declare its reach through
notes_visibility_clause / browsable_notes_clause — never a bare owner
filter (#47, #2462).
filter.
Source inspection, because this is the shape no behavioural test catches:
an owner-only list returns correct-looking rows and simply omits the shared
@@ -204,7 +204,7 @@ def test_list_shaped_services_use_the_shared_visibility_clause():
or "browsable_notes_clause" in source
), (
f"{label} does not reference a shared visibility clause — a bare "
f"owner filter silently hides shared-project records (#2462)"
f"owner filter silently hides shared-project records"
)
# And it must not ALSO carry a bare owner filter on the main query,
# which would quietly re-narrow whatever the clause granted.
+6 -6
View File
@@ -1,8 +1,8 @@
"""The specs are the registry (milestone 456 step 5).
"""The specs are the registry.
Three lists used to be kept by hand beside the arms — the tuning table, the
ranked rows of the telemetry registry, and the rule-usage denominator — with
tests to make them agree. They are now read off the pipeline's specs. These
Three lists sit beside the arms — the tuning table, the ranked rows of the
telemetry registry, and the rule-usage denominator — and all three are read
off the pipeline's specs rather than kept by hand. These
pin the derivation itself: every arm is tunable, every ranked source is
measured with the declaration its spec carries, and the rule-usage
denominator counts exactly the rule surfaces a ranker chose.
@@ -34,7 +34,7 @@ def test_every_ranked_source_is_measured_as_its_spec_declares():
point = POINTS[spec.source]
assert point.kind == UNBIDDEN
assert point.what == d.what
# A quiet source must say why, and only a quiet source may (#2475).
# A quiet source must say why, and only a quiet source may.
assert point.expects_traffic == (not d.quiet_because)
assert point.quiet_because == d.quiet_because
@@ -59,7 +59,7 @@ def test_the_rule_denominator_is_every_rule_surface_a_ranker_chose():
def test_an_arm_added_without_tuning_would_be_caught():
"""Rule 167: the first test has to be able to bite. SURFACES skips an arm
"""The first test has to be able to bite. SURFACES skips an arm
with no tuning rather than raising, so an arm added without one would
silently be untunable — and the order-equality assertion is what notices,
as this replays with one such arm appended."""
+4 -4
View File
@@ -77,11 +77,11 @@ def test_service_functions_take_user_id():
def test_agent_and_web_surfaces_stay_at_parity():
"""The MCP tools and the REST routes are two callers of one service; a
capability on one has to exist on the other (rule #33).
capability on one has to exist on the other.
This guard exists because the snippet surfaces drifted apart once — MCP had
no delete, the web side had no near-duplicate gate — and neither failed
anything until someone went looking.
Two surfaces drift apart without failing anything — one door with no
delete, the other with no near-duplicate gate — until someone goes
looking.
"""
from scribe.mcp.tools import design_systems as tools
from scribe.routes import design_systems as routes
+1 -1
View File
@@ -1,4 +1,4 @@
"""Structural tests for the family blueprint (milestone 463 steps 3 and 4) — every
"""Structural tests for the family blueprint — every
endpoint is routed, and every write hands the service its caller, where the
note's write gate lives. What the writes do is in
tests/test_integration_family_promotion.py."""
+1 -1
View File
@@ -1,4 +1,4 @@
"""Structural tests for the platforms blueprint (milestone 463 step 2) —
"""Structural tests for the platforms blueprint —
registration and the route-to-service contract. What the writes do is in
tests/test_integration_platforms.py."""
import inspect
+4 -4
View File
@@ -1,4 +1,4 @@
"""Both rule-creation tools run the propose-then-approve loop (#3557).
"""Both rule-creation tools run the propose-then-approve loop.
WHY THIS EXISTS
@@ -38,8 +38,8 @@ prevents a rule earns more than any question that improves one's wording.
WHAT THIS PINS, AND WHAT IT DOES NOT
STRUCTURE, never wording — the same bargain the disambiguator guard (#3123)
strikes next door. Each element matches a family of synonyms, so the prose
STRUCTURE, never wording — the same bargain the disambiguator guard
(test_create_tools_disambiguate) strikes next door. Each element matches a family of synonyms, so the prose
stays free to be rewritten, reordered or sharpened; only DELETING one fails.
Pinning phrasing would make every improvement a red build, and a test that
punishes editing is a test someone deletes.
@@ -76,7 +76,7 @@ _ELEMENTS = {
),
"how it would be enforced": ("enforc",),
"the answers offered back": ("as written", "talk about it", "discuss"),
# The fourth and fifth answers (#3733, #3896). They are what makes the
# The fourth and fifth answers. They are what makes the
# question a ROUTING question rather than a yes/no one: an observation
# that turns out to be a preference or a lesson has found its home, not
# failed to become a rule. Written in one pass, because two people each
+2 -2
View File
@@ -1,4 +1,4 @@
"""Every door that writes a rule can mount it on moments (milestone 458 step 3).
"""Every door that writes a rule can mount it on moments.
THE SHAPE, NOT ONE FUNCTION. test_system_tagging_door_parity records how a
capability goes missing: whichever door nobody exercised for a kind never
@@ -80,7 +80,7 @@ def test_every_rest_rule_door_takes_moments_and_checks_them_first(handler, write
def test_the_guard_can_fail():
"""A door with no check at all must be reported, not skipped (rule 167)."""
"""A door with no check at all must be reported, not skipped."""
fn = ast.parse("async def f():\n await svc.create_rule()\n").body[0]
assert _first_line_calling(fn, "require_moments") is None
+3 -3
View File
@@ -1,8 +1,8 @@
"""Moment proposals and judgments — what a mock can see (milestone 458 step 7).
"""Moment proposals and judgments — what a mock can see.
The behaviour against real tables is tests/test_integration_rule_moment_judgments.py.
These pin the pieces that need no database: that the migration's CHECKs and
the model agree (rule 36), which moments count as evidence, and that the line
the model agree, which moments count as evidence, and that the line
the signal hands a session names the tool that answers it.
"""
from __future__ import annotations
@@ -61,7 +61,7 @@ def test_the_proposal_line_names_the_tool_that_answers_it():
assert "judge_rule_moments" in line and '"reject"' in line
# ── The misfire (step 7b) ────────────────────────────────────────────────────
# ── The misfire ──────────────────────────────────────────────────────────────
def test_a_day_is_one_situation_and_days_are_distinct():
+2 -2
View File
@@ -1,4 +1,4 @@
"""The plugin half of the rule↔moment signal (milestone 458 step 7), run for real.
"""The plugin half of the rule↔moment signal, run for real.
`scribe_moment.sh` writes every call to an acts ledger; `scribe_record_opened.sh`
sends the last few minutes of it with the rule a session opened; the server
@@ -158,7 +158,7 @@ def test_session_end_removes_the_sessions_moment_files_and_no_others(sandbox):
def test_the_hook_and_the_route_agree_on_the_body():
"""Rule 33 across the shell/Python seam: a renamed field fails silently."""
"""A contract across the shell/Python seam: a renamed field fails silently."""
src = (HOOKS / "scribe_record_opened.sh").read_text()
route = inspect.getsource(plugin_routes.rule_opened)
assert "/api/plugin/rule-opened" in src
+1 -1
View File
@@ -1,4 +1,4 @@
"""Rule and preference creates surface the records they overlap (#4134).
"""Rule and preference creates surface the records they overlap.
Rules and preferences share one table and one ranking, so a preference that
restates a rule splits one instruction in two — and when only the preference
+3 -3
View File
@@ -1,4 +1,4 @@
"""A rule cannot be created, or edited into, having no trigger (#4099).
"""A rule cannot be created, or edited into, having no trigger.
WHY THIS EXISTS
@@ -18,10 +18,10 @@ WHAT THIS PINS
The guard lives in the SERVICE, because both doors reach it: the MCP tools and
the frontend's fast path in `routes/rulebooks.py`. A guard written in either
one alone would leave the other able to create a rule that never fires, which
is the failure mode this whole change exists to remove.
is the failure mode this guard exists to remove.
These call the service directly and assert on the refusal, so an empty guard
cannot make them pass by accident (rule 167).
cannot make them pass by accident.
"""
from __future__ import annotations
+5 -5
View File
@@ -1,4 +1,4 @@
"""A rule reached through its lessons (milestone 440, #4633).
"""A rule reached through its lessons.
The step runs after each of the three rule arms. These pin, with the database
and the embedder stubbed: that nothing is searched when no lesson carries a
@@ -125,7 +125,7 @@ async def test_a_failure_brings_no_rule_and_raises_nothing():
@pytest.mark.asyncio
async def test_the_step_follows_the_arm_on_its_query_and_skips_what_it_named():
"""One composer for all three rule builders (milestone 456 step 6): the
"""One composer for all three rule builders: the
via-lesson step searches with the query the arm searched with, skips the
session's ledger and every rule the arm's band named, and its lines and
fresh ids join the arm's — while `shown_rule_ids` stays the direct band."""
@@ -153,9 +153,9 @@ async def test_a_blank_prompt_runs_neither_the_arm_nor_the_step():
def test_only_the_composer_runs_a_rule_arm_or_the_via_lesson_step():
"""Structural (rule 167): every rule builder goes through `_rule_moment`,
so a builder that runs the arm or the via-lesson step by hand again is the
copy this step removed coming back — and a count is what lets it fail."""
"""Structural: every rule builder goes through `_rule_moment`, so a
builder that runs the arm or the via-lesson step by hand is a second copy
of the composer — and a count is what lets it fail."""
import ast
from pathlib import Path
+4 -5
View File
@@ -1,9 +1,8 @@
"""`.scribe` — the second key a directory can be scoped by (#4085).
"""`.scribe` — the second key a directory can be scoped by.
Every hook in `plugin/hooks/` scopes its request to a project, and until this
existed all six turned on one key: `git remote get-url origin`. That key does
not exist outside a git repo, so a session in a plain directory was unscoped
everywhere at once — silently, because a missing remote is indistinguishable
Every hook in `plugin/hooks/` scopes its request to a project. With only one
key, `git remote get-url origin`, a session outside a git repo has no key at
all, so a session in a plain directory is unscoped everywhere at once — silently, because a missing remote is indistinguishable
from a remote nobody bound.
`scribe_scope_query` is the shared answer, so these run the real shell rather
+3 -3
View File
@@ -1,8 +1,8 @@
"""/api/search — the kind filter it offers, and what it does with a bad one.
This route used to carry its own two-value map (`note`/`task`/everything else
is `all`), which made it the third hand-kept copy of a vocabulary that lives in
one table. Both halves of #4250 are pinned here: every declared kind is
The route reads its kinds from the one table the vocabulary lives in, rather
than keeping a map of its own (`note`/`task`/everything else is `all`). Both
halves are pinned here: every declared kind is
reachable, and a kind that does not exist is refused rather than widened.
"""
import pytest
+3 -3
View File
@@ -1,4 +1,4 @@
"""Every semantic search scopes first, then ranks (#4958, #4961).
"""Every semantic search scopes first, then ranks.
Ordered straight off a `*_embeddings` table, the planner walks the HNSW index:
it takes ~`hnsw.ef_search` (40) nearest chunks from every owner and project
@@ -59,8 +59,8 @@ def test_every_semantic_search_ranks_through_the_scoped_shape():
assert "order_by" not in calls, f"{name} orders by distance itself"
def test_the_old_shape_would_be_caught():
"""Rule 167: replay the guard against the query every search used to run."""
def test_a_rank_then_scope_query_is_caught():
"""Replay the guard against a query that ranks first and scopes after."""
old = ast.parse(
"async def semantic_search_x():\n"
" rows = await session.execute(select(Note, distance).select_from(E)"
+1 -1
View File
@@ -5,7 +5,7 @@ list query can't use. These express the same resolution as set membership. They
are pure SQL builders — group membership is a subquery rather than a fetched
list, so they need no session and callers' unit tests need not know they exist.
Two scopes, deliberately different (decision note 2094):
Two scopes, deliberately different:
readable_* — everything the ACL permits, for explicit acts (a typed search, a
fetch by id).
browsable_* — owner + project access only, for passive surfaces (browse lists,
+2 -3
View File
@@ -1,8 +1,7 @@
"""The canonical-slug matcher — the line between what maps mechanically and
what a human is asked to confirm (milestone 307, note 3026).
what a human is asked to confirm.
These cases are the real drift found across the author's own instance: one area
carrying three spellings, and two areas that LOOK alike and are not the same.
These cases are real-world drift: one area carrying three spellings, and two areas that LOOK alike and are not the same.
Getting the boundary wrong in either direction is a silent failure — a missed
mapping nobody thinks to make again, or a cross-project record surfacing in the
wrong project.
+1 -1
View File
@@ -18,7 +18,7 @@ def test_task_row_maps_fields():
n.priority = None
# Named, not left to MagicMock: an unset attribute is a truthy Mock, so
# the assertion would pass on a field the row never really carried
# (note 2109 — the reason fake_note exists).
# (the reason fake_note exists).
n.task_kind = "spike"
assert _task_row(n) == {
"id": 5, "title": "Wire reminders", "status": "in_progress",
+1 -1
View File
@@ -185,7 +185,7 @@ async def test_clearing_a_projects_design_system_skips_the_system_acl():
acc.can_read_design_system.assert_not_awaited()
# --- resolution (step 2) ----------------------------------------------------
# --- resolution -------------------------------------------------------------
@pytest.mark.asyncio
async def test_resolve_denied_returns_none_not_an_empty_set():
+7 -9
View File
@@ -1,11 +1,10 @@
"""get_knowledge_counts — one number per facet, and an honest total.
Two grouped queries, one per typing axis. It used to be three: a grouped count
over note_type restricted to ("note", "process"), a scalar count of tasks, and
a second scalar just for plans. That shape is why `issue` had no number —
every kind needed a query of its own and nobody added one — and why the "All"
chip sat ~90 below the list it labelled, since snippets were in the feed but
in no count (#3128).
Two grouped queries, one per typing axis. A query per kind — a grouped count
over note_type restricted to ("note", "process"), a scalar count of tasks, a
second scalar just for plans — leaves every new kind without a number until
someone adds its query, and puts the "All" chip below the list it labels,
since snippets are in the feed but in no count.
"""
from unittest.mock import AsyncMock, MagicMock, patch
@@ -71,9 +70,8 @@ async def test_total_counts_snippets_and_counts_no_task_twice():
async def test_absent_facets_report_zero_rather_than_missing():
counts, _ = await _counts([("note", 1)], [])
assert counts["note"] == 1
# `lesson` is here from milestone 385 step 2: a kind added to the facet
# table and not to the counts would show an empty chip beside a feed that
# has rows in it, which is defect 3b in #3161 repeating itself.
# A kind added to the facet table and not to the counts would show an
# empty chip beside a feed that has rows in it.
for key in ("process", "snippet", "lesson", "task", "work", "issue", "spike", "plan"):
assert counts[key] == 0, key
assert counts["total"] == 1
+3 -3
View File
@@ -1,4 +1,4 @@
"""The note staleness sweep (milestone 317 step 3).
"""The note staleness sweep.
The query is a sibling of the rules sweep, not a shared implementation — a
rule scopes by rulebook ownership, a note by the note ACL, so only the
@@ -103,7 +103,7 @@ async def test_trashed_notes_are_excluded():
@pytest.mark.asyncio
async def test_the_scope_is_browse_not_read():
"""Decision note 2094: a sweep is a PASSIVE surface, so a record shared
"""A sweep is a PASSIVE surface, so a record shared
one-to-one must not arrive in one unasked. `note_shares` is the tell —
its presence would mean the read scope leaked in."""
sql = await _sweep_sql()
@@ -207,7 +207,7 @@ async def test_a_note_with_no_check_cannot_be_verified():
@pytest.mark.asyncio
async def test_a_reader_cannot_stamp():
"""Stamping is a mutation (rules 47/78) — an editor-share holder may make
"""Stamping is a mutation — an editor-share holder may make
it, a viewer may not."""
note = fake_note(verify_with="curl the docs", verified_at=None)
out, _ = await _mark(note, writable=False)
+2 -2
View File
@@ -1,6 +1,6 @@
"""A note's own check — verify_with / expires_when (milestone 317 step 2).
"""A note's own check — verify_with / expires_when.
Three behaviours the rules path (#3096) had to get right and this one inherits:
Three behaviours this path shares with a rule's check:
empty means NULL, clearing is explicit, and rewriting the check drops the
stamp. Plus one this path adds: not every record may carry a check, and the
rule is an INVARIANT over the record rather than a filter on the write.
+8 -8
View File
@@ -1,10 +1,10 @@
"""Batched project summaries (#2384).
"""Batched project summaries.
The bug was not wrong output — it was CONNECTION COUNT. A per-project summary
opened its own session and then one more per milestone, and the route fanned
that out with asyncio.gather. For 25 projects that asked for roughly 250
checkouts against a pool of 15, so most waited out the 30-second timeout and
every other route on the instance queued behind them:
The property is CONNECTION COUNT, not output. A per-project summary that opens
its own session and then one more per milestone, fanned out with
asyncio.gather, asks 25 projects for roughly 250 checkouts against a pool of
15, so most wait out the 30-second timeout and every other route on the
instance queues behind them:
QueuePool limit of size 5 overflow 10 reached, connection timed out
GET /api/settings 500 30584.0ms
@@ -12,7 +12,7 @@ every other route on the instance queued behind them:
So these tests assert the number of sessions opened, not only the values
returned. A version that produced identical output while opening a session per
project would pass a correctness test and reproduce the outage.
project would pass a correctness test and exhaust the pool.
"""
from datetime import datetime, timezone
from unittest.mock import AsyncMock, MagicMock, patch
@@ -118,7 +118,7 @@ async def test_milestone_summaries_for_many_projects_open_ONE_session():
assert opened[0] == 1
# Touched is the later of the milestone's own edit and its newest step
# update (#4045): steps closing today count, an old step doesn't pull it back.
# update: steps closing today count, an old step doesn't pull it back.
assert out[1][0]["last_touched_at"] == step_closed.isoformat()
assert out[2][0]["last_touched_at"] == written.isoformat()
assert out[1][0]["completed"] == 2 and out[1][0]["total"] == 3
+2 -2
View File
@@ -1,6 +1,6 @@
"""Guessed record ids are refused; `{{ref:N}}` placeholders resolve (#4016).
"""Guessed record ids are refused; `{{ref:N}}` placeholders resolve.
The defect: a session predicts the ids its next creates will get and writes
The failure: a session predicts the ids its next creates will get and writes
them into a body before the records exist; any other session's create takes
those numbers. These pin the recogniser — what counts as a `#N`, where a guess
can sit relative to the highest id — and the placeholder rewrite the batch
+4 -4
View File
@@ -1,7 +1,7 @@
"""The report-shape check (milestone 409 step 5): which completion sections a
task-closing reply lacks, the words it is sent back with, and the outcome
record. Since milestone 500 step 4 the check itself runs here, inside the one
end-of-turn request, rather than in a Stop hook of its own."""
"""The report-shape check: which completion sections a task-closing reply
lacks, the words it is sent back with, and the outcome record. The check runs
here, inside the one end-of-turn request, rather than in a Stop hook of its
own."""
import json
from unittest.mock import AsyncMock, patch
+1 -1
View File
@@ -1,4 +1,4 @@
"""The server half of the end-of-turn shape check (milestone 439): what the
"""The server half of the end-of-turn shape check: what the
agent is asked, what the hook's ledger may say, and the outcome record."""
import json
from unittest.mock import patch
+2 -2
View File
@@ -61,7 +61,7 @@ async def test_count_open_issues_denied_returns_zero():
assert result == 0
# ── assess_system_name — the one gate both doors call (milestone 307) ──
# ── assess_system_name — the one gate both doors call ──
@pytest.mark.asyncio
async def test_assess_matches_an_existing_system_on_case_and_spacing():
@@ -128,7 +128,7 @@ async def test_assess_says_nothing_about_a_nameless_system():
assert await assess_system_name(1, 5, " ") == {"duplicate": None, "canonical": None}
# ── path patterns — the files that ARE the area (milestone 444, #4756) ──
# ── path patterns — the files that ARE the area ──
def test_patterns_are_tidied_to_repo_relative_and_deduplicated():
from scribe.services.systems import normalize_path_patterns
-1
View File
@@ -49,7 +49,6 @@ async def test_delete_project_cascades_to_notes_milestones_and_project_rules():
# exists-check + 4 cascade ops:
# notes (soft) → milestones (soft) → project-scoped rules (soft) →
# project (soft)
# The two suppression hard-DELETEs went with their tables (milestone 414).
session.execute = AsyncMock(side_effect=[
_exists_result(True),
MagicMock(), MagicMock(), MagicMock(),
+7 -7
View File
@@ -1,11 +1,11 @@
"""A SessionStart that loads no project says why, and names the first move (#4366).
"""A SessionStart that loads no project says why, and names the first move.
The fetch used to be `curl -f … || body=""`: a timeout, an HTTP error and a
refused key all produced one sentence, which ended by suggesting
`enter_project()` "as needed". A session that read it as optional started with
no recent milestones or open tasks, and so had no way to know which prior work
existed to look for. These pin the two halves of the fix on the path that needs
no server: the cause is named, and the fallback is a concrete first step.
A fetch written as `curl -f … || body=""` collapses a timeout, an HTTP error
and a refused key into one sentence, and a fallback that suggests
`enter_project()` "as needed" reads as optional. A session that skips it starts
with no recent milestones or open tasks, and so has no way to know which prior
work exists to look for. These pin both halves on the path that needs no
server: the cause is named, and the fallback is a concrete first step.
"""
from __future__ import annotations
+1 -1
View File
@@ -1,4 +1,4 @@
"""The end-of-turn shape check (milestone 439): the write hooks note what each
"""The end-of-turn shape check: the write hooks note what each
write defined, and the Stop hook asks the instance which of those nobody has
judged, blocking once in the server's words.
+3 -3
View File
@@ -311,10 +311,10 @@ def test_a_resemblance_score_must_positively_agree() -> None:
assert not _stamp_allowed(1, "type", "fn")
def test_the_portal_case_no_longer_stamps_one_canon_onto_a_whole_file() -> None:
def test_one_canon_is_not_stamped_onto_a_whole_file() -> None:
"""A class, an async method and a sync helper in one file, against one
payload-level score. Previously all three were stamped; now the score
alone cannot carry any of them, and only a matching form could."""
payload-level score. The score alone cannot carry any of them; only a
matching form can."""
canon = "async-fn"
written = [("class SessionAbsent(RuntimeError):", False),
("async def attach(self) -> None:", True),
+1 -1
View File
@@ -85,7 +85,7 @@ async def test_merge_requires_write_on_target():
@pytest.mark.asyncio
async def test_merge_skips_sources_owned_by_someone_else():
"""Cross-owner merge stays out of scope (#231): a source belonging to a
"""Cross-owner merge stays out of scope: a source belonging to a
different owner than the target is skipped, not silently folded in and
trashed."""
from scribe.services import snippets as svc
+1 -1
View File
@@ -1,4 +1,4 @@
"""The bundled skills declare the moments they are for (milestone 458 step 5).
"""The bundled skills declare the moments they are for.
Loading the reporting procedure is a report; loading the planning procedure
is planning. Each SKILL.md says which moment it is for in its frontmatter
+2 -2
View File
@@ -1,4 +1,4 @@
"""Tests for the drift check (#2086) — does a snippet still match its source?
"""Tests for the drift check — does a snippet still match its source?
The check itself runs agent-side (Scribe has no checkout), so what's testable
here is everything around it: the verdict's shape, the rule that a verdict
@@ -175,7 +175,7 @@ def test_python_dialect_on_a_row_with_no_data_at_all():
[("ok", False), ("attention", True), ("drifted", False), ("unverified", False)],
)
def test_python_dialect_on_a_push_invalidated_ok_verdict(value, expected):
"""#2691: a push touched the recorded location since the verdict. Like the
"""A push touched the recorded location since the verdict. Like the
expired case, it is neither drifted (nothing found wrong) nor unverified (a
check happened) — but the repo moved under the blessing, so `attention`
must include it and `ok` must not."""
+2 -2
View File
@@ -1,4 +1,4 @@
"""Merge records what it absorbed (#2087).
"""Merge records what it absorbed.
Merging keeps the target's scalar fields, unions locations + tags, and trashes
the sources. Without provenance the fact that a variant ever existed survives
@@ -59,7 +59,7 @@ async def test_merge_accumulates_across_merges():
@pytest.mark.asyncio
async def test_merge_does_not_record_a_skipped_cross_owner_source():
"""A source owned by someone else is skipped, not folded in (#231) — so it
"""A source owned by someone else is skipped, not folded in — so it
must not appear in the provenance either, or the record would claim to
contain something it never absorbed."""
kwargs = await _run_merge(
+6 -6
View File
@@ -1,10 +1,10 @@
"""A snippet's notes — the explanation that had nowhere to go (#4378).
"""A snippet's notes — where the explanation goes.
Before this field, a snippet had name, code, signature, locations and
`when_to_use`, and nothing for prose. What a session learned about a snippet
went into `when_to_use` — the trigger, joined onto the title of every chunk the
snippet is embedded as — so a multi-paragraph write-up there blurred the one
situation it is ranked on.
A snippet has name, code, signature, locations and `when_to_use`. Without a
place for prose, what a session learned about a snippet goes into
`when_to_use` — the trigger, joined onto the title of every chunk the snippet
is embedded as — so a multi-paragraph write-up there blurs the one situation
it is ranked on.
The rules pinned here, each with a way to rot silently:
+3 -3
View File
@@ -1,7 +1,7 @@
"""Tests for un-merge (#2165) — reversing one source out of a merged survivor.
"""Tests for un-merge — reversing one source out of a merged survivor.
The design question this task said to settle first was how to subtract without
stripping call sites the survivor legitimately owns. The answer is per-source
The design question is how to subtract without stripping call sites the
survivor legitimately owns. The answer is per-source
attribution recorded AT MERGE TIME: each `merged_from` entry holds only what
that source actually added. These tests pin that rule, and the refusal that
guards the case where the attribution isn't there.
+1 -1
View File
@@ -1,4 +1,4 @@
"""An area's rulings reach the work that touches its files (milestone 444, #4757).
"""An area's rulings reach the work that touches its files.
These pin:
+3 -3
View File
@@ -9,8 +9,8 @@ and means nothing. The signal is not "has a check" — it is "has a check AND
almost nothing else does".
So the guidance is not decoration on this feature, it IS the feature's
precondition, and rule 119 puts it in the tool docstrings and the skill rather
than in a Scribe rule. That makes it exactly the kind of prose a later
precondition, and product guidance lives in the tool docstrings and the skill
rather than in a Scribe rule. That makes it exactly the kind of prose a later
docstring tidy-up deletes without noticing what it was for.
WHAT THIS PINS, AND WHAT IT DOES NOT
@@ -94,7 +94,7 @@ def test_the_skill_carries_the_test_a_writer_can_actually_apply():
from tests.helpers import skill_text
# Reference files included: the full statement sits in writing-records.md
# and SKILL.md keeps the pointer (#4398).
# and SKILL.md keeps the pointer.
text = " ".join(skill_text("using-scribe").split())
assert "could this note become false without anyone editing it" in text.lower(), (
"the using-scribe skill no longer carries the one-question test. That "
-2
View File
@@ -1,7 +1,5 @@
"""Tests for the prune logic — both rolling (pin_kind IS NULL) and the
auto-pin bucket (pin_kind='auto').
Design: docs/superpowers/specs/2026-05-13-note-version-pinning-design.md
"""
from unittest.mock import AsyncMock, MagicMock, patch
from tests.helpers import make_mock_session
+4 -4
View File
@@ -1,8 +1,8 @@
"""The write-path design arm: a UI write is told which design system binds it (#4256).
"""The write-path design arm: a UI write is told which design system binds it.
A design system binds like a rule, and before this it reached a session only
through the session-start block — complete for a session that knows to ask,
silent for one writing a component. These tests pin:
A design system binds like a rule. Delivered only through the session-start
block, it is complete for a session that knows to ask and silent for one
writing a component. These tests pin:
- THE TRIGGER is the file, not a search: a UI path in a project that has a
design system. No vectors, no score, no slot taken from the ranked menu.
+5 -4
View File
@@ -1776,14 +1776,15 @@ async def test_a_record_this_arm_withheld_itself_is_not_a_near_miss():
@pytest.mark.asyncio
async def test_a_ledger_repeat_no_longer_produces_that_case_at_all():
async def test_a_ledger_repeat_is_counted_not_withheld():
"""The other direction: a ledger repeat is not withheld.
A record on the session ledger now clears the search, is rendered with a
A record on the session ledger clears the search, is rendered with a
marker, and is counted in `suppressed` — so nothing is withheld and the
reported score describes the bar. Pinned because the tempting shortcut is
to null the score whenever the ledger matches, which would delete the near-miss measurement on exactly the calls
where the bar is most worth reading.
to null the score whenever the ledger matches, which would delete the
near-miss measurement on exactly the calls where the bar is most worth
reading.
"""
rec = MagicMock()
row = await _write_path_row(rec, exclude_ids=[7])