rename(547): the server is grimoire — package, image, DB defaults, backups
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 56s
CI & Build / integration (push) Successful in 1m4s
CI & Build / Python tests (push) Successful in 1m55s
CI & Build / Build & push image (push) Successful in 44s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 56s
CI & Build / integration (push) Successful in 1m4s
CI & Build / Python tests (push) Successful in 1m55s
CI & Build / Build & push image (push) Successful in 44s
Step 2 of the Scribe -> Grimoire rename. - src/scribe -> src/grimoire, and every import, patch target, module path, hypercorn entry point and `python -m grimoire.restore` with it; pyproject, uv.lock and the frontend package are named grimoire. - Image git.fabledsword.com/bvandeusen/fabledgrimoire; CI's Postgres service runs as grimoire / grimoire_test. - Compose files default the DB user, database and (prod) volume to grimoire, each overridable from .env, so an install created as Scribe keeps its data by setting POSTGRES_USER, POSTGRES_DB and PGDATA_VOLUME. - Backups are written as grimoire-<ts>.tar with format grimoire-backup-tar. Archives named scribe-* with the scribe-backup-tar manifest still restore and are still counted at the destination (folder and S3), with tests. - Exports, the MCP auth realm, the ASGI scope key and the persistent logger carry the new name. Kept on purpose: the secret-sealing key label stays "scribe.secrets.v1", because every stored secret was derived under it. Plugin identity (plugin name, hooks, env vars, .scribe marker, skills) is step 3. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -15,7 +15,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from scribe.services import retrieval_surfaces as rs
|
||||
from grimoire.services import retrieval_surfaces as rs
|
||||
from tests.helpers import fake_note, fake_rule, writepath_cfg
|
||||
|
||||
# The MCP tool layer reads its caller from a ContextVar the HTTP transport sets
|
||||
@@ -82,7 +82,7 @@ def _arm_patches(pc, hits, recorder, prior_art=None, cfg=None, rule_search=None,
|
||||
|
||||
|
||||
async def _run_arm(hits, recorder, prior_art=None, retrieval_log=None, **kwargs):
|
||||
from scribe.services import plugin_context as pc
|
||||
from grimoire.services import plugin_context as pc
|
||||
with ExitStack() as stack:
|
||||
for ctx in _arm_patches(pc, hits, recorder, prior_art,
|
||||
retrieval_log=retrieval_log):
|
||||
@@ -148,7 +148,7 @@ async def test_the_arm_searches_on_its_OWN_bar_not_the_code_one():
|
||||
The two values are deliberately different here so the assertion can tell
|
||||
them apart.
|
||||
"""
|
||||
from scribe.services import plugin_context as pc
|
||||
from grimoire.services import plugin_context as pc
|
||||
|
||||
search = AsyncMock(return_value=[])
|
||||
with ExitStack() as stack:
|
||||
@@ -217,12 +217,12 @@ async def test_mcp_get_rule_records_an_agent_pull():
|
||||
get_rule(N)", so this is the exact action a landed hint produces."""
|
||||
rec = MagicMock()
|
||||
rule = fake_rule(id=156, title="A wait with no deadline is a bug")
|
||||
with patch("scribe.mcp.tools.rulebooks.rulebooks_svc.get_rule",
|
||||
with patch("grimoire.mcp.tools.rulebooks.rulebooks_svc.get_rule",
|
||||
AsyncMock(return_value=rule)), \
|
||||
patch("scribe.mcp.tools.rulebooks.rulebooks_svc.rule_detail",
|
||||
patch("grimoire.mcp.tools.rulebooks.rulebooks_svc.rule_detail",
|
||||
AsyncMock(return_value={"id": 156})), \
|
||||
patch("scribe.mcp.tools.rulebooks.record_rule_pulled", rec):
|
||||
from scribe.mcp.tools.rulebooks import get_rule
|
||||
patch("grimoire.mcp.tools.rulebooks.record_rule_pulled", rec):
|
||||
from grimoire.mcp.tools.rulebooks import get_rule
|
||||
await get_rule(rule_id=156)
|
||||
|
||||
assert rec.call_args.kwargs["rule_id"] == 156
|
||||
@@ -234,10 +234,10 @@ async def test_a_rule_that_cannot_be_read_is_not_a_pull():
|
||||
"""Recorded after the access check. A refused read is not a pull, and
|
||||
counting it would credit the arm for a hint nobody could open."""
|
||||
rec = MagicMock()
|
||||
with patch("scribe.mcp.tools.rulebooks.rulebooks_svc.get_rule",
|
||||
with patch("grimoire.mcp.tools.rulebooks.rulebooks_svc.get_rule",
|
||||
AsyncMock(return_value=None)), \
|
||||
patch("scribe.mcp.tools.rulebooks.record_rule_pulled", rec):
|
||||
from scribe.mcp.tools.rulebooks import get_rule
|
||||
patch("grimoire.mcp.tools.rulebooks.record_rule_pulled", rec):
|
||||
from grimoire.mcp.tools.rulebooks import get_rule
|
||||
with pytest.raises(ValueError):
|
||||
await get_rule(rule_id=156)
|
||||
|
||||
@@ -260,8 +260,8 @@ def test_every_rule_detail_door_records_a_pull():
|
||||
note). What it can still prove is that the handler names the recorder —
|
||||
which is the thing that gets forgotten when a door is added.
|
||||
"""
|
||||
rest = _source_of("src/scribe/routes/rulebooks.py")
|
||||
mcp = _source_of("src/scribe/mcp/tools/rulebooks.py")
|
||||
rest = _source_of("src/grimoire/routes/rulebooks.py")
|
||||
mcp = _source_of("src/grimoire/mcp/tools/rulebooks.py")
|
||||
assert 'source="rest_rule"' in rest, (
|
||||
"the REST rule-detail route does not record a pull"
|
||||
)
|
||||
@@ -280,8 +280,8 @@ def test_the_bulk_loaders_are_not_counted_as_pulls():
|
||||
front of an agent, so "surely those are pulls too" is the reading someone
|
||||
arrives at without the argument.
|
||||
"""
|
||||
for path in ("src/scribe/mcp/tools/rulebooks.py",
|
||||
"src/scribe/mcp/tools/projects.py"):
|
||||
for path in ("src/grimoire/mcp/tools/rulebooks.py",
|
||||
"src/grimoire/mcp/tools/projects.py"):
|
||||
src = _source_of(path)
|
||||
for door in ("list_always_on_rules", "enter_project"):
|
||||
if f"async def {door}" not in src:
|
||||
@@ -312,7 +312,7 @@ def test_rules_payload_records_both_the_family_and_project_halves():
|
||||
remembers goes un-emitted with nothing forcing its surface to be accounted
|
||||
for.
|
||||
"""
|
||||
from scribe.services import rulebooks as svc
|
||||
from grimoire.services import rulebooks as svc
|
||||
|
||||
rec = MagicMock()
|
||||
with patch.object(svc, "record_rule_surfaced", rec):
|
||||
@@ -335,7 +335,7 @@ def test_rules_payload_records_both_the_family_and_project_halves():
|
||||
def test_brief_rules_payload_lists_titles_and_records_only_what_it_shows():
|
||||
"""The handshake's form: project rules by id and title, nothing else. A
|
||||
global rule it doesn't show must not count as surfaced."""
|
||||
from scribe.services import rulebooks as svc
|
||||
from grimoire.services import rulebooks as svc
|
||||
|
||||
rec = MagicMock()
|
||||
with patch.object(svc, "record_rule_surfaced", rec):
|
||||
@@ -361,7 +361,7 @@ def test_every_rules_payload_caller_names_itself():
|
||||
import re
|
||||
|
||||
seen = set()
|
||||
for path in Path("src/scribe").rglob("*.py"):
|
||||
for path in Path("src/grimoire").rglob("*.py"):
|
||||
for m in re.finditer(r"rules_payload\([^)]*source=\"([a-z_]+)\"", path.read_text()):
|
||||
seen.add(m.group(1))
|
||||
assert seen == {
|
||||
@@ -398,7 +398,7 @@ def _tool_patches(pc, hits, recorder, cfg=None, retrieval_log=None):
|
||||
|
||||
async def _run_tool_arm(hits, recorder, command="curl -s https://git.example/api/v1/runs",
|
||||
tool="Bash", retrieval_log=None, **kwargs):
|
||||
from scribe.services import plugin_context as pc
|
||||
from grimoire.services import plugin_context as pc
|
||||
with ExitStack() as stack:
|
||||
for ctx in _tool_patches(pc, hits, recorder, retrieval_log=retrieval_log):
|
||||
stack.enter_context(ctx)
|
||||
@@ -432,7 +432,7 @@ def _prompt_patches(pc, hits, recorder, retrieval_log=None):
|
||||
|
||||
async def _run_prompt_arm(hits, recorder, prompt="please merge to main",
|
||||
retrieval_log=None, **kwargs):
|
||||
from scribe.services import plugin_context as pc
|
||||
from grimoire.services import plugin_context as pc
|
||||
with ExitStack() as stack:
|
||||
for ctx in _prompt_patches(pc, hits, recorder, retrieval_log=retrieval_log):
|
||||
stack.enter_context(ctx)
|
||||
@@ -455,7 +455,7 @@ async def test_the_prompt_arm_retrieves_against_what_the_operator_SAID():
|
||||
id=2, title="`main` — never without explicit request",
|
||||
when_to_apply="opening or merging a dev→main pull request",
|
||||
))])
|
||||
from scribe.services import plugin_context as pc
|
||||
from grimoire.services import plugin_context as pc
|
||||
with ExitStack() as stack:
|
||||
stack.enter_context(patch.object(rs, "get_setting", AsyncMock(return_value="0.6")))
|
||||
stack.enter_context(patch.object(pc, "semantic_search_rules", search))
|
||||
@@ -489,7 +489,7 @@ async def test_the_prompt_arm_says_nothing_when_asked_nothing():
|
||||
retrieval_logs that no operator action produced."""
|
||||
search = AsyncMock(return_value=[])
|
||||
log = MagicMock()
|
||||
from scribe.services import plugin_context as pc
|
||||
from grimoire.services import plugin_context as pc
|
||||
with ExitStack() as stack:
|
||||
stack.enter_context(patch.object(rs, "get_setting", AsyncMock(return_value="0.6")))
|
||||
stack.enter_context(patch.object(pc, "semantic_search_rules", search))
|
||||
@@ -547,7 +547,7 @@ async def test_turning_off_the_notes_menu_does_not_turn_off_rules():
|
||||
structural fact rather than a simulation of the setting. A future refactor
|
||||
that reaches for that config here fails, whatever it then does with it.
|
||||
"""
|
||||
from scribe.services import plugin_context as pc
|
||||
from grimoire.services import plugin_context as pc
|
||||
|
||||
notes_cfg = AsyncMock(return_value={
|
||||
"enabled": False, "threshold": 0.55, "top_k": 3,
|
||||
@@ -591,7 +591,7 @@ async def test_the_tool_arm_is_a_ranked_source():
|
||||
"""It CHOSE what it showed, so a pull can settle whether the choice was any
|
||||
good — unlike a preload, which chose nothing. If this drifts into the
|
||||
ambient class the arm becomes unjudgeable."""
|
||||
from scribe.services.rule_usage import is_ambient
|
||||
from grimoire.services.rule_usage import is_ambient
|
||||
|
||||
assert not is_ambient("pre_tool_rule")
|
||||
|
||||
@@ -636,7 +636,7 @@ async def test_a_rule_the_session_already_holds_is_referenced_not_re_offered():
|
||||
async def test_an_empty_command_asks_the_ranker_nothing():
|
||||
"""Every Bash call reaches this. A blank payload must cost no embedding
|
||||
query at all, not merely return nothing after paying for one."""
|
||||
from scribe.services import plugin_context as pc
|
||||
from grimoire.services import plugin_context as pc
|
||||
|
||||
search = AsyncMock(return_value=[])
|
||||
rec = MagicMock()
|
||||
@@ -654,7 +654,7 @@ async def test_an_empty_command_asks_the_ranker_nothing():
|
||||
async def test_the_tool_arm_fails_open():
|
||||
"""A recall aid may never break the operator's action. A ranker that raises
|
||||
must cost the hint, not the command."""
|
||||
from scribe.services import plugin_context as pc
|
||||
from grimoire.services import plugin_context as pc
|
||||
|
||||
with ExitStack() as stack:
|
||||
stack.enter_context(patch.object(pc, "get_writepath_config",
|
||||
@@ -668,7 +668,7 @@ async def test_the_tool_arm_fails_open():
|
||||
async def test_a_long_command_is_bounded_before_it_reaches_the_ranker():
|
||||
"""A heredoc or a pasted script would push the verb and its target — the
|
||||
part a rule is about — out of the embedding window."""
|
||||
from scribe.services import plugin_context as pc
|
||||
from grimoire.services import plugin_context as pc
|
||||
|
||||
search = AsyncMock(return_value=[])
|
||||
with ExitStack() as stack:
|
||||
@@ -727,7 +727,7 @@ def test_the_hook_and_the_route_agree_on_every_parameter_name():
|
||||
|
||||
hook = Path("plugin/hooks/scribe_tool_rules.sh").read_text()
|
||||
defs = Path("plugin/hooks/scribe_defs.sh").read_text()
|
||||
route = Path("src/scribe/routes/plugin.py").read_text()
|
||||
route = Path("src/grimoire/routes/plugin.py").read_text()
|
||||
handler = route.split("async def pre_tool_rules")[1].split("\n@plugin_bp")[0]
|
||||
|
||||
sent = set(re.findall(r"[?&]([a-z_]+)=", hook))
|
||||
@@ -863,7 +863,7 @@ async def test_a_command_the_arm_never_searched_writes_no_row_at_all():
|
||||
A row here would report a call that never happened and drag the clear-rate
|
||||
down with phantom declines — the mirror of the defect, from the other side.
|
||||
"""
|
||||
from scribe.services import plugin_context as pc
|
||||
from grimoire.services import plugin_context as pc
|
||||
|
||||
log = MagicMock()
|
||||
with ExitStack() as stack:
|
||||
@@ -885,9 +885,9 @@ def test_no_rule_arm_logs_its_call_behind_a_results_guard():
|
||||
alone stays behind `if rule_ids:`. And no arm writes a rule-source row of
|
||||
its own any more — a fourth copy would be the defect's way back in.
|
||||
"""
|
||||
from scribe.services import retrieval_pipeline as rp
|
||||
from grimoire.services import retrieval_pipeline as rp
|
||||
|
||||
src = Path("src/scribe/services/retrieval_pipeline.py").read_text()
|
||||
src = Path("src/grimoire/services/retrieval_pipeline.py").read_text()
|
||||
tree = ast.parse(src)
|
||||
fns = {n.name: n for n in ast.walk(tree) if isinstance(n, ast.AsyncFunctionDef)}
|
||||
|
||||
@@ -936,7 +936,7 @@ def test_no_rule_arm_logs_its_call_behind_a_results_guard():
|
||||
|
||||
# 4. No arm keeps a private copy: outside the pipeline, nothing writes a
|
||||
# call row under a rule arm's source.
|
||||
pc_src = Path("src/scribe/services/plugin_context.py").read_text()
|
||||
pc_src = Path("src/grimoire/services/plugin_context.py").read_text()
|
||||
for arm in rp.RULE_ARMS:
|
||||
assert f'source="{arm.source}"' not in pc_src, (
|
||||
f"plugin_context writes a {arm.source} row of its own again — the "
|
||||
@@ -1053,7 +1053,7 @@ def test_the_three_hit_fixture_sits_inside_the_rule_band():
|
||||
retighten these scores, and says so here rather than through four
|
||||
confusing failures elsewhere.
|
||||
"""
|
||||
from scribe.services import plugin_context as pc
|
||||
from grimoire.services import plugin_context as pc
|
||||
|
||||
spread = _THREE_HITS[0][0] - _THREE_HITS[-1][0]
|
||||
assert spread < pc._RULEHINT_BAND, (
|
||||
@@ -1410,7 +1410,7 @@ def _search_by_kind(general, preference):
|
||||
|
||||
|
||||
async def _run_slot(general, preference, recorder=None, retrieval_log=None, **kwargs):
|
||||
from scribe.services import plugin_context as pc
|
||||
from grimoire.services import plugin_context as pc
|
||||
rec = recorder or MagicMock()
|
||||
with ExitStack() as stack:
|
||||
stack.enter_context(patch.object(rs, "get_setting", AsyncMock(return_value="0.6")))
|
||||
@@ -1474,7 +1474,7 @@ async def test_the_slot_query_can_only_answer_with_a_preference():
|
||||
place on score — a slot silently spent on the wrong kind is worse than no
|
||||
slot at all.
|
||||
"""
|
||||
from scribe.services import plugin_context as pc
|
||||
from grimoire.services import plugin_context as pc
|
||||
search = _search_by_kind(_RULES_FILLING_THE_LIMIT, _PREF_HIT)
|
||||
with ExitStack() as stack:
|
||||
stack.enter_context(patch.object(rs, "get_setting", AsyncMock(return_value="0.6")))
|
||||
@@ -1609,7 +1609,7 @@ async def test_a_preference_on_the_ledger_keeps_the_slot_and_is_not_recounted():
|
||||
@pytest.mark.asyncio
|
||||
async def test_each_act_arm_searches_at_its_own_bar():
|
||||
"""The split, where it actually takes effect."""
|
||||
from scribe.services import plugin_context as pc
|
||||
from grimoire.services import plugin_context as pc
|
||||
|
||||
cfg = writepath_cfg(threshold=0.6, top_k=3, rule_threshold=0.77, tool_rule_threshold=0.61)
|
||||
|
||||
@@ -1637,7 +1637,7 @@ async def test_an_act_arm_reports_the_bar_it_actually_searched_at():
|
||||
as having cleared a bar it never faced — and the reader's conclusion would
|
||||
be to move the bar that was already right.
|
||||
"""
|
||||
from scribe.services import plugin_context as pc
|
||||
from grimoire.services import plugin_context as pc
|
||||
|
||||
cfg = writepath_cfg(threshold=0.6, top_k=3, rule_threshold=0.77, tool_rule_threshold=0.61)
|
||||
|
||||
@@ -1673,7 +1673,7 @@ def test_every_hook_rule_search_says_which_project_it_is_for():
|
||||
# The hook arms search through the pipeline's one
|
||||
# ranked-search stage, checked below — so a direct search appearing
|
||||
# here again is a copy of the arm coming back, and fails the count.
|
||||
"src/scribe/services/plugin_context.py": 0,
|
||||
"src/grimoire/services/plugin_context.py": 0,
|
||||
}
|
||||
for path, expected in sources.items():
|
||||
calls = [
|
||||
@@ -1704,7 +1704,7 @@ def test_every_hook_rule_search_says_which_project_it_is_for():
|
||||
# arms, their slots, and the via-lesson arm's lesson search), so the
|
||||
# searches are counted per function: a rule search copied anywhere else
|
||||
# is a new function in this map, and fails it.
|
||||
pipeline = ast.parse(Path("src/scribe/services/retrieval_pipeline.py").read_text())
|
||||
pipeline = ast.parse(Path("src/grimoire/services/retrieval_pipeline.py").read_text())
|
||||
by_function = {}
|
||||
for fn in ast.walk(pipeline):
|
||||
if isinstance(fn, ast.AsyncFunctionDef):
|
||||
@@ -1741,7 +1741,7 @@ def test_every_hook_rule_search_says_which_project_it_is_for():
|
||||
async def test_the_act_arms_scope_their_search_to_the_bound_project(bound, scope):
|
||||
"""A bound session searches its own project; an unbound one (0) searches
|
||||
global rules only, which the search spells as `project_id=None`."""
|
||||
from scribe.services import plugin_context as pc
|
||||
from grimoire.services import plugin_context as pc
|
||||
|
||||
cfg = writepath_cfg(threshold=0.6, top_k=3, rule_threshold=0.6, tool_rule_threshold=0.6)
|
||||
tool_search = AsyncMock(return_value=[])
|
||||
|
||||
Reference in New Issue
Block a user