"""Every create_* tool says what it is NOT for (#3123). WHY THIS EXISTS Scribe's record kinds are reached for interchangeably — a note written where a task was owed, a rule written where a snippet belonged — and the moment of choice is the only moment a correction is cheap. The tool docstring IS the agent-facing contract (rule 119 puts product guidance there and nowhere else), so a docstring that only documents parameters answers "how do I call this" while leaving "should I be calling this at all" unasked. The gap was lopsided before this: `create_rule` and `start_planning` both carried a real disambiguator, and `create_note` — far and away the highest-volume surface — carried none. Guidance sat in the rarest tool and was missing from the most common one. WHAT THIS PINS, AND WHAT IT DOES NOT It asserts STRUCTURE, never wording: each create surface must name at least two sibling surfaces, so a caller who reached for the wrong one is told where the right one is. Prose stays free to be rewritten — pinning phrasing would make every improvement a test failure, and a test that punishes editing is a test that gets deleted. It cannot tell whether the guidance is any GOOD. It only catches the regression that actually happens: a docstring rewritten down to its parameters, with the "is this even the right tool" paragraph quietly gone. """ import re import pytest from tests.helpers import tool_doc as _doc # The create surfaces and where they live. `start_planning` is here because # it is a create in everything but name — it is how a plan comes into being. _SURFACES = [ ("scribe.mcp.tools.notes", "create_note"), ("scribe.mcp.tools.tasks", "create_task"), ("scribe.mcp.tools.tasks", "start_planning"), ("scribe.mcp.tools.snippets", "create_snippet"), ("scribe.mcp.tools.processes", "create_process"), ("scribe.mcp.tools.rulebooks", "create_rule"), ("scribe.mcp.tools.rulebooks", "create_project_rule"), ] # What a caller could have wanted instead. A surface naming two of these has # pointed somewhere; naming none has left them where they were. _ALTERNATIVES = [ "create_note", "create_task", "create_rule", "create_project_rule", "create_snippet", "create_process", "start_planning", "design system", ] @pytest.mark.parametrize(("module", "name"), _SURFACES) def test_a_create_surface_names_at_least_two_alternatives(module, name): """Reaching for the wrong tool must still put the right one in view.""" doc = _doc(module, name) named = { alt for alt in _ALTERNATIVES if alt != name and re.search(re.escape(alt), doc, re.IGNORECASE) } assert len(named) >= 2, ( f"{name}'s docstring names {sorted(named) or 'no'} alternative " f"surface(s). A caller who reached for it by mistake gets no " f"correction. Say what belongs elsewhere and why — see create_rule " f"or create_note for the shape." ) @pytest.mark.parametrize(("module", "name"), _SURFACES) def test_a_create_surface_still_documents_its_arguments(module, name): """The guard above must not be satisfiable by deleting the parameter docs. Both halves matter and they pull in opposite directions: a docstring can be made to pass the disambiguator check by becoming an essay about the other tools, which would be a worse contract than the one being fixed. """ assert "Args:" in _doc(module, name), ( f"{name}'s docstring lost its Args: block — the parameter contract is " f"what a caller reads to make the call at all." )