Compare commits

...
2 Commits
Author SHA1 Message Date
bvandeusenandClaude Opus 5.5 b3e6a27dc1 docs(6209): forge, dedup, tuning, registry, reply shapes and discipline, checkpoint, lesson kind tests
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 25s
CI & Build / TypeScript typecheck (push) Successful in 56s
CI & Build / integration (push) Successful in 1m24s
CI & Build / Python tests (push) Successful in 2m13s
CI & Build / Build & push image (push) Successful in 13s
Eight more test files state their properties without issue, milestone,
rule and decision numbers. The tuning and suspension notes describe the
symptom a shipped default or a late ledger produces instead of one
install's dated readings; the reply-discipline and checkpoint docstrings
state the failure they prevent instead of the session that measured it.
test_empty_selector_is_the_rule_115_baseline is renamed for what it
checks: the no-forge baseline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 21:30:54 -04:00
bvandeusenandClaude Opus 5.5 0e658bf275 docs(6209): task document, query scrubbing, note pipeline, lesson door, live body, ledger clear, rule usage
Seven more test files: docstrings and comments state the property and its
reason, the SQL-twin note states why `\m` and not `\b` without the install
it broke, and fixture provenance says "real near-miss queries" rather than
naming the spike that collected them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 21:29:49 -04:00
15 changed files with 178 additions and 202 deletions
+9 -10
View File
@@ -1,18 +1,17 @@
"""The `lesson` kind — its trigger, its title, and its place in the vocabulary.
WHY THIS EXISTS (milestone 385 step 2, #3729)
WHY THIS EXISTS
A lesson is a note that must be findable by WHEN IT APPLIES rather than by what
it is about. That is a document-shape fact: the trigger has to reach the
embedded text, and decision #4157 put it in `notes.data` with a mirror in the
title and the head of the body — the shape snippets already use.
embedded text, so it lives in `notes.data` with a mirror in the title and the
head of the body — the shape snippets use.
THE ONE THAT MATTERS MOST
`test_one_join_builds_every_trigger_title`. Three kinds now rank on a
`{subject} — {trigger}` title: rules, snippets and lessons. That join had three
implementations before this step and would have had four; #3207 records what
that costs. The guard is behavioural rather than `assert a is b`, because the
`{subject} — {trigger}` title: rules, snippets and lessons, through ONE join —
a copy per kind is a fix that has to be found in three places. The guard is behavioural rather than `assert a is b`, because the
three are reached through different public names and a test that compared
identities would pass on a re-implementation that merely re-exported.
@@ -75,7 +74,7 @@ def test_one_join_builds_every_trigger_title():
expected = f"{subject} — {trigger}"
assert trigger_title(subject, trigger) == expected
# The two note kinds reach it through the EMBEDDED title (milestone 427),
# The two note kinds reach it through the EMBEDDED title,
# from their own mirror key — never a hand-rolled join.
from scribe.services.embeddings import document_title
assert document_title(subject, "lesson", {"when_to_apply": trigger}) == expected
@@ -95,7 +94,7 @@ def test_the_kind_is_in_the_browse_vocabulary():
Asserted through the public table rather than a literal, because the door's
validation, the counts and both dialects of the type filter are all
generated from it (#3161) — so this is the one place that decides.
generated from it — so this is the one place that decides.
"""
assert lessons_svc.LESSON_NOTE_TYPE in knowledge_svc.FACET_TYPES
assert lessons_svc.LESSON_NOTE_TYPE in knowledge_svc.NON_TASK_FACETS
@@ -104,7 +103,7 @@ def test_the_kind_is_in_the_browse_vocabulary():
def test_the_global_kinds_list_names_the_lesson_and_nothing_else():
"""`embeddings.GLOBAL_NOTE_TYPES` spells "lesson" as a literal because
`services.lessons` imports `trigger_title` from that module, so importing
it back would close a cycle (milestone 385 step 3). The copy is pinned
it back would close a cycle. The copy is pinned
here instead — the one thing a literal costs is that it can drift, and
this is what stops it.
"""
@@ -114,7 +113,7 @@ def test_the_global_kinds_list_names_the_lesson_and_nothing_else():
def test_a_lesson_is_not_a_task_on_either_arm_of_the_filter():
"""The cell left empty on purpose (#3163).
"""The cell left empty on purpose.
`is_task` IS `status is not None`, so a lesson that acquired a status would
become a task and start appearing in open-work listings — the one way
+11 -11
View File
@@ -1,11 +1,11 @@
"""The REST door for lessons — the half the web UI can actually reach (#3734).
"""The REST door for lessons — the half the web UI can actually reach.
WHY THIS EXISTS AT ALL
Milestone 385 built the lesson kind through the MCP tools, which is the surface
an agent uses. The Vue app speaks REST, so until this blueprint existed a lesson
was a record a person could not create, read, edit or retire from the UI — rule
27's "no UI, no ship" failing at the door rather than in the view.
The MCP tools are the surface an agent uses; the Vue app speaks REST. Without
this blueprint a lesson is a record a person cannot create, read, edit or
retire from the UI — a feature with no UI, failing at the door rather than in
the view.
WHAT THESE PIN
@@ -17,7 +17,7 @@ kind:
own title would produce lessons that rank differently from the agent's, and
the document IS what ranks.
- ACL (rule 78). Share-aware resolve, write as the owner — the pattern
- ACL. Share-aware resolve, write as the owner — the pattern
routes/snippets.py sets — so a shared editor isn't rejected by the
owner-scoped service.
@@ -50,7 +50,7 @@ def test_both_doors_share_one_serializer():
def test_the_rest_door_composes_nothing_itself():
"""Asserted on structure (rule 167). The document is what ranks, so a door
"""Asserted on structure. The document is what ranks, so a door
that built its own title would produce lessons that rank differently from
the ones the agent writes."""
from scribe.routes import lessons as routes
@@ -124,7 +124,7 @@ def test_the_subject_is_required_too():
assert "what is required" in src
# ── ACL: rule 78's pattern, not a bare owner filter ──────────────────────────
# ── ACL: the shared-access pattern, not a bare owner filter ──────────────────
@pytest.mark.parametrize(
@@ -139,12 +139,12 @@ def test_writes_check_permission_and_act_as_the_owner(handler):
assert "can_write_note" in src, f"{handler} does not check write permission"
assert "note.user_id" in src, (
f"{handler} writes as the caller rather than as the owner, which "
f"rejects a legitimately shared editor (rule 78)"
f"rejects a legitimately shared editor"
)
def test_no_handler_builds_its_own_owner_filter():
"""Rule 78's actual failure mode: a route assembling its own
"""The access layer's actual failure mode: a route assembling its own
`Note.user_id == uid` clause instead of going through the service and the
access helpers. Passing `user_id=uid` INTO a service is the correct call
and is not what this looks for."""
@@ -238,7 +238,7 @@ def test_the_payload_reads_back_the_composed_fields():
def test_the_rest_guards_can_fail():
"""Rule 167: shown turning red once."""
"""Shown turning red once."""
from scribe.routes import lessons as routes
src = inspect.getsource(routes)
+8 -8
View File
@@ -1,9 +1,9 @@
"""The notes corpus on the one retrieval pipeline (milestone 456 step 4).
"""The notes corpus on the one retrieval pipeline.
`run_note_arm` is driven directly here, with the search and both recorders
stubbed through `NoteIO`: the stage order, the invariants the rule arms
already pin (#3497's call row before any return, #3752's fresh-only cut,
#4101's ledger-renders-not-removes), the write path's withheld menu (#3739),
already pin (the call row before any return, the fresh-only cut, the ledger
renders rather than removes), the write path's withheld menu,
and the two reserved slots in their load-bearing order. The builders that
compose these arms keep their own tests; these pin the stages they share.
"""
@@ -53,7 +53,7 @@ async def _run(arm, route, *, budget=3, **moment):
return result, io, calls
# ── the call row (#3497, #3752) ───────────────────────────────────────────
# ── the call row ──────────────────────────────────────────────────────────
@pytest.mark.asyncio
@@ -75,7 +75,7 @@ async def test_repeats_and_named_records_are_counted_not_reported():
(row,) = _rows(io.record_retrieval, "auto_inject")
assert [int(n.id) for _s, n in row["results"]] == [22]
assert row["suppressed"] == 2
# The repeat stays on the menu (#4101); the named record does not — it is
# The repeat stays on the menu; the named record does not — it is
# shown by its own block.
assert [int(n.id) for _s, n in result.menu] == [11, 22]
(surfaced,) = _rows(io.record_surfaced, "auto_inject")
@@ -105,7 +105,7 @@ def test_every_notes_stage_logs_before_it_can_return():
@pytest.mark.asyncio
async def test_the_band_narrows_the_menu_after_the_row_is_written():
"""Notes log BEFORE the band (#2085) — the row is the candidate set a
"""Notes log BEFORE the band — the row is the candidate set a
floor is tuned against, the surfacing rows are what the reader saw."""
hits = [_note(1, 0.90), _note(2, 0.70)]
result, io, _ = await _run(rp.WRITE_PATH, lambda kw: hits)
@@ -141,7 +141,7 @@ async def test_a_recorder_that_raises_costs_only_its_row():
assert [int(n.id) for _s, n in result.menu] == [1]
# ── the write path's withheld menu (#3739) ────────────────────────────────
# ── the write path's withheld menu ────────────────────────────────────────
@pytest.mark.asyncio
@@ -223,7 +223,7 @@ async def test_a_slot_stands_down_when_its_kind_won_on_score():
@pytest.mark.asyncio
async def test_a_slot_spends_itself_on_a_repeat_without_booking_it_again():
"""A record shown in an EARLIER call is exactly what a slot may spend
itself on (#4101) — it is rendered, but it is not a new surfacing."""
itself on — it is rendered, but it is not a new surfacing."""
main = [_note(1, 0.70)]
lesson = [_note(42, 0.58, note_type=LESSON_NOTE_TYPE)]
result, io, _ = await _run(
+11 -13
View File
@@ -1,13 +1,12 @@
"""A rule put IN FRONT of an act, not beside its result (#4214, milestone 419).
"""A rule put IN FRONT of an act, not beside its result.
WHY THIS EXISTS
Every other rule surface in this plugin returns `additionalContext`, which
Claude Code delivers alongside the tool RESULT. By the time the line is read
the call is written, so the rule reads as commentary on a decision already
made. Milestone 419 measured the cost over one session: seven misses, three
caught by the operator and none by this system, five of them the same move —
acting on the thing in hand without reading the contract around it.
made — and the common miss is acting on the thing in hand without reading
the contract around it.
A checkpoint spends the same retrieval differently. The hook returns a `deny`,
the act does not run, and the rule's own text can be read before the call
@@ -22,11 +21,10 @@ itself (a tuning value, and a test asserting 0.80 would fail on every retune
while proving nothing) or the wording of the reason (prose, and it will be
rewritten). The cases below express scores relative to the constant.
THE ONE BOUNDARY THAT IS A RECORDED DECISION, NOT A DESIGN CHOICE. Only the
THE ONE BOUNDARY THAT IS A STANDING DECISION, NOT A DESIGN CHOICE. Only the
ACTION arm can hold. `scribe_prior_art.sh` carries a tested property that it
never returns a permissionDecision — the operator's decision that a recall aid
may not stand in the way of a write — and this milestone does not get to
quietly overturn it. The write-path arm computes the same block and returns
never returns a permissionDecision — a recall aid may not stand in the way of
a write — and a checkpoint does not get to quietly overturn that. The write-path arm computes the same block and returns
it, so the decision can be revisited with evidence; the hook ignores it. The
last test here asserts that boundary holds, because it is exactly the kind of
property that erodes when someone extends the feature later.
@@ -101,7 +99,7 @@ def test_a_preference_never_holds_an_act():
def test_a_rule_the_session_already_opened_never_holds_an_act():
"""`held` is observable — a PostToolUse hook watches for the `get_rule`
call (#4100) — so this is a recorded event, not a model's self-report
call — so this is a recorded event, not a model's self-report
about its own context. A session that read the rule has already had the
thing the checkpoint exists to produce, and holding it again would punish
the behaviour being asked for."""
@@ -167,7 +165,7 @@ def test_a_rule_with_no_trigger_renders_without_an_empty_bracket():
def test_every_held_act_carries_a_rendered_reason():
"""Rendered at the one call site inside `checkpoint_for`, never by each
arm: two arms that each remember to render it are two arms that can stop
agreeing on what a stop says, which is #3497's history for this pair."""
agreeing on what a stop says."""
assert hold([hit(0.99, 1)])["reason"].strip()
@@ -253,8 +251,8 @@ def test_only_the_action_hook_can_hold_an_act():
than left to memory.
`scribe_prior_art.sh` runs before every Write and Edit and has never been
able to stop one. That is the operator's decision — a recall aid may not
stand in the way of the work — and `test_hook_never_returns_a_permission_
able to stop one. That is a standing decision — a recall aid may not stand
in the way of the work — and `test_hook_never_returns_a_permission_
decision` in test_write_path_trigger.py holds the other half of it.
This milestone has a live argument for extending the checkpoint to writes:
@@ -273,7 +271,7 @@ def test_only_the_action_hook_can_hold_an_act():
def test_the_action_hook_caps_every_hold_it_emits():
"""Structural, and able to fail (rule 167): the deny and the ledger call
"""Structural, and able to fail: the deny and the ledger call
must appear together. A deny emitted outside the guard is a session that
can be held by the same rule on every command, which is the one outcome
both guards exist to prevent."""
+15 -20
View File
@@ -1,17 +1,13 @@
"""A reply's sections are chosen, not filled (#4153, milestone 409 step 7).
"""A reply's sections are chosen, not filled.
WHY THIS EXISTS
Step 6 measured the scaffold on live sessions and found the two halves
disagreeing: **adherence passed and the read test failed.** Completion replies
carried every section the table asks for — placement, what changed, what needs
the operator, what comes next — and the operator still could not read them.
The cause was in the skill rather than in compliance with it. It said to pick a
kind of reply "then fill its sections… keep them even when one is short", which
is an instruction to complete a form. Nothing anywhere set a ceiling, and a
section with a heading and nothing to say gets filled rather than dropped. So a
faithful reply and an unreadable one were the same reply.
A reply can carry every section the table asks for — placement, what changed,
what needs the operator, what comes next — and still be unreadable: adherence
passes and the read test fails. An instruction to pick a kind of reply "then
fill its sections" is an instruction to complete a form, and with no ceiling a
section with a heading and nothing to say gets filled rather than dropped. A
faithful reply and an unreadable one become the same reply.
WHAT IS PINNED
@@ -25,18 +21,18 @@ unchanged and are not this file's subject:
3. "Needs you" takes BOTH tests: theirs to decide, and blocking.
4. A decision already made is acted on rather than re-argued.
RULE 167, AND WHY THERE IS NO ABSENCE CHECK HERE
WHY THERE IS NO ABSENCE CHECK HERE
The obvious guard — assert the skill no longer tells anyone to "fill" a
section — is the trap snippet #3352 names. This skill legitimately discusses
The obvious guard — assert the skill never tells anyone to "fill" a section —
is a trap. This skill legitimately discusses
filling in order to warn against it ("not a form to complete", "a section
filled because it was in the table"), so an absence check would fail on the
corrected text: a false alarm about the very thing it protects.
So every assertion here is a PRESENCE check, and one is POSITIONAL —
`test_the_needs_you_test_sits_with_the_needs_you_section` pins where the test
lives, not merely that the words occur somewhere in the file. All four were
falsified against the pre-#4153 text before being committed.
lives, not merely that the words occur somewhere in the file. Each fails
against a skill that says to fill its sections.
"""
from __future__ import annotations
@@ -111,7 +107,7 @@ def test_the_needs_you_test_sits_with_the_needs_you_section():
def test_a_settled_decision_is_acted_on_rather_than_re_argued():
"""Re-opening a decision reads as self-contradiction, not as diligence.
Phrased as a practice rather than a prohibition (rule 165): the instruction
Phrased as a practice rather than a prohibition: the instruction
is what to DO with a decision, with the failure named after it.
"""
text = _flat()
@@ -126,14 +122,13 @@ def test_the_scaffold_itself_is_untouched():
"""This step changed the discipline AROUND the sections, not the sections.
If a future edit deletes a category while tightening the prose, that is a
change to #4009's subject and should not ride along silently here.
change to the scaffold itself and should not ride along silently here.
"""
text = _flat()
for section in ("where this sits", "what now works", "how / why",
"needs you", "next"):
assert section in text, f"completion-report section {section!r} is gone"
# The kinds live in the delivered shapes since milestone 500 step 4; the
# skill keeps the reasoning and the worked completion report.
# The kinds live in the delivered shapes; the skill keeps the reasoning and the worked completion report.
from scribe.services import reply_shapes
shapes = " ".join(s.title + " " + s.text for s in reply_shapes.SHAPES.values()).lower()
+9 -9
View File
@@ -1,4 +1,4 @@
"""The default reply shapes and the moments they ride (milestone 500 step 1).
"""The default reply shapes and the moments they ride.
What this pins is what delivery will depend on: every shape rides a moment that
exists, the core is small enough to pay for on every turn, no slice grows back
@@ -15,7 +15,7 @@ _REAL_RECORD = reply_shapes.record_delivery
def test_there_is_a_core_and_at_least_one_slice():
"""The sweeps below are vacuous over an empty catalog (rule 167)."""
"""The sweeps below are vacuous over an empty catalog."""
assert reply_shapes.CORE_KEY in reply_shapes.SHAPES
assert len(reply_shapes.SHAPES) >= 2
@@ -74,16 +74,16 @@ def test_every_shape_says_what_it_is_and_when_it_arrives(key):
def test_the_core_carries_the_length_discipline():
"""Milestone 409's live read: replies that had every section were still
too long. Delivery cannot fix that; the core has to say it."""
"""A reply that has every section can still be too long. Delivery cannot
fix that; the core has to say it."""
text = reply_shapes.core().text.lower()
assert "shortest reply that carries the answer" in text
assert "what can go" in text
def test_who_decides_what_rides_every_reply_and_every_ask():
"""The operator's line (milestone 500 step 2): the agent settles what
only it can see, the operator decides direction. A one-liner every turn,
"""Who decides: the agent settles what only it can see, the operator
decides direction. A one-liner every turn,
in full where a question is about to be handed back."""
core = reply_shapes.core().text.lower()
asks = reply_shapes.SHAPES["asks"].text.lower()
@@ -130,7 +130,7 @@ def test_the_tool_is_registered_as_a_read():
def test_both_doors_read_one_catalog():
"""Rule 33 parity: the session and the Settings view cannot show
"""Parity: the session and the Settings view cannot show
different defaults."""
from scribe.mcp.tools import moments as tool
from scribe.routes import retrieval as routes
@@ -158,7 +158,7 @@ async def test_the_route_returns_the_operators_overview_with_a_bounded_window():
assert view.await_args.kwargs == {"days": days}
# ── the operator's view (milestone 500 step 5) ──────────────────────────
# ── the Settings view ───────────────────────────────────────────────────
async def test_the_overview_lists_the_preferences_mounted_on_each_shapes_moment():
@@ -226,7 +226,7 @@ async def test_delivery_counts_tally_each_shape_by_form_and_skip_what_does_not_p
assert "bogus" not in counts
# ── delivery (milestone 500 step 3) ─────────────────────────────────────
# ── delivery ────────────────────────────────────────────────────────────
@pytest.mark.parametrize("key", list(reply_shapes.SHAPES))
+14 -17
View File
@@ -1,4 +1,4 @@
"""A logged query never carries a credential (#3925).
"""A logged query never carries a credential.
WHY THIS EXISTS
@@ -9,8 +9,7 @@ that exported a token therefore stored the token.
And storing it was not the worst of it. `near_miss_samples` is the readout the
threshold documentation tells you to open before moving a bar, so the value
came back OUT into an agent's context on the next tuning pass — which is
exactly how this was found, during #3853's threshold spike.
would come back OUT into an agent's context on the next tuning pass.
WHAT THIS PINS, IN BOTH DIRECTIONS, AND WHY THE SECOND HALF IS THE HARD ONE
@@ -21,12 +20,11 @@ A scrubber has two ways to fail and only one of them is obvious.
because it is silent: the whole worth of a near-miss sample is reading the
query that was actually refused, and a scrubber that chewed up ordinary
commands would turn the one instrument for tuning a bar into unreadable
stubs while still looking like it worked. That is the #2663 shape — a
surface that reads fine and has quietly stopped saying anything.
stubs while still looking like it worked — a surface that reads fine and
has quietly stopped saying anything.
So the second block is not padding. Its cases are REAL queries taken from this
install's `near_miss_samples` during #3853, and they must survive byte for
byte. If a future pattern is added and one of them changes, the pattern is too
So the second block is not padding. Its cases are REAL near-miss queries, and
they must survive byte for byte. If a future pattern is added and one of them changes, the pattern is too
greedy — tighten it rather than editing the expectation.
The secret cases use FABRICATED values in the real formats. Nothing here is or
@@ -59,7 +57,7 @@ _SECRETS = [
"MIIEowIBAAKC"),
]
# Real queries, from this install's near-miss samples during #3853.
# Real queries, from near-miss samples.
_EVIDENCE = [
"git push origin dev",
'git pull --rebase origin dev 2>&1 | tail -3; echo "=== HEAD ==="; git log --oneline -2',
@@ -114,7 +112,7 @@ def test_empty_and_missing_queries_pass_through():
def test_the_write_path_scrubs_rather_than_the_read_path():
"""The payload built for storage carries the redacted text (#3925).
"""The payload built for storage carries the redacted text.
Pinned on `_build_payload` because that is the single seam every source
reaches the column through. A per-caller scrub would be three places for
@@ -135,21 +133,20 @@ def test_the_write_path_scrubs_rather_than_the_read_path():
assert "git push" in payload["query"]
# ── the SQL twin has its own word-boundary spelling (#3925) ─────────────
# ── the SQL twin has its own word-boundary spelling ─────────────────────
#
# Migration 0099 carries an inlined copy of these patterns, deliberately: a
# migration is a frozen record of what already ran, and importing the live
# ones would mean it quietly did something different next year.
#
# Frozen is not the same as correct, and the first cut was neither. The
# boundary was dropped in the port, so `sk-` matched inside any word
# containing it — `<task-notification>` became `<ta[redacted:token]>` across
# thousands of rows on the one install that ran it. And writing `\b` would not
# have saved it: in Postgres ARE `\b` is a BACKSPACE, not a word boundary.
# Frozen is not the same as correct. Without the boundary, `sk-` matches
# inside any word containing it — `<task-notification>` becomes
# `<ta[redacted:token]>`. And writing `\b` does not help: in Postgres ARE `\b`
# is a BACKSPACE, not a word boundary.
# `\m` (start of word) is the spelling that means what Python's `\b` means.
#
# So this pins the property no reader can eyeball, and it is a PRESENCE check
# on a token that must appear rather than an absence check on prose (#3352).
# on a token that must appear rather than an absence check on prose.
def test_the_migrations_patterns_anchor_to_a_word_start_the_postgres_way():
"""`\\m`, never `\\b` — the two are unrelated in Postgres."""
+7 -7
View File
@@ -1,10 +1,10 @@
"""Every retrieval point is declared, derived from the CALL SITES (#3431).
"""Every retrieval point is declared, derived from the CALL SITES.
WHY AST AND NOT GREP, demonstrated rather than asserted. Two of the sources in
this system reach their recorder as `source=SOURCE` through a module-level
constant — `wide_net` and `preference_slot` — so a grep for `source="` finds
neither. A registry test built on that grep would pass while being blind to
two arms, which is the narrowing #3191 warns about: a check that looks
two arms — a check that looks
thorough and quietly covers less than it claims.
So the extractor below parses each module, resolves module-level string
@@ -35,13 +35,13 @@ from scribe.services.retrieval_surfaces import SURFACES
SRC = Path(__file__).resolve().parents[1] / "src"
# The recorders, plus the one FORWARDER. `rules_payload` takes a `source` and
# passes it to `record_rule_surfaced` on its caller's behalf (snippet #2858),
# passes it to `record_rule_surfaced` on its caller's behalf,
# so its callers are the real declaration site and a scan of recorders alone
# would miss every ambient rule surfacing.
RECORDERS = {
"record_retrieval", "record_surfaced", "record_pulled",
"record_rule_surfaced", "record_rule_pulled", "rules_payload",
# The rulings arm (#4769): its two recorders, and `rulings_for_paths`,
# The rulings arm: its two recorders, and `rulings_for_paths`,
# which forwards its caller's `source` the way `rules_payload` does.
"record_system_surfaced", "record_system_pulled", "rulings_for_paths",
}
@@ -90,7 +90,7 @@ def call_sites() -> tuple[dict[str, set[str]], list[str]]:
def test_the_extractor_finds_something() -> None:
"""The guard has to be able to fail (rule 167).
"""The guard has to be able to fail.
An extractor that silently matched nothing would make every assertion
below vacuously true — a green suite proving the opposite of what it
@@ -130,7 +130,7 @@ def test_every_unresolved_call_site_is_declared() -> None:
# EQUALITY, not prefix. The first spelling of this compared against a
# prefix that could never match — the paths begin `scribe/` — so the check
# passed by matching nothing, which is the failure mode a guard is most
# likely to have and least likely to show (rule 167).
# likely to have and least likely to show.
undeclared = sorted({u for u in unresolved if u not in FAN_OUT_SITES})
assert not undeclared, (
"these call sites pass `source` as a value this test cannot resolve, "
@@ -157,7 +157,7 @@ def test_every_tunable_surface_is_also_a_registered_point() -> None:
def test_a_quiet_point_says_why() -> None:
"""A justified silence must carry its justification (#2475).
"""A justified silence must carry its justification.
Without the reason the reader cannot tell a decision from an oversight,
which is the whole difference this field exists to record.
+20 -24
View File
@@ -1,11 +1,10 @@
"""Moving a floor, and the reason that has to come with it (#4102).
"""Moving a floor, and the reason that has to come with it.
WHY THIS EXISTS
The operator handed the dial to the model: *"the user should be able to touch it
but the model should be the thing handling it 9 times out of 10."* Everything
here guards the half of that sentence people skip — the operator still has to be
able to see what was done on their behalf and disagree with it.
The model moves these dials most of the time, and a person can too. Everything
here guards the half of that arrangement people skip — the person still has to
be able to see what was done on their behalf and disagree with it.
WHAT THIS PINS
@@ -20,7 +19,7 @@ WHAT THIS PINS
question anyone asks of a surface behaving oddly.
3. **A clamp is reported, never swallowed.** A caller that believes it set 1.4
will read the next telemetry as evidence about a bar that was never in
force — the same class of error as #3739, one layer up.
force.
4. **An unknown surface is refused before anything is written.** Settings keys
are free-form strings, so a typo'd surface would write a key nothing reads:
a change that reports success and alters nothing.
@@ -167,7 +166,7 @@ async def test_an_unknown_actor_is_refused():
def test_the_tool_teaches_reading_the_records_not_the_percentile():
"""The contract an agent actually reads (rule 167).
"""The contract an agent actually reads.
The failure this prevents is a model moving a bar because `near_misses.p90`
sat close to it. That has been measured pointing the wrong way — 69 declines
@@ -198,20 +197,19 @@ def test_every_tool_in_the_module_is_registered():
]
# ── record_release_defaults / floor_moves_since (#4225) ───────────────────
# ── record_release_defaults / floor_moves_since ───────────────────────────
#
# THE HOLE THESE FILL. This table records dial turns — a person or a model
# choosing a number. It was silent about the other way a floor moves: somebody
# edits `floor_default` in the registry and ships it. That change is invisible
# to every consumer of this table, and one consumer is `band_hugs_floor`, which
# compares a band against a floor and could not notice they came from different
# regimes.
# choosing a number. The other way a floor moves is somebody editing
# `floor_default` in the registry and shipping it, and without a release row
# that change is invisible to every consumer of this table — including
# `band_hugs_floor`, which compares a band against a floor and cannot notice
# they came from different regimes.
#
# Measured on the instance that found it: `write_path_rule` went 0.68 -> 0.72
# on 2026-09-02 as a shipped default, so a 30-day window opening 2026-08-22
# held six days of calls made under the old bar. The readout reported the band
# sitting "-0.0216 above" its floor — which is what a two-population comparison
# looks like when it finally says so out loud.
# The symptom: a window that spans a shipped default change holds calls made
# under the old bar, and the readout reports the band a negative distance
# "above" its floor — what a two-population comparison looks like when it
# finally says so out loud.
def _release_rows(rows):
@@ -350,12 +348,10 @@ async def test_a_baseline_is_not_reported_as_a_floor_move():
# WATCHING. Before an arm's first row there is nothing to move, nothing to
# report, and no way to tell a steady floor from an unrecorded change.
#
# Measured the day the ledger was seeded on this instance: its earliest event
# of any kind is 2026-09-17, its `write_path_rule` floor rows begin at
# 2026-09-21, and a 30-day window opening 2026-08-22 reported that arm's band
# sitting "-0.0208 above" its floor. That negative distance is the documented
# tell of a raise inside the window — announced by the check the suspension
# was built to stop, because the suspension had nothing to read.
# The symptom: a window that opens before an arm's first floor row reports the
# band a negative distance "above" its floor. That is the documented tell of a
# raise inside the window — announced by the check the suspension exists to
# stop, because the suspension has nothing to read.
def _gap_rows(rows):
+15 -15
View File
@@ -43,7 +43,7 @@ async def test_short_body_skips_semantic_check():
@pytest.mark.asyncio
async def test_semantic_match_when_body_substantial():
# A note blocks only in the copy band (#4306).
# A note blocks only in the copy band.
hit = fake_note(id=20, title="Existing", note_type="note")
sem = AsyncMock(return_value=[(0.99, hit)])
with patch("scribe.services.dedup.async_session",
@@ -60,8 +60,8 @@ async def test_semantic_match_when_body_substantial():
@pytest.mark.asyncio
async def test_a_near_note_is_surfaced_not_blocked():
"""#4306: sibling notes — the next dev-log, another part of one design —
measured 0.90–0.98, so a match there is shown for the session to judge
"""Sibling notes — the next dev-log, another part of one design — measure
0.90–0.98, so a match there is shown for the session to judge
instead of refusing the write."""
from scribe.services.dedup import _NOTE_OVERLAP_FLOOR
@@ -133,9 +133,9 @@ def test_a_batch_overlap_names_its_record():
@pytest.mark.asyncio
async def test_gate_catches_a_duplicate_hiding_in_a_later_chunk():
"""The capability #280 adds to the gate: a long candidate that duplicates
an existing record in ONE SECTION is caught, where the whole-document
query this replaces diluted exactly the section that mattered. The gate
"""A long candidate that duplicates an existing record in ONE SECTION is
caught; a whole-document query would dilute exactly the section that
matters. The gate
queries once per chunk and any chunk's hit blocks."""
para = ("This section restates an existing decision in enough words to be "
"a real paragraph of content for the chunker to keep. ") * 4
@@ -202,13 +202,13 @@ def test_duplicate_response_shape():
assert "update_task" in r["message"]
# --- snippet structural identity (#2518) -------------------------------------
# --- snippet structural identity ---------------------------------------------
#
# The gate used to compare a snippet's rendered DOCUMENT, which is mostly prose
# about the code. Measured on the button corpus, that failed in both directions
# at once: two deliberately-parallel variants were refused at 0.92, while a
# verbatim re-record of one snippet under a different name scored below 0.90 and
# was created. These tests pin the structural signals that replaced it.
# A snippet's rendered DOCUMENT is mostly prose about the code, and comparing
# it fails in both directions at once: two deliberately-parallel variants
# refused at 0.92, while a verbatim re-record under a different name scores
# below 0.90 and is created. These tests pin the structural signals used
# instead.
def _session_sequence(results):
@@ -371,7 +371,7 @@ def test_duplicate_response_names_what_matched_for_structural_hits():
assert "identical code" in r["message"]
# --- the generalised report (#2547) ------------------------------------------
# --- the generalised report -------------------------------------------------
@pytest.mark.asyncio
@@ -404,7 +404,7 @@ def test_every_kind_has_a_suggestion_and_none_proposes_merging_notes():
"""The suggestion is the report's point: what to DO differs by what the
records are, and 'merge' is only ever the answer for snippets — folding two
notes destroys what each said, which is why consolidated_at was dropped
rather than built (#2483)."""
rather than built."""
from scribe.services.dedup import _KIND_SUGGESTION, _REPORT_KINDS
for kind in _REPORT_KINDS:
@@ -418,7 +418,7 @@ def test_every_kind_has_a_suggestion_and_none_proposes_merging_notes():
assert "supersedes" in _KIND_SUGGESTION["note"]
# ── the plan gate (milestone 415, step 4) ─────────────────────────────────────
# ── the plan gate ─────────────────────────────────────────────────────────────
@pytest.mark.asyncio
+11 -12
View File
@@ -1,4 +1,4 @@
"""Forge adapter contract tests (#2689) — the Gitea implementation against a
"""Forge adapter contract tests — the Gitea implementation against a
mocked transport, plus the configuration gate.
httpx.MockTransport is the fake forge: the adapter takes an injectable
@@ -9,7 +9,7 @@ reproduce.
The most load-bearing tests are the OFF ones: a user with no usable connection
must get an empty selector, because every consumer treats that as "behave as
if the module didn't exist" (rule #115 — the baseline user has no forge).
if the module didn't exist" (the baseline user has no forge).
DB-backed keyring behavior (get_forges: rows, the project pin, the admin-only
env entry) lives in tests/test_integration_forge_keyring.py.
"""
@@ -175,9 +175,9 @@ def test_full_values_build_a_github_adapter():
)
# --- the selector (#2778): host-keyed resolution over a keyring ---------------
# --- the selector: host-keyed resolution over a keyring ----------------------
def test_empty_selector_is_the_rule_115_baseline():
def test_empty_selector_is_the_no_forge_baseline():
selector = ForgeSelector()
assert not selector.configured
assert selector.resolve(f"{BASE}/alice/widget") is None
@@ -204,7 +204,7 @@ def test_forge_error_taxonomy_is_catchable_as_one_family():
def test_adapter_contract_surface():
"""Both adapters implement exactly this surface — the second
implementation is what proves it's a contract (#2693)."""
implementation is what proves it's a contract."""
from scribe.services.forge import FORGE_KINDS, GitHubForge
for cls in (GiteaForge, GitHubForge):
@@ -222,7 +222,7 @@ def test_forge_routes_registered_at_their_post_2778_homes():
"""Connections are USER settings; only the webhook secret stays admin.
The old instance-wide /api/admin/forge endpoints must be GONE, not
coexisting — a legacy config surface that still wrote admin settings
would silently configure nothing (rule #22: no dual path)."""
would silently configure nothing (one path, not two)."""
from scribe.app import create_app
from scribe.routes import admin as admin_routes
@@ -239,11 +239,10 @@ def test_forge_routes_registered_at_their_post_2778_homes():
def test_settings_kv_surface_masks_the_webhook_secret():
"""The generic /api/settings dump masked nothing — the admin endpoints'
masking was bypassable by reading the raw KV rows (found while wiring the
forge token; smtp_password had the same exposure). forge_token left the
KV with #2778 (keyring rows carry it now), so it is deliberately no
longer a secret KEY here."""
"""The generic /api/settings dump masks secrets too — otherwise the admin
endpoints' masking is bypassable by reading the raw KV rows. The forge
token lives on keyring rows, not in the KV, so it is deliberately not a
secret KEY here."""
from scribe.routes.settings import _SECRET_KEYS, _masked
out = _masked({"forge_webhook_secret": "sec", "smtp_password": "pw", "theme": "dark"})
@@ -270,7 +269,7 @@ def test_config_has_the_docker_secret_channel():
assert hasattr(Config, attr)
# --- the GitHub adapter (#2693) ----------------------------------------------
# --- the GitHub adapter -----------------------------------------------------
# Same contract, second implementation. Where behavior below differs from the
# Gitea tests above, that difference IS the adapter's job: API host mapping,
# Bearer auth, the missing last_commit_sha, the codeload redirect.
+15 -17
View File
@@ -1,4 +1,4 @@
"""Rule usage telemetry — the parts that need no database (milestone 333 step 1).
"""Rule usage telemetry — the parts that need no database.
The round trip lives in `test_integration_backup_rule_usage_roundtrip.py`.
What is here is the payload building and the zero shape: cheap, and the half
@@ -143,10 +143,10 @@ def test_the_model_serialises_the_fields_the_ratio_needs():
assert row["created_at"] is None
# ─── the readout (milestone 333 step 5) ──────────────────────────────────────
# Integration: a real GROUP BY over a real table. Step 1 unit-tested the WRITE
# path and the zero shape and left the aggregate uncovered, which only became
# load-bearing when the rule list started rendering it.
# ─── the readout ─────────────────────────────────────────────────────────────
# Integration: a real GROUP BY over a real table. The unit tests above cover
# the WRITE path and the zero shape; the aggregate the rule list renders needs
# a real database.
@pytest.mark.integration
@@ -212,7 +212,7 @@ async def test_usage_for_rules_on_an_empty_id_list_asks_the_database_nothing(
@pytest.mark.integration
@pytest.mark.asyncio
async def test_a_preloaded_rule_does_not_read_as_a_ranked_surfacing(_dispose_engine):
"""The split that makes the always-on set judgeable (#3473).
"""The split that makes the always-on set judgeable.
A resident rule is delivered every session by a surface that chose
nothing. Counting those as `surfaced_count` would rank the always-on set
@@ -262,12 +262,11 @@ async def test_a_preloaded_rule_does_not_read_as_a_ranked_surfacing(_dispose_eng
await s.commit()
# ── the outcome stream (#4212, milestone 419) ─────────────────────────────
# ── the outcome stream ────────────────────────────────────────────────────
#
# What these guard is a distinction, not a payload. Before this existed, a
# rule read and obeyed and a rule read and ignored left byte-identical
# telemetry, so the readout could not name the failure the whole milestone
# was opened on. The tests that matter most below are the ones asserting
# What these guard is a distinction, not a payload. Without outcomes, a rule
# read and obeyed and a rule read and ignored leave byte-identical telemetry,
# so the readout cannot name the failure. The tests that matter most below are the ones asserting
# that a REASONLESS DEPARTURE IS NEVER WRITTEN, and that an unacted rule is
# a state in its own right rather than the absence of one.
@@ -301,7 +300,7 @@ def test_a_departure_with_no_reason_is_never_written(captured, reason):
miss, so writing one would collapse the two states this table exists to
separate — silently, in the readout, where nobody would see it happen.
Dropped and reported, never stored: telemetry that lies is worse than
telemetry that is absent (#2663)."""
telemetry that is absent."""
rule_usage.record_rule_outcome(
user_id=7, rule_id=156, outcome=DEPARTED,
source="mcp_rule_outcome", detail=reason or "",
@@ -357,10 +356,9 @@ def test_a_rule_opened_and_departed_from_is_departed():
def test_a_rule_opened_and_never_acted_on_is_unacted() -> None:
"""THE STATE THAT DID NOT EXIST, and the reason for the milestone. Not
"no data": the rule was surfaced, deliberately opened, and then left no
trace of having mattered. Until now that was arithmetically identical to
compliance, which is why nothing could report it."""
"""THE UNACTED STATE. Not "no data": the rule was surfaced, deliberately
opened, and then left no trace of having mattered. Without this state it
is arithmetically identical to compliance, and nothing can report it."""
assert rule_usage.outcome_state(
_usage(surfaced_count=4, pull_count=2)
) == rule_usage.UNACTED
@@ -389,5 +387,5 @@ def test_a_departure_outranks_an_application():
def test_the_state_reads_the_aggregate_the_readout_already_returns():
"""`outcome_state` takes `usage_for_rules`' own shape, so the badge, the
readout and any later session summary cannot disagree about what
"followed" means — the drift #3246 found across the rules system."""
"followed" means."""
assert rule_usage.outcome_state(rule_usage.empty_rule_usage()) == rule_usage.UNREAD
+12 -16
View File
@@ -1,22 +1,18 @@
"""A compaction clears every session ledger, by convention not by list (#4101).
"""A compaction clears every session ledger, by convention not by list.
WHY THIS EXISTS
Milestone 386 established the claim: a ledger describes what a session HOLDS,
so the events that destroy context must destroy it too, or the records it names
become permanently unreachable mid-session. `scribe_session_context.sh`
implemented that — for the rules ledger, by name.
Five ledgers live in that directory and two were on the list. The note arms'
three (`.ids`, `.sync.ids`, `.derive.ids`) were left, under a comment asserting
this was "a decision rather than an oversight". It was not a decision, and the
arms left out are the ones where it costs most:
A ledger describes what a session HOLDS, so the events that destroy context
must destroy it too, or the records it names become permanently unreachable
mid-session. A clear that names ledgers one by one misses the next one, and the
note arms' three (`.ids`, `.sync.ids`, `.derive.ids`) are where a miss costs
most:
- their exclusions are HARD — `exclude_ids` is passed into
`semantic_search_notes` itself, so a surfaced note leaves the result set
entirely, with no weaker rendering to fall back to the way #3750 gave a
repeated rule one;
- and they never AGE — #3751's TTL was added to the rules ledger only.
entirely, with no weaker rendering to fall back to the way a repeated
rule has one;
- and they never AGE — the TTL is on the rules ledger only.
Hard, permanent and never cleared: a note surfaced in a session's first minute
is unreachable for the rest of it, through any number of compactions.
@@ -38,7 +34,7 @@ test. Both sides are asserted:
And the negative: `<sid>.unreached` is not a ledger of held context but a
record that the instance could not be reached, and a glob that swept it away
would make a hook forget an outage it is meant to report (#2932).
would make a hook forget an outage it is meant to report.
"""
import json
import os
@@ -137,7 +133,7 @@ def test_a_source_that_kept_the_context_keeps_the_ledgers(source, tmp_path):
def test_the_outage_marker_is_not_swept_with_them(tmp_path):
"""`.unreached` records that the instance was down, not what was surfaced.
Different lifetime, different question. #2932's whole point is that "we
Different lifetime, different question. The marker's whole point is that "we
checked and found nothing" and "we never managed to check" must stay
distinguishable, and a clear that took this file out would quietly answer
the second with the first.
@@ -235,7 +231,7 @@ def test_the_clear_is_derived_and_not_a_list_of_names():
def test_the_evidence_twin_is_not_swept_with_them(tmp_path):
"""The second thing that survives, and for the `.unreached` reason (#4217).
"""The second thing that survives, and for the `.unreached` reason.
`scribe_rules_append` writes each ledger twice: `<sid>.<kind>.ids`, which
says what this context HOLDS and must be forgotten here, and
+9 -9
View File
@@ -1,9 +1,9 @@
"""Pull-time freshness (#2690) — attach_live_body against a mocked forge.
"""Pull-time freshness — attach_live_body against a mocked forge.
The properties that must hold, each with its own way of rotting:
- A no-forge instance's pull response is BYTE-IDENTICAL to today's (rule
#115 — the baseline, not a degraded mode).
- A no-forge instance's pull response is BYTE-IDENTICAL to a plain read
(the baseline, not a degraded mode).
- The probe never rewrites the body: "current" refreshes provenance,
"diverged" reports, and neither clobbers the record mid-read.
- A 404 stamps the mechanically-true 'missing' verdict — once, not on
@@ -57,8 +57,8 @@ def _file_response(content: str, commit_sha: str = SHA) -> httpx.Response:
def _patched(forge):
"""Stub the owner-keyring lookup (#2778): None → an empty selector, i.e.
the owner has no connections — the old 'no forge configured' state."""
"""Stub the owner-keyring lookup: None → an empty selector, i.e. the
owner has no connections — no forge configured."""
from scribe.services.forge import ForgeSelector
selector = ForgeSelector(() if forge is None else (forge,))
@@ -112,7 +112,7 @@ async def test_current_with_same_stored_sha_skips_the_write():
async def test_stored_sha_short_circuit_skips_the_content_fetch():
"""#2693: when provenance already names a commit and the forge reports no
"""When provenance already names a commit and the forge reports no
newer commit touching the path, the pull is confirmed current WITHOUT a
content transfer — the economy that fits pull-time freshness inside
GitHub's rate limits."""
@@ -274,7 +274,7 @@ async def test_forge_failure_inside_lookup_never_breaks_the_pull():
assert "body_source" not in data
# --- #2782: an annotated record is not a diverged one ------------------------
# --- an annotated record is not a diverged one ------------------------------
# Containment is right for a verbatim record and wrong for a deliberately
# annotated one: the commentary that makes the record worth reading is exactly
# what makes `cached in fetched` false, forever. These pin the escape hatch —
@@ -326,7 +326,7 @@ async def test_an_expired_verdict_does_not_vouch():
async def test_a_push_invalidated_verdict_does_not_vouch():
"""A push touched the recorded location since the check (#2691) — the repo
"""A push touched the recorded location since the check — the repo
moved under the verdict even though the record didn't."""
data = _data(code=ANNOTATED, verification=_ok_verdict(invalidated_by="c" * 40))
assert await _freshness(data) == ("cache", "diverged")
@@ -349,7 +349,7 @@ async def test_a_verbatim_record_still_takes_the_containment_path():
async def test_a_verdict_predating_commit_stamping_does_not_vouch():
"""Verdicts recorded before `commit_sha` existed (#2688) carry no commit to
"""Verdicts recorded before `commit_sha` existed carry no commit to
compare, so they cannot tie the body to a known state of the source. They
fall through to containment rather than vouching on age alone."""
verdict = svc.compose_verification(
+12 -14
View File
@@ -1,10 +1,9 @@
"""task_document — the document a TASK is embedded as, work logs included (#4251).
"""task_document — the document a TASK is embedded as, work logs included.
A work log is the richest record Scribe holds of *why* something is the way it
is: prose written during the work, saying what was tried and ruled out. #4241
made it readable. It was still not findable, so "has anyone tried this
approach?" — the question a log answers — could not reach one, and #4208 was
rebuilt in this very session because its logs were unreachable.
is: prose written during the work, saying what was tried and ruled out. A log
that is readable but not embedded is not findable, so "has anyone tried this
approach?" — the question a log answers — cannot reach it.
These tests pin the shape and the two properties the decision rested on: a task
without logs embeds EXACTLY as it always did, and a task with them stays as
@@ -30,7 +29,7 @@ D2 = datetime.datetime(2026, 9, 21, 11, 0)
def test_a_task_with_no_logs_embeds_exactly_as_it_did_before():
"""Most notes are not tasks and most tasks carry no log. Their vectors are
the corpus this instance's thresholds were measured against, and changing
them for nothing would move every tuned number underneath itself (#4225)."""
them for nothing would move every tuned number underneath itself."""
assert task_document("A title", "A body") == ("A title", "A body")
assert task_document("A title", "A body", []) == ("A title", "A body")
assert task_document("T", None) == ("T", None)
@@ -81,10 +80,9 @@ def test_an_entry_with_no_timestamp_still_gets_its_own_section():
def test_each_log_becomes_its_own_title_anchored_chunk():
"""THE decision this build turns on. The objection to putting logs in the
task's document is that a long log drowns a short title — true before #280,
when one vector per record meant a 2,000-word log averaged the task's
subject away and everything past ~400 words was truncated unread. Chunking
answers both: separate vectors, each carrying the title as its anchor."""
task's document is that a long log drowns a short title — true of one
vector per record, where a 2,000-word log averages the task's subject away
and everything past ~400 words is truncated unread. Chunking answers both: separate vectors, each carrying the title as its anchor."""
def _entry(subject: str) -> str:
para = " ".join([f"This log entry discusses the {subject} in detail."] * 8)
return "\n\n".join(f"{para} (p{i})" for i in range(6))
@@ -102,7 +100,7 @@ def test_each_log_becomes_its_own_title_anchored_chunk():
# a log section and scored as part of it.
assert any("Short body." in c and "alternative" not in c for c in chunks)
# And nothing is lost: this is what #280 exists for. Paragraph-shaped,
# And nothing is lost: this is what chunking exists for. Paragraph-shaped,
# because that is what a log is and because a single unbroken 3,000-char
# line is hard-split mid-line by design — `test_chunking` owns that case.
joined = "\n".join(chunks)
@@ -113,7 +111,7 @@ def test_each_log_becomes_its_own_title_anchored_chunk():
def test_a_short_task_with_a_short_log_is_still_one_sharp_chunk():
"""Over-splitting is not the goal either. A task and one short log stay a
single document, the shape note #2485 measured as the sharpest."""
single document, the shape that measures sharpest."""
title, body = task_document("T", "body", [(D1, "a short note about it")])
assert chunk_document(title, body) == [embedding_text(title, body)]
@@ -122,8 +120,8 @@ def test_a_short_task_with_a_short_log_is_still_one_sharp_chunk():
#
# The shaper above is pure. These pin that the writers actually call it — a
# task whose logs are in the document but never re-indexed after one is written
# is #4241's half-surface one layer down: the entry is readable, and the search
# still answers as though it were never written.
# is a half-surface one layer down: the entry is readable, and the search still
# answers as though it were never written.
from unittest.mock import AsyncMock, MagicMock, patch # noqa: E402