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

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:
2026-10-22 10:50:45 -04:00
co-authored by Claude Opus 5.5
parent e99f1169f7
commit 0f5ef0be63
447 changed files with 3611 additions and 3558 deletions
+39 -39
View File
@@ -16,7 +16,7 @@ from pathlib import Path
import pytest
import pytest_asyncio
from scribe.services.coverage import (
from grimoire.services.coverage import (
ArchiveScan,
class_references,
coverage_line,
@@ -26,7 +26,7 @@ from scribe.services.coverage import (
scannable,
shapes_from_archive,
)
from scribe.services.shape_ledger import location_covers
from grimoire.services.shape_ledger import location_covers
from tests.helpers import ensure_user, forge_tarball
PLUGIN = Path(__file__).resolve().parents[1] / "plugin"
@@ -339,7 +339,7 @@ def test_type_import_specifiers_are_not_definitions():
"""`import { type Foo, bar }` is the same two words as `type Foo =`
and defines nothing; only a `type` line with a declaration after the
name counts (TS alias, Go/Rust type)."""
from scribe.services.coverage import extract_shapes
from grimoire.services.coverage import extract_shapes
src = (
'import { type DesignSystem, fetchDesignSystems } from "@/api/designSystems";\n'
'import { type Project } from "./x";\n'
@@ -371,13 +371,13 @@ def test_coverage_line_is_evidence_carrying_and_labeled_estimate():
def test_bind_repo_tool_takes_a_ref():
"""The binding names the branch the ledger follows."""
from scribe.mcp.server import build_mcp_server
from grimoire.mcp.server import build_mcp_server
tool = build_mcp_server()._tool_manager.get_tool("bind_repo")
assert "ref" in tool.parameters.get("properties", {})
def test_coverage_routes_are_registered():
from scribe.app import create_app
from grimoire.app import create_app
rules = {r.rule for r in create_app().url_map.iter_rules()}
assert "/api/projects/<int:project_id>/coverage" in rules
@@ -390,7 +390,7 @@ def test_coverage_routes_are_registered():
def _forge(tar_bytes: bytes):
import httpx
from scribe.services.forge import GiteaForge
from grimoire.services.forge import GiteaForge
def handler(request: httpx.Request) -> httpx.Response:
path = request.url.path
@@ -409,7 +409,7 @@ def _forge(tar_bytes: bytes):
def _selector(tar_bytes: bytes):
"""The keyring shape compute_coverage consumes — one owner
keyring holding the mocked Gitea adapter."""
from scribe.services.forge import ForgeSelector
from grimoire.services.forge import ForgeSelector
return ForgeSelector((_forge(tar_bytes),))
@@ -417,10 +417,10 @@ def _selector(tar_bytes: bytes):
@pytest_asyncio.fixture
async def seeded(_dispose_engine):
"""User + project + binding + two snippets that cover 2 of TREE's 4 shapes."""
from scribe.models import async_session
from scribe.models.project import Project
from scribe.services import snippets as svc
from scribe.services.repo_bindings import set_binding
from grimoire.models import async_session
from grimoire.models.project import Project
from grimoire.services import snippets as svc
from grimoire.services.repo_bindings import set_binding
async with async_session() as s:
user = await ensure_user(s, "coverage_itest")
@@ -447,7 +447,7 @@ async def seeded(_dispose_engine):
@pytest.mark.integration
async def test_coverage_measures_the_tree_exactly_and_caches(seeded):
from scribe.services.coverage import (
from grimoire.services.coverage import (
cached_coverage,
compute_coverage,
refresh_coverage,
@@ -455,8 +455,8 @@ async def test_coverage_measures_the_tree_exactly_and_caches(seeded):
from sqlalchemy import select
from scribe.models import async_session
from scribe.models.code_shape import CodeShape
from grimoire.models import async_session
from grimoire.models.code_shape import CodeShape
uid, pid = seeded["uid"], seeded["pid"]
selector = _selector(_tarball(TREE))
@@ -518,9 +518,9 @@ async def test_ledger_keeps_judgments_and_stamps_vanished_shapes(seeded):
from sqlalchemy import select
from scribe.models import async_session
from scribe.models.code_shape import CodeShape
from scribe.services.coverage import compute_coverage
from grimoire.models import async_session
from grimoire.models.code_shape import CodeShape
from grimoire.services.coverage import compute_coverage
uid, pid = seeded["uid"], seeded["pid"]
await compute_coverage(uid, pid, selector=_selector(_tarball(TREE)))
@@ -562,9 +562,9 @@ async def test_ledger_keeps_judgments_and_stamps_vanished_shapes(seeded):
@pytest.mark.integration
async def test_enter_project_surfaces_the_line_only_once_computed(seeded):
from scribe.mcp._context import _user_id_ctx
from scribe.mcp.tools.projects import enter_project
from scribe.services.coverage import refresh_coverage
from grimoire.mcp._context import _user_id_ctx
from grimoire.mcp.tools.projects import enter_project
from grimoire.services.coverage import refresh_coverage
uid, pid = seeded["uid"], seeded["pid"]
token = _user_id_ctx.set(uid)
@@ -590,8 +590,8 @@ async def test_enter_project_surfaces_the_line_only_once_computed(seeded):
async def test_explicit_refresh_names_its_failures(seeded):
"""refresh_for_caller raises fixable errors instead of silence: an agent
mid-task must learn WHY nothing measured, and 'None' would strand it."""
from scribe.models import async_session
from scribe.services.coverage import refresh_for_caller
from grimoire.models import async_session
from grimoire.services.coverage import refresh_for_caller
uid, pid = seeded["uid"], seeded["pid"]
# The owner has no forge connection rows → the error names the fix.
@@ -613,7 +613,7 @@ async def test_explicit_refresh_names_its_failures(seeded):
async def test_background_seed_is_quiet_without_a_forge(seeded):
"""refresh_if_stale must exit silently for a forge-less owner — the
baseline install — and treat a fresh cache as nothing-to-do."""
from scribe.services.coverage import refresh_coverage, refresh_if_stale
from grimoire.services.coverage import refresh_coverage, refresh_if_stale
uid, pid = seeded["uid"], seeded["pid"]
# Absent cache + no forge rows: returns without raising, writes nothing.
@@ -627,11 +627,11 @@ async def test_background_seed_is_quiet_without_a_forge(seeded):
async def test_unservable_binding_measures_nothing(seeded):
"""A project bound only to a host the forge doesn't serve returns None —
the same silence as no forge at all, never an error."""
from scribe.services.coverage import compute_coverage
from scribe.services.repo_bindings import set_binding
from grimoire.services.coverage import compute_coverage
from grimoire.services.repo_bindings import set_binding
from scribe.models import async_session
from scribe.models.project import Project
from grimoire.models import async_session
from grimoire.models.project import Project
uid = seeded["uid"]
async with async_session() as s:
@@ -652,7 +652,7 @@ def test_extract_definitions_fingerprints_each_block():
"""The block rule across the language families the extractor knows: a
Python def ends at the next top-level statement, a braces/CSS block keeps
its closer, and comments/decorators don't move the hash."""
from scribe.services.coverage import extract_definitions
from grimoire.services.coverage import extract_definitions
text = (
"import os\n\n"
@@ -676,7 +676,7 @@ def test_extract_definitions_fingerprints_each_block():
changed = {d.name: d for d in extract_definitions(text.replace("x + 1", "x + 2"))}
assert changed["a"].body_sha != defs["a"].body_sha
# And the identity view is unchanged for the hook mirror.
from scribe.services.coverage import extract_shapes
from grimoire.services.coverage import extract_shapes
assert extract_shapes(text) == [(d.kind, d.name) for d in extract_definitions(text)]
# A CSS rule's fingerprint is its declarations: the same body
# under another selector is the same shape to the derive grouping.
@@ -699,7 +699,7 @@ def test_extract_definitions_fingerprints_each_block():
def test_coverage_line_names_the_proposers_standing():
from scribe.services.coverage import coverage_line
from grimoire.services.coverage import coverage_line
base = {
"total": 100, "accounted": 10, "unclassified": 90,
@@ -736,7 +736,7 @@ def test_coverage_line_shows_standing_work_even_with_nothing_unclassified():
still carry derive groups / proposals / divergence — the line names
them as `standing:` instead of hiding them behind the todo count, and
names the drift since the previous refresh first-copy-first."""
from scribe.services.coverage import coverage_line
from grimoire.services.coverage import coverage_line
base = {
"total": 4693, "accounted": 4693, "unclassified": 0,
@@ -767,7 +767,7 @@ def test_coverage_line_shows_standing_work_even_with_nothing_unclassified():
def test_coverage_line_names_divergence_and_recheck():
from scribe.services.coverage import coverage_line
from grimoire.services.coverage import coverage_line
base = {
"total": 100, "accounted": 40, "unclassified": 60,
@@ -786,7 +786,7 @@ def test_scoped_definitions_are_vue_script_setup_and_scoped_style_only():
"""One-offs by construction — every sym in a .vue and every css
rule inside <style scoped>; an unscoped <style> block and non-.vue files
stay ordinary."""
from scribe.services.coverage import extract_definitions, scoped_definitions
from grimoire.services.coverage import extract_definitions, scoped_definitions
vue = (
"<script setup lang=\"ts\">\n"
"function load() {\n return 1;\n}\n"
@@ -814,7 +814,7 @@ def test_svelte_scopes_by_default_and_its_module_script_stays_importable():
component's own unless it opts out, and only <script module> /
<script context="module"> can be imported elsewhere. Without this a
Svelte project's every `.card` and `.title` sits in the human todo."""
from scribe.services.coverage import extract_definitions, scoped_definitions
from grimoire.services.coverage import extract_definitions, scoped_definitions
svelte = (
"<script module lang=\"ts\">\n"
"export function stageOf(x: number) {\n return x;\n}\n"
@@ -843,7 +843,7 @@ def test_svelte_scopes_by_default_and_its_module_script_stays_importable():
def test_the_line_names_the_review_queue_and_the_tool_that_holds_it():
"""`stamps_to_review` goes uncalled unless something on arrival says
there is anything to review."""
from scribe.services.coverage import coverage_line
from grimoire.services.coverage import coverage_line
base = {"accounted": 10, "total": 10, "counts": {"exempt": 10},
"unclassified": 0, "computed_at": "2026-09-30T00:00:00"}
assert "stamps_to_review" not in coverage_line(base)
@@ -856,8 +856,8 @@ def test_the_line_names_the_review_queue_and_the_tool_that_holds_it():
async def test_binding_ref_is_the_branch_the_ledger_follows(seeded):
"""A binding that names a ref is read at that ref (not the forge's
default branch); "" clears it; None on a re-bind leaves it standing."""
from scribe.services.coverage import compute_coverage
from scribe.services.repo_bindings import bindings_for_project, set_binding
from grimoire.services.coverage import compute_coverage
from grimoire.services.repo_bindings import bindings_for_project, set_binding
uid, pid = seeded["uid"], seeded["pid"]
b = await set_binding(uid, "https://git.example.com/alice/widget.git", pid, "dev")
assert b.ref == "dev"
@@ -889,7 +889,7 @@ async def test_binding_ref_is_the_branch_the_ledger_follows(seeded):
])
def test_a_file_is_a_unit_when_it_renders_and_nothing_inside_carries_its_name(path, text, unit):
"""One structural test, not a framework list."""
from scribe.services.coverage import class_references, extract_definitions, is_file_unit
from grimoire.services.coverage import class_references, extract_definitions, is_file_unit
assert is_file_unit(path, text, extract_definitions(text), class_references(path, text)) is unit
@@ -897,7 +897,7 @@ def test_a_file_is_a_unit_when_it_renders_and_nothing_inside_carries_its_name(pa
def test_the_line_says_whether_writers_judged_what_they_wrote():
"""The write-time figure — silent until the question has
been put, then turns checked, asked, and asks walked past."""
from scribe.services.coverage import coverage_line
from grimoire.services.coverage import coverage_line
base = {"accounted": 10, "total": 10, "counts": {"exempt": 10},
"unclassified": 0, "computed_at": "2026-10-01T00:00:00"}
assert "written-shape check" not in coverage_line(base)