Compare commits
3
Commits
b3e6a27dc1
...
d0d278dcce
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0d278dcce | ||
|
|
ead4a7e2ad | ||
|
|
fa41e006a7 |
@@ -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,13 +1,13 @@
|
||||
"""The starter role set (#2349).
|
||||
"""The starter role set.
|
||||
|
||||
The premise: a literal gets written when there is no role to reach for. This
|
||||
codebase demonstrated it — the house style had no "text on a filled colour"
|
||||
role, so 76 call sites wrote `color: #fff` (#2275). Naming the roles at
|
||||
creation removes the occasion.
|
||||
The premise: a literal gets written when there is no role to reach for. A
|
||||
house style with no "text on a filled colour" role collects `color: #fff` at
|
||||
every call site that needs one. Naming the roles at creation removes the
|
||||
occasion.
|
||||
|
||||
The tests that matter here are about the BOUNDARY, not the content. Roles ship
|
||||
with the product; values never do. A default palette would be one operator's
|
||||
taste shipped as product code (rule #115), and it would be very easy to add by
|
||||
taste shipped as product code, and it would be very easy to add by
|
||||
accident while "being helpful".
|
||||
"""
|
||||
import re
|
||||
@@ -28,9 +28,8 @@ def test_no_role_carries_a_VALUE():
|
||||
def _colour_literals(text: str) -> list[str]:
|
||||
"""Hex colours in `text`, NOT counting issue references.
|
||||
|
||||
`#2275` is four hex-valid digits and also how this codebase cites an issue —
|
||||
so the naive pattern flags its own documentation, which is the third time
|
||||
that has happened here (#2353). A real colour either contains a letter
|
||||
`#2275` is four hex-valid digits and also how an issue is cited — so the
|
||||
naive pattern flags documentation. A real colour either contains a letter
|
||||
a–f or is a full 6/8-digit value; an all-decimal 3- or 4-digit match is an
|
||||
issue number.
|
||||
"""
|
||||
@@ -64,7 +63,7 @@ def test_the_colour_check_does_not_flag_issue_references():
|
||||
|
||||
|
||||
def test_every_group_is_individually_selectable():
|
||||
"""Operator's call: one flat list, all skippable. An install that wants
|
||||
"""One flat list, all skippable. An install that wants
|
||||
three tokens must be able to get three."""
|
||||
only_text = roles.starter_tokens(["text"])
|
||||
assert {r["group_name"] for r in only_text} == {"text"}
|
||||
@@ -99,7 +98,7 @@ def test_text_on_action_is_in_the_starter_set():
|
||||
"""The specific role whose absence produced 76 literals. It is separate
|
||||
from text-primary on purpose: the surfaces it sits on do not change with
|
||||
the mode, while the page does — so reusing text-primary there passes in
|
||||
dark and fails contrast in light (#2275)."""
|
||||
dark and fails contrast in light."""
|
||||
names = {r["name"] for r in roles.starter_tokens(["text"])}
|
||||
assert "--ds-text-on-action" in names
|
||||
assert "--ds-text-primary" in names
|
||||
|
||||
@@ -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,12 +1,10 @@
|
||||
"""JSON, read and written by the hooks without jq (#4107).
|
||||
"""JSON, read and written by the hooks without jq.
|
||||
|
||||
WHAT THIS REPLACED, AND WHY IT NEEDS TESTING AT ALL. Every hook used to open
|
||||
`command -v jq >/dev/null 2>&1 || exit 0`, so on a machine without jq the
|
||||
operator got no session context, no rules, no prior art and no process sync,
|
||||
and not one word saying why — `exit 0` is indistinguishable from "ran fine,
|
||||
nothing to say". jq is absent by default on macOS, on the Debian/Ubuntu slim
|
||||
images, on Alpine and in most CI containers. The parser that replaced it is
|
||||
`plugin/hooks/scribe_json.awk`, written in POSIX awk, and a parser is exactly
|
||||
WHY NOT jq, AND WHY THIS NEEDS TESTING AT ALL. jq is absent by default on
|
||||
macOS, on the Debian/Ubuntu slim images, on Alpine and in most CI containers,
|
||||
and a hook that exits quietly without it gives the operator no session context,
|
||||
no rules, no prior art and no process sync, with not one word saying why. The
|
||||
parser is `plugin/hooks/scribe_json.awk`, written in POSIX awk, and a parser is exactly
|
||||
the kind of thing that works on the six documents you tried it on.
|
||||
|
||||
So the centre of this file is a DIFFERENTIAL against Python's `json`: the same
|
||||
@@ -16,7 +14,7 @@ is one the hooks can be handed an arbitrary event with.
|
||||
|
||||
The rest pins the three jobs around it — writing the hook envelope,
|
||||
percent-encoding a URL, and bounding a turn in a transcript — and that each
|
||||
guard here can fail (rule 167).
|
||||
guard here can fail.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -74,7 +72,7 @@ def reference_flatten(value, path="", out=None):
|
||||
shell reads it back with. Every array also reports its LENGTH at `[#]`,
|
||||
including an empty one, and an empty object reports `{#}` 0 — that is what
|
||||
lets a caller tell "the server answered with zero notes" from "the server
|
||||
did not answer", which #2932 built an outage marker around.
|
||||
did not answer", which the outage marker is built around.
|
||||
"""
|
||||
out = [] if out is None else out
|
||||
if isinstance(value, dict):
|
||||
@@ -221,7 +219,7 @@ def test_an_absent_field_and_a_null_field_both_read_as_empty():
|
||||
def test_an_absent_array_length_is_empty_not_zero():
|
||||
"""NOT the same answer as 0, and the distinction is load-bearing: the
|
||||
record nudge fires when the server said "zero notes" and must not fire
|
||||
when the server said nothing at all (#2932)."""
|
||||
when the server said nothing at all."""
|
||||
out = sh(r"""flat=$(scribe_json_flat)
|
||||
printf '[%s][%s]\n' "$(scribe_json_len "$flat" '.note_ids')" \
|
||||
"$(scribe_json_len "$flat" '.missing')" """,
|
||||
@@ -272,7 +270,7 @@ def test_urlenc_percent_encodes_exactly_the_unreserved_set(value):
|
||||
|
||||
|
||||
def test_urlenc_and_the_envelope_can_fail():
|
||||
"""Rule 167 — a guard that cannot fail is decoration. If the escaper were a
|
||||
"""A guard that cannot fail is decoration. If the escaper were a
|
||||
no-op, this pair would be the assertion that noticed."""
|
||||
assert sh("scribe_urlenc", stdin="a b") != "a b"
|
||||
raw = sh('scribe_json_out PreToolUse "$(cat)"', stdin='he said "hi"')
|
||||
@@ -376,7 +374,7 @@ def test_a_tool_result_does_not_read_as_a_prompt():
|
||||
|
||||
|
||||
def test_the_turn_guards_can_fail():
|
||||
"""Rule 167. If the analyzer counted everything, or nothing, these are the
|
||||
"""If the analyzer counted everything, or nothing, these are the
|
||||
assertions that would notice."""
|
||||
assert _turn([_prompt(), _close("t1", 41), _text("r")])["closed"] == "1"
|
||||
assert _turn([_prompt(), _text("r")])["closed"] == "0"
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""Real-Postgres round trip for rule_usage_events (milestone 333 step 1).
|
||||
"""Real-Postgres round trip for rule_usage_events.
|
||||
|
||||
**This file is the reason the table exists.** `rule_usage_events` could have
|
||||
been a `rule_id` column on `note_usage_events` — the row carries no
|
||||
note-specific field and the readout is the same shape, which is the strongest
|
||||
case for sharing that note #3163 admits. What decided against it is identity at
|
||||
case for sharing. What decides against it is identity at
|
||||
restore, and that is a claim only a real round trip can support.
|
||||
|
||||
The failure it guards is the quiet kind. `note_usage_events`'s importer maps
|
||||
@@ -17,7 +17,7 @@ So the assertions below are about WHICH MAP resolved the id, and they are
|
||||
written to fail if the answer ever becomes "the note one" or "neither".
|
||||
|
||||
Same shape as `test_integration_backup_rule_version_roundtrip.py`, which guards
|
||||
`rule_versions` against #3182's `arose_from_id` trap on the same seam.
|
||||
`rule_versions` against the same id-remapping trap on the same seam.
|
||||
"""
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
@@ -143,7 +143,7 @@ async def source():
|
||||
user_id=None, rule_id=rule.id,
|
||||
event=SURFACED, source="write_path_rule",
|
||||
),
|
||||
# A departure and its reason (#4212). Seeded here because
|
||||
# A departure and its reason. Seeded here because
|
||||
# `detail` is the ONE field on this table that cannot be
|
||||
# recomputed: a fresh install re-earns its counts by being used,
|
||||
# but a stated reason exists once and is gone if a restore drops
|
||||
@@ -290,7 +290,7 @@ async def test_the_actor_is_remapped_and_a_missing_one_survives(restored):
|
||||
attributed = [e for e in restored["events"] if e.user_id is not None]
|
||||
orphaned = [e for e in restored["events"] if e.user_id is None]
|
||||
# Four attributed: the surfacing, the pull, and the two outcome rows
|
||||
# added with `detail` (#4212). One orphaned, deliberately.
|
||||
# that carry `detail`. One orphaned, deliberately.
|
||||
assert len(attributed) == 4
|
||||
assert len(orphaned) == 1, (
|
||||
"the event with no actor did not come back. Telemetry outlives the "
|
||||
@@ -325,10 +325,9 @@ async def test_a_departures_reason_survives_the_round_trip(restored):
|
||||
419 was opened to create, undone silently at the one moment nobody is
|
||||
watching.
|
||||
|
||||
#4197 is the standing warning behind this test: the backup column guard
|
||||
watches the export side only, so a column added to the model and to the
|
||||
exporter and NOT to the importer round-trips as null with nothing to say
|
||||
so.
|
||||
A column added to the model and to the exporter and NOT to the importer
|
||||
round-trips as null with nothing to say so; this round trip is where that
|
||||
shows up in the data.
|
||||
"""
|
||||
departures = [e for e in restored["events"] if e.event == DEPARTED]
|
||||
assert len(departures) == 1
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
"""Real-Postgres tests for lesson → rule links (milestone 440 step 1, #4630).
|
||||
"""Real-Postgres tests for lesson → rule links.
|
||||
|
||||
What a mock cannot show: that the CHECK holds the three states, that deleting
|
||||
either end takes the link with it, that a rule the caller does not own cannot
|
||||
@@ -162,7 +162,7 @@ async def test_one_row_per_pair(world):
|
||||
await s.commit()
|
||||
|
||||
|
||||
# ── #4631: "no rule fits" and the unjudged list ─────────────────────────────
|
||||
# ── "no rule fits" and the unjudged list ────────────────────────────────────
|
||||
|
||||
|
||||
async def _judgment(lesson_id: int) -> dict:
|
||||
@@ -243,7 +243,7 @@ async def test_deleting_the_lesson_takes_its_answer_with_it(world):
|
||||
assert left == []
|
||||
|
||||
|
||||
# ── #4637: soft links from co-surfacing ─────────────────────────────────────
|
||||
# ── soft links from co-surfacing ────────────────────────────────────────────
|
||||
|
||||
|
||||
async def _link(lesson_id: int, rule_id: int):
|
||||
@@ -316,12 +316,12 @@ async def test_confirming_a_suggestion_keeps_what_it_rested_on(world):
|
||||
assert len(out["evidence"]["situations"]) == 3
|
||||
|
||||
|
||||
# ── #4633: which links can carry a rule ─────────────────────────────────────
|
||||
# ── which links can carry a rule ────────────────────────────────────────────
|
||||
|
||||
|
||||
async def test_only_a_confirmed_link_can_carry_its_rule(world):
|
||||
"""A suggested link never expands: it would manufacture the co-arrival
|
||||
that #4637 counts, and prove itself. A rejected one obviously not."""
|
||||
that co-surfacing counts, and prove itself. A rejected one obviously not."""
|
||||
owner, lesson = world["owner"], world["lesson"]
|
||||
await links_svc.co_surfaced(owner, [lesson], [world["r1"]], arm="p", situation="one ask here")
|
||||
await links_svc.judge_link(owner, lesson, world["r2"], "reject", "not this")
|
||||
@@ -335,7 +335,7 @@ async def test_only_a_confirmed_link_can_carry_its_rule(world):
|
||||
|
||||
|
||||
async def test_a_project_rule_stays_in_its_project_even_through_a_lesson(world):
|
||||
"""Milestone 414's scope, kept on the side door: the lesson surfaces
|
||||
"""Rule scope, kept on the side door: the lesson surfaces
|
||||
everywhere, its project rule only in its project."""
|
||||
owner, lesson = world["owner"], world["lesson"]
|
||||
await links_svc.set_lesson_rules(owner, lesson, [world["r1"]])
|
||||
@@ -349,7 +349,7 @@ async def test_a_stranger_reaches_no_rule_through_someone_elses_link(world):
|
||||
assert await links_svc.confirmed_lessons(world["stranger"]) == set()
|
||||
|
||||
|
||||
# ── #4634: which lessons count toward convergence ───────────────────────────
|
||||
# ── which lessons count toward convergence ──────────────────────────────────
|
||||
|
||||
|
||||
async def test_only_answered_lessons_are_no_rule_lessons(world):
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
"""Real-Postgres tests for the rule-history write path (milestone 323 step 2).
|
||||
"""Real-Postgres tests for the rule-history write path.
|
||||
|
||||
Every assertion here is about ORDERING or ABSENCE, which is why none of them
|
||||
can be made with mocks.
|
||||
|
||||
What a version is for: it holds what the rule USED TO SAY. The edit destroys
|
||||
that, and nothing can recompute it — the rescoping of rule 79 had to be
|
||||
hand-copied into a task log to survive (#3237), which is not a process, it is
|
||||
a person remembering.
|
||||
that, and nothing can recompute it — without a version, a rescoped rule's old
|
||||
wording survives only if a person copies it somewhere, which is not a
|
||||
process.
|
||||
|
||||
Two of these pin a DELIBERATE ABSENCE. `note_versions` carries a 300-second
|
||||
interval gate and a 50-version cap, both defences against note autosave. Rules
|
||||
@@ -199,9 +199,9 @@ async def test_history_reads_newest_first(constraint):
|
||||
]
|
||||
|
||||
|
||||
# ── The read path is ACL-scoped (milestone 323 step 3) ─────────────────
|
||||
# ── The read path is ACL-scoped ───────────────────────────────────────
|
||||
#
|
||||
# Rule 47: every read of user data is scoped by owner. A version carries a
|
||||
# Every read of user data is scoped by access. A version carries a
|
||||
# `user_id`, which makes it tempting to scope the history by it — that would
|
||||
# be wrong in both directions, and these pin which way round it goes.
|
||||
|
||||
|
||||
@@ -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,11 +1,11 @@
|
||||
"""A lesson reaches the session it applies to — milestone 385, step 5.
|
||||
"""A lesson reaches the session it applies to.
|
||||
|
||||
Three decisions are guarded here, and each has a way of going quietly wrong
|
||||
that these tests are shaped to catch rather than to describe:
|
||||
|
||||
WHICH ARM. The two note arms, and no new one. An arm that FILTERS kinds does
|
||||
not merely outrank a kind it omits — it makes it unreachable, and nothing
|
||||
reports an arm that never had the candidate (#3702). The write path filters;
|
||||
reports an arm that never had the candidate. The write path filters;
|
||||
the prompt menu does not, but is project-scoped, which for a kind whose whole
|
||||
claim is that it transfers amounts to the same silence.
|
||||
|
||||
@@ -115,8 +115,8 @@ async def test_the_write_path_asks_for_lessons_at_all():
|
||||
async def test_a_lesson_takes_a_reserved_slot_and_the_query_is_logged():
|
||||
"""The slot is a claim that a kind deserves a guaranteed line. A claim like
|
||||
that has to be falsifiable from the first deploy, which means its own query
|
||||
in `retrieval_logs` — #2463's finding, recorded about the slot that shipped
|
||||
without one."""
|
||||
in `retrieval_logs`; a slot without one cannot be judged against what it
|
||||
displaced."""
|
||||
rec = MagicMock()
|
||||
out, calls = await _menu(
|
||||
[(0.70, fake_note(id=1, title="Badge layout task", user_id=1, is_task=True))],
|
||||
@@ -143,7 +143,7 @@ async def test_the_reserved_lesson_extends_and_never_evicts():
|
||||
"""It sides with `preference_slot` over `reuse_slot`, and the reason is the
|
||||
ledger rather than taste: an evicted hit was RETURNED by the general search
|
||||
and sits in that call's log row, so displacing it makes the two tables
|
||||
disagree about one call for a reason nothing in the data explains (#3668).
|
||||
disagree about one call for a reason nothing in the data explains.
|
||||
The voice argument points the same way — a record that binds nothing should
|
||||
not be able to throw a better-scoring one off the menu."""
|
||||
main = [(0.72, fake_note(id=1, title="a", user_id=1)),
|
||||
@@ -190,7 +190,7 @@ async def test_a_weak_lesson_does_not_buy_the_slot():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_the_slot_surfacing_matches_its_own_retrieval_row():
|
||||
"""#3668's identity, on the new arm: what a source says it retrieved and
|
||||
"""The two-table identity, on this arm: what a source says it retrieved and
|
||||
what it says it showed must be the same records. The reserved lesson is
|
||||
fetched by `lesson_slot`'s query, so it is `lesson_slot`'s surfacing —
|
||||
counting it under `auto_inject` as well would book one delivery twice and
|
||||
@@ -275,7 +275,7 @@ async def test_the_write_path_names_the_register_for_a_lesson_too():
|
||||
|
||||
|
||||
def test_the_surfacing_guards_can_fail():
|
||||
"""Rule 167: each assertion above has to be able to bite. The three shapes
|
||||
"""Each assertion above has to be able to bite. The three shapes
|
||||
it would take to break this feature silently, each checked here against the
|
||||
condition the tests actually assert on."""
|
||||
# An arm that stops asking for the kind.
|
||||
|
||||
@@ -15,10 +15,10 @@ pytestmark = pytest.mark.usefixtures("_bind_user")
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _no_supersession():
|
||||
"""Every note read/write now asks for its supersession relations (#278).
|
||||
"""Every note read/write asks for its supersession relations.
|
||||
|
||||
These are unit tests of the TOOL layer and this job has no database — the
|
||||
same hazard the `_fake_note` comment below records for note 2109. Stubbed
|
||||
same hazard the `_fake_note` comment below records. Stubbed
|
||||
to "no relations", which is the state of essentially every note; the
|
||||
relation's own behaviour is covered in test_services_supersession.py, and
|
||||
the attachment is covered explicitly below.
|
||||
@@ -120,7 +120,7 @@ async def test_get_note_returns_dict():
|
||||
# Own record: no provenance noise.
|
||||
assert "shared" not in out
|
||||
# No supersession relations: both keys ABSENT, not present-and-empty. A
|
||||
# field that always says nothing trains readers to skip fields (#2483).
|
||||
# field that always says nothing trains readers to skip fields.
|
||||
assert "supersedes" not in out
|
||||
assert "superseded_by" not in out
|
||||
|
||||
@@ -218,7 +218,7 @@ async def test_update_note_only_sends_non_default_fields():
|
||||
|
||||
`clear` is always forwarded and is not a field: it is how this door says
|
||||
"unset these", and an empty one says "unset nothing". Same shape the rules
|
||||
door took in #3096, and the same test that had to learn about it there."""
|
||||
door takes, and the same test."""
|
||||
fake = fake_note()
|
||||
mock = AsyncMock(return_value=fake)
|
||||
with patch("scribe.mcp.tools.notes.notes_svc.update_note", mock):
|
||||
@@ -232,7 +232,7 @@ async def test_update_note_only_sends_non_default_fields():
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_note_forwards_a_check_but_not_an_empty_one():
|
||||
""""" means "leave this alone" at this door — an agent updating a body must
|
||||
not wipe a check it was never asked about (milestone 317)."""
|
||||
not wipe a check it was never asked about."""
|
||||
mock = AsyncMock(return_value=fake_note())
|
||||
with patch("scribe.mcp.tools.notes.notes_svc.update_note", mock):
|
||||
await update_note(note_id=1, verify_with="curl the docs")
|
||||
@@ -305,7 +305,7 @@ async def test_delete_note_soft_deletes_and_returns_batch():
|
||||
):
|
||||
result = await delete_note(note_id=7)
|
||||
assert result["deleted_batch_id"] == "batch-1"
|
||||
# The confirmation NAMES what went (#3273). After the delete the row is
|
||||
# The confirmation NAMES what went. After the delete the row is
|
||||
# trashed, so this line is the last chance to say which note it was.
|
||||
assert result["title"] == "the note that went"
|
||||
assert "the note that went" in result["message"]
|
||||
|
||||
@@ -5,7 +5,7 @@ import pytest
|
||||
from tests.helpers import fake_note, fake_system, fake_task
|
||||
|
||||
|
||||
# The name assessment both doors run before minting (milestone 307). A test
|
||||
# The name assessment both doors run before minting. A test
|
||||
# that patches systems_svc wholesale must stub it, or the awaited MagicMock
|
||||
# raises — this shape is the "nothing matched" answer.
|
||||
_NO_MATCH = {"duplicate": None, "canonical": None}
|
||||
@@ -179,8 +179,8 @@ async def test_untagged_hint_zero_systems_prompts_first_create_and_fails_open():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_untagged_hint_escalates_in_a_mature_zero_systems_project():
|
||||
"""#2683: at 282 records and zero Systems (Minstrel), the generic question
|
||||
had demonstrably never converted. The escalated ask must carry the
|
||||
"""In a mature project with zero Systems the generic question does not
|
||||
convert. The escalated ask must carry the
|
||||
project's OWN evidence — record count and recent titles — and demand a
|
||||
concrete deliverable, because that is the property separating the nudges
|
||||
that convert from the prose that doesn't."""
|
||||
@@ -190,7 +190,7 @@ async def test_untagged_hint_escalates_in_a_mature_zero_systems_project():
|
||||
patch("scribe.mcp.tools.systems.notes_svc") as notes:
|
||||
svc.list_systems = AsyncMock(return_value=[])
|
||||
notes.list_notes = AsyncMock(return_value=(recent, 282))
|
||||
# The standard names come from the GLOBAL catalog now (milestone 307),
|
||||
# The standard names come from the GLOBAL catalog,
|
||||
# not a module constant — so the ask reads them through the service.
|
||||
# That the SEEDED vocabulary is these eight is migration 0087's
|
||||
# business, asserted against a real database in the inception
|
||||
@@ -203,7 +203,7 @@ async def test_untagged_hint_escalates_in_a_mature_zero_systems_project():
|
||||
assert "282 records" in hint
|
||||
assert "Fix scrape retry backoff" in hint # the project's own evidence
|
||||
assert "3-6" in hint and "create_system" in hint
|
||||
# NOT an approval flow (#2798): the agent mints directly, and the
|
||||
# NOT an approval flow: the agent mints directly, and the
|
||||
# standard cross-project names carry the consistency instead.
|
||||
assert "without asking permission" in hint
|
||||
assert "propose" not in hint and "confirmed" not in hint
|
||||
@@ -262,7 +262,7 @@ async def test_populated_vocabulary_never_counts_records():
|
||||
|
||||
# The name gate's own cases (case/whitespace normalisation, exact-over-overlap,
|
||||
# fail-open) moved to tests/test_services_systems.py with the logic itself —
|
||||
# services/systems.assess_system_name, so both doors share one answer (#2482).
|
||||
# services/systems.assess_system_name, so both doors share one answer.
|
||||
# What stays the TOOL's job — rendering a duplicate, applying an exact area,
|
||||
# offering an overlap — is covered at the top of this file.
|
||||
|
||||
@@ -345,7 +345,7 @@ async def test_add_task_log_on_untagged_project_task_asks_the_question():
|
||||
notes_svc.get_note_for_user = AsyncMock(return_value=(task, "owner"))
|
||||
seam_svc.list_record_systems = AsyncMock(return_value=[])
|
||||
seam_svc.list_systems = AsyncMock(return_value=[])
|
||||
# Young project: below the #2683 bootstrap threshold, so the mild
|
||||
# Young project: below the bootstrap threshold, so the mild
|
||||
# question is the expected form here.
|
||||
seam_notes.list_notes = AsyncMock(return_value=([], 2))
|
||||
from scribe.mcp.tools.tasks import add_task_log
|
||||
|
||||
@@ -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,14 +1,13 @@
|
||||
"""The preference write path, and how it differs from a rule's (milestone 399).
|
||||
"""The preference write path, and how it differs from a rule's.
|
||||
|
||||
WHY TWO DOORS AT ALL
|
||||
|
||||
`create_rule`'s docstring IS the approval gate (#3557): it tells its caller to
|
||||
`create_rule`'s docstring IS the approval gate: it tells its caller to
|
||||
propose, offer three answers, and wait. That is right for a rule — the person
|
||||
a rule binds should have agreed to be bound.
|
||||
|
||||
A preference inverts it. The operator's framing: *"preferences are rules that
|
||||
scribe can and should update during use."* A preference that asks every time
|
||||
never drifts, and drifting is the whole feature. Reaching one through
|
||||
A preference inverts it: it is a rule Scribe can and should update during
|
||||
use. A preference that asks every time never drifts, and drifting is the whole feature. Reaching one through
|
||||
`create_rule(kind=..., when_to_apply="when the moment this fixture stands in for arises")` would mean reading it through the gate's prose, and
|
||||
the caller would hesitate over exactly the act this kind exists to make
|
||||
routine.
|
||||
@@ -18,7 +17,7 @@ So the asymmetry is the product, and these tests pin it.
|
||||
TWO PRESENCE CHECKS, NEVER AN ABSENCE
|
||||
|
||||
The tempting guard is "create_preference's docstring does NOT run the approval
|
||||
loop". That is the shape snippet #3352 warns against: an absence check passes
|
||||
loop". That is a trap: an absence check passes
|
||||
against a docstring that has been deleted, emptied, or rewritten into
|
||||
something else entirely, and it reads as coverage while proving nothing.
|
||||
|
||||
@@ -35,7 +34,7 @@ from tests.helpers import fake_rule, plain_rule_detail as _plain_detail
|
||||
from tests.helpers import tool_doc as _doc
|
||||
|
||||
# The tool layer reads its caller from a ContextVar the HTTP transport sets.
|
||||
# With no request in flight, the module binds it itself (snippet #2836).
|
||||
# With no request in flight, the module binds it itself.
|
||||
pytestmark = pytest.mark.usefixtures("_bind_user")
|
||||
|
||||
MODULE = "scribe.mcp.tools.rulebooks"
|
||||
@@ -176,7 +175,7 @@ def test_the_preference_door_says_to_write_it():
|
||||
"""Half two. The inverting instruction has to be PRESENT, not merely
|
||||
unaccompanied by a gate.
|
||||
|
||||
An agent that has internalised #3557 will hesitate to write or rewrite a
|
||||
An agent that has internalised the rule gate will hesitate to write or rewrite a
|
||||
preference unless told plainly that this door is different. Silence here
|
||||
does not read as permission — it reads as an omission, and the caller
|
||||
falls back on the behaviour it already knows.
|
||||
@@ -194,7 +193,7 @@ def test_the_preference_door_says_to_write_it():
|
||||
|
||||
|
||||
def test_the_preference_door_names_the_force_distinction():
|
||||
"""The routing test, stated positively (rule 165).
|
||||
"""The routing test, stated positively.
|
||||
|
||||
The confusion this milestone exists to fix is that a session cannot tell
|
||||
which kind it is holding. If the docstring stops drawing the line, the
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,19 +1,11 @@
|
||||
"""The surface registry, and the identity the whole tuning story rests on (#4102).
|
||||
"""The surface registry, and the identity the whole tuning story rests on.
|
||||
|
||||
WHY THIS EXISTS
|
||||
|
||||
Six arms used to own their own numbers: a settings key, a default, and a limit
|
||||
that was usually a module constant nobody could change. That survived while the
|
||||
values were shipped constants. It stops surviving once the operator's decision
|
||||
is that the numbers MOVE:
|
||||
|
||||
"the floor should be chosen and adjusted by the model using it… the user
|
||||
should be able to touch it but the model should be the thing handling it 9
|
||||
times out of 10."
|
||||
|
||||
A tuning surface cannot be model-consistent across arms that each spell their
|
||||
configuration differently, so the arms now read `{floor, budget}` from one
|
||||
registry.
|
||||
The retrieval floors MOVE: the model using an arm chooses and adjusts them, and
|
||||
a person can too. A tuning surface cannot be model-consistent across arms that
|
||||
each own a settings key, a default and a limit spelled their own way, so the
|
||||
arms read `{floor, budget}` from one registry.
|
||||
|
||||
WHAT THIS PINS
|
||||
|
||||
@@ -24,9 +16,8 @@ WHAT THIS PINS
|
||||
different file. Renaming one without the other produces a surface that can
|
||||
be tuned and cannot be measured, or measured and not tuned, and both fail
|
||||
silently.
|
||||
2. **Keys are unique.** Two surfaces sharing a settings key is how the
|
||||
completion-report arm (since retired) spent its first release moving
|
||||
whenever the prompt arm was tuned (#3860) — one dial wearing two labels.
|
||||
2. **Keys are unique.** Two surfaces sharing a settings key move together
|
||||
whenever either is tuned — one dial wearing two labels.
|
||||
3. **An unknown surface is refused.** Settings keys are free-form strings in
|
||||
a generic table, so a typo'd name would write a key nothing reads: a
|
||||
change that appears to succeed, reports a new value, and alters nothing.
|
||||
@@ -63,7 +54,7 @@ def test_every_surface_name_is_a_real_telemetry_source():
|
||||
to justify a change describes a different arm from the one the change hits.
|
||||
"""
|
||||
blob = "\n".join(_service_source(n) for n in SERVICES)
|
||||
# The rule arms record through the one pipeline (milestone 456), where
|
||||
# The rule arms record through the one pipeline, where
|
||||
# `source` is the spec's own field — so for them the spec IS the string
|
||||
# `record_retrieval` receives, and the join key is checked against it.
|
||||
from scribe.services.retrieval_pipeline import NOTE_ARMS, RULE_ARMS
|
||||
@@ -81,7 +72,7 @@ def test_every_surface_name_is_a_real_telemetry_source():
|
||||
|
||||
|
||||
def test_no_two_surfaces_share_a_settings_key():
|
||||
"""One dial, one label. #3860 is what the other way costs."""
|
||||
"""One dial, one label."""
|
||||
floors = [s.floor_key for s in rs.SURFACES.values()]
|
||||
budgets = [s.budget_key for s in rs.SURFACES.values()]
|
||||
assert len(set(floors)) == len(floors), f"duplicate floor key in {floors}"
|
||||
@@ -189,6 +180,6 @@ async def test_the_write_path_config_carries_every_arm_it_drives():
|
||||
"tool_rule_threshold", "tool_rule_top_k",
|
||||
# Not a surface, and that is why it is spelled out here: it
|
||||
# has no floor/budget pair to derive from, so nothing else in
|
||||
# this file would notice it going missing (#4214).
|
||||
# this file would notice it going missing.
|
||||
"checkpoint_threshold"):
|
||||
assert key in cfg, f"{key} missing — its arm will silently no-op"
|
||||
|
||||
@@ -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,17 +1,16 @@
|
||||
"""Named is not read: the ledger's third state (#4100).
|
||||
"""Named is not read: the ledger's third state.
|
||||
|
||||
WHY THIS EXISTS
|
||||
|
||||
Milestone 386 made a repeat REFERENCED rather than withheld, and the line it
|
||||
chose says "You saw it earlier this session". That claim was never checked.
|
||||
The arms emit a TEASER — title, trigger, `get_rule(N)` — so a session can be
|
||||
A repeat is REFERENCED rather than withheld, and a line that says "You saw it
|
||||
earlier this session" claims something nobody checked. The arms emit a TEASER — title, trigger, `get_rule(N)` — so a session can be
|
||||
shown a rule twenty times and never read a word of it, and a compaction
|
||||
summarises the teaser away leaving nothing at all. The server was asserting
|
||||
something about the reader's context it had no way to know.
|
||||
summarises the teaser away leaving nothing at all. The server would be
|
||||
asserting something about the reader's context it has no way to know.
|
||||
|
||||
`.opened.ids` is the observable half, written by a PostToolUse hook from the
|
||||
`get_rule` call itself. That is why this is not the self-report 386 rejected:
|
||||
the objection was to ASKING a model about its own context, and a tool call is
|
||||
`get_rule` call itself. That is why this is not a self-report: the objection
|
||||
is to ASKING a model about its own context, and a tool call is
|
||||
an event the harness reports whether anyone asks or not.
|
||||
|
||||
WHAT THIS PINS
|
||||
@@ -82,8 +81,8 @@ def test_the_three_states_produce_three_different_lines():
|
||||
|
||||
|
||||
def test_held_outranks_seen_regardless_of_kind():
|
||||
"""`kind` moves the head and the ledger moves the tail; #3497's history is
|
||||
the two being reasoned about together and one of them being forgotten."""
|
||||
"""`kind` moves the head and the ledger moves the tail; reasoning about
|
||||
the two together is how one of them gets forgotten."""
|
||||
for kind in ("rule", "preference"):
|
||||
line = _rule_hint_line(_rule(kind=kind), where="here", seen=True, held=True)
|
||||
assert "opened it earlier" in line
|
||||
@@ -139,7 +138,7 @@ def test_an_event_with_nothing_usable_records_nothing_and_still_exits_zero(event
|
||||
# that would be worst to get wrong — is asserted against the running hook in
|
||||
# tests/test_session_ledger_clear.py, so there is one home for it.
|
||||
#
|
||||
# Since #4217 the recorder writes TWO files and only this one is cleared.
|
||||
# The recorder writes TWO files and only this one is cleared.
|
||||
# `.opened.ids` says "this context holds rule 156", which a compaction makes
|
||||
# false, and three hooks read it to decide whether to stay quiet. Its twin
|
||||
# `.opened.keep.ids` says "rule 156 was opened", which a compaction does not
|
||||
|
||||
@@ -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"]}])
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""The Settings UI shows the default the server actually uses (#3927).
|
||||
"""The Settings UI shows the default the server actually uses.
|
||||
|
||||
WHY THIS EXISTS
|
||||
|
||||
@@ -17,8 +17,7 @@ WHAT THIS PINS
|
||||
|
||||
The RELATIONSHIP, never the number: for each threshold, the Vue ref's initial
|
||||
string equals the Python default. Retuning either stays free as long as both
|
||||
move — which is the point, because these are tuning values and #3853 moved one
|
||||
of them the day this was written.
|
||||
move — which is the point, because these are tuning values and they move.
|
||||
|
||||
The Vue side is read from SOURCE, because that file cannot be imported at all.
|
||||
The Python side is read from source for a loose constant and IMPORTED for a
|
||||
@@ -42,7 +41,7 @@ ROOT = pathlib.Path(__file__).resolve().parents[1]
|
||||
_SERVICES = ROOT / "src" / "scribe" / "services"
|
||||
_VUE = ROOT / "frontend" / "src" / "views" / "SettingsView.vue"
|
||||
|
||||
# THE SIX RETRIEVAL FLOORS now live in one registry (#4102), so their side of
|
||||
# THE SIX RETRIEVAL FLOORS live in one registry, so their side of
|
||||
# the pairing is a SURFACE NAME rather than a module constant. That is strictly
|
||||
# better for this guard: the surface name is also the telemetry source, so a row
|
||||
# here names the same arm the readout does, and a floor that moved cannot be
|
||||
@@ -62,7 +61,7 @@ _SURFACE_PAIRS = (
|
||||
# exist on both sides, so a rename fails loudly rather than silently dropping
|
||||
# that threshold from the check.
|
||||
_CONSTANT_PAIRS = (
|
||||
# The plan gate (milestone 415): it blocks a create, so a form showing a
|
||||
# The plan gate: it blocks a create, so a form showing a
|
||||
# looser bar than the one in force would be the more misleading drift. Not
|
||||
# a push surface, so it has no registry entry and keeps the older shape.
|
||||
("dedup.py", "PLAN_MATCH_DEFAULT_THRESHOLD", "kbPlanMatchThreshold"),
|
||||
@@ -87,7 +86,7 @@ def _vue_default(ref_name: str) -> float:
|
||||
assert m, (
|
||||
f"{ref_name} is no longer a `ref(\"<number>\")` in SettingsView.vue. "
|
||||
f"If the control was renamed, update _PAIRS; if it was removed, the "
|
||||
f"setting has lost its UI and that is the thing to fix (rule 25)."
|
||||
f"setting has lost its UI and that is the thing to fix."
|
||||
)
|
||||
return float(m.group(1))
|
||||
|
||||
@@ -112,7 +111,7 @@ def test_the_form_shows_the_floor_the_surface_uses(surface, ref_name):
|
||||
|
||||
|
||||
def test_every_tunable_surface_has_a_control():
|
||||
"""Rule 25/27: a number an operator may need different has a UI or it is
|
||||
"""A number an operator may need different has a UI or it is
|
||||
not shipped. Derived from the registry, so a seventh surface arrives here
|
||||
as a failure rather than as a setting only the model can reach."""
|
||||
from scribe.services.retrieval_surfaces import surface_names
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""Tests for the snippet location reverse lookup (#2083).
|
||||
"""Tests for the snippet location reverse lookup.
|
||||
|
||||
Covers the pure half of the predicate — `location_parts`, `location_matches`,
|
||||
`location_jsonpath` — plus the parameter plumbing across the three surfaces
|
||||
(service / REST route / MCP tool), which rule #33 requires to agree on names and
|
||||
(service / REST route / MCP tool), which have to agree on names and
|
||||
defaults.
|
||||
|
||||
The SQL half can only be proved against real Postgres — see
|
||||
@@ -56,7 +56,7 @@ def test_no_parts_matches_everything():
|
||||
def test_matches_exact_repo_path_and_symbol():
|
||||
data = _data({"repo": "Scribe", "path": "src/scribe/x.py", "symbol": "helper"})
|
||||
assert location_matches(data, {"repo": "Scribe"})
|
||||
assert location_matches(data, {"repo": "scribe"}) # repo names: case never distinguishes (#2874)
|
||||
assert location_matches(data, {"repo": "scribe"}) # repo names: case never distinguishes
|
||||
assert location_matches(data, {"path": "src/scribe/x.py"})
|
||||
assert location_matches(data, {"symbol": "helper"})
|
||||
assert location_matches(data, {"repo": "Scribe", "symbol": "helper"})
|
||||
@@ -102,7 +102,7 @@ def test_blank_recorded_part_does_not_match_a_requested_one():
|
||||
def test_jsonpath_filters_within_one_locations_entry():
|
||||
expr = location_jsonpath({"repo": "Scribe", "symbol": "helper"})
|
||||
assert expr.startswith("$.locations[*] ? (")
|
||||
# repo: anchored, case-insensitive (#2874) — mirrors location_matches.
|
||||
# repo: anchored, case-insensitive — mirrors location_matches.
|
||||
assert '@.repo like_regex "^Scribe$" flag "i"' in expr
|
||||
assert '@.symbol == "helper"' in expr
|
||||
assert " && " in expr
|
||||
@@ -149,7 +149,7 @@ def test_location_clause_renders_a_jsonpath_containment():
|
||||
assert "data" in sql
|
||||
|
||||
|
||||
# --- surface plumbing (rule #33) ---------------------------------------------
|
||||
# --- surface plumbing ---------------------------------------------------------
|
||||
|
||||
def test_agent_and_service_surfaces_take_the_same_location_params():
|
||||
from scribe.mcp.tools.snippets import list_snippets as mcp_list
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"""Body provenance — the cache-with-provenance half of the pointer model (#2688).
|
||||
"""Body provenance — the cache-with-provenance half of the pointer model.
|
||||
|
||||
Decision #2686: the recorded location is the source of truth for a snippet's
|
||||
The recorded location is the source of truth for a snippet's
|
||||
code and the stored body is a CACHE of it. `data.provenance` records what that
|
||||
cache is a cache OF — the commit the body was read at, and when — so staleness
|
||||
becomes judgeable instead of guessed, and so the forge fetch (milestone 288
|
||||
step 5) has something to refresh.
|
||||
becomes judgeable instead of guessed, and so the forge fetch has something to
|
||||
refresh.
|
||||
|
||||
The rules under test, because each has a way to rot silently:
|
||||
|
||||
@@ -18,8 +18,8 @@ The rules under test, because each has a way to rot silently:
|
||||
cached body matches the source there.
|
||||
|
||||
Unit tests cover the compose/carry logic; the integration section runs the
|
||||
same rules through the real service paths on real Postgres (the #2663 lesson —
|
||||
a DB-touching path with only mocked coverage is a path with no coverage).
|
||||
same rules through the real service paths on real Postgres (a DB-touching path
|
||||
with only mocked coverage is a path with no coverage).
|
||||
"""
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
@@ -49,7 +49,7 @@ def test_compose_provenance_stamps_sha_and_time():
|
||||
|
||||
|
||||
def test_compose_provenance_without_a_sha_is_none_not_an_empty_record():
|
||||
# Absent provenance must stay ABSENT (the pre-#2688 semantics), never an
|
||||
# Absent provenance must stay ABSENT, never an
|
||||
# empty dict that readers would have to distinguish from a real one.
|
||||
assert compose_provenance(commit_sha="") is None
|
||||
assert compose_provenance(commit_sha=" ") is None
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Systems become findable by meaning — the charter as an ANSWER (#4251).
|
||||
"""Systems become findable by meaning — the charter as an ANSWER.
|
||||
|
||||
A System's `description` is a charter: several hundred words saying what
|
||||
belongs in that area and what does not. It is the answer to "where does this
|
||||
@@ -20,7 +20,7 @@ from tests.helpers import make_mock_session
|
||||
|
||||
def _system(system_id=3, name="Retrieval & recall", description="the charter"):
|
||||
s = MagicMock(id=system_id, project_id=2, description=description)
|
||||
s.name = name # never via the constructor — see note #2833
|
||||
s.name = name # never via the constructor: `name` names the mock
|
||||
return s
|
||||
|
||||
|
||||
@@ -65,8 +65,7 @@ def _rows_ctx(rows):
|
||||
async def test_the_search_collapses_to_best_chunk_and_reports_which_one_won():
|
||||
"""A charter is long, and a result shows its NAME — so the paragraph that
|
||||
actually decides where a record belongs has to come back with it, or the
|
||||
caller judges from two words that cannot say (#4243). Published from the
|
||||
start here rather than retrofitted, which is what #4251 asked for."""
|
||||
caller judges from two words that cannot say."""
|
||||
a, b = _system(3), _system(4, name="Data Model")
|
||||
rows = [
|
||||
(a, 0.10, 2, "retrieval telemetry belongs here"),
|
||||
@@ -94,7 +93,7 @@ async def test_the_search_collapses_to_best_chunk_and_reports_which_one_won():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_a_project_the_caller_cannot_read_returns_nothing():
|
||||
"""Rule 78 — the charter of a project someone was not given is not an
|
||||
"""The charter of a project someone was not given is not an
|
||||
answer to any question they are entitled to ask."""
|
||||
_session, ctx = _rows_ctx([])
|
||||
with (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Timestamp filters must bind datetimes, never strings (#1727, #2254).
|
||||
"""Timestamp filters must bind datetimes, never strings.
|
||||
|
||||
`AppLog.created_at` is `timestamp with time zone`. asyncpg binds a Python `str`
|
||||
as VARCHAR, and Postgres has no `timestamptz >= text` operator — so comparing
|
||||
@@ -22,7 +22,7 @@ from pathlib import Path
|
||||
SRC = Path(__file__).resolve().parents[1] / "src" / "scribe"
|
||||
|
||||
|
||||
# --- #1727: the reminder dedup window ----------------------------------------
|
||||
# --- the reminder dedup window ----------------------------------------------
|
||||
|
||||
def test_utc_day_start_is_an_aware_datetime_not_a_string():
|
||||
from scribe.services.notifications import utc_day_start
|
||||
@@ -38,7 +38,7 @@ def test_utc_day_start_is_an_aware_datetime_not_a_string():
|
||||
assert (start.hour, start.minute, start.second, start.microsecond) == (0, 0, 0, 0)
|
||||
|
||||
|
||||
# --- #2254: the admin log viewer's date filters ------------------------------
|
||||
# --- the admin log viewer's date filters ------------------------------------
|
||||
|
||||
def test_filter_datetime_parses_dates_and_datetimes_to_aware_utc():
|
||||
from scribe.services.logging import parse_filter_datetime
|
||||
@@ -83,17 +83,16 @@ def test_filter_datetime_rejects_garbage_instead_of_raising():
|
||||
def _timestamp_comparison_offenders() -> list[str]:
|
||||
"""Every place a `*_at` column is compared against a known-string name.
|
||||
|
||||
TWO shapes, because the two real bugs had different ones and a guard built
|
||||
from only the first would have missed the second:
|
||||
TWO shapes, because a guard built from only the first misses the second:
|
||||
|
||||
1. a local bound to `.isoformat()` — #1727, `today_str = today.isoformat()`
|
||||
2. a parameter annotated `str` — #2254, `date_from: str | None`, straight
|
||||
off `request.args`, never touching
|
||||
isoformat at all
|
||||
1. a local bound to `.isoformat()` — `today_str = today.isoformat()`
|
||||
2. a parameter annotated `str` — `date_from: str | None`, straight
|
||||
off `request.args`, never touching
|
||||
isoformat at all
|
||||
|
||||
Shape 2 is the nastier one: nothing in the file looks date-ish, so grepping
|
||||
for isoformat finds nothing. It was caught by reading, not by tooling —
|
||||
which is exactly why it belongs in tooling now.
|
||||
for isoformat finds nothing. Only reading catches it — which is exactly
|
||||
why it belongs in tooling.
|
||||
|
||||
Deliberately narrow on both. A blanket "no strings near SQL" rule would fire
|
||||
on every to_dict() in the codebase and get muted, and a muted guard is worse
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
"""`/api/version` reports three values, and never folds them together.
|
||||
|
||||
WHAT THIS IS ABOUT (rule 149). Until 2026-08-31 the endpoint returned
|
||||
`{"version": "main"}` — CI set `BUILD_VERSION` to the CHANNEL, so a running
|
||||
instance answered the question "which build are you?" with the name of a
|
||||
branch. The cost was concrete rather than theoretical: during #3244's live
|
||||
acceptance a deploy was behaving as though it held older code, and the one
|
||||
endpoint whose job is to settle that could not.
|
||||
WHAT THIS IS ABOUT. A CI that sets `BUILD_VERSION` to the CHANNEL makes a
|
||||
running instance answer "which build are you?" with the name of a branch — and
|
||||
when a deploy behaves as though it holds older code, the one endpoint whose job
|
||||
is to settle that cannot.
|
||||
|
||||
The three values answer different questions and so cannot be one value:
|
||||
|
||||
@@ -78,7 +76,7 @@ def test_the_channel_is_never_inside_the_name():
|
||||
})
|
||||
assert NAME_RE.match(out["version"]), (
|
||||
f"the version name is {out['version']!r} — not YYYY.MM.DD.HHMM. A "
|
||||
f"channel or branch name here is the 2026-08-31 bug returning."
|
||||
f"channel or branch name here is the channel folded into the version."
|
||||
)
|
||||
assert "stable" not in out["version"]
|
||||
|
||||
@@ -120,7 +118,7 @@ def test_a_malformed_ordering_key_is_dropped_not_passed_through():
|
||||
|
||||
def test_the_channel_is_reported_verbatim():
|
||||
"""Never validated against an enum — a build claiming something
|
||||
unexpected is better shown than dropped (rule 149)."""
|
||||
unexpected is better shown than dropped."""
|
||||
out = _version_payload({"APP_VERSION": "dev", "APP_CHANNEL": "canary"})
|
||||
assert out["channel"] == "canary"
|
||||
|
||||
@@ -138,7 +136,7 @@ def test_ci_does_not_stamp_the_channel_as_the_version():
|
||||
for wrong in ('BUILD_VERSION="main"', 'BUILD_VERSION="dev"'):
|
||||
assert wrong not in text, (
|
||||
f"CI sets {wrong} — that is the channel in the version field, "
|
||||
f"which is the 2026-08-31 regression."
|
||||
f"folding the three values into one."
|
||||
)
|
||||
|
||||
|
||||
@@ -168,7 +166,7 @@ def test_ci_passes_all_three_plus_the_commit():
|
||||
|
||||
@pytest.mark.parametrize("commit_epoch,expected", [
|
||||
# Midnight, where a naive formatter drops the leading zeros and yields
|
||||
# "2026.01.05.0" — rule 149 names this case specifically.
|
||||
# "2026.01.05.0" — the case the padding exists for.
|
||||
(datetime(2026, 1, 5, 0, 0, tzinfo=timezone.utc), "2026.01.05.0000"),
|
||||
(datetime(2026, 1, 5, 0, 7, tzinfo=timezone.utc), "2026.01.05.0007"),
|
||||
(datetime(2026, 12, 31, 23, 59, tzinfo=timezone.utc), "2026.12.31.2359"),
|
||||
|
||||
@@ -6,11 +6,10 @@ shells. They are deliberately few and deliberately about ONE property each,
|
||||
because a grep-shaped test that asserts a whole file's contents fails on every
|
||||
refactor and gets deleted.
|
||||
|
||||
WHY THIS FILE EXISTS. #3298: with a deploy misbehaving, nothing on the instance
|
||||
could say which commit was serving it, and the one endpoint whose job that is
|
||||
answered with the name of a branch. The value was fixed then. This is the other
|
||||
half — the value reaching a person — and #3127 checklist 12 is specific about
|
||||
the way it goes wrong: *never let a blank stand in for `unknown`*. A readout
|
||||
WHY THIS FILE EXISTS. With a deploy misbehaving, the instance has to be able to
|
||||
say which commit is serving it. `/api/version` answers that; this is the other
|
||||
half — the value reaching a person — and it goes wrong one way in particular:
|
||||
*never let a blank stand in for `unknown`*. A readout
|
||||
that renders a plausible value it never received is worse than one that renders
|
||||
nothing, because it ends the investigation instead of starting it.
|
||||
"""
|
||||
@@ -25,9 +24,9 @@ FRONTEND = Path(__file__).resolve().parents[1] / "frontend" / "src"
|
||||
def test_something_actually_reads_the_version_endpoint():
|
||||
"""The endpoint is not enough; something must ask it.
|
||||
|
||||
`/api/version` answered correctly for weeks with no caller — an endpoint
|
||||
reachable only by someone who already knew to curl it. Rule 27: a
|
||||
capability with no surface the operator can touch is not shipped.
|
||||
An endpoint with no caller is reachable only by someone who already knows
|
||||
to curl it, and a capability with no surface the operator can touch is
|
||||
not shipped.
|
||||
"""
|
||||
hits = [p for p in FRONTEND.rglob("*.ts") if "/api/version" in p.read_text()]
|
||||
assert hits, "nothing under frontend/src fetches /api/version"
|
||||
@@ -52,8 +51,8 @@ def test_the_footer_does_not_default_to_a_plausible_version():
|
||||
decl = match.group(0)
|
||||
assert '"dev"' not in decl and "'dev'" not in decl, (
|
||||
f"appVersion defaults to a version-shaped literal: {decl}\n"
|
||||
"A failed fetch would render as a real-looking version (#3127 "
|
||||
"checklist 12). Start from a not-answered-yet value instead."
|
||||
"A failed fetch would render as a real-looking version. "
|
||||
"Start from a not-answered-yet value instead."
|
||||
)
|
||||
|
||||
|
||||
@@ -75,7 +74,7 @@ def test_optional_version_fields_are_read_by_absence_not_falsiness():
|
||||
|
||||
|
||||
def test_the_version_request_carries_a_deadline():
|
||||
"""Rule 156. A wait with no deadline cannot report that it failed.
|
||||
"""A wait with no deadline cannot report that it failed.
|
||||
|
||||
This readout is consulted when an instance is misbehaving, which is exactly
|
||||
when it may never answer. Without a deadline the surface sits on "still
|
||||
|
||||
Reference in New Issue
Block a user