From c440c49f5bf98549e0c5883f34dbb620b46b70d1 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Mon, 14 Sep 2026 13:29:34 -0400 Subject: [PATCH] test(410): an exactly-one-owner guard replaces the tests that required every surface to repeat itself (#4033) Step 6 of milestone 410 "One owner per piece of guidance". CI now keeps the shape decision #4027 set, so the next feature cannot quietly add a copy. tests/test_guidance_ownership.py, 33 topics, each with an owner, markers, a statement distinctive to the owner's full wording, and optional index markers: - test_every_topic_is_stated_by_its_owner: markers and statement on the owner - test_no_topic_is_stated_in_full_off_its_owner: the statement appears on no other session surface (index, adapter static text and commands, live context, other skills). Tool docstrings are not scanned; a contract may elaborate the reflex that calls it. - test_the_index_names_each_reflex_it_points_at: _INSTRUCTIONS keeps a one-line pointer for each session-start reflex - shared_with declares the one deliberate sharing: the note-check question lives in create_note and in using-scribe for two different moments, already pinned by test_verification_guidance_survives - test_the_ownership_guards_can_fail shows each guard turning red (rule 167) - the process topic now keys on get_process's "follow the returned body"; it had been passing on an unrelated "verbatim" in two other tools tests/test_instruction_surfaces_agree.py keeps only what ownership cannot enforce: the fold budget, rules-bind-names-preferences, and using-scribe's pointer to reporting-back. Retired: the every-session-start-surface ask and absence tests, the Systems and snippet owner pins (now registry topics), and the SessionStart-without-ask test (#2497's shape). The push has carried no rules since milestone 394, and requiring the ask beside every mention of it would force a copy. The module docstring records where each protection went. Co-Authored-By: Claude Opus 5 (1M context) --- tests/test_guidance_ownership.py | 285 ++++++++++++++--------- tests/test_instruction_surfaces_agree.py | 215 +++-------------- 2 files changed, 203 insertions(+), 297 deletions(-) diff --git a/tests/test_guidance_ownership.py b/tests/test_guidance_ownership.py index af5ad2a..7ae0e23 100644 --- a/tests/test_guidance_ownership.py +++ b/tests/test_guidance_ownership.py @@ -1,4 +1,4 @@ -"""Every piece of agent guidance has one owner — and none of it is lost on the way there. +"""Every piece of agent guidance has exactly one owner that states it. WHY THIS EXISTS (milestone 410, decision #4027) @@ -8,29 +8,36 @@ the server builds, the `using-scribe` skill, and tool docstrings. An earlier design (#2494) made that deliberate — insurance against any one surface failing silently — and the copies drifted apart instead (#2497, #4022). -Decision #4027 replaced the redundancy with ownership: the server orients, the -skills hold the depth, and each client adapter holds only its own timing and -conventions. This module is the registry that decision is enforced from. +Decision #4027 replaced the redundancy with ownership: the server orients with +a short index, the skills hold the depth, tool docstrings hold each tool's +contract, and each client adapter holds only its own timing and conventions. +This module is the registry that decision is enforced from. -WHAT IT PINS NOW, AND WHAT COMES LATER +WHAT IT PINS -Step 1 — the LOSS GUARD. Every topic below must still be stated on at least one -surface a session actually receives. Milestone 410's later steps delete copies; -this is what stops the last copy of a topic going with them. +1. OWNERS STATE THEIR TOPIC. Every topic's markers and its distinctive + statement appear on its owner — so moving or trimming text cannot drop a + topic without failing here. +2. NOWHERE ELSE STATES IT IN FULL. The statement — a phrase distinctive to + the owner's full wording — must not appear on any other session surface + (the index, the adapter's static text and commands, the live context, the + other skills). A topic legitimately stated in two places at two different + moments declares that in `shared_with`, with the reason beside it. +3. THE INDEX NAMES THE SESSION-START REFLEXES. `_INSTRUCTIONS` is the one + surface every MCP client receives, so each reflex it indexes keeps its + `index` markers there — a one-line pointer, not a copy. -Step 6 will add the OWNERSHIP guard: a topic's full statement on its `owner` -and nowhere else. The `owner` column is recorded here from the start so the -registry is written once, but nothing asserts it yet — during the moves a -topic is legitimately in several places at once. +WHAT IT CANNOT SEE -MARKERS ARE PHRASES, NOT WORDS +A copy reworded so it no longer contains the statement phrase passes. The +guard catches the ordinary way duplication happens — pasting a paragraph +into a second surface — not a determined paraphrase. Tool docstrings are not +scanned for copies: a tool's contract may elaborate the reflex that calls it. -A topic is "stated" when ALL of its markers appear on one surface — so a -topic's markers must travel together, and a lone common word never counts -(BINDING_CLAIMS in test_instruction_surfaces_agree explains the false alarm a -bare word raises). Tool names are the preferred marker: they change only when -the tool does. Reword a topic deliberately and update its markers in the same -commit; the failure message names which marker went missing where. +MARKERS AND STATEMENTS ARE PHRASES, NOT WORDS + +Reword a topic deliberately and update its markers/statement in the same +commit; each failure message names the topic, the surface and the phrase. """ from __future__ import annotations @@ -42,7 +49,7 @@ ROOT = pathlib.Path(__file__).resolve().parents[1] def _norm(text: str) -> str: - # Whitespace-flattened and lowercased: prose is hard-wrapped, so a marker + # Whitespace-flattened and lowercased: prose is hard-wrapped, so a phrase # can straddle a line break without the guidance having changed. return " ".join(text.split()).lower() @@ -62,7 +69,7 @@ def _live_session_context_source() -> str: def delivered_surfaces() -> dict[str, str]: """Every surface a session receives as guidance, by label. - The one definition of "delivered" for this module and its successors: + The one definition of "delivered" for this module: - `instructions` — the MCP server's `_INSTRUCTIONS` (every MCP client) - `docstrings` — the MCP tool modules (tool descriptions, every client) - `skill:` — each bundled Agent Skill @@ -85,99 +92,155 @@ def delivered_surfaces() -> dict[str, str]: return {label: _norm(text) for label, text in surfaces.items()} +# Not scanned for copies (see the module docstring). +NOT_COPY_SCANNED = frozenset({"docstrings"}) + + class Topic(NamedTuple): key: str - owner: str # a label from delivered_surfaces(); asserted from step 6 - markers: tuple[str, ...] + owner: str # a label from delivered_surfaces() + markers: tuple[str, ...] # what the topic is about; all on the owner + statement: str # distinctive to the owner's full wording + index: tuple[str, ...] = () # required in _INSTRUCTIONS, if it indexes this + shared_with: tuple[str, ...] = () # other surfaces allowed the statement, with a reason -# The ownership map from milestone 410's body, one row per topic, plus the -# topics once guarded as "displaced from _INSTRUCTIONS" (#2562), folded in so -# there is one list. Retired topics (the surface-precedence tiebreaker) are -# absent on purpose: nothing has to keep saying them. +U = "skill:using-scribe" + TOPICS: tuple[Topic, ...] = ( # ── the working reflexes — owned by the using-scribe skill ── - Topic("scribe is the system of record; keep one copy", "skill:using-scribe", ("one copy",)), - Topic("orient: enter the project, check repo bindings", "skill:using-scribe", - ("enter_project", "list_repo_bindings")), - Topic("rules are retrieved; ask before a consequential act", "skill:using-scribe", - ('content_type="rule"', "nothing matched")), - Topic("rules bind, preferences guide and are kept current", "skill:using-scribe", - ("preference", "update_preference")), - Topic("recall before acting", "skill:using-scribe", ("recall before acting",)), - Topic("stay inside the active project's scope", "skill:using-scribe", - ("stay inside the active project", "cross-project")), - Topic("record as you go; honest status; fixes are issues", "skill:using-scribe", - ("add_task_log", "in_progress", 'kind="issue"')), - Topic("an id exists only once a create returns it", "skill:using-scribe", - ("exists only once a create", "{{ref:")), - Topic("tag records to systems as you write", "skill:using-scribe", ("system_ids", "create_system")), - Topic("answer the systems_hint at the moment of work", "skill:using-scribe", ("systems_hint",)), - Topic("a retrieved rule outranks a default habit", "skill:using-scribe", ("outranks a default habit",)), - Topic("log on completion and on a problem", "skill:using-scribe", ("hit or discover a problem",)), - Topic("the project's design system binds ui", "skill:using-scribe", ("resolve_design_system",)), - Topic("name the record, never just its number", "skill:using-scribe", ("name the record",)), - Topic("project inception is a decision", "skill:using-scribe", ("decide_project_inception",)), - Topic("where a new rule goes, and its trigger", "skill:using-scribe", - ("create_project_rule", "when_to_apply")), - Topic("a rule vs the other entities", "skill:using-scribe", ("standing instruction",)), - Topic("reference notes update in place; dev-logs don't", "skill:using-scribe", ("reference note",)), + Topic("scribe is the system of record; keep one copy", U, ("one copy",), + "let any existing local memory shrink", index=("one copy",)), + Topic("orient: enter the project, check repo bindings", U, ("enter_project", "list_repo_bindings"), + "returns the project plus the rules bound to the areas it works in", + index=("enter_project",)), + Topic("rules are retrieved; ask before a consequential act", U, ('content_type="rule"', "nothing matched"), + "an empty session is not evidence of an empty rulebook", + index=('content_type="rule"', "nothing matched")), + Topic("rules bind, preferences guide and are kept current", U, ("preference", "update_preference"), + "a preference is the one record you keep current yourself", index=("preferences guide",)), + Topic("recall before acting", U, ("recall before acting",), "for related prior work", index=("recall",)), + Topic("stay inside the active project's scope", U, ("stay inside the active project", "cross-project"), + "stay inside the active project's scope", index=("project_id",)), + Topic("record as you go; honest status; fixes are issues", U, ("add_task_log", "in_progress", 'kind="issue"'), + "fixes are issues, not work-logs", index=("add_task_log", 'kind="issue"')), + Topic("an id exists only once a create returns it", U, ("exists only once a create", "{{ref:"), + "exists only once a create call returns it", index=("create_records", "{{ref:n}}")), + Topic("tag records to systems as you write", U, ("system_ids", "create_system"), + "would someone investigating that subsystem want this record", index=("system_ids",)), + Topic("answer the systems_hint at the moment of work", U, ("systems_hint",), + "treat it as the tagging question asked at the moment of work"), + Topic("a retrieved rule outranks a default habit", U, ("outranks a default habit",), + "a retrieved rule outranks a default habit"), + Topic("log on completion and on a problem", U, ("hit or discover a problem",), "hit or discover a problem"), + Topic("the project's design system binds ui", U, ("resolve_design_system",), + "building ui: the project's design system binds", index=("resolve_design_system",)), + Topic("name the record, never just its number", U, ("name the record",), + "a bare id reads as complete to you and as homework to them"), + Topic("project inception is a decision", U, ("decide_project_inception",), + "starting a project: decide what it inherits", index=("decide_project_inception",)), + Topic("where a new rule goes, and its trigger", U, ("create_project_rule", "when_to_apply"), + "whichever home it gets"), + Topic("a rule vs the other entities", U, ("standing instruction",), "first ask whether it's a rule at all"), + Topic("reference notes update in place; dev-logs don't", U, ("reference note",), + "state updates in place; chronicles don't"), # ── process arcs — owned by their skills ── - Topic("plan in a milestone, steps created together", "skill:writing-plans", ("start_planning", "{{ref:")), + Topic("plan in a milestone, steps created together", "skill:writing-plans", ("start_planning", "{{ref:"), + "a milestone earns its place when the work has an arc", index=("start_planning",)), Topic("reuse recorded shapes; record at first build", "skill:reusing-code", - ("create_snippet", "when_to_use", "first build", "second copy")), - Topic("report back where the work stands", "skill:reporting-back", ("reporting-back", "placement")), + ("create_snippet", "when_to_use", "first build", "second copy"), + "prior art offered beside a write is not noise", index=("create_snippet",)), + Topic("report back where the work stands", "skill:reporting-back", ("reporting-back", "placement"), + "take the placement from the record", index=("placement",)), # ── per-tool contracts and in-band behaviour — owned by the server ── - Topic("closing a task cues the report", "docstrings", ("report_back",)), - Topic("a note that asserts a fact carries its check", "docstrings", ("verify_with", "expires_when")), - Topic("supersession demotes, never hides", "docstrings", ("supersedes",)), - Topic("deletes are recoverable from the trash", "docstrings", ("deleted_batch_id",)), - Topic("creates are duplicate-gated", "docstrings", ("near-duplicate",)), - Topic("shared records are another user's suggestion", "docstrings", ("shared: true",)), - Topic("stored processes are followed verbatim", "docstrings", ("stored processes", "verbatim")), - Topic("a project is never guessed", "docstrings", ("never guessing a project",)), - Topic("an unbound repo gets a bind hint", "live", ("bind_repo",)), + Topic("closing a task cues the report", "docstrings", ("report_back",), "reporting this to the operator?"), + Topic("a note that asserts a fact carries its check", "docstrings", ("verify_with", "expires_when"), + "could this note become false without anyone editing it", + # Stated at two different moments on purpose: the tool contract is + # read when the field is about to be filled, the skill while deciding + # what to write at all (test_verification_guidance_survives pins both). + shared_with=(U,)), + Topic("supersession demotes, never hides", "docstrings", ("supersedes",), + "it simply stops competing with this one"), + Topic("deletes are recoverable from the trash", "docstrings", ("deleted_batch_id",), "deleted_batch_id"), + Topic("creates are duplicate-gated", "docstrings", ("near-duplicate",), "bypass the near-duplicate gate", + index=("duplicate-gated",)), + Topic("shared records are another user's suggestion", "docstrings", ("shared: true",), + "belongs to another user", index=("shared:true",)), + Topic("stored processes are followed as written", "docstrings", ("get_process",), "follow the returned body"), + Topic("a project is never guessed", "docstrings", ("never guessing a project",), "never guessing a project"), + Topic("an unbound repo gets a bind hint", "live", ("bind_repo",), "isn't mapped to a scribe project"), # ── the Claude Code adapter's own conventions ── - Topic("compact at clean seams", "static", ("/compact",)), - Topic("stored processes sync into local skills", "commands", ("scribe-proc-",)), - Topic("say so when scribe's tools are unavailable", "static", ("tools are unavailable",)), + Topic("compact at clean seams", "static", ("/compact",), "compact at clean seams"), + Topic("stored processes sync into local skills", "commands", ("scribe-proc-",), "regenerate the local skill stubs"), + Topic("say so when scribe's tools are unavailable", "static", ("tools are unavailable",), + "rather than silently falling back to local notes"), ) -def missing_topics(topics, surfaces: dict[str, str]) -> list[str]: - """Topics no single surface states in full — with the nearest miss named. - - Pure, so the guard's ability to fail is itself testable. - """ - missing = [] - for topic in topics: - markers = [m.lower() for m in topic.markers] - if any(all(m in text for m in markers) for text in surfaces.values()): - continue - partial = { - label: [m for m in markers if m not in text] - for label, text in surfaces.items() - if any(m in text for m in markers) - } - missing.append(f"{topic.key!r} — markers {topic.markers}; nearest: {partial or 'nowhere'}") - return missing +def owner_gaps(topics, surfaces: dict[str, str]) -> list[str]: + """Topics whose owner no longer carries every marker and the statement.""" + gaps = [] + for t in topics: + text = surfaces.get(t.owner, "") + absent = [p for p in (*t.markers, t.statement) if p.lower() not in text] + if absent: + elsewhere = [label for label, other in surfaces.items() if t.statement.lower() in other] + gaps.append(f"{t.key!r} on {t.owner}: missing {absent}; statement found on {elsewhere or 'nothing'}") + return gaps -def test_no_guidance_topic_has_fallen_off_every_surface(): - missing = missing_topics(TOPICS, delivered_surfaces()) - assert not missing, ( - "these guidance topics are no longer stated in full on ANY delivered " - "surface:\n " + "\n ".join(missing) + "\nMilestone 410 moves guidance " - "to one owner per topic (decision #4027); a move that deletes a copy " - "must leave the topic stated on its owner. If the topic was reworded on " - "purpose, update its markers here in the same commit." +def copies(topics, surfaces: dict[str, str]) -> list[str]: + """Topics whose full statement also appears on a surface that doesn't own it.""" + found = [] + for t in topics: + allowed = {t.owner, *t.shared_with} + extra = [label for label, text in surfaces.items() + if label not in NOT_COPY_SCANNED and label not in allowed and t.statement.lower() in text] + if extra: + found.append(f"{t.key!r} (owner {t.owner}) is also stated on {extra}") + return found + + +def index_gaps(topics, surfaces: dict[str, str]) -> list[str]: + text = surfaces.get("instructions", "") + return [f"{t.key!r}: {[m for m in t.index if m.lower() not in text]}" + for t in topics if any(m.lower() not in text for m in t.index)] + + +def test_every_topic_is_stated_by_its_owner(): + gaps = owner_gaps(TOPICS, delivered_surfaces()) + assert not gaps, ( + "these topics are no longer stated in full by their owner:\n " + + "\n ".join(gaps) + "\nEvery guidance topic has one owner (decision " + "#4027). If the text moved on purpose, move the topic's owner here in the " + "same commit; if it was reworded, update its markers and statement." ) -def test_every_owner_is_a_surface_that_exists(): +def test_no_topic_is_stated_in_full_off_its_owner(): + found = copies(TOPICS, delivered_surfaces()) + assert not found, ( + "guidance stated in full on a surface that doesn't own it:\n " + + "\n ".join(found) + "\nOne owner per topic (decision #4027): replace " + "the copy with a one-line pointer to the owner. If both places genuinely " + "need it at different moments, declare it in `shared_with` with the reason." + ) + + +def test_the_index_names_each_reflex_it_points_at(): + gaps = index_gaps(TOPICS, delivered_surfaces()) + assert not gaps, ( + f"_INSTRUCTIONS no longer carries the index line for: {gaps}. It is the " + f"one guidance surface every MCP client receives; keep a one-line pointer " + f"per session-start reflex (see the comment above _INSTRUCTIONS)." + ) + + +def test_every_owner_and_sharer_is_a_surface_that_exists(): labels = set(delivered_surfaces()) - unknown = [(t.key, t.owner) for t in TOPICS if t.owner not in labels] - assert not unknown, f"owners that name no delivered surface: {unknown}" + unknown = [(t.key, s) for t in TOPICS for s in (t.owner, *t.shared_with) if s not in labels] + assert not unknown, f"owners or sharers that name no delivered surface: {unknown}" def test_topic_keys_are_unique(): @@ -185,20 +248,24 @@ def test_topic_keys_are_unique(): assert len(keys) == len(set(keys)) -def test_the_loss_guard_can_fail(): - """Rule 167: a guard that cannot fail protects nothing. - - A topic whose markers are split across two surfaces is NOT stated — the - phrases have to travel together — and one whose marker is nowhere is - reported with 'nowhere'. - """ - surfaces = {"a": "enter_project here", "b": "list_repo_bindings there"} - split = Topic("split", "a", ("enter_project", "list_repo_bindings")) - absent = Topic("absent", "a", ("no such phrase",)) - whole = Topic("whole", "a", ("enter_project",)) - reported = missing_topics((split, absent, whole), surfaces) - assert len(reported) == 2 - assert reported[0].startswith("'split'") and "nowhere" in reported[1] +def test_the_ownership_guards_can_fail(): + """Rule 167: each guard is shown turning red once.""" + surfaces = { + "instructions": "use the widget", + "docstrings": "the widget owner statement lives here", + "skill:a": "widget owner statement lives here, and widget tool", + "skill:b": "a pasted copy: widget owner statement lives here", + "static": "", + } + topic = Topic("widget", "skill:a", ("widget tool",), "widget owner statement lives here", + index=("use the widget", "not in the index")) + moved = topic._replace(owner="static") + assert owner_gaps((topic,), surfaces) == [] + assert owner_gaps((moved,), surfaces) and "missing" in owner_gaps((moved,), surfaces)[0] + # skill:b pasted it; docstrings are not scanned for copies. + assert copies((topic,), surfaces) == ["'widget' (owner skill:a) is also stated on ['skill:b']"] + assert copies((topic._replace(shared_with=("skill:b",)),), surfaces) == [] + assert index_gaps((topic,), surfaces) == ["'widget': ['not in the index']"] # ── The skills are client-neutral (milestone 410 step 2) ──────────────── diff --git a/tests/test_instruction_surfaces_agree.py b/tests/test_instruction_surfaces_agree.py index cc8abbc..fd57988 100644 --- a/tests/test_instruction_surfaces_agree.py +++ b/tests/test_instruction_surfaces_agree.py @@ -1,39 +1,35 @@ -"""The instruction surfaces must agree on how a rule reaches a session. +"""What the instruction surfaces must say that one owner per topic cannot enforce. -WHY THIS EXISTS +WHY THIS FILE IS SMALLER THAN IT WAS -Rule #119 makes the instruction surfaces the SPECIFICATION for product -behaviour — there is no other place the "load the operator's rules" obligation -is written down, and no code path enforces it. So a surface that states it -differently isn't a documentation slip; it is the product behaving differently. +Until milestone 410 this file pinned REDUNDANCY: every session-start surface — +the MCP `_INSTRUCTIONS`, the plugin's static context, the `using-scribe` skill — +had to restate how a rule reaches a session, what an empty session means, the +Systems reflex and the snippet-recording triggers. That was the design #2494 +chose after #2497 (two surfaces disagreeing about who loads the rules) and +#2198 (every hook silently inert): say it everywhere, so no single failure +loses it. -That happened (#2497). `_INSTRUCTIONS` said the SessionStart hook "is the -bridge" for getting rules into a session, while the `using-scribe` skill said to -pull them yourself and treat any push as a bonus. An agent weighting the first -would reasonably skip the pull. +Decision #4027 replaced that with ownership, and tests/test_guidance_ownership.py +now enforces it: every topic stated in full by exactly one owner, with a +one-line pointer in the server's index. The tests that required every surface +to repeat itself were retired there, because they enforced the very duplication +that drifted (#4022). What they protected is still protected: + - "state how to ask for a rule" / "say an empty session is not an empty + rulebook" → the rules topic: owned by using-scribe, indexed in + `_INSTRUCTIONS` with both phrases; + - the Systems reflex and the snippet-recording triggers → their topics' + markers, required on their owners; + - "never name the SessionStart push without stating the ask" (#2497's exact + shape) → retired outright. Since milestone 394 the push carries no rules, + so naming it can no longer imply rules were handled, and requiring every + surface that mentions it to restate the ask would force a copy. -#2198 is the case where that is wrong: every plugin hook was silently inert for -an extended period, and nothing announced it. An agent trusting the push would -have run with no binding rules and no signal — while those rules govern branch, -commit, push and other hard-to-reverse actions. +WHAT STAYS HERE -The asymmetry is the whole argument, and it is what these tests pin: asking -when a rule had already arrived costs one redundant call; not asking when -nothing arrived costs the operator's rules entirely. - -MILESTONE 394 SHARPENED IT RATHER THAN RETIRING IT. There is no longer a -resident set to pull, so "no rule in front of me" went from a rare and -suspicious state to the ordinary state of most turns. The instruction that -used to be supplementary — go and ask — is now the only route a rule has, and -the surfaces must additionally say what an EMPTY session means, or a session -reads silence as permission on nearly every turn. - -WHAT THIS DOES NOT DO - -It cannot tell whether two surfaces contradict each other in prose generally — -that needs a reader. It pins the instructions whose absence is known to be -load-bearing, and the specific shape the #2497 defect took: naming the push -without also stating how to ask. +Properties of a CLAIM rather than of who owns a topic: the index fits the +fold; a surface that says rules bind also names what does not; using-scribe +still sends a session to the reporting-back skill. """ from __future__ import annotations @@ -41,50 +37,6 @@ import pathlib ROOT = pathlib.Path(__file__).resolve().parents[1] -# THE PULL IS NOW THE ASK (milestone 394). This was `list_always_on_rules`, -# the call that fetched the resident set. There is no resident set and no such -# call: a rule reaches a session by retrieval, and the only thing a session can -# DO about a rule it has not been handed is go looking for one. -# -# So the two halves this file used to pin separately — "pull the resident set" -# and "and retrieve the conditional ones too" — have collapsed into one -# instruction, and it is the load-bearing one rather than the supplementary -# one it used to be. -ASK = 'content_type="rule"' - -# A surface must also say what an EMPTY session means, which is the half that -# is newly dangerous. Under residency, "no rule in front of me" was rare and -# suspicious. Under retrieval it is the ordinary state of most turns, so a -# session that reads it as "there is no rule" is wrong on nearly every turn -# rather than occasionally — the #3720 defect at session scale. -# -# Claim phrases, not a single word, for the reason BINDING_CLAIMS gives below: -# a bare "matched" or "silence" appears in prose that is not making this claim -# at all. A surface passes by asserting the distinction however it words it. -ABSENCE_CLAIMS = ( - "nothing matched", - "is not the same as \"there is no rule", - "never \"there is no rule", - "silence is not absence", - "not evidence there is none", -) - -# The surfaces that STATE the rules reflex. Hand-written because "who owns -# this?" is an editorial fact, not a derivable one — but each entry is asserted -# to EXIST, so a move or rename fails loudly here instead of quietly dropping -# that surface from the check. -# -# Since milestone 410 (decision #4027) that is the owner and the index, not -# every surface a session loads: `using-scribe` states the reflex in full and -# the server's `_INSTRUCTIONS` gives it one line for every MCP client. The -# Claude Code adapter's static context used to be a third copy; it now points -# at the skill instead, and tests/test_guidance_ownership.py keeps the topic -# from falling off. -SESSION_START_SURFACES = ( - ROOT / "src" / "scribe" / "mcp" / "server.py", - ROOT / "plugin" / "skills" / "using-scribe" / "SKILL.md", -) - def _all_surfaces() -> list[tuple[str, str]]: """(label, text) for every file a SESSION loads as instructions. @@ -105,54 +57,6 @@ def _all_surfaces() -> list[tuple[str, str]]: return found -def test_every_session_start_surface_states_the_ask(): - """Retrieval is the only delivery, so asking is the only recourse.""" - missing = [] - for path in SESSION_START_SURFACES: - assert path.exists(), ( - f"{path.relative_to(ROOT)} is gone — it was one of the surfaces " - f"carrying the rules instruction. If it moved, update " - f"SESSION_START_SURFACES; if it was retired, check the instruction " - f"still lives somewhere a fresh session reads." - ) - if ASK not in path.read_text(): - missing.append(str(path.relative_to(ROOT))) - assert not missing, ( - f"these surfaces never tell the agent how to ask for a rule " - f"({ASK}): {missing}. Nothing is pushed and nothing is resident, so a " - f"surface that omits this leaves a session with no way to reach a rule " - f"it was not handed — bound by nothing (#2198, #2497, milestone 394)." - ) - - -def test_every_session_start_surface_says_an_empty_session_is_not_an_empty_rulebook(): - """The half that got dangerous when residency went away. - - Under the old model a session opened holding every applicable rule, so - "nothing is in front of me" was a rare state and a suspicious one. Under - retrieval it is the NORMAL state of most turns. A surface that describes - where rules come from, without also saying what their absence means, leaves - a session reading silence as permission — on nearly every turn rather than - occasionally. - - That is #3720's defect ("absence reads as non-existence") moved from a - readout to the session itself, and this milestone is what makes every - session start in the absent state. - """ - missing = [] - for path in SESSION_START_SURFACES: - text = path.read_text().lower() - if not any(c.lower() in text for c in ABSENCE_CLAIMS): - missing.append(str(path.relative_to(ROOT))) - assert not missing, ( - f"these surfaces say how a rule arrives but never what it means when " - f"none does: {missing}. 'No rule arrived' means 'nothing matched', " - f"never 'there is no rule' — and only one of those has been checked. " - f"Say it however you like; one of {ABSENCE_CLAIMS} is what this looks " - f"for." - ) - - def _instructions_text() -> str: """The _INSTRUCTIONS literal from server.py, as the client would see it.""" import re @@ -180,71 +84,6 @@ def test_instructions_fit_the_fold(): ) -def test_the_systems_reflex_is_stated_by_its_owner(): - """Write-time tagging guidance must be stated as a reflex, not only per tool. - - #2562's behavioral finding: with the guidance only in tool descriptions, - sessions filed records untagged. The tag-as-you-write reflex was pinned on - the static context then; since milestone 410 its owner is using-scribe - (decision #4027), with the in-band `systems_hint` as the half that fires on - its own. - """ - owner = (ROOT / "plugin" / "skills" / "using-scribe" / "SKILL.md").read_text() - for needle in ("system_ids", "create_system", "systems_hint"): - assert needle in owner, ( - f"plugin/skills/using-scribe/SKILL.md no longer mentions " - f"{needle} — the Systems tagging reflex must be stated by its " - f"owner, not only in tool descriptions (#2562)." - ) - - -def test_the_snippet_recording_triggers_are_stated_by_their_owner(): - """The pattern-library recording model must be stated, by name. - - #2664's behavioral finding: recording guidance as a trailing clause of the - reuse bullet converted zero times outside snippet-minded sessions. The - 2026-08-16 ruling (decision #2686) then replaced the reactive model - entirely: every shape is recorded at FIRST build — no "will it recur?" - judgment — and second-copy consolidation is only the backstop. All three - elements must stay stated: the tool, the first-build trigger, and the - backstop. Since milestone 410 the owner is the reusing-code skill. - """ - owner = " ".join((ROOT / "plugin" / "skills" / "reusing-code" / "SKILL.md").read_text().split()) - for needle in ("create_snippet", "first build", "second copy"): - assert needle in owner, ( - f"plugin/skills/reusing-code/SKILL.md no longer states the " - f"snippet-recording model ({needle!r}) — record-every-shape-at-" - f"first-build with second-copy consolidation as the backstop must " - f"be stated by its owner (#2664, decision #2686)." - ) - - -# The "displaced from _INSTRUCTIONS" topics (#2562) used to be listed here with -# their own delivered-surface check. They are folded into the one topic registry -# in tests/test_guidance_ownership.py (milestone 410), which covers every topic -# of the ownership map and defines "delivered surface" once. - - -def test_no_surface_names_the_push_without_stating_the_ask(): - """The exact shape #2497 took. - - Mentioning the SessionStart hook is fine and often useful. Mentioning it - *instead of* the pull is the defect: it reads as "this is handled", and the - surface that says so is the one an agent has least reason to doubt. - """ - offenders = [ - label for label, text in _all_surfaces() - if "SessionStart" in text and ASK not in text - ] - assert not offenders, ( - f"these surfaces describe the SessionStart push but never state how to " - f"ask: {offenders}. The push is a delivery optimisation, not the " - f"bridge — it can be absent without saying so, and since milestone 394 " - f"it carries no rules at all. Name it if it helps, but say how to ask " - f"({ASK}) regardless." - ) - - # ── force: a surface that says rules bind must say what does not ──────── # # Added with the preference kind (milestone 399). Before it, "rules bind" was