feat(moments): the moment catalog rules will mount on, readable in-session (milestone 458 step 1, #4919)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 15s
CI & Build / TypeScript typecheck (push) Successful in 55s
CI & Build / integration (push) Successful in 58s
CI & Build / Python tests (push) Successful in 1m53s
CI & Build / Build & push image (push) Successful in 32s

Fourteen generic moments of work (session.start, work.start … reply.ask)
plus the skill.<name> family, each with what it means and the kinds of
action that reach it, written for any kind of work rather than software
alone. The catalog is code because every install needs the same mount
points; which actions reach a moment is per-install data (step 2).

require_moment refuses an unknown name with the catalog listed, so a
typo cannot become a mount that never fires. list_moments (read-only)
and GET /api/retrieval/moments hand out the same catalog.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-05 10:50:45 -04:00
co-authored by Claude Opus 5.5
parent 0720ab6dcf
commit 2ff7f2f34f
7 changed files with 333 additions and 1 deletions
+119
View File
@@ -0,0 +1,119 @@
"""The moment catalog — the vocabulary rules mount on (milestone 458 step 1).
What this pins is what a mount depends on: every name is well-formed and
resolvable, an unknown name is refused rather than stored, the catalog speaks
for any kind of work rather than only software, and both doors — the MCP tool
and the REST read — hand out the same catalog.
"""
import re
import pytest
from scribe.services import moments
from tests.helpers import FakeMCP
_NAME = re.compile(r"^[a-z]+\.[a-z]+$")
def test_the_catalog_is_not_empty():
"""The sweeps below are vacuous over an empty catalog (rule 167)."""
assert len(moments.MOMENTS) >= 10
def test_every_moment_is_keyed_by_its_own_well_formed_name():
for key, m in moments.MOMENTS.items():
assert key == m.name, f"{key!r} is filed under a name it does not carry"
assert _NAME.match(key), f"{key!r} is not <area>.<verb>"
def test_every_moment_says_what_it_means_and_how_it_is_reached():
for m in moments.MOMENTS.values():
assert m.means.strip() and m.reached_by.strip(), m.name
assert "\n" not in m.means, f"{m.name}: `means` is one line"
def test_the_skill_family_is_not_a_catalog_entry():
"""A family is a shape, not a moment — no rule mounts on `skill.<name>`
literally, and a catalog key starting with the prefix would shadow it."""
assert not any(k.startswith(moments.SKILL_PREFIX) for k in moments.MOMENTS)
# Software-only vocabulary, the same guard the completion query carries. The
# moments are named for any work a person drives through an agent; the actions
# particular to one kind of work belong in the mappings, not in the meaning.
_DEV_ONLY = (r"\bCI\b", r"\bcommit", r"\bpull request", r"\bcode\b", r"\btest",
r"\bgit\b", r"\brepo(s|sitor\w*)?\b", r"\bbranch", r"\bcompil", r"\bbuild\b")
@pytest.mark.parametrize("field", ["means", "reached_by"])
def test_the_catalog_assumes_no_particular_domain(field):
found = {
m.name: hits
for m in [*moments.MOMENTS.values(), moments.SKILL_FAMILY]
if (hits := [w for w in _DEV_ONLY
if re.search(w, getattr(m, field), re.IGNORECASE)])
}
assert not found, f"software-only vocabulary in `{field}`: {found}"
def test_the_guard_can_fail():
assert re.search(_DEV_ONLY[1], "after the commit lands", re.IGNORECASE)
@pytest.mark.parametrize("name", list(moments.MOMENTS))
def test_every_catalog_moment_is_mountable(name):
assert moments.require_moment(name) == name
@pytest.mark.parametrize("raw,clean", [
(" Work.Finish ", "work.finish"),
("skill.verification", "skill.verification"),
("skill.scribe:writing-plans", "skill.scribe:writing-plans"),
("skill.scribe-proc-release_notes", "skill.scribe-proc-release_notes"),
])
def test_a_name_is_normalised_before_it_is_judged(raw, clean):
assert moments.require_moment(raw) == clean
@pytest.mark.parametrize("bad", [
"work.finished", "finish", "", "skill.", "skill.two words", "work.",
])
def test_an_unknown_moment_is_refused_with_the_catalog(bad):
"""A typo stored as a mount never fires — the silent failure moments end."""
with pytest.raises(ValueError) as exc:
moments.require_moment(bad)
assert "work.finish" in str(exc.value)
assert moments.SKILL_FAMILY.name in str(exc.value)
def test_the_catalog_payload_carries_every_moment_and_the_family():
data = moments.catalog()
assert [m["name"] for m in data["moments"]] == list(moments.MOMENTS)
assert data["total"] == len(moments.MOMENTS)
assert data["families"][0]["prefix"] == moments.SKILL_PREFIX
async def test_the_tool_returns_the_service_catalog():
from scribe.mcp.tools import moments as tool
assert await tool.list_moments() == moments.catalog()
def test_the_tool_is_registered_and_read_only():
from scribe.mcp.server import _READ_ONLY_TOOLS
from scribe.mcp.tools import moments as tool
mcp = FakeMCP()
tool.register(mcp)
assert mcp.names == ["list_moments"]
assert "list_moments" in _READ_ONLY_TOOLS
def test_both_doors_read_one_catalog():
"""Rule 33 parity: the tool and the route call the same service, so the
session and the Settings view cannot name different moments."""
from scribe.mcp.tools import moments as tool
from scribe.routes import retrieval as routes
assert tool.moments_svc is moments
assert routes.moments_svc is moments
+1
View File
@@ -42,6 +42,7 @@ def test_every_endpoint_is_reachable_on_the_app():
"/api/retrieval/surfaces",
"/api/retrieval/surfaces/<surface>",
"/api/retrieval/tuning-history",
"/api/retrieval/moments",
}