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
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
+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
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"
+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,
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
+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
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
+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
#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))]
+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
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:
+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
@@ -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
+6 -7
View File
@@ -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
+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
#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
+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
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")
+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
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
+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():
"""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"]}])