Compare commits

...
2 Commits
Author SHA1 Message Date
bvandeusenandClaude Opus 5.5 63e97a6f0f docs(6209): seventeen more test files — lesson shape and round trip, rule scope/moments/kind/surfacing, drift, deadlock race, backup, inception, backfill, claims, supersession, ledgers, family shapes
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / integration (push) Successful in 1m7s
CI & Build / Python tests (push) Successful in 1m54s
CI & Build / Build & push image (push) Successful in 12s
Docstrings and comments state the property and its reason in the present
tense. The session-context ledger test no longer describes the decision it
"used to pin"; the supersession tests state demote-not-filter without the
quote; one-row measurements become the general shape with their figures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 21:37:15 -04:00
bvandeusenandClaude Opus 5.5 ba0b56018e docs(6209): calibration stamp, after-write hook, usage seam, work log, duplicates, skill structure, tuning and rulebook routes, kind surface, prompt filter, moments, strict args, auth, lesson write path
Fourteen more test files: docstrings state the failure each guard defends
as a present-tense shape instead of the incident that found it, and issue,
milestone and rule numbers leave comments and assertion messages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 21:35:44 -04:00
31 changed files with 185 additions and 227 deletions
+6 -6
View File
@@ -1,4 +1,4 @@
"""The PostToolUse after-write hook (#2901): code written through Bash — sed,
"""The PostToolUse after-write hook: code written through Bash — sed,
heredocs, scripts — gets the same prior-art / ledger checks as a Write/Edit.
Runs the real shell against a temp git repo and a throwaway HTTP sink, like
@@ -103,7 +103,7 @@ def test_after_write_is_silent_where_it_has_nothing_to_say(tmp_path):
assert _run(loose, env, session="s-loose") == ""
# A change that defines nothing (prose, a call-site edit) → nothing, even
# with the server unreachable (port 9 refuses): no definitions, no call
# owed, so not even the #2932 outage line. (a.css above is removed first:
# owed, so not even the outage line. (a.css above is removed first:
# it DOES define a shape, and an unanswered call for it would rightly speak.)
(repo / "a.css").unlink()
(repo / "README.md").write_text("# notes\n")
@@ -112,9 +112,9 @@ def test_after_write_is_silent_where_it_has_nothing_to_say(tmp_path):
def test_after_write_local_arm_works_without_a_server_and_says_the_server_did_not_answer(tmp_path):
"""The local by-name arm needs no instance (#2280). A configured instance
that does not ANSWER (a refused connection stands in for it) is said, once
per outage (#2932) — and the record nudge, which claims "nothing recorded",
"""The local by-name arm needs no instance. A configured instance that
does not ANSWER (a refused connection stands in for it) is said, once per
outage — and the record nudge, which claims "nothing recorded",
is withheld: no answer backs that claim."""
env = _env(tmp_path)
repo = _repo(tmp_path, env)
@@ -138,7 +138,7 @@ def test_after_write_local_arm_works_without_a_server_and_says_the_server_did_no
def test_after_write_unconfigured_install_owes_no_call_and_keeps_the_record_nudge(tmp_path):
"""No URL/token → no call was owed, so nothing is "unreached"; the local
arm and the record nudge (#2664) stand on their own, as before."""
arm and the record nudge stand on their own."""
env = {k: v for k, v in _env(tmp_path).items() if k not in ("SCRIBE_URL", "SCRIBE_TOKEN")}
repo = _repo(tmp_path, env)
(repo / "d.py").write_text("def slug(t):\n return t.lower()\n")
+4 -4
View File
@@ -1,4 +1,4 @@
"""A backfill repairs to current truth; it does not replay a snapshot (#4262).
"""A backfill repairs to current truth; it does not replay a snapshot.
The bug these pin: every backfill used to SELECT the text alongside the id and
then iterate that snapshot, sleeping between records. A run over a real corpus
@@ -90,8 +90,8 @@ async def test_a_record_deleted_between_the_scan_and_the_loop_is_skipped():
@pytest.mark.asyncio
async def test_a_record_whose_text_outran_its_vectors_is_re_embedded():
"""The repair half (#4202 — a guard that refuses to write a bad value does
not undo the bad values already stored). Note 5 IS at the current version,
"""The repair half: a guard that refuses to write a bad value does not
undo the bad values already stored. Note 5 IS at the current version,
so the version comparison alone would skip it forever; its text is newer
than its vectors, which is the only signal that separates "embedded from
stale text" from "embedded correctly"."""
@@ -131,7 +131,7 @@ async def test_a_task_logged_since_its_vectors_is_re_embedded():
@pytest.mark.asyncio
async def test_a_corpus_that_is_genuinely_current_embeds_nothing():
"""The guard must be able to stay quiet, or it is not a guard (#167) — and
"""The guard must be able to stay quiet, or it is not a guard — and
a backfill that re-embeds everything every boot is a different bug."""
scan = _scan_session(all_ids=[1, 2, 3], current_ids=[1, 2, 3])
+6 -8
View File
@@ -1,10 +1,9 @@
"""A tuned number carries the space it was measured in — and only that (#4104).
"""A tuned number carries the space it was measured in — and only that.
WHY THIS EXISTS
Milestone 416 step 6 answers *"a path for thresholds to be inherited by the next
version or different model so that they don't have to recalibrate a lot."* The
mechanism is a stamp: every tuning event records the embedding model and
A threshold should be inheritable by the next version or a different model
without recalibrating from scratch. The mechanism is a stamp: every tuning event records the embedding model and
document shape the number was chosen under, and a mismatch is reported rather
than left to be noticed.
@@ -49,7 +48,7 @@ def test_calibration_stamp_names_the_embedder_and_the_shape():
"embedding_model": emb.EMBEDDING_MODEL,
"shape_version": emb.CHUNKER_VERSION,
}
# Two keys, never one fused string (rule 149): a mismatch has to be able to
# Two keys, never one fused string: a mismatch has to be able to
# say WHICH half moved, because a new embedder and a re-cut document
# invalidate the same numbers for different reasons.
assert len(stamp) == 2
@@ -136,7 +135,7 @@ def test_a_dial_moved_before_stamps_existed_is_unknown_not_fine():
def test_a_model_change_and_a_shape_change_are_reported_apart():
"""Rule 149. Two conditions, two answers — they call for different work."""
"""Two conditions, two answers — they call for different work."""
s = get_surface("prompt_rule")
live = {"embedding_model": "new/model", "shape_version": 1}
cal = rt._calibration(_row(model="old/model", shape=1), s, live)
@@ -182,8 +181,7 @@ def test_the_tool_says_nothing_auto_retunes():
"""The contract an agent reads. A stale stamp is a prompt to MEASURE.
Without this, the obvious next move on seeing `stale: true` is to move the
dial — which is tuning from a statistic, the exact failure #4102 measured
pointing the wrong way.
dial — which is tuning from a statistic, which can point the wrong way.
"""
doc = tool_doc("scribe.mcp.tools.retrieval_tuning", "retrieval_surfaces")
assert "calibration" in doc
+4 -5
View File
@@ -1,10 +1,9 @@
"""The shape ledger judging against family ideas, without a database
(milestone 463 step 5).
"""The shape ledger judging against family ideas, without a database.
Pinned here: what a project's shapes say about an idea, when an answer
contradicts them, which reference a shape in a given language is judged
against, and the family arm's rule — measured on 2026-10-06 (#4991), and
replayed below as the table it was chosen from. The two ledgers moving
against, and the family arm's rule — measured, and replayed below as the
table it was chosen from. The two ledgers moving
together against Postgres are in tests/test_integration_family_adoption.py.
"""
from __future__ import annotations
@@ -95,7 +94,7 @@ OWN, REF, OTHER = 1, 2, 3
@pytest.mark.parametrize("name,hits,expected", [
# #4991's pairs, the reference as REF and the best other hit as OTHER.
# The measured pairs, the reference as REF and the best other hit as OTHER.
("A py->go", [(0.728, REF), (0.703, OTHER)], "family"),
("B py->go", [(0.715, REF), (0.675, OTHER)], "family"),
("D kt->kt", [(0.816, REF), (0.734, OTHER)], "family"),
+7 -8
View File
@@ -1,11 +1,10 @@
"""Project inception (milestone 297) — step 1: the record's shape.
"""Project inception — the record's shape.
The WHY a project inherits what it does lives on projects.inception. Pure
validation is pinned here; the effects are step 3's integration tests.
validation is pinned here; the effects are the integration tests.
Rules left inception in two steps: the always-on opt-out with the tier
(milestone 394), and the rulebook subscription with subscriptions (milestone
414). A rule in a rulebook is global, so there is nothing to choose.
Rules are not an inception choice: a rule in a rulebook is global, so there is
nothing to choose.
"""
from scribe.models.project import Project
from scribe.services.inception import (
@@ -39,7 +38,7 @@ def test_validate_inception_pins_the_choice_vocabulary():
# believe a rulebook had been declined.
assert "unknown inception choice" in validate_inception(
{"exclude_always_on_rulebooks": [1]})
# Same for the subscription choice since milestone 414.
# Same for a rulebook subscription choice.
assert "unknown inception choice" in validate_inception({"subscribe_rulebooks": [2]})
assert "positive id or null" in validate_inception({"design_system_id": 0})
assert "positive id or null" in validate_inception({"design_system_id": True})
@@ -71,8 +70,8 @@ def test_normalize_choices_is_canonical_and_complete():
def test_standard_systems_vocabulary_reads_the_catalog_not_a_constant():
"""The vocabulary moved from a module constant to the global catalog table
(milestone 307): a constant cannot be a foreign key, so nothing outside a
"""The vocabulary is the global catalog table, not a module constant: a
constant cannot be a foreign key, so nothing outside a
project could reference an area. The seed and the bootstrap ask must both
read the table, or the list they show and the list they mint diverge."""
import inspect
@@ -1,4 +1,4 @@
"""Real-Postgres round trip for the note fields #3182 restored.
"""Real-Postgres round trip for the note fields a restore carries.
The unit lane can prove a serialiser EMITS a key. It cannot prove a restore
puts the value back on the right row, and the note->note edges are exactly
@@ -7,8 +7,6 @@ the SOURCE database, so a restore that writes them straight through succeeds,
reports success, and silently points every edge at whatever record happens to
hold that number here.
That is why #3182 was not fixed in passing.
These drive the REAL `restore_full_backup`, not a reimplementation of its
loop. A test that re-derives the remap it is checking would agree with
whatever the product does, including nothing.
@@ -162,10 +160,10 @@ async def _no_leftover_restored_user():
async def test_a_restored_record_keeps_what_it_IS(restored):
"""#3182's headline. Without note_type and task_kind a restore reported
success and handed back a corpus where every snippet and process was a
plain note and every issue and spike was `work` — the whole vocabulary
milestone 312 and #3128 were about, gone, with nothing to notice it by."""
"""Without note_type and task_kind 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."""
by_title, _ = restored
assert by_title["debounce — rate-limit"].note_type == "snippet"
@@ -1,17 +1,17 @@
"""#3262 against a real Postgres: the embedder loses the race, it doesn't run it.
"""Against a real Postgres: the embedder loses the race, it doesn't run it.
The reported failure was a deadlock — `DELETE FROM rulebooks` killed by the
server while a detached `upsert_rule_embedding` held the other half of the
cycle. It cannot be reproduced with mocks, because there is nothing to
The failure is a deadlock — `DELETE FROM rulebooks` killed by the server
while a detached `upsert_rule_embedding` holds the other half of the cycle.
It cannot be reproduced with mocks, because there is nothing to
deadlock: the whole bug lives in the ORDER two transactions take two row
locks, which only a lock manager can adjudicate.
So each test here holds a real delete open in one transaction and calls the
embedder in another. What is being pinned is that the embedder RETURNS —
promptly, having written nothing. Before the fix it would sit on the chunk
rows waiting for a delete that is itself waiting on the insert's foreign key,
and the test would hang rather than fail, which is why every call carries a
deadline (rule 156).
promptly, having written nothing. An embedder that sits on the chunk rows
waiting for a delete that is itself waiting on the insert's foreign key would
hang the test rather than fail it, which is why every call carries a
deadline.
"""
import asyncio
from unittest.mock import AsyncMock, patch
@@ -53,8 +53,7 @@ SETTLE_DEADLINE_SECONDS = 30
async def seeded():
"""A rule and a note to race against.
CLEANED AT SETUP, NOT TEARDOWN — the same constraint #3241 hit and the
reason this file exists. `create_rule` fires its own detached embedding
CLEANED AT SETUP, NOT TEARDOWN. `create_rule` fires its own detached embedding
task; a teardown that deleted the rulebook would be racing exactly the
thing under test, on a loop that is closing.
"""
@@ -141,7 +140,7 @@ async def test_a_rule_refresh_yields_to_a_delete_cascading_from_its_rulebook(see
async def test_a_note_refresh_yields_to_a_delete_of_the_note(seeded):
"""The note twin, which #3262 recorded as unverified. Notes are soft-deleted
"""The note twin. Notes are soft-deleted
day to day, so the hard delete a trash purge issues is the one that can put
a lock on the row while a refresh is in flight."""
async with async_session() as blocker:
+6 -6
View File
@@ -1,4 +1,4 @@
"""Real-Postgres tests for the drift surface (milestone 399 step 5, #3895).
"""Real-Postgres tests for the drift surface.
WHY THIS EXISTS
@@ -20,8 +20,8 @@ subquery picking the newest version per rule, an ownership clause spanning two
paths, and a write that has to leave the history longer than it found it. A
stand-in session returns whatever the test handed it, so it could confirm none
of those. `test_the_restore_is_recorded_as_an_edit` is the one that matters
most: it is the whole basis for allowing a one-click revert here when
milestone 323 refused one for rules, and if it were ever to pass while the
most: it is the whole basis for allowing a one-click revert here when rules
have none, and if it were ever to pass while the
restore silently overwrote history, the carve-out would be indefensible.
"""
import pytest
@@ -228,8 +228,8 @@ async def test_restoring_puts_the_old_wording_back(world):
async def test_the_restore_is_recorded_as_an_edit(world):
"""THE ONE THAT MATTERS MOST — it is why this carve-out is defensible.
Milestone 323 refused a one-click restore for rules because "a silent
revert would erase the only record of why the rewrite happened". The
Rules have no one-click restore because a silent revert would erase the
only record of why the rewrite happened. The
preference carve-out survives that objection only while nothing is
erased: the restore goes through `update_rule`, so it takes its own
snapshot and the history GAINS an entry.
@@ -262,7 +262,7 @@ async def test_the_restore_is_recorded_as_an_edit(world):
async def test_a_rule_refuses_the_one_click_restore(world):
"""Milestone 323 stands for rules, and the kind check is in the service
"""Rules still have no restore, and the kind check is in the service
rather than only in the route — so a caller reaching this directly cannot
forget it."""
await rulebooks_svc.update_rule(
+5 -6
View File
@@ -1,6 +1,6 @@
"""Real-Postgres tests for `rules.kind` and its CHECK (0098, #3849 step 1).
"""Real-Postgres tests for `rules.kind` and its CHECK (migration 0098).
Rule 36 exists because a value and its constraint drift apart: the code
A value and its constraint drift apart: the code
starts writing a new kind while the database still refuses it, and nothing
catches it until a write fails in front of someone. A mock cannot show that —
it has no CHECK — so the constraint gets a real-DB test, exactly as 0091's
@@ -94,8 +94,7 @@ async def test_an_unknown_kind_is_refused(topic_id):
"""The half that proves the constraint is there at all.
Without this, every other assertion in this file would pass against a
table whose CHECK had been dropped — the exact failure rule 36 is written
against.
table whose CHECK had been dropped — the exact drift this guards.
"""
_uid, topic = topic_id
with pytest.raises(IntegrityError):
@@ -119,8 +118,8 @@ async def test_a_rule_can_become_a_preference_after_the_fact(topic_id):
"""The read-back is the whole test.
A version asserting only that the update call succeeded would pass against
code that accepted `kind` and dropped it — which is how the same bug
survived on `task_kind` long enough to be found by hand (#3129).
code that accepted `kind` and dropped it — a bug that otherwise surfaces
only when someone finds it by hand.
This is also the migration path the always-on triage needs: the rules that
turn out to be preferences change one column and keep their id, history
+4 -4
View File
@@ -1,4 +1,4 @@
"""Real-Postgres tests for rule mounts (milestone 458 step 3).
"""Real-Postgres tests for rule mounts.
A mount is the claim "this rule arrives at this moment". These pin what that
claim depends on:
@@ -7,7 +7,7 @@ claim depends on:
- the rule's detail reads its mounts back;
- a backup carries them, attached to the RESTORED rule's new id rather than
the old number;
- the delivery lookup (step 4) answers by home: a project's mounted rule
- the delivery lookup answers by home: a project's mounted rule
reaches only work bound to that project, and nobody else's mount arrives.
"""
import pytest
@@ -150,7 +150,7 @@ async def test_a_backup_carries_the_mounts_to_the_restored_rule(world):
assert sorted(r["moment"] for r in payload["rule_moments"]) == [
"reply.report", "work.finish",
]
# The mount was an edit, so it was also recorded as two judgments (step 7)
# The mount was an edit, so it was also recorded as two judgments
# — and those travel too, onto the restored rule.
payload["rule_moment_judgments"] = [
row for row in exported["rule_moment_judgments"] if row["rule_id"] == rule.id
@@ -238,7 +238,7 @@ async def test_the_delivery_lookup_answers_by_home(world):
async def test_moment_usage_counts_deliveries_and_the_agent_opens_that_followed(world):
"""The per-moment readout (step 6), on real SQL: an EXISTS inside a
"""The per-moment readout, on real SQL: an EXISTS inside a
COUNT(CASE …) is a shape no mock can vouch for, and the block's own guard
would turn a database refusal into a quiet `moment_usage_failed`."""
from datetime import datetime, timedelta, timezone
+6 -6
View File
@@ -1,4 +1,4 @@
"""Real-Postgres tests for WHERE a rule reaches (milestone 414, step 1).
"""Real-Postgres tests for WHERE a rule reaches.
A rule lives in a rulebook topic (global) or on one project. Retrieval used to
ignore that and search every rule the user owned, so each project's rules were
@@ -78,7 +78,7 @@ async def _found(user_id: int, **scope) -> set[int]:
"""The rule ids a search finds — and a failure, not an empty set, when the
search never ran. `semantic_search_rules` fails open (an exception becomes
[]), so without `report["searched"]` an error and a scoping miss read the
same (#4958)."""
same."""
report: dict = {}
raised: list[str] = []
with patch("scribe.services.embeddings.get_embedding",
@@ -109,7 +109,7 @@ async def test_retrieval_reaches_a_rule_only_from_its_home(homes):
async def test_a_shared_project_brings_its_rules_to_a_collaborator(homes):
"""Readability goes through access.can_read_project (rule 78): a viewer on
"""Readability goes through access.can_read_project: a viewer on
A gets A's rules. Not the owner's global rules — rulebooks are the
owner's — and not B's, which is not shared."""
collaborator = homes["collaborator"]
@@ -118,9 +118,9 @@ async def test_a_shared_project_brings_its_rules_to_a_collaborator(homes):
async def test_an_in_scope_rule_is_found_however_many_closer_rules_are_out_of_scope():
"""#4958. Ordered through the HNSW index, the search took the ~40 nearest
chunks from every owner and filtered them AFTERWARDS — so a reader's own
rule ranked past the 40th chunk overall was never returned. Here another
"""A search ordered through the HNSW index takes the ~40 nearest chunks
from every owner, and filtering them AFTERWARDS loses a reader's own rule
ranked past the 40th chunk overall. Here another
user's 80 rules all sit nearer the query than the reader's one rule; the
reader's search must still find it."""
tag = uuid.uuid4().hex[:8]
+6 -9
View File
@@ -10,12 +10,9 @@ planning read carries — and what mocks can't prove about it:
comparison, never a similarity score.
2. An UNTAGGED global rule is not listed: it applies everywhere and arrives by
retrieval, and listing every one under every project would say nothing.
3. A `co_surfaces` partner arrives with its other half — the failure that made
merging rule 144 into rule 46 look like the only fix — unless the partner
lives on a different project, because an edge is not a way in.
Milestone 414 dropped the suppression claim ("an explicit suppression outranks
an edge") with suppressions themselves.
3. A `co_surfaces` partner arrives with its other half — without it, merging
two rules that must arrive together looks like the only fix — unless the
partner lives on a different project, because an edge is not a way in.
"""
import pytest
import pytest_asyncio
@@ -102,9 +99,9 @@ async def test_a_tagged_global_rule_is_listed_for_a_project_that_works_in_its_ar
@pytest.mark.integration
async def test_co_surfaces_drags_in_the_half_that_would_have_been_missed(world):
"""Rule 144 was split off rule 46 and folded back the same day because
"either rule could surface without the other". This is the edge that makes
that unnecessary: the partner arrives even though nothing else selected
"""Two rules that must arrive together get folded into one because either
could surface without the other. This is the edge that makes that
unnecessary: the partner arrives even though nothing else selected
it, and says why it is here."""
partner = await rulebooks_svc.create_rule(
world["topic"], world["owner"], "Version names are labels",
+9 -10
View File
@@ -1,4 +1,4 @@
"""The document a lesson is embedded as (milestone 385 step 3; milestone 427).
"""The document a lesson is embedded as.
WHY THIS IS THE STEP THAT DECIDES THE MILESTONE
@@ -9,17 +9,16 @@ anywhere would report that.
WHERE THE SHARP SHAPE LIVES
A snippet, which note #2485 measured as the only sharp record in the corpus (a
0.153 top-to-second gap against 0.010–0.023 for everything else), is sharp
A snippet, measured as the sharpest record kind in a corpus (a 0.153
top-to-second gap against 0.010–0.023 for everything else), is sharp
because its document states its purpose twice: `name — when to use` as the
title, and again as the body's first line. A lesson follows the snippet.
Until milestone 427 that `subject — trigger` title was also the STORED title,
so every listing, menu and search row showed the trigger too — kilobytes of it.
Now the stored title is the subject, and `embeddings.document_title` joins the
trigger back from `data` at embed time. The embedded TEXT is what it always
was, which is the property these guards pin: nothing re-embeds, and the floors
tuned against these vectors stay calibrated.
The STORED title is the subject alone, so listings, menus and search rows do
not carry kilobytes of trigger; `embeddings.document_title` joins the trigger
back from `data` at embed time. The embedded TEXT keeps the `subject —
trigger` shape, which is the property these guards pin: the floors tuned
against these vectors stay calibrated.
No similarity number is asserted anywhere: a threshold pins the embedder's
behaviour rather than this code's, and breaks on a model change that is not a
@@ -44,7 +43,7 @@ def _embedded(what: str, trigger: str, insight: str) -> tuple[str, str]:
def test_the_stored_title_is_the_subject_alone():
"""Milestone 427. The trigger lives in `data` and the body's first line; the
"""The trigger lives in `data` and the body's first line; the
title a listing shows is what the lesson is ABOUT."""
title, _ = lessons_svc.lesson_document(SUBJECT, TRIGGER, INSIGHT)
assert title == SUBJECT
+6 -8
View File
@@ -1,4 +1,4 @@
"""Reading a lesson's insight back and re-composing it returns the same body (#4272).
"""Reading a lesson's insight back and re-composing it returns the same body.
THE PROPERTY. `update_lesson` is field-wise: an edit that touches only
`system_ids`, or only `what`, passes `insight=None`, and the service then
@@ -24,11 +24,9 @@ insight", the full trigger was prepended again, and the stored lesson gained
(n-1) paragraphs per update — silently, compounding, and in the half of the
document that does the retrieving.
Measured on the one real row this damaged (lesson #4207, a three-paragraph
trigger): the body grew by exactly 607 characters per edit — the length of
paragraphs 2 and 3 with their separators — taking it from 1291 characters to
7361 over ten edits, at which point the trigger appears eleven times and the
insight is 17% of the record.
On a three-paragraph trigger the body grows by the length of paragraphs 2 and
3 with their separators on every edit — after ten edits the trigger appears
eleven times and the insight is a sixth of the record.
WHY A MARKER COULD NOT HAVE FIXED IT. There is no end-of-trigger marker in
the body and adding one would not repair a row already written without it.
@@ -99,7 +97,7 @@ def _stored(trigger: str, insight: str, sources: list[int]):
MagicMock auto-creates truthy and coerces to `1`. A no-sources shape then
recomposes with `**Learned from:** #1` and the round trip fails for a
reason that has nothing to do with the trigger. `fake_lesson` pins that
field to None — note #2109's point, which is why the fixture exists.
field to None, which is why the fixture exists.
"""
title, body = lessons_svc.lesson_document(WHAT, trigger, insight, sources)
data = lessons_svc.compose_data(WHAT, trigger, sources)
@@ -137,7 +135,7 @@ def test_an_edit_that_changes_nothing_changes_nothing(_name, trigger, insight, s
# Reported as a size delta rather than left to a diff of two multi-kilobyte
# strings: the failure this catches is duplication, and the number of
# characters gained says which paragraph came back far faster than an
# equality dump does (rule 167 — a guard has to name what broke).
# equality dump does (a guard has to name what broke).
assert (title, body) == (note.title, note.body), (
f"a no-op edit rewrote the lesson: title {len(title) - len(note.title):+d} "
f"chars, body {len(body) - len(note.body):+d} chars"
+5 -5
View File
@@ -1,4 +1,4 @@
"""Creating a lesson, and tying it to what taught it (milestone 385 step 4).
"""Creating a lesson, and tying it to what taught it.
THE TRIGGER IS THE RECORD, so the door refuses a lesson without one.
@@ -136,8 +136,8 @@ async def test_the_gate_compares_the_composed_document_not_the_raw_fields():
def test_a_lesson_is_judged_at_the_trigger_dominated_bar():
"""#2518 measured deliberately-parallel siblings at 0.92 on a document that
is mostly prose ABOUT the thing. A lesson's document is that shape, so the
"""Deliberately-parallel siblings measure 0.92 on a document that is
mostly prose ABOUT the thing. A lesson's document is that shape, so the
bar sits above that band — otherwise two different lessons about one area
block each other, which is the failure step 4 asked to check for."""
from scribe.services.dedup import (
@@ -153,7 +153,7 @@ def test_a_lesson_is_judged_at_the_trigger_dominated_bar():
)
assert _LESSON_SEMANTIC_THRESHOLD > _SEMANTIC_THRESHOLD
# The carve-out is per kind, not a loosening of the gate: a lesson's bar is
# its own, apart from the note's copy band (#4306).
# its own, apart from the note's copy band.
from scribe.services.dedup import _NOTE_COPY_THRESHOLD
assert _semantic_threshold("note") == _NOTE_COPY_THRESHOLD
@@ -163,7 +163,7 @@ def test_a_lesson_is_judged_at_the_trigger_dominated_bar():
@pytest.mark.asyncio
async def test_an_update_recomposes_both_halves_of_the_document():
"""A new trigger has to reach the mirror AND the head of the body — the two
places the embedded document reads it from (milestone 427). Patching one
places the embedded document reads it from. Patching one
would leave a lesson that reads correctly and ranks on the old situation —
the failure mode with no symptom. The title stays the subject, even when
the stored one was an un-migrated composed title."""
+8 -9
View File
@@ -108,16 +108,15 @@ def test_every_registered_tool_is_classified_exactly_once():
"""Every tool must be declared a read, a write, or a read-shaped write.
`_READ_ONLY_TOOLS` is hand-maintained, and default-deny means a read
omitted from it fails CLOSED — safe, but silent. That is how a read key
ended up able to `get_note` and not `get_snippet` (#2496), and how
`rules_due_for_verification` and `rule_history` sat denied (#3191).
omitted from it fails CLOSED — safe, but silent: a read key able to
`get_note` and not `get_snippet`.
The fix keeps the DECISION explicit and derives the CANDIDATES. Deriving
So the DECISION stays explicit and the CANDIDATES are derived. Deriving
the decision would make a security boundary follow a naming convention, so
any future `get_*` grants itself access. This test used to derive
candidates from names too — tools starting `get_`, `list_`, `search`… — and
that narrowing is exactly what let the two #3191 reads through: neither
name looked like a read. So the candidates are now EVERY registered tool.
any future `get_*` grants itself access. Deriving the candidates from names
— tools starting `get_`, `list_`, `search`… — misses reads whose names do
not look like reads (`rules_due_for_verification`, `rule_history`). So the
candidates are EVERY registered tool.
"""
from scribe.mcp.server import (
_DELIBERATELY_WRITE_SCOPED, _READ_ONLY_TOOLS, _WRITE_TOOLS,
@@ -156,7 +155,7 @@ def test_every_registered_tool_is_classified_exactly_once():
def test_the_completeness_check_can_fail():
"""A guard that cannot fail is indistinguishable from one that is broken
(rule 167). Drop a real tool from its set and the check must notice."""
Drop a real tool from its set and the check must notice."""
from scribe.mcp.server import _READ_ONLY_TOOLS, _WRITE_TOOLS
registered = _registered_tool_names()
+8 -9
View File
@@ -1,13 +1,12 @@
"""Unknown tool arguments are rejected, never silently dropped (#2709).
"""Unknown tool arguments are rejected, never silently dropped.
The failure this pins: MCPServer validates tool arguments with a pydantic model
whose extra-field policy is "ignore", so `create_note(content=...)` — a
plausible near-miss for `body=`, primed by add_task_log's `content` — ran
successfully, stored `body: ""`, and left a record embedding/search cannot
see. The call REPORTED SUCCESS. Two real notes were persisted body-less
before the pattern was noticed.
plausible near-miss for `body=`, primed by add_task_log's `content` — would
run, store `body: ""`, and leave a record embedding/search cannot see, while
REPORTING SUCCESS.
The fix is at the dispatch seam, not per-tool: StrictArgsMCPServer rejects any
The rejection is at the dispatch seam, not per-tool: StrictArgsMCPServer rejects any
call carrying arguments the tool does not declare, with a did-you-mean when
one is close. An error the caller sees once beats data half-written forever.
"""
@@ -59,11 +58,11 @@ async def test_unknown_tool_keeps_the_upstream_error():
assert "no_such_tool" in str(exc.value)
# --- a refusal's text reaches the agent (#4794) -------------------------------
# --- a refusal's text reaches the agent ---------------------------------------
#
# SDK 2.x passes on only a ToolError's text; anything else is wrapped as
# "Error executing tool X" and its message is kept on the server. ValueError is
# how every Scribe tool refuses, so for weeks every refusal arrived bare.
# how every Scribe tool refuses, so without this every refusal arrives bare.
def _raising_server() -> StrictArgsMCPServer:
@@ -112,7 +111,7 @@ async def test_a_real_tools_refusal_reaches_the_caller():
async def test_the_original_regression_create_note_with_content():
"""Pin #2709 itself against the REAL server: `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."""
from scribe.mcp.server import build_mcp_server
+4 -4
View File
@@ -1,4 +1,4 @@
"""Delivering mounted rules when their moment happens (milestone 458 step 4).
"""Delivering mounted rules when their moment happens.
The database is stubbed: the lookup's scoping is pinned against Postgres in
tests/test_integration_rule_moments.py. These pin what is decided above it:
@@ -186,7 +186,7 @@ async def _reachable(mounted, mappings=()):
# The tools the shipped defaults map onto a moment that carries a default reply
# shape (milestone 500): closing work, a structured question, opening a plan.
# shape: closing work, a structured question, opening a plan.
SHAPED = ["askuserquestion", "create_milestone", "enterplanmode", "exitplanmode",
"skill", "start_planning", "update_milestone", "update_task"]
@@ -273,7 +273,7 @@ def test_the_hook_is_registered_on_every_tool():
def test_the_hook_and_the_routes_agree_on_every_name():
"""Rule 33 across the shell/Python seam: a renamed arg fails silently."""
"""Parity across the shell/Python seam: a renamed arg fails silently."""
src = HOOK.read_text()
assert "/api/plugin/moment-tools" in src and "/api/plugin/moment" in src
assert "exclude_rule_ids" in src and "scribe_held_query" in src
@@ -391,7 +391,7 @@ def test_every_scribe_tool_the_defaults_name_attaches_its_moment_rules():
)
# ── reply shapes ride the moments (milestone 500 step 3) ────────────────
# ── reply shapes ride the moments ───────────────────────────────────────
from scribe.services import reply_shapes # noqa: E402
+7 -7
View File
@@ -1,4 +1,4 @@
"""The prompt boundary retrieves against PROMPTS, not against plumbing (#4200).
"""The prompt boundary retrieves against PROMPTS, not against plumbing.
Claude Code submits more than typed words through `UserPromptSubmit`. A task
notification, the echo of a slash command and the caveat banner a local command
@@ -10,11 +10,11 @@ never returns anything. The cost that matters is not the wasted embedding, it
is the LOG ROW. Every one of those calls counts in the denominator of every
prompt-boundary surface, so delivery rate reads low for a reason unrelated to
retrieval quality; and each refusal lands in `near_misses`, where a later
tuning decision reads it as demand. Measured on #3898: 15 of the top 20
preference near-misses were `<task-notification>` blocks, all matching one
record, all within thousandths of the floor. A floor lowered to serve that
apparent demand would have injected that record into every notification — an
instrument arguing for the wrong fix, which is exactly #379.
tuning decision reads it as demand. Notification blocks can fill most of a
preference arm's top near-misses, all matching one record, all within
thousandths of the floor. A floor lowered to serve that apparent demand would
inject that record into every notification — an instrument arguing for the
wrong fix.
So the assertions split in two, and the SECOND half is the one guarding
against the obvious wrong implementation:
@@ -52,7 +52,7 @@ SYNTHETIC = [
"<local-command-stderr>Failed to reconnect</local-command-stderr>",
"<local-command-caveat>Caveat: The messages below were generated…</local-command-caveat>",
" \n<task-notification>\n<task-id>abc</task-id>", # whitespace must not walk the filter
# A subagent's hand-back (#4785) — it carries attributes, so the tag is
# A subagent's hand-back — it carries attributes, so the tag is
# matched up to its name, not to a closing `>`.
'<agent-message from="a93b1b587d7a718b0">\n[Subagent hand-back] The text below is the final report',
"<agent-message>\nreport</agent-message>",
+8 -9
View File
@@ -1,20 +1,19 @@
"""Every typed record kind gets the same doors (#4164).
"""Every typed record kind gets the same doors.
WHY THIS IS ONE GUARD AND NOT FIFTEEN
Milestone 385 shipped `lesson` across four steps and it still arrived with
three of five tools, no way to list it, and no duplicate report. Nothing was
red, because every test asked "does what I built work" and none asked "is the
kind finished". A per-tool test cannot catch a MISSING tool.
A kind can ship across several steps and still arrive with three of five
tools, no way to list it, and no duplicate report — with nothing red, because
every test asks "does what I built work" and none asks "is the kind
finished". A per-tool test cannot catch a MISSING tool.
So this asserts the property directly — a kind with its own `note_type` has a
full create/read/update/delete plus a listing, each one registered and each one
classified for auth — and derives the expectation from the kinds themselves. A
fourth kind added later inherits the bar without anyone remembering to.
#2250 is the same failure one kind earlier: processes could always be deleted
through `delete_note`, but nothing said so, and "a kind whose own tools offer
create/read/update reads as one you cannot retire".
A kind deletable only through `delete_note` is the same failure: "a kind whose
own tools offer create/read/update reads as one you cannot retire".
"""
from __future__ import annotations
@@ -103,7 +102,7 @@ def test_every_kind_can_be_asked_whether_it_holds_duplicates(singular):
def test_the_surface_guards_can_fail():
"""Rule 167: falsify the shape these assert against, so a guard that has
"""Falsify the shape these assert against, so a guard that has
quietly stopped describing anything cannot pass by describing nothing."""
assert _expected_tools("lesson", "lessons") == [
"list_lessons", "create_lesson", "get_lesson",
+4 -5
View File
@@ -1,9 +1,8 @@
"""The operator's half of the tuning surface (#4102).
"""The operator's half of the tuning surface.
WHY THIS EXISTS
The model moves these dials — that is the operator's decision for milestone
416 — so the browser's job is no longer "set the number". It is to show what
The model moves these dials, so the browser's job is not "set the number". It is to show what
was set, by whom, and on what argument, and to let the operator disagree.
The load-bearing guard here is the last one. `/api/settings` is a generic
@@ -51,7 +50,7 @@ def test_every_endpoint_is_reachable_on_the_app():
def test_the_browser_and_the_agent_call_the_same_service():
"""Rule 33 parity. Two callers, one service — or the two surfaces drift and
"""Parity. Two callers, one service — or the two surfaces drift and
the guard that a reason is required exists on only one of them."""
from scribe.mcp.tools import retrieval_tuning as tool
from scribe.routes import retrieval as routes
@@ -138,7 +137,7 @@ async def test_setting_a_dial_to_the_value_it_already_has_records_nothing():
setter.assert_not_called()
# ── the moments view (milestone 458 step 6) ─────────────────────────────
# ── the moments view ────────────────────────────────────────────────────
async def _moments_call(handler_name, path, method="GET", **kw):
from types import SimpleNamespace
+4 -4
View File
@@ -69,7 +69,7 @@ def test_create_project_rule_route_exists():
def test_subscriptions_and_suppressions_are_gone():
"""Milestone 414: a rule's home is its scope. Nothing subscribes a project
"""A rule's home is its scope. Nothing subscribes a project
to a rulebook or mutes a rule for one — not a route, a service function or
a table. A partial removal would leave a door that 500s on a missing table.
"""
@@ -94,14 +94,14 @@ def test_rule_handlers_callable():
for name in (
"list_rules", "create_rule", "get_rule", "update_rule", "delete_rule",
"get_project_rules", "move_rule",
# The typed edges — both doors carry them (rule 33).
# The typed edges — both doors carry them.
"relate_rules", "unrelate_rules",
):
assert callable(getattr(rb_routes, name))
def test_the_rule_list_zero_fills_usage_on_every_row():
"""Milestone 333 step 5, asserted the only way this harness allows.
"""Asserted the only way this harness allows.
There is no live-HTTP fixture here (see this module's docstring), so this
reads the handler's source. What it can still prove is the property that
@@ -110,7 +110,7 @@ def test_the_rule_list_zero_fills_usage_on_every_row():
existing install predates `rule_usage_events`, so a route that only
attached the key when it found something would leave the badge component
reading `undefined` on almost every row — and the difference between "no
events" and "no field" is exactly the distinction #2663 is about.
events" and "no field" is exactly the distinction a readout must keep.
"""
import inspect
+3 -3
View File
@@ -1,7 +1,7 @@
"""The supersession claim — who may make it, and what it refuses.
Step 2 of #278. Ranking behaviour is step 3; this covers only recording and
reading the relation.
Ranking behaviour is in test_supersession_ranking.py; this covers only
recording and reading the relation.
The cycle tests are the ones worth reading. Under FLAT demotion a ring of
records that supersede each other claims every member is obsolete, so all of
@@ -53,7 +53,7 @@ async def test_superseding_a_note_you_can_only_READ_is_refused_not_dropped():
Demoting someone else's record out of their retrieval is damage that is
invisible from the outside — the caller would believe it worked, and the
owner would have no symptom to trace. Rule #47.
owner would have no symptom to trace.
"""
# writable for the superseder (id 1), not for the target (id 2)
writable = AsyncMock(side_effect=lambda uid, nid: nid == 1)
+7 -19
View File
@@ -1,4 +1,4 @@
"""The SessionStart hook clears the rule ledger exactly when context dies (#3749).
"""The SessionStart hook clears the rule ledger exactly when context dies.
WHY THIS EXISTS
@@ -109,28 +109,16 @@ def test_the_rules_ledger_survives_exactly_when_the_context_does(tmp_path):
def test_the_note_ledgers_are_cleared_too_now_that_a_repeat_is_shown(tmp_path):
"""The decision this test used to pin, reversed — and why it could be.
"""The note ledgers are cleared on a compaction too.
It read: the note arms were deliberately left out of #3749, whether a
surfaced NOTE should come back after a compaction is a different question,
and a glob over `<sid>.*` would decide it by accident. That was right when
it was written, and the reason was the note arms' behaviour rather than the
note arms' nature: a note repeat was WITHHELD, so clearing their ledger
meant re-injecting whole menu lines the session had already been given.
Against that, leaving them was the cheaper mistake.
#4101 removed the premise. A note repeat is now rendered again with a
`seen` marker instead of dropped from the search, so the ledger no longer
decides whether a record is reachable — only whether its line says the
A note repeat is rendered again with a `seen` marker rather than dropped
from the search, so the ledger does not decide whether a record is
reachable — only whether its line says the
session has met it before. Across a compaction that marker becomes a false
statement: it tells a freshly-summarised session it has already seen a
record that is nowhere in its context. Keeping the ledger buys nothing and
asserts something untrue.
Kept as a named test rather than deleted, because the roster it guards
moved to tests/test_session_ledger_clear.py and what is worth keeping HERE
is the record that the answer changed, with the reason attached — a
deleted test takes the old decision's reasoning with it.
asserts something untrue. The full roster is guarded in
tests/test_session_ledger_clear.py.
"""
env = _env(tmp_path)
sid = "sess-scope"
+8 -9
View File
@@ -1,4 +1,4 @@
"""Every bundled skill keeps the shape Agent Skills can load well (#4398).
"""Every bundled skill keeps the shape Agent Skills can load well.
WHY THIS EXISTS
@@ -8,11 +8,10 @@ keep the SKILL.md body under 500 lines, split into reference files as it
nears that, and link each reference file ONE level deep from SKILL.md —
Claude may only preview a file reached through another reference file.
using-scribe reached 478 lines because every new practice lands there, the
same squeeze `_INSTRUCTIONS` was in before #4389. It was split: what matters
on most turns stays in SKILL.md, what matters at one moment (placing a rule,
a missed retrieval, starting a project) moved to files SKILL.md names with
the moment to open them. These tests keep that shape from quietly undoing
using-scribe grows because every new practice lands there. What matters on
most turns stays in SKILL.md; what matters at one moment (placing a rule, a
missed retrieval, starting a project) lives in files SKILL.md names with the
moment to open them. These tests keep that shape from quietly undoing
itself.
WHAT IT PINS
@@ -71,8 +70,8 @@ def test_every_skill_md_fits_its_budget():
SKILL_LINE_BUDGET)
assert not found, (
f"SKILL.md over {SKILL_LINE_BUDGET} lines: {found}. Move what matters at "
f"one moment into a reference file SKILL.md links with that moment "
f"(#4398), rather than raising the budget toward 500."
f"one moment into a reference file SKILL.md links with that moment, "
f"rather than raising the budget toward 500."
)
@@ -102,7 +101,7 @@ def test_long_reference_files_open_with_contents():
def test_the_guards_can_fail():
"""Rule 167: each guard bites on the failure it exists for."""
"""Each guard bites on the failure it exists for."""
assert over_budget({"s": "x\n" * 400}, SKILL_LINE_BUDGET) == ["s: 400 lines"]
assert unlinked("see [a](a.md)", ["a.md", "b.md"]) == ["b.md"]
assert nested({"a.md": "then [b](b.md)"}) == ["a.md → b.md"]
+4 -4
View File
@@ -1,4 +1,4 @@
"""Tests for the near-duplicate finder (#2088).
"""Tests for the near-duplicate finder.
The SQL half needs a database and lives in the integration lane; what's covered
here is the grouping rule — which is where the interesting decisions are — plus
@@ -98,13 +98,13 @@ def test_the_snippet_report_threshold_is_looser_than_the_write_gate():
report only suggests a merge the operator reviews, so it has to reach
further or it would never surface the pairs the gate already let through.
Snippet-only on purpose: note/task floors sit ABOVE the gate because chunk
grain (#280) lifts related families over it — see the module comment."""
grain lifts related families over it — see the module comment."""
assert DUPLICATE_DEFAULT_THRESHOLDS["snippet"] < dedup_svc._SEMANTIC_THRESHOLD
def test_the_note_and_task_floors_are_stricter_than_the_snippet_floor():
"""At chunk grain, 0.82 is a related-families view; the report's default
must point at genuinely-alike records (measured 2026-08-09, note #2551)."""
must point at genuinely-alike records."""
assert DUPLICATE_DEFAULT_THRESHOLDS["note"] == 0.93
assert DUPLICATE_DEFAULT_THRESHOLDS["task"] == 0.93
@@ -119,7 +119,7 @@ async def test_a_failed_scan_returns_an_empty_report_not_an_error():
patch.object(dedup_svc, "async_session", side_effect=RuntimeError("boom")),
):
out = await dedup_svc.find_duplicate_snippets(1)
# The empty report still carries its per-kind `suggestion` (#2547) — a
# The empty report still carries its per-kind `suggestion` — a
# failed scan must be indistinguishable in SHAPE from a clean one, or every
# consumer needs a second code path for the degraded case.
assert out["groups"] == [] and out["pairs"] == []
+6 -9
View File
@@ -1,9 +1,7 @@
"""Demotion: superseded records rank behind their equals, and are never hidden.
Step 3 of #278. The distinction these tests exist to protect is DEMOTE vs
FILTER. The operator was explicit:
"the failure is pollution, not existence"
The distinction these tests exist to protect is DEMOTE vs FILTER: the failure
is pollution, not existence.
Hiding a superseded record would turn every one of them into something you must
already know exists in order to find, and would destroy "what did we think
@@ -131,11 +129,10 @@ async def test_an_empty_candidate_set_short_circuits():
def test_the_penalty_is_sized_to_reorder_a_cluster_not_shuffle_within_it():
"""Sized against a measurement, not by feel.
Measured 2026-08-07 (#2486): the notes competing on a dev-log's own title
phrase sat within ~0.014 of each other, spanning 0.6506 down to 0.6120. A
penalty smaller than that spread would move a record within a tie without
changing which one wins — the failure #2486 already proved cannot be tuned
away, because the neighbours are not barely passing, they are tied.
Notes competing on a dev-log's own title phrase sit within ~0.014 of each
other (0.6506 down to 0.6120 on one measured cluster). A penalty smaller
than that spread moves a record within a tie without changing which one
wins — a failure that cannot be tuned away, because the neighbours are not barely passing, they are tied.
The upper bound is the operator's constraint, not an optimisation: a penalty
large enough to bury a superseded record outright is hiding by another name.
+3 -4
View File
@@ -1,7 +1,6 @@
"""Rulings are counted where someone will read them (#4769).
"""Rulings are counted where someone will read them.
`system_usage_events` was written from milestone 444 step 4 and read by
nothing. These pin the two readouts:
`system_usage_events` is read in two places, and these pin both readouts:
- PER SYSTEM: `usage_for_systems` — one GROUP BY, a zero shape for a System
nothing touched — carried by the Systems list, the REST detail and MCP
@@ -10,7 +9,7 @@ nothing. These pin the two readouts:
the areas named, and deliberately NO pull-through ratio.
Integration for both aggregates, not mocked: a GROUP BY the database rejects
is swallowed by the fail-open guard and reads as zero, which is #2663 exactly.
is swallowed by the fail-open guard and reads as zero.
"""
from unittest.mock import AsyncMock, patch
+4 -4
View File
@@ -1,9 +1,9 @@
"""A task's claim — which session is working it, and how it dies (milestone 381 step 2).
"""A task's claim — which session is working it, and how it dies.
WHAT THIS PINS
1. **The write that is the work stamps it.** Reaching `in_progress` — by
update OR by a create that names it (#3683) — claims the task for whoever
update OR by a create that names it — claims the task for whoever
made the change. Nothing asks the model to claim anything.
2. **Ending or un-starting the work releases it**, and releasing is
idempotent.
@@ -287,8 +287,8 @@ async def test_ending_a_session_releases_only_that_sessions_claims(users):
@pytest.mark.integration
async def test_a_collaborator_with_write_access_can_log_and_so_claims(users):
"""create_log used to filter on the task's OWNER (a rule #78 violation), so
a collaborator on a shared task could not log on it — nor, since logging
"""create_log filters on who may write the TASK, not on its owner, so a
collaborator on a shared task can log on it — nor, since logging
stamps the claim, ever be seen working it."""
from scribe.services import sharing, task_logs
+7 -9
View File
@@ -1,10 +1,8 @@
"""The work log is READABLE from the tools an agent opens a task with (#4241).
"""The work log is READABLE from the tools an agent opens a task with.
`add_task_log` wrote to a surface no agent could read back: the entries
reached the web UI through routes/task_logs.py and nothing else. So a session
opening a task saw only the body — a claim written once, before the work —
with the record written during it invisible beside it. A stale body had
nothing to contradict it, and shipped work got rebuilt.
A session that opens a task and sees only the body sees a claim written once,
before the work, with the record written during it invisible beside it. A
stale body has nothing to contradict it, and shipped work gets rebuilt.
These pin the read, not the write. The payload shape is tested against the
real `work_log_payload`; the three tools are tested by re-patching the arm
@@ -235,7 +233,7 @@ async def test_log_limit_zero_asks_for_every_entry_and_skips_the_count_query():
@pytest.mark.asyncio
async def test_get_task_docstring_tells_a_reader_the_log_outranks_the_body():
"""#2846: the docstring IS the agent-facing contract. A `work_log` key
"""The docstring IS the agent-facing contract. A `work_log` key
that nothing tells the reader to prefer over a stale body is the same
failure one layer up."""
doc = (get_task.__doc__ or "").lower()
@@ -305,7 +303,7 @@ def test_brief_row_without_a_mapping_is_unchanged():
# ---------------------------------------------------------------------------
# The service read — scoped by who may read the TASK (rule #78)
# The service read — scoped by who may read the TASK
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
@@ -332,7 +330,7 @@ async def test_logs_for_task_is_not_filtered_by_who_wrote_the_entry():
@pytest.mark.asyncio
async def test_logs_for_task_refuses_a_task_the_caller_cannot_read():
"""Unscoped would have been the mirror-image hole: rule #78 is about
"""Unscoped would be the mirror-image hole: access control is about
routing the question through the access layer, in both directions."""
opened = MagicMock()
with patch("scribe.services.task_logs.can_read_note",
+11 -16
View File
@@ -1,19 +1,14 @@
"""One seam attaches `usage`, and every door that shows it uses that seam (#4230).
"""One seam attaches `usage`, and every door that shows it uses that seam.
WHAT WENT WRONG. `usage_for_notes` is named for notes and works on every note
row. Yet the surfaced-vs-opened chip reached snippets and rules only: notes had
it nowhere, and lessons had it collected but shown nowhere a person could
reach, because the only lesson LIST in the UI is the Knowledge browse and that
route never attached it.
The cause was not any one missing line. Seven call sites carried their own copy
of the same few lines — two REST lists, two REST details, two MCP lists, one
MCP detail — and each read perfectly well on its own. Nobody was comparing
them, so "which doors attach usage?" had no answer anywhere in the code. That
is the same failure `test_system_tagging_door_parity.py` records for System
tagging (#4249): whichever door nobody exercised for a kind is the one that
never grew the feature, and a human reviewer does not reliably catch it because
each door is only ever read alone.
WHY. `usage_for_notes` is named for notes and works on every note row, and
seven doors show its result — two REST lists, two REST details, two MCP
lists, one MCP detail. Seven copies of the same few lines each read perfectly
well on their own, and nobody compares them, so "which doors attach usage?"
has no answer anywhere in the code. That is the same failure
`test_system_tagging_door_parity.py` records for System tagging: whichever
door nobody exercises for a kind is the one that
never grows the feature, and a human reviewer does not reliably catch it
because each door is only ever read alone.
So this file asserts the PROPERTY, not the behaviour of one route: the attach
logic exists once, and no door re-implements it. A kind added next month either
@@ -156,7 +151,7 @@ def test_no_door_calls_the_aggregate_directly() -> None:
Keyed on the CALL, not on the text, so a module that merely names the
function in a comment explaining the seam is not a false positive — and a
hand-kept skip list, which would itself go stale, is not needed (rule 167).
hand-kept skip list, which would itself go stale, is not needed.
"""
offenders = []
for path in _door_modules():