@@ -107,6 +107,17 @@ async def create_note(
|
||||
) -> dict:
|
||||
"""Create a new note in Scribe.
|
||||
|
||||
WHAT ELSE COULD HOLD THIS? A note is the right home when the answer is
|
||||
"nothing": it records what you know, nobody owes anything on it, and
|
||||
nothing enforces it. Otherwise —
|
||||
- someone has to DO something -> create_task. A note titled "we should…"
|
||||
is a task nobody will ever see again.
|
||||
- future sessions must OBEY it -> create_rule. The test is whether
|
||||
ignoring it would be a mistake, not merely uninformed.
|
||||
- reusable code with a place in a repo -> create_snippet. The location is
|
||||
what lets it be found from the file someone is about to edit.
|
||||
- a procedure followed start to finish -> create_process.
|
||||
|
||||
Args:
|
||||
title: Note title (required).
|
||||
body: Markdown content. Supports [[wikilinks]] to other notes by title.
|
||||
|
||||
@@ -54,6 +54,12 @@ async def create_process(
|
||||
) -> dict:
|
||||
"""Create a stored process (a reusable saved prompt).
|
||||
|
||||
FOLLOWED, OR READ? A process is invoked deliberately and worked through
|
||||
start to finish. If it should apply whether or not anyone invokes it, it
|
||||
is a rule (create_rule) — that is the whole difference between a procedure
|
||||
and a standing instruction. If it is knowledge to consult rather than
|
||||
steps to execute, it is a note (create_note).
|
||||
|
||||
AUTHOR IT AS A SHAPE, NOT A SCRIPT. A process's value is the accumulated
|
||||
procedure — the steps, the taxonomy, the quality bar, the failure modes
|
||||
worth guarding. It must not force anything the invoking conversation
|
||||
|
||||
@@ -407,6 +407,13 @@ async def create_project_rule(
|
||||
the rule is returned in get_project's applicable_rules (under
|
||||
project_rules) and in list_rules(project_id=...).
|
||||
|
||||
Check first whether a rule is the right shape at all — create_rule's
|
||||
opening asks that question and it applies identically here. A visual
|
||||
standard is a design system; a procedure is a process (create_process);
|
||||
reusable code is a snippet (create_snippet). Each of those is structure a
|
||||
tool can resolve, render and check, where a rule is only prose someone
|
||||
has to remember and apply.
|
||||
|
||||
ONE RULE = ONE THING YOU COULD VIOLATE — see create_rule. A rule that
|
||||
STRICTENS or REPLACES an inherited one is not a fresh rule: write it, then
|
||||
relate_rules(kind="overrides") to the rule it supersedes, so the pair stays
|
||||
|
||||
@@ -115,6 +115,13 @@ async def create_snippet(
|
||||
"""Record a shape in the project's pattern library, so every later
|
||||
instance starts from it instead of re-deriving it.
|
||||
|
||||
IS THE SHAPE THE POINT, OR THE ADVICE? A snippet is code with a LOCATION —
|
||||
that is what lets it surface from the file someone is about to edit. If
|
||||
what wants recording is a standing instruction about how to work, it is a
|
||||
rule (create_rule); a procedure followed start to finish is a process
|
||||
(create_process); what you LEARNED rather than what to copy is a note
|
||||
(create_note).
|
||||
|
||||
Reach for this the FIRST time any shape is built — a component, a control,
|
||||
a route handler, a service class, a helper, a test scaffold — not only
|
||||
when something is judged "reusable": the builder of the first instance
|
||||
|
||||
@@ -135,6 +135,13 @@ async def create_task(
|
||||
) -> dict:
|
||||
"""Create a new task in Scribe.
|
||||
|
||||
IS ANYTHING ACTUALLY OWED? A task carries a status and someone is on the
|
||||
hook to move it. If nothing is owed — you are recording what you learned,
|
||||
decided or observed — that is a note (create_note), and filing it here
|
||||
leaves a to-do nobody will ever close. If the work is an ARC of several
|
||||
steps toward one goal, start_planning makes the milestone that holds
|
||||
them; a task is one step, not the plan.
|
||||
|
||||
Args:
|
||||
title: Task title (required).
|
||||
body: Markdown description / notes for the task.
|
||||
@@ -323,7 +330,9 @@ async def start_planning(project_id: int, title: str) -> dict:
|
||||
Reach for this when the work has an ARC — several steps toward one goal,
|
||||
worth tracking as a unit. Work without one (a fix, a one-file change, a
|
||||
question answered) is a task, not a plan: create_task, drive its status, and
|
||||
record progress with add_task_log. A milestone holding a single step is
|
||||
record progress with add_task_log. A design or decision you are RECORDING
|
||||
rather than executing is a note (create_note) — a plan nobody is going to
|
||||
work through is a document filed in the place reserved for open work. A milestone holding a single step is
|
||||
ceremony, and it leaves the project with a plan that never meant anything.
|
||||
|
||||
Creates a MILESTONE that IS the plan: its `body` is seeded with a design
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
"""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
|
||||
|
||||
# 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",
|
||||
]
|
||||
|
||||
|
||||
def _doc(module: str, name: str) -> str:
|
||||
"""The docstring with its whitespace flattened.
|
||||
|
||||
Flattened because a docstring is hard-wrapped: "design system" spans a
|
||||
line break in at least one of these, and matching the raw text would
|
||||
report it absent. The first draft of this check did exactly that, and
|
||||
caught it on itself.
|
||||
"""
|
||||
import importlib
|
||||
|
||||
fn = getattr(importlib.import_module(module), name)
|
||||
assert fn.__doc__, f"{name} has no docstring at all"
|
||||
return re.sub(r"\s+", " ", fn.__doc__)
|
||||
|
||||
|
||||
@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."
|
||||
)
|
||||
Reference in New Issue
Block a user