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
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:
@@ -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
|
||||
|
||||
Milestone 419's most repeated miss, five of seven: acting on the thing in hand
|
||||
without reading the contract around it. Lesson #4207 says it in words —
|
||||
"widening a tuple is an interface change to every unpack site, and the
|
||||
compiler will not tell you" — and was written by its author HOURS before a
|
||||
structurally identical mistake, having been surfaced twice in the turns
|
||||
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.
|
||||
The commonest miss is acting on the thing in hand without reading the contract
|
||||
around it: "widening a tuple is an interface change to every unpack site, and
|
||||
the compiler will not tell you". Knowing that in words does not stop it — 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
|
||||
question exists between a definition and its callers.
|
||||
|
||||
@@ -21,18 +19,17 @@ nothing alike in source:
|
||||
arg its parameter names arity and order
|
||||
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
|
||||
one a signature-watcher would have missed: `get_writepath_config` gained one
|
||||
dict key, three arms read that dict inside a fail-open `except`, every one
|
||||
silently became a no-op, and ten tests went red at once with nothing pointing
|
||||
at the cause. No signature changed.
|
||||
The third is the one a signature-watcher misses: a function gains one dict
|
||||
key, three callers 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
|
||||
the cause. No signature changed.
|
||||
|
||||
WHAT IS PINNED: which edits speak and which stay silent, and that both gates
|
||||
are load-bearing. NOT pinned: the wording, which is prose.
|
||||
|
||||
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
|
||||
renames something (rule 115's reasoning, one floor down).
|
||||
renames something.
|
||||
"""
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
@@ -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
|
||||
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),
|
||||
and that the seed ships nothing an install other than this one would find
|
||||
foreign (rule 115).
|
||||
and that the seed ships nothing a stranger's install would find foreign.
|
||||
"""
|
||||
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():
|
||||
"""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
|
||||
reaches (#4221's two-sources-of-truth shape)."""
|
||||
reaches — two sources of truth."""
|
||||
from scribe.models.rulebook import RulebookTopic
|
||||
|
||||
assert "topic_id" in family.FamilyIdea.__table__.c
|
||||
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():
|
||||
reason = "the APK is built in CI and signed with the one release key"
|
||||
|
||||
@@ -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,
|
||||
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
|
||||
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
|
||||
resolves is indistinguishable from work still in progress. Rule 156 names
|
||||
resolves is indistinguishable from work still in progress. The practice names
|
||||
`fetch` specifically:
|
||||
|
||||
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
|
||||
]
|
||||
assert not naked, (
|
||||
"these fetch calls carry no AbortSignal, so they wait forever "
|
||||
"(rule 156):\n " + "\n ".join(naked)
|
||||
"these fetch calls carry no AbortSignal, so they wait forever:\n " + "\n ".join(naked)
|
||||
)
|
||||
|
||||
|
||||
def test_the_client_applies_its_deadline_by_default():
|
||||
"""The specific regression that would silently undo this.
|
||||
|
||||
An earlier pass (#3329) made `timeoutMs` OPT-IN and used it at exactly one
|
||||
call site, which left ~330 others waiting forever while the mechanism
|
||||
looked present. Reverting to that shape would not fail the guard above —
|
||||
An OPT-IN `timeoutMs` used at one call site leaves every other call
|
||||
waiting forever while the mechanism looks present. That shape would not fail the guard above —
|
||||
every call would still reach `fetch` through `request()` — so the default
|
||||
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():
|
||||
"""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)`
|
||||
as an object with no `body`, so every catch site in the app would print its
|
||||
|
||||
@@ -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
|
||||
|
||||
The step asked for "a guard that the CHECK actually accepts `lesson` and
|
||||
rejects a typo", citing #3128 — a `spike` kind that could not be written on an
|
||||
instance predating its migration. That failure mode belongs to `task_kind`,
|
||||
The obvious ask is a guard that a CHECK accepts `lesson` and rejects a typo —
|
||||
the failure where a new kind cannot be written on an instance predating its
|
||||
migration. That failure mode belongs to `task_kind`,
|
||||
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
|
||||
server default and nothing has constrained it since.
|
||||
|
||||
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
|
||||
does not exist; writing it as "no constraint exists" would pin today's schema
|
||||
guard as "the constraint admits lesson" would pin a constraint that does not
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
# the queries are built from the mirror.
|
||||
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 "**When to apply:**" in (stored.body or "")
|
||||
|
||||
|
||||
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
|
||||
observable on a stored row. A lesson that arrived with a status would join
|
||||
|
||||
@@ -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
|
||||
|
||||
#3750 settled the argument for rules: 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
|
||||
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
|
||||
the best answer the session gets the line again with a tail saying so — never
|
||||
silence, which is indistinguishable from "nothing matched".
|
||||
|
||||
The note and snippet arms never got that fix. Worse, their ledger went into
|
||||
`semantic_search_notes` as `exclude_ids`, so the repeat left the candidate set
|
||||
entirely, which had three consequences:
|
||||
The note and snippet arms hold the same line. A ledger passed into
|
||||
`semantic_search_notes` as `exclude_ids` takes the repeat out of the candidate
|
||||
set entirely, with three consequences:
|
||||
|
||||
- the session got silence the second time, on the arms that fire most;
|
||||
- a compaction made it permanent, because the ledger outlived the context it
|
||||
described (the same step fixes that one layer down);
|
||||
- and `best_available` was measured against a candidate set the caller had
|
||||
already edited, so the bar could be blamed for a record the caller withheld
|
||||
(#3739, from the side its fix never reached).
|
||||
- the session gets silence the second time, on the arms that fire most;
|
||||
- a compaction makes it permanent, because the ledger outlives the context it
|
||||
describes;
|
||||
- and `best_available` is measured against a candidate set the caller has
|
||||
already edited, so the bar can be blamed for a record the caller withheld.
|
||||
|
||||
WHAT THIS PINS
|
||||
|
||||
@@ -28,11 +26,11 @@ WHAT THIS PINS
|
||||
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
|
||||
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
|
||||
`suppressed` instead. This is what makes a zero-result call readable —
|
||||
`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
|
||||
claim is about an edit in progress, not about a record's continuing
|
||||
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
|
||||
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()
|
||||
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))]
|
||||
|
||||
@@ -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
|
||||
record, full rule text and ~9k of design guidance. On a project with 39
|
||||
milestones that came to ~222k characters, past what an MCP client accepts as
|
||||
a tool result, so the session handshake arrived as a file to page through.
|
||||
Every milestone's full plan body, the whole project record, full rule text and
|
||||
the design guidance add up, on a project with a few dozen milestones, to more
|
||||
than an MCP client accepts as a tool result — and the session handshake
|
||||
arrives as a file to page through.
|
||||
"""
|
||||
import contextlib
|
||||
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",
|
||||
"order_index": 1, "total": 4, "completed": 2, "pct": 50.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
|
||||
# what invites a reader to name the open step from memory.
|
||||
"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
|
||||
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), []))
|
||||
assert len(out["milestone_summary"]) == 3
|
||||
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():
|
||||
"""#4081: a project's listing is every global rule tagged to its areas, so
|
||||
the full rule_brief of each put start_planning at 92k characters. Planning
|
||||
"""A project's listing is every global rule tagged to its areas, so the
|
||||
full rule_brief of each would make a planning read enormous. Planning
|
||||
reads name the rules; get_rule reads one."""
|
||||
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))
|
||||
|
||||
|
||||
# ── Milestones with no steps are open work (milestone 415) ─────────────────
|
||||
# ── Milestones with no steps are open work ─────────────────────────────────
|
||||
|
||||
|
||||
def _planless(mid: int, status: str = "active") -> dict:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
completion report keeps its five sections, that placement is taken from the
|
||||
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
|
||||
real replies, not something a test can see.
|
||||
Whether the guidance is any good is read against real replies, not something a
|
||||
test can see.
|
||||
"""
|
||||
import pathlib
|
||||
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():
|
||||
"""The operator, 2026-09-15 (#4084): a session's go-ahead request sat inside
|
||||
a completion list as "Blocked by the permission check", and read as a
|
||||
fault rather than a question waiting on them. The heading is what they
|
||||
scan for, so it is pinned by name."""
|
||||
"""A go-ahead request inside a completion list, as "Blocked by the
|
||||
permission check", reads as a fault rather than a question waiting on the
|
||||
operator. The heading is what they scan for, so it is pinned by name."""
|
||||
from scribe.services import reply_shapes
|
||||
|
||||
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.
|
||||
assert "**Approval**" in reply_shapes.SHAPES["asks"].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():
|
||||
"""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."""
|
||||
text = _text()
|
||||
assert "list_reply_shapes" in text and "Reply shape · Every reply" in text
|
||||
|
||||
@@ -41,9 +41,8 @@ def test_service_functions_take_user_id():
|
||||
|
||||
def test_agent_and_web_surfaces_stay_at_parity():
|
||||
"""The MCP tools and the REST routes are two callers of one service; a
|
||||
capability on one has to exist on the other (rule #33). This guard exists
|
||||
because they drifted apart once: MCP had no delete or `locations`, and the
|
||||
web side had no `system_ids` and no near-duplicate gate."""
|
||||
capability on one has to exist on the other. Two doors maintained
|
||||
separately drift — a delete on one, a near-duplicate gate on the other."""
|
||||
from scribe.mcp.tools import snippets as tools
|
||||
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 "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
|
||||
# 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.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.
|
||||
assert callable(getattr(tools, "find_duplicate_snippets", 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():
|
||||
"""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
|
||||
file (rule #33)."""
|
||||
file."""
|
||||
from scribe.mcp.tools import snippets as tools
|
||||
from scribe.routes import snippets as routes
|
||||
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():
|
||||
"""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.services import snippets as svc
|
||||
params = inspect.signature(svc.update_snippet).parameters
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
"""Entries in the rule exclusion ledger age out (#3751).
|
||||
"""Entries in the rule exclusion ledger age out.
|
||||
|
||||
WHY THIS EXISTS
|
||||
|
||||
#3749 clears the ledger when an EVENT destroys context — a compaction, a
|
||||
/clear. This covers the case with no event at all: a long session where a rule
|
||||
was named two hundred turns ago and has simply fallen out of attention. Same
|
||||
argument #3702 made about tiers (present in context and salient at the
|
||||
moment are different properties), applied to time instead. The tier itself is
|
||||
gone since milestone 394; the distinction it taught is what survives.
|
||||
The ledger is cleared when an EVENT destroys context — a compaction, a /clear.
|
||||
This covers the case with no event at all: a long session where a rule was
|
||||
named two hundred turns ago and has simply fallen out of attention. Present in
|
||||
context and salient at the moment are different properties, and time
|
||||
separates them as surely as a compaction does.
|
||||
|
||||
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):
|
||||
"""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.
|
||||
|
||||
A test that only checks the old id is gone passes against a helper that
|
||||
|
||||
@@ -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
|
||||
record created through MCP stayed out of semantic search and auto-inject until
|
||||
the next restart's backfill. Putting it in the service means every caller — REST,
|
||||
MCP, recurrence, snippets — gets it by construction rather than by remembering.
|
||||
Embedding triggered at each REST route and nowhere else would leave a record
|
||||
created through MCP out of semantic search and auto-inject until the next
|
||||
restart's backfill. In the service, every caller — REST, MCP, recurrence,
|
||||
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
|
||||
place to test.
|
||||
These test the helper directly: there is ONE place to test.
|
||||
"""
|
||||
import pytest
|
||||
|
||||
from scribe.services import notes as notes_svc
|
||||
|
||||
# --- inline embedding (#2056) -----------------------------------------------
|
||||
# --- inline embedding -------------------------------------------------------
|
||||
|
||||
def test_embed_note_uses_the_OWNER_not_the_caller():
|
||||
"""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()
|
||||
assert upsert.call_args.args[0] == 5
|
||||
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.
|
||||
assert upsert.call_args.args[2] == "T"
|
||||
assert upsert.call_args.args[3] == "B"
|
||||
|
||||
|
||||
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 —
|
||||
so embed_note schedules the call unconditionally rather than deciding
|
||||
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
|
||||
|
||||
|
||||
# --- #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")
|
||||
|
||||
|
||||
@@ -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
|
||||
the snippet was the only discriminative record in it — a 0.153 top-to-second
|
||||
gap against 0.010-0.023 for everything else — and that the cause was its shape:
|
||||
purpose stated twice in a short, single-topic document. These cases pin that
|
||||
This shape is measured, not chosen. Probed across a corpus, the snippet is the
|
||||
most discriminative record kind — a 0.153 top-to-second gap against
|
||||
0.010-0.023 for everything else — and the cause is its shape: purpose stated
|
||||
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.
|
||||
"""
|
||||
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():
|
||||
"""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
|
||||
is what made sixteen dev-logs mutually indistinguishable: the average lands
|
||||
narrative — it can run to thousands of characters — and long multi-topic
|
||||
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
|
||||
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():
|
||||
"""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
|
||||
whatever text is lying around, which would be the tempting fix and the
|
||||
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():
|
||||
"""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
|
||||
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
|
||||
|
||||
@@ -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():
|
||||
"""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."""
|
||||
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.
|
||||
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
|
||||
# must not strip a call site the survivor owns in its own right.
|
||||
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",
|
||||
"debounce(fn, ms)", "ts")
|
||||
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.
|
||||
title = name
|
||||
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
|
||||
|
||||
|
||||
# --- merge provenance (#2087) ------------------------------------------------
|
||||
# --- merge provenance -------------------------------------------------------
|
||||
|
||||
def test_normalize_merged_from_keeps_history_order_and_drops_junk():
|
||||
"""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():
|
||||
"""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."""
|
||||
loc = {"repo": "a", "path": "a.py", "symbol": "f"}
|
||||
out = s._normalize_merged_from([{"id": 7, "locations": [loc], "tags": ["x"]}])
|
||||
|
||||
Reference in New Issue
Block a user