docs(6209): snippets, rule embeddings, notes, ledger ageing and references, routes, reporting, handshake, lesson kind, request deadlines, contract-around-change, family models
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 14s
CI & Build / TypeScript typecheck (push) Successful in 1m0s
CI & Build / integration (push) Successful in 1m14s
CI & Build / Python tests (push) Successful in 1m59s
CI & Build / Build & push image (push) Successful in 15s

Twelve more test files drop issue, milestone, rule, note, lesson and
snippet numbers and the dated or one-project figures that recorded how
each property was found; the docstrings state the property and why it
holds. The contract-around-the-change guard keeps the dict-key example as
the failure a signature-watcher misses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-21 21:34:24 -04:00
co-authored by Claude Opus 5.5
parent ead4a7e2ad
commit d0d278dcce
12 changed files with 100 additions and 112 deletions
+12 -15
View File
@@ -1,16 +1,14 @@
"""You altered the shape of something — here is everything that reads it (#4215). """You altered the shape of something — here is everything that reads it.
WHY THIS EXISTS WHY THIS EXISTS
Milestone 419's most repeated miss, five of seven: acting on the thing in hand The commonest miss is acting on the thing in hand without reading the contract
without reading the contract around it. Lesson #4207 says it in words — around it: "widening a tuple is an interface change to every unpack site, and
"widening a tuple is an interface change to every unpack site, and the the compiler will not tell you". Knowing that in words does not stop it — text
compiler will not tell you" — and was written by its author HOURS before a delivered at the moment of acting is too weak a carrier for a reflex that has
structurally identical mistake, having been surfaced twice in the turns to change what the act IS. This looks it up instead.
between. Text delivered at the moment of acting is too weak a carrier for a
reflex that has to change what the act IS. This looks it up instead.
Rule 33 one scope down: its checks are between layers ("every parameter the Layer parity one scope down: its checks are between layers ("every parameter the
caller sends is read by the handler under the same name"), and the same caller sends is read by the handler under the same name"), and the same
question exists between a definition and its callers. question exists between a definition and its callers.
@@ -21,18 +19,17 @@ nothing alike in source:
arg its parameter names arity and order arg its parameter names arity and order
key quoted keys of dict literals the shape of what it RETURNS key quoted keys of dict literals the shape of what it RETURNS
The third is here because of the miss that produced this step, and it is the The third is the one a signature-watcher misses: a function gains one dict
one a signature-watcher would have missed: `get_writepath_config` gained one key, three callers read that dict inside a fail-open `except`, every one
dict key, three arms read that dict inside a fail-open `except`, every one silently becomes a no-op, and tests go red at once with nothing pointing at
silently became a no-op, and ten tests went red at once with nothing pointing the cause. No signature changed.
at the cause. No signature changed.
WHAT IS PINNED: which edits speak and which stay silent, and that both gates WHAT IS PINNED: which edits speak and which stay silent, and that both gates
are load-bearing. NOT pinned: the wording, which is prose. are load-bearing. NOT pinned: the wording, which is prose.
A FIXTURE REPO, NEVER THIS ONE. Asserting against Scribe's own files would A FIXTURE REPO, NEVER THIS ONE. Asserting against Scribe's own files would
make the test a description of today's tree, failing the next time someone make the test a description of today's tree, failing the next time someone
renames something (rule 115's reasoning, one floor down). renames something.
""" """
import subprocess import subprocess
from pathlib import Path from pathlib import Path
+5 -6
View File
@@ -1,11 +1,10 @@
"""Family canon's schema — the parts that need no database (milestone 463 step 1). """Family canon's schema — the parts that need no database.
The constraints themselves, against real Postgres, are in The constraints themselves, against real Postgres, are in
tests/test_integration_family_canon.py. These pin that the migration and the tests/test_integration_family_canon.py. These pin that the migration and the
model agree on every whitelist (rule 36: a value one side accepts and the model agree on every whitelist (a value one side accepts and the
other refuses only fails at INSERT time, which CI's unit lane never reaches), other refuses only fails at INSERT time, which CI's unit lane never reaches),
and that the seed ships nothing an install other than this one would find and that the seed ships nothing a stranger's install would find foreign.
foreign (rule 115).
""" """
from __future__ import annotations from __future__ import annotations
@@ -62,14 +61,14 @@ def test_the_column_defaults_match_the_first_state_of_each_lifecycle():
def test_a_canon_ideas_scope_lives_on_the_idea_and_nowhere_else(): def test_a_canon_ideas_scope_lives_on_the_idea_and_nowhere_else():
"""One scope source. A rule topic is linked FROM the idea and carries no """One scope source. A rule topic is linked FROM the idea and carries no
platforms of its own, so the two cannot disagree about who a standard platforms of its own, so the two cannot disagree about who a standard
reaches (#4221's two-sources-of-truth shape).""" reaches — two sources of truth."""
from scribe.models.rulebook import RulebookTopic from scribe.models.rulebook import RulebookTopic
assert "topic_id" in family.FamilyIdea.__table__.c assert "topic_id" in family.FamilyIdea.__table__.c
assert not {c.name for c in RulebookTopic.__table__.c} & {"platform_id", "platform_ids"} assert not {c.name for c in RulebookTopic.__table__.c} & {"platform_id", "platform_ids"}
# --- a decision read as precedent (#5131) ------------------------------------------ # --- a decision read as precedent ------------------------------------------------
def _assessment(): def _assessment():
reason = "the APK is built in CI and signed with the one release key" reason = "the APK is built in CI and signed with the one release key"
+6 -8
View File
@@ -1,4 +1,4 @@
"""Every request the web UI makes has a deadline (rule 156). """Every request the web UI makes has a deadline.
A source-inspection guard in the unit lane — there is no frontend test runner, A source-inspection guard in the unit lane — there is no frontend test runner,
and this is a property of the source rather than of a rendered result, so and this is a property of the source rather than of a rendered result, so
@@ -7,7 +7,7 @@ reading the source is the honest way to check it.
WHY. `fetch`'s default is to wait as long as the browser will. That is not a WHY. `fetch`'s default is to wait as long as the browser will. That is not a
long timeout, it is the absence of one, and there is no state a surface can long timeout, it is the absence of one, and there is no state a surface can
render for "pending forever" that is not a lie — the spinner that never render for "pending forever" that is not a lie — the spinner that never
resolves is indistinguishable from work still in progress. Rule 156 names resolves is indistinguishable from work still in progress. The practice names
`fetch` specifically: `fetch` specifically:
When a library's default is "wait indefinitely" — `fetch`, most HTTP When a library's default is "wait indefinitely" — `fetch`, most HTTP
@@ -67,17 +67,15 @@ def test_every_fetch_passes_a_signal():
if "signal:" not in call if "signal:" not in call
] ]
assert not naked, ( assert not naked, (
"these fetch calls carry no AbortSignal, so they wait forever " "these fetch calls carry no AbortSignal, so they wait forever:\n " + "\n ".join(naked)
"(rule 156):\n " + "\n ".join(naked)
) )
def test_the_client_applies_its_deadline_by_default(): def test_the_client_applies_its_deadline_by_default():
"""The specific regression that would silently undo this. """The specific regression that would silently undo this.
An earlier pass (#3329) made `timeoutMs` OPT-IN and used it at exactly one An OPT-IN `timeoutMs` used at one call site leaves every other call
call site, which left ~330 others waiting forever while the mechanism waiting forever while the mechanism looks present. That shape would not fail the guard above —
looked present. Reverting to that shape would not fail the guard above —
every call would still reach `fetch` through `request()` — so the default every call would still reach `fetch` through `request()` — so the default
is pinned here separately. is pinned here separately.
@@ -93,7 +91,7 @@ def test_the_client_applies_its_deadline_by_default():
def test_a_timeout_arrives_as_the_error_shape_callers_already_handle(): def test_a_timeout_arrives_as_the_error_shape_callers_already_handle():
"""Rule 156's second half: expiry surfaces as a NAMED failure. """The deadline's second half: expiry surfaces as a NAMED failure.
A raw `DOMException: TimeoutError` reaches `apiErrorMessage(e, fallback)` A raw `DOMException: TimeoutError` reaches `apiErrorMessage(e, fallback)`
as an object with no `body`, so every catch site in the app would print its as an object with no `body`, so every catch site in the app would print its
+9 -9
View File
@@ -1,17 +1,17 @@
"""A lesson is a row the database actually accepts (milestone 385 step 2). """A lesson is a row the database actually accepts.
WHY THIS IS THE REAL GUARD, AND WHY IT IS HERE WHY THIS IS THE REAL GUARD, AND WHY IT IS HERE
The step asked for "a guard that the CHECK actually accepts `lesson` and The obvious ask is a guard that a CHECK accepts `lesson` and rejects a typo —
rejects a typo", citing #3128 — a `spike` kind that could not be written on an the failure where a new kind cannot be written on an instance predating its
instance predating its migration. That failure mode belongs to `task_kind`, migration. That failure mode belongs to `task_kind`,
which IS gated (`notes_task_kind_check`, migrations 0056 and 0065). which IS gated (`notes_task_kind_check`, migrations 0056 and 0065).
`note_type` is not gated at all: migration 0036 added it as plain Text with a `note_type` is not gated at all: migration 0036 added it as plain Text with a
server default and nothing has constrained it since. server default and nothing has constrained it since.
So there is no whitelist to expand and no rejection to assert. Writing the So there is no whitelist to expand and no rejection to assert. Writing the
guard as "the constraint admits lesson" would have pinned a constraint that guard as "the constraint admits lesson" would pin a constraint that does not
does not exist; writing it as "no constraint exists" would pin today's schema exist; writing it as "no constraint exists" would pin today's schema
rather than the behaviour that matters, and would go red on a change that is rather than the behaviour that matters, and would go red on a change that is
perfectly fine. perfectly fine.
@@ -19,7 +19,7 @@ Asserting the WRITE covers both worlds. It passes today, it keeps passing if a
CHECK is added that admits `lesson`, and it goes red the day one is added that CHECK is added that admits `lesson`, and it goes red the day one is added that
does not — which is the only outcome anyone needs to be told about. does not — which is the only outcome anyone needs to be told about.
The second test is the cell #3163 asks to be left empty on purpose: a lesson The second test is the cell left empty on purpose: a lesson
must not be a task. `is_task` is a read-only property over `status`, so this must not be a task. `is_task` is a read-only property over `status`, so this
cannot be asserted by setting a flag — it has to be observed on a real row. cannot be asserted by setting a flag — it has to be observed on a real row.
""" """
@@ -83,13 +83,13 @@ async def test_a_lesson_is_a_row_the_database_accepts(owner_id):
# and the readable body — because the vector is built from the text and # and the readable body — because the vector is built from the text and
# the queries are built from the mirror. # the queries are built from the mirror.
assert lessons_svc.lesson_trigger(stored) == TRIGGER assert lessons_svc.lesson_trigger(stored) == TRIGGER
# The subject alone (milestone 427): the trigger is in `data` and the body. # The subject alone: the trigger is in `data` and the body.
assert stored.title == SUBJECT assert stored.title == SUBJECT
assert "**When to apply:**" in (stored.body or "") assert "**When to apply:**" in (stored.body or "")
async def test_a_lesson_is_not_a_task(owner_id): async def test_a_lesson_is_not_a_task(owner_id):
"""#3163's cell left empty on purpose. """The cell left empty on purpose.
`is_task` is `status is not None` and is read-only, so this is only `is_task` is `status is not None` and is read-only, so this is only
observable on a stored row. A lesson that arrived with a status would join observable on a stored row. A lesson that arrived with a status would join
+13 -15
View File
@@ -1,22 +1,20 @@
"""A repeat on a note arm is referenced, not withheld (#4101). """A repeat on a note arm is referenced, not withheld.
WHY THIS EXISTS WHY THIS EXISTS
#3750 settled the argument for rules: a record the session was told about an A record the session was told about an hour ago is not a record in front of the reader now, so the second time it is
hour ago is not a record in front of the reader now, so the second time it is
the best answer the session gets the line again with a tail saying so — never the best answer the session gets the line again with a tail saying so — never
silence, which is indistinguishable from "nothing matched". silence, which is indistinguishable from "nothing matched".
The note and snippet arms never got that fix. Worse, their ledger went into The note and snippet arms hold the same line. A ledger passed into
`semantic_search_notes` as `exclude_ids`, so the repeat left the candidate set `semantic_search_notes` as `exclude_ids` takes the repeat out of the candidate
entirely, which had three consequences: set entirely, with three consequences:
- the session got silence the second time, on the arms that fire most; - the session gets silence the second time, on the arms that fire most;
- a compaction made it permanent, because the ledger outlived the context it - a compaction makes it permanent, because the ledger outlives the context it
described (the same step fixes that one layer down); describes;
- and `best_available` was measured against a candidate set the caller had - and `best_available` is measured against a candidate set the caller has
already edited, so the bar could be blamed for a record the caller withheld already edited, so the bar can be blamed for a record the caller withheld.
(#3739, from the side its fix never reached).
WHAT THIS PINS WHAT THIS PINS
@@ -28,11 +26,11 @@ WHAT THIS PINS
and a dev-log borrowing it would claim authority it does not have — so and a dev-log borrowing it would claim authority it does not have — so
what is pinned is that the line appears and is distinguishable, not its what is pinned is that the line appears and is distinguishable, not its
prose. prose.
3. The telemetry splits the two (#3752 / #3668): the row's result set and the 3. The telemetry splits the two: the row's result set and the
surfaced table both take FRESH only, and the repeat is COUNTED in surfaced table both take FRESH only, and the repeat is COUNTED in
`suppressed` instead. This is what makes a zero-result call readable — `suppressed` instead. This is what makes a zero-result call readable —
`result_count == 0` with `suppressed_count > 0` says "everything that `result_count == 0` with `suppressed_count > 0` says "everything that
matched, the session has already seen", which was unreportable here. matched, the session has already seen".
4. The deliberate exception: the write-path SYNC class still shows once. Its 4. The deliberate exception: the write-path SYNC class still shows once. Its
claim is about an edit in progress, not about a record's continuing claim is about an edit in progress, not about a record's continuing
relevance, and repeating it would be nagging rather than reminding. relevance, and repeating it would be nagging rather than reminding.
@@ -141,7 +139,7 @@ async def test_a_call_where_everything_was_already_seen_is_readable():
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_a_repeat_is_not_recorded_as_a_fresh_surfacing(): async def test_a_repeat_is_not_recorded_as_a_fresh_surfacing():
"""#3668's identity: this table and the log row describe the same call.""" """The two-table identity: this table and the log row describe the same call."""
surf = MagicMock() surf = MagicMock()
hits = [(0.80, fake_note(id=11, title="Pool sizing decision", user_id=1)), hits = [(0.80, fake_note(id=11, title="Pool sizing decision", user_id=1)),
(0.78, fake_note(id=22, title="run_maintenance thresholds", user_id=1))] (0.78, fake_note(id=22, title="run_maintenance thresholds", user_id=1))]
+10 -10
View File
@@ -1,9 +1,9 @@
"""The enter_project handshake stays a small primer (#4045). """The enter_project handshake stays a small primer.
enter_project once carried every milestone's full plan body, the whole project Every milestone's full plan body, the whole project record, full rule text and
record, full rule text and ~9k of design guidance. On a project with 39 the design guidance add up, on a project with a few dozen milestones, to more
milestones that came to ~222k characters, past what an MCP client accepts as than an MCP client accepts as a tool result — and the session handshake
a tool result, so the session handshake arrived as a file to page through. arrives as a file to page through.
""" """
import contextlib import contextlib
import json import json
@@ -66,7 +66,7 @@ def test_brief_rows_leave_out_the_plan_and_what_the_caller_already_knows():
"id": 1, "title": "M1", "description": "what M1 is for", "status": "active", "id": 1, "title": "M1", "description": "what M1 is for", "status": "active",
"order_index": 1, "total": 4, "completed": 2, "pct": 50.0, "order_index": 1, "total": 4, "completed": 2, "pct": 50.0,
"status_counts": {"todo": 2, "in_progress": 0, "done": 2, "cancelled": 0}, "status_counts": {"todo": 2, "in_progress": 0, "done": 2, "cancelled": 0},
# Trimmed of the plan body, but NOT of which step is next (#4154): the # Trimmed of the plan body, but NOT of which step is next: the
# listing is where "2 of 4 done" gets read, and that number is exactly # listing is where "2 of 4 done" gets read, and that number is exactly
# what invites a reader to name the open step from memory. # what invites a reader to name the open step from memory.
"next_step": {"id": 901, "title": "M1 step 3", "status": "todo"}, "next_step": {"id": 901, "title": "M1 step 3", "status": "todo"},
@@ -187,7 +187,7 @@ async def test_open_tasks_name_their_milestone_even_when_it_is_not_listed():
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_omitted_key_is_absent_when_nothing_was_left_out(): async def test_omitted_key_is_absent_when_nothing_was_left_out():
"""Attached only when it applies (#2483).""" """Attached only when it applies."""
out, _ = await _enter(*_enter_stubs(fake_project(id=5), _history(3), [])) out, _ = await _enter(*_enter_stubs(fake_project(id=5), _history(3), []))
assert len(out["milestone_summary"]) == 3 assert len(out["milestone_summary"]) == 3
assert "milestone_summary_omitted" not in out assert "milestone_summary_omitted" not in out
@@ -217,8 +217,8 @@ async def test_list_milestones_lists_every_milestone_without_plans():
def test_a_planning_read_lists_rules_without_restating_them(): def test_a_planning_read_lists_rules_without_restating_them():
"""#4081: a project's listing is every global rule tagged to its areas, so """A project's listing is every global rule tagged to its areas, so the
the full rule_brief of each put start_planning at 92k characters. Planning full rule_brief of each would make a planning read enormous. Planning
reads name the rules; get_rule reads one.""" reads name the rules; get_rule reads one."""
from scribe.services.rulebooks import rules_payload from scribe.services.rulebooks import rules_payload
@@ -241,7 +241,7 @@ def test_a_planning_read_lists_rules_without_restating_them():
assert len(json.dumps(out)) < 6_000, len(json.dumps(out)) assert len(json.dumps(out)) < 6_000, len(json.dumps(out))
# ── Milestones with no steps are open work (milestone 415) ───────────────── # ── Milestones with no steps are open work ─────────────────────────────────
def _planless(mid: int, status: str = "active") -> dict: def _planless(mid: int, status: str = "active") -> dict:
+8 -9
View File
@@ -1,4 +1,4 @@
"""The reporting-back skill keeps its shape (milestone 409 step 2). """The reporting-back skill keeps its shape.
WHY THIS EXISTS WHY THIS EXISTS
@@ -14,8 +14,8 @@ Structure, never wording — the same reason test_create_tools_disambiguate
gives: a test that punishes rewriting gets deleted. It pins that the gives: a test that punishes rewriting gets deleted. It pins that the
completion report keeps its five sections, that placement is taken from the completion report keeps its five sections, that placement is taken from the
record rather than recalled, and that the shipped shapes stay domain-neutral. record rather than recalled, and that the shipped shapes stay domain-neutral.
Whether the guidance is any good is milestone 409's last step, read against Whether the guidance is any good is read against real replies, not something a
real replies, not something a test can see. test can see.
""" """
import pathlib import pathlib
import re import re
@@ -39,14 +39,13 @@ def test_the_completion_report_keeps_its_sections():
def test_a_request_for_approval_has_its_own_named_section(): def test_a_request_for_approval_has_its_own_named_section():
"""The operator, 2026-09-15 (#4084): a session's go-ahead request sat inside """A go-ahead request inside a completion list, as "Blocked by the
a completion list as "Blocked by the permission check", and read as a permission check", reads as a fault rather than a question waiting on the
fault rather than a question waiting on them. The heading is what they operator. The heading is what they scan for, so it is pinned by name."""
scan for, so it is pinned by name."""
from scribe.services import reply_shapes from scribe.services import reply_shapes
assert "Approval requested" in _text() assert "Approval requested" in _text()
# The kinds moved to the delivered shapes (milestone 500): the asks shape # The kinds live in the delivered shapes: the asks shape
# carries the Approval kind, and the core sends a held action there. # carries the Approval kind, and the core sends a held action there.
assert "**Approval**" in reply_shapes.SHAPES["asks"].text assert "**Approval**" in reply_shapes.SHAPES["asks"].text
assert "Approval requested" in reply_shapes.core().text assert "Approval requested" in reply_shapes.core().text
@@ -70,7 +69,7 @@ def test_the_shipped_shapes_assume_no_particular_domain():
def test_the_skill_points_at_the_delivered_shapes_instead_of_restating_them(): def test_the_skill_points_at_the_delivered_shapes_instead_of_restating_them():
"""Milestone 500 step 4: the shapes are server product, delivered at their """The shapes are server product, delivered at their
moments. The skill says where they come from and holds the reasoning.""" moments. The skill says where they come from and holds the reasoning."""
text = _text() text = _text()
assert "list_reply_shapes" in text and "Reply shape · Every reply" in text assert "list_reply_shapes" in text and "Reply shape · Every reply" in text
+6 -7
View File
@@ -41,9 +41,8 @@ def test_service_functions_take_user_id():
def test_agent_and_web_surfaces_stay_at_parity(): def test_agent_and_web_surfaces_stay_at_parity():
"""The MCP tools and the REST routes are two callers of one service; a """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). This guard exists capability on one has to exist on the other. Two doors maintained
because they drifted apart once: MCP had no delete or `locations`, and the separately drift — a delete on one, a near-duplicate gate on the other."""
web side had no `system_ids` and no near-duplicate gate."""
from scribe.mcp.tools import snippets as tools from scribe.mcp.tools import snippets as tools
from scribe.routes import snippets as routes from scribe.routes import snippets as routes
@@ -65,13 +64,13 @@ def test_agent_and_web_surfaces_stay_at_parity():
assert "system_ids" in inspect.getsource(route) assert "system_ids" in inspect.getsource(route)
assert "find_duplicate_note" in inspect.getsource(routes.create_snippet_route) assert "find_duplicate_note" in inspect.getsource(routes.create_snippet_route)
# The drift check (#2086) reaches both: recording a verdict, and filtering # The drift check reaches both: recording a verdict, and filtering
# the list by one. A verdict an agent records has to be visible to the human # the list by one. A verdict an agent records has to be visible to the human
# looking at the same corpus, or the two surfaces disagree about what's rotten. # looking at the same corpus, or the two surfaces disagree about what's rotten.
assert "verification" in inspect.signature(tools.list_snippets).parameters assert "verification" in inspect.signature(tools.list_snippets).parameters
assert "verification" in inspect.getsource(routes.list_snippets_route) assert "verification" in inspect.getsource(routes.list_snippets_route)
# The near-duplicate report (#2088) reaches both. The web side can only hand # The near-duplicate report reaches both. The web side can only hand
# a group to the merge flow if it can get the groups in the first place. # a group to the merge flow if it can get the groups in the first place.
assert callable(getattr(tools, "find_duplicate_snippets", None)) assert callable(getattr(tools, "find_duplicate_snippets", None))
assert callable(getattr(routes, "duplicate_snippets_route", None)) assert callable(getattr(routes, "duplicate_snippets_route", None))
@@ -90,7 +89,7 @@ def test_project_scoping_reaches_every_caller():
def test_location_lookup_reaches_every_caller(): def test_location_lookup_reaches_every_caller():
"""The reverse lookup — "what already lives here?" — has to be askable from """The reverse lookup — "what already lives here?" — has to be askable from
both surfaces, or an agent and a human get different answers about the same both surfaces, or an agent and a human get different answers about the same
file (rule #33).""" file."""
from scribe.mcp.tools import snippets as tools from scribe.mcp.tools import snippets as tools
from scribe.routes import snippets as routes from scribe.routes import snippets as routes
from scribe.services import snippets as svc from scribe.services import snippets as svc
@@ -102,7 +101,7 @@ def test_location_lookup_reaches_every_caller():
def test_update_field_map_matches_service_kwargs(): def test_update_field_map_matches_service_kwargs():
"""Every field the PATCH route forwards must be a real update_snippet kwarg """Every field the PATCH route forwards must be a real update_snippet kwarg
(rule #33 interface-contract parity).""" (interface-contract parity)."""
from scribe.routes import snippets as routes from scribe.routes import snippets as routes
from scribe.services import snippets as svc from scribe.services import snippets as svc
params = inspect.signature(svc.update_snippet).parameters params = inspect.signature(svc.update_snippet).parameters
+7 -8
View File
@@ -1,13 +1,12 @@
"""Entries in the rule exclusion ledger age out (#3751). """Entries in the rule exclusion ledger age out.
WHY THIS EXISTS WHY THIS EXISTS
#3749 clears the ledger when an EVENT destroys context — a compaction, a The ledger is cleared when an EVENT destroys context — a compaction, a /clear.
/clear. This covers the case with no event at all: a long session where a rule This covers the case with no event at all: a long session where a rule was
was named two hundred turns ago and has simply fallen out of attention. Same named two hundred turns ago and has simply fallen out of attention. Present in
argument #3702 made about tiers (present in context and salient at the context and salient at the moment are different properties, and time
moment are different properties), applied to time instead. The tier itself is separates them as surely as a compaction does.
gone since milestone 394; the distinction it taught is what survives.
WHAT IS PINNED, AND WHAT DELIBERATELY IS NOT WHAT IS PINNED, AND WHAT DELIBERATELY IS NOT
@@ -76,7 +75,7 @@ def _write_ledger(ledger: Path, entries: list[tuple[str, int | None]]) -> None:
def test_an_old_entry_ages_out_and_a_recent_one_does_not(tmp_path): def test_an_old_entry_ages_out_and_a_recent_one_does_not(tmp_path):
"""BOTH DIRECTIONS IN ONE ASSERTION (rule 167), because either half alone """BOTH DIRECTIONS IN ONE ASSERTION, because either half alone
passes against a broken implementation. passes against a broken implementation.
A test that only checks the old id is gone passes against a helper that A test that only checks the old id is gone passes against a helper that
+10 -11
View File
@@ -1,18 +1,17 @@
"""services/notes.py — the inline embedding moved here from the routes (#2056). """services/notes.py — inline embedding lives in the service.
Why it moved: embedding was triggered at each REST route and nowhere else, so a Embedding triggered at each REST route and nowhere else would leave a record
record created through MCP stayed out of semantic search and auto-inject until created through MCP out of semantic search and auto-inject until the next
the next restart's backfill. Putting it in the service means every caller — REST, restart's backfill. In the service, every caller — REST, MCP, recurrence,
MCP, recurrence, snippets — gets it by construction rather than by remembering. snippets — gets it by construction rather than by remembering.
These test the helper directly. The point of the change is that there is now ONE These test the helper directly: there is ONE place to test.
place to test.
""" """
import pytest import pytest
from scribe.services import notes as notes_svc from scribe.services import notes as notes_svc
# --- inline embedding (#2056) ----------------------------------------------- # --- inline embedding -------------------------------------------------------
def test_embed_note_uses_the_OWNER_not_the_caller(): def test_embed_note_uses_the_OWNER_not_the_caller():
"""LOAD-BEARING for shared records. An embedding belongs to the record; a """LOAD-BEARING for shared records. An embedding belongs to the record; a
@@ -31,14 +30,14 @@ def test_embed_note_uses_the_OWNER_not_the_caller():
upsert.assert_called_once() upsert.assert_called_once()
assert upsert.call_args.args[0] == 5 assert upsert.call_args.args[0] == 5
assert upsert.call_args.args[1] == 42 # owner, never the caller assert upsert.call_args.args[1] == 42 # owner, never the caller
# Title and body travel separately since #280 — chunking happens inside # Title and body travel separately — chunking happens inside
# upsert_note_embedding, the one path every writer shares. # upsert_note_embedding, the one path every writer shares.
assert upsert.call_args.args[2] == "T" assert upsert.call_args.args[2] == "T"
assert upsert.call_args.args[3] == "B" assert upsert.call_args.args[3] == "B"
def test_embed_note_hands_even_an_empty_record_to_the_one_path(): def test_embed_note_hands_even_an_empty_record_to_the_one_path():
"""The empty-record decision moved INTO upsert_note_embedding (#280): an """The empty-record decision lives IN upsert_note_embedding: an
emptied record must have its stale vectors CLEARED, not merely skipped — emptied record must have its stale vectors CLEARED, not merely skipped —
so embed_note schedules the call unconditionally rather than deciding so embed_note schedules the call unconditionally rather than deciding
here. The clearing behaviour itself is pinned in test_chunking.py.""" here. The clearing behaviour itself is pinned in test_chunking.py."""
@@ -73,7 +72,7 @@ def test_embed_note_swallows_an_indexing_failure():
notes_svc.embed_note(note) # must not raise notes_svc.embed_note(note) # must not raise
# --- #3683: a create that names a status is the transition an update is ------ # --- a create that names a status is the transition an update is -------------
_LIFECYCLE = ("started_at", "completed_at", "recurrence_next_spawn_at") _LIFECYCLE = ("started_at", "completed_at", "recurrence_next_spawn_at")
+9 -9
View File
@@ -1,9 +1,9 @@
"""The document a rule is EMBEDDED as (milestone 307 step 4, note 3026). """The document a rule is EMBEDDED as.
This shape is measured, not chosen. Note 2485 probed the live corpus and found This shape is measured, not chosen. Probed across a corpus, the snippet is the
the snippet was the only discriminative record in it — a 0.153 top-to-second most discriminative record kind — a 0.153 top-to-second gap against
gap against 0.010-0.023 for everything else — and that the cause was its shape: 0.010-0.023 for everything else — and the cause is its shape: purpose stated
purpose stated twice in a short, single-topic document. These cases pin that twice in a short, single-topic document. These cases pin that
recipe onto rules, and pin the exclusion that matters more than any of it. recipe onto rules, and pin the exclusion that matters more than any of it.
""" """
import pytest import pytest
@@ -27,8 +27,8 @@ def test_the_trigger_appears_twice_which_is_what_makes_a_vector_sharp():
def test_why_is_never_embedded(): def test_why_is_never_embedded():
"""The exclusion this whole design turns on. `why` is dated incident """The exclusion this whole design turns on. `why` is dated incident
narrative — rule 46's runs to 4,300 characters — and long multi-topic prose narrative — it can run to thousands of characters — and long multi-topic
is what made sixteen dev-logs mutually indistinguishable: the average lands prose is what makes dev-logs mutually indistinguishable: the average lands
on a centroid they all share. Adding it would not give the vector more to on a centroid they all share. Adding it would not give the vector more to
work with; it would give every rule the SAME thing to work with. work with; it would give every rule the SAME thing to work with.
@@ -41,7 +41,7 @@ def test_why_is_never_embedded():
def test_a_rule_with_no_trigger_still_embeds_just_less_sharply(): def test_a_rule_with_no_trigger_still_embeds_just_less_sharply():
"""Every rule written before milestone 307 has no trigger. Degrading to """A rule can have no trigger. Degrading to
title + statement keeps them findable; it does NOT pad the document with title + statement keeps them findable; it does NOT pad the document with
whatever text is lying around, which would be the tempting fix and the whatever text is lying around, which would be the tempting fix and the
wrong one.""" wrong one."""
@@ -60,7 +60,7 @@ def test_an_empty_rule_yields_no_document_and_therefore_no_vector():
def test_a_long_rule_chunks_and_every_chunk_carries_the_trigger(): def test_a_long_rule_chunks_and_every_chunk_carries_the_trigger():
"""Rule 46's statement is ~3,600 characters across two headed sections. """A ~3,600-character statement across two headed sections.
The existing chunker splits it at headings and prefixes each chunk with the The existing chunker splits it at headings and prefixes each chunk with the
title — which now CONTAINS the trigger, so each half stays anchored to what title — which now CONTAINS the trigger, so each half stays anchored to what
the rule is for. This is why splitting a merged rule costs nothing at the rule is for. This is why splitting a merged rule costs nothing at
+5 -5
View File
@@ -3,7 +3,7 @@ from scribe.services import snippets as s
def test_the_embedded_title_joins_the_trigger_the_stored_one_does_not_carry(): def test_the_embedded_title_joins_the_trigger_the_stored_one_does_not_carry():
"""Milestone 427: stored title = name; the trigger joins it at embed time, """Stored title = name; the trigger joins it at embed time,
idempotently, so an old composed title comes out the same.""" idempotently, so an old composed title comes out the same."""
from scribe.services.embeddings import document_title from scribe.services.embeddings import document_title
@@ -167,7 +167,7 @@ def test_merge_snippet_fields_unions_locations_and_tags():
# extra tags unioned, language + "snippet" markers excluded. # extra tags unioned, language + "snippet" markers excluded.
assert extra == ["core", "helper"] assert extra == ["core", "helper"]
# Attribution (#2165): only what each source ACTUALLY added. src2's location # Attribution: only what each source ACTUALLY added. src2's location
# is one the target already had, so it contributed nothing — un-merging it # is one the target already had, so it contributed nothing — un-merging it
# must not strip a call site the survivor owns in its own right. # must not strip a call site the survivor owns in its own right.
assert contributions[0]["locations"] == [{"repo": "b", "path": "b.py", "symbol": "g"}] assert contributions[0]["locations"] == [{"repo": "b", "path": "b.py", "symbol": "g"}]
@@ -249,7 +249,7 @@ def test_data_and_body_round_trip_to_the_same_fields():
name, when, sig, lang = ("debounce", "rate-limit a callback", name, when, sig, lang = ("debounce", "rate-limit a callback",
"debounce(fn, ms)", "ts") "debounce(fn, ms)", "ts")
locs = [{"repo": "web", "path": "src/util.ts", "symbol": "debounce"}] locs = [{"repo": "web", "path": "src/util.ts", "symbol": "debounce"}]
# compose_body takes no `name` — the name IS the title (milestone 427) — so # compose_body takes no `name` — the name IS the title — so
# the two serializers get their own argument lists rather than a shared spread. # the two serializers get their own argument lists rather than a shared spread.
title = name title = name
body = s.compose_body(code="const x = 1", language=lang, signature=sig, body = s.compose_body(code="const x = 1", language=lang, signature=sig,
@@ -267,7 +267,7 @@ def test_data_and_body_round_trip_to_the_same_fields():
assert from_body[key] == from_data[key], key assert from_body[key] == from_data[key], key
# --- merge provenance (#2087) ------------------------------------------------ # --- merge provenance -------------------------------------------------------
def test_normalize_merged_from_keeps_history_order_and_drops_junk(): def test_normalize_merged_from_keeps_history_order_and_drops_junk():
"""Order is history, not sorting — earlier merges stay first.""" """Order is history, not sorting — earlier merges stay first."""
@@ -276,7 +276,7 @@ def test_normalize_merged_from_keeps_history_order_and_drops_junk():
def test_normalize_merged_from_carries_per_source_attribution(): def test_normalize_merged_from_carries_per_source_attribution():
"""The shape that makes un-merge exact (#2165): each entry holds what THAT """The shape that makes un-merge exact: each entry holds what THAT
source contributed, so reversing one can't strip what the survivor owns.""" source contributed, so reversing one can't strip what the survivor owns."""
loc = {"repo": "a", "path": "a.py", "symbol": "f"} loc = {"repo": "a", "path": "a.py", "symbol": "f"}
out = s._normalize_merged_from([{"id": 7, "locations": [loc], "tags": ["x"]}]) out = s._normalize_merged_from([{"id": 7, "locations": [loc], "tags": ["x"]}])