rename(547): what people read says Grimoire
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 52s
CI & Build / integration (push) Successful in 1m1s
CI & Build / Python tests (push) Successful in 1m55s
CI & Build / Build & push image (push) Successful in 36s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 52s
CI & Build / integration (push) Successful in 1m1s
CI & Build / Python tests (push) Successful in 1m55s
CI & Build / Build & push image (push) Successful in 36s
Step 1 of the Scribe -> Grimoire rename: every capitalised "Scribe" in prose
becomes "Grimoire" ("Fabled Scribe" -> "Fabled Grimoire") across the UI,
the PWA manifest and page title, the MCP instructions and tool docstrings,
hook and skill text, docs and README, with the tests that assert on that
prose moving with it. Old migrations keep their wording.
Identifiers are untouched here: the package, plugin name, MCP server key,
hook file names, env vars, the .scribe marker and FabledScribe repo/image
names move in steps 2 and 3, along with prose that quotes them.
The sweep matched "Scribe" only as a whole word, so describe/subscribe and
friends are untouched. The longer name took the MCP instructions one
character over their 1,600 budget; two phrases are shortened to fit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -107,7 +107,7 @@ def _no_reply_shape_log():
|
||||
"""Stub the reply-shape delivery row.
|
||||
|
||||
Autouse for _no_system_labels' reason: every turn's retrieval, every
|
||||
moment request and every Scribe tool that reaches a shaped moment writes
|
||||
moment request and every Grimoire tool that reaches a shaped moment writes
|
||||
one AppLog row, and those paths are tested without a database.
|
||||
tests/test_reply_shapes.py binds the real function at import time,
|
||||
before this patch runs.
|
||||
|
||||
+2
-2
@@ -346,7 +346,7 @@ def http_sink(reply: bytes = b'{"context":"","note_ids":[]}',
|
||||
"""A throwaway local HTTP listener for hook end-to-end tests: yields
|
||||
``(port, seen)`` where ``seen`` collects every GET's parsed query string
|
||||
(one dict per request, in order). Lets the shell be tested end to end —
|
||||
the extraction, the encoding, the URL — without a Scribe instance.
|
||||
the extraction, the encoding, the URL — without a Grimoire instance.
|
||||
|
||||
Pass ``reply`` for the body the hook should see.
|
||||
|
||||
@@ -482,7 +482,7 @@ async def rule_row(rule_id: int):
|
||||
return await s.get(Rule, rule_id)
|
||||
|
||||
|
||||
# Software-only vocabulary. Scribe's product text speaks for any work a person
|
||||
# Software-only vocabulary. Grimoire's product text speaks for any work a person
|
||||
# drives through an agent — configuration, infrastructure, documents — so a
|
||||
# catalog or a shipped shape that reads as software-only is wrong on most of
|
||||
# the installs it ships to. The actions particular to one kind of work belong
|
||||
|
||||
@@ -122,7 +122,7 @@ def test_after_write_local_arm_works_without_a_server_and_says_the_server_did_no
|
||||
out = _run(repo, env, session="s-local")
|
||||
ctx = json.loads(out)["hookSpecificOutput"]["additionalContext"]
|
||||
assert "`slug` is already defined in 1 other file(s): c.py" in ctx
|
||||
assert "Scribe did not answer the prior-art check for `d.py` within 8s" in ctx
|
||||
assert "Grimoire did not answer the prior-art check for `d.py` within 8s" in ctx
|
||||
assert "UNCHECKED" in ctx
|
||||
assert "None of those existing copies is recorded" not in ctx
|
||||
marker = tmp_path / "scribe-priorart" / "s-local.unreached"
|
||||
|
||||
@@ -27,7 +27,7 @@ the cause. No signature changed.
|
||||
WHAT IS PINNED: which edits speak and which stay silent, and that both gates
|
||||
are load-bearing. NOT pinned: the wording, which is prose.
|
||||
|
||||
A FIXTURE REPO, NEVER THIS ONE. Asserting against Scribe's own files would
|
||||
A FIXTURE REPO, NEVER THIS ONE. Asserting against Grimoire's own files would
|
||||
make the test a description of today's tree, failing the next time someone
|
||||
renames something.
|
||||
"""
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
WHY THIS EXISTS
|
||||
|
||||
Scribe's record kinds are reached for interchangeably — a note written where
|
||||
Grimoire'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 (product guidance lives there and nowhere else), so a docstring that only documents parameters answers "how do I call
|
||||
|
||||
@@ -35,14 +35,14 @@ from tests.helpers import fake_lesson, fake_note, fake_snippet
|
||||
OLD_MIRROR = {
|
||||
"name": "debounce",
|
||||
"language": "javascript",
|
||||
"locations": [{"repo": "Scribe", "path": "old/place.js", "symbol": "debounce"}],
|
||||
"locations": [{"repo": "Grimoire", "path": "old/place.js", "symbol": "debounce"}],
|
||||
"verification": {"status": "ok", "code_sha": "abc", "checked_at": "2026-01-01"},
|
||||
"provenance": {"commit_sha": "deadbeef"},
|
||||
}
|
||||
|
||||
MOVED_BODY = (
|
||||
"**Locations:**\n"
|
||||
"- `Scribe` · `new/place.ts` · `debounce`\n\n"
|
||||
"- `Grimoire` · `new/place.ts` · `debounce`\n\n"
|
||||
"```typescript\nexport const debounce = 1;\n```\n"
|
||||
)
|
||||
|
||||
@@ -52,7 +52,7 @@ async def test_a_body_write_moves_the_mirror_with_it():
|
||||
note = fake_snippet(data=dict(OLD_MIRROR), project_id=None)
|
||||
await _update(note, body=MOVED_BODY)
|
||||
assert note.data["locations"] == [
|
||||
{"repo": "Scribe", "path": "new/place.ts", "symbol": "debounce"}
|
||||
{"repo": "Grimoire", "path": "new/place.ts", "symbol": "debounce"}
|
||||
], "the mirror still describes where the snippet used to live"
|
||||
assert note.data["language"] == "typescript"
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
WHY THIS EXISTS
|
||||
|
||||
Scribe's guidance to agents reaches a session through five surfaces: the MCP
|
||||
Grimoire's guidance to agents reaches a session through five surfaces: the MCP
|
||||
`_INSTRUCTIONS`, the plugin's static session context, the live session context
|
||||
the server builds, the skills, and tool docstrings. A practice stated in full
|
||||
on several of them drifts apart, copy by copy.
|
||||
@@ -320,11 +320,11 @@ TOPICS: tuple[Topic, ...] = (
|
||||
"belongs to another user", index=("shared:true",)),
|
||||
Topic("stored processes are followed as written", "docstrings", ("get_process",), "follow the returned body"),
|
||||
Topic("a project is never guessed", "docstrings", ("never guessing a project",), "never guessing a project"),
|
||||
Topic("an unbound repo gets a bind hint", "live", ("bind_repo",), "isn't mapped to a scribe project"),
|
||||
Topic("an unbound repo gets a bind hint", "live", ("bind_repo",), "isn't mapped to a grimoire project"),
|
||||
# ── the Claude Code adapter's own conventions ──
|
||||
Topic("compact at clean seams", "static", ("/compact",), "compact at clean seams"),
|
||||
Topic("stored processes sync into local skills", "commands", ("scribe-proc-",), "regenerate the local skill stubs"),
|
||||
Topic("say so when scribe's tools are unavailable", "static", ("tools are unavailable",),
|
||||
Topic("say so when grimoire's tools are unavailable", "static", ("tools are unavailable",),
|
||||
"rather than silently falling back to local notes"),
|
||||
)
|
||||
|
||||
|
||||
@@ -57,13 +57,13 @@ async def seeded():
|
||||
data=compose_data(name=name, language="python", locations=locations),
|
||||
)
|
||||
|
||||
nested = _snippet("nested", [loc(repo="Scribe", path="frontend/src/lib/x.ts", symbol="helper")])
|
||||
sibling = _snippet("sibling", [loc(repo="Scribe", path="frontend/srcmap.ts", symbol="other")])
|
||||
# Two locations, deliberately crossed: repo Scribe at src/a.py and repo
|
||||
# Portal at src/b.py. repo=Scribe + path=src/b.py must NOT match it.
|
||||
nested = _snippet("nested", [loc(repo="Grimoire", path="frontend/src/lib/x.ts", symbol="helper")])
|
||||
sibling = _snippet("sibling", [loc(repo="Grimoire", path="frontend/srcmap.ts", symbol="other")])
|
||||
# Two locations, deliberately crossed: repo Grimoire at src/a.py and repo
|
||||
# Portal at src/b.py. repo=Grimoire + path=src/b.py must NOT match it.
|
||||
multi = _snippet(
|
||||
"multi",
|
||||
[loc(repo="Scribe", path="src/a.py", symbol="alpha"), loc(repo="Portal", path="src/b.py", symbol="beta")],
|
||||
[loc(repo="Grimoire", path="src/a.py", symbol="alpha"), loc(repo="Portal", path="src/b.py", symbol="beta")],
|
||||
)
|
||||
# No structured location at all — must never satisfy a location filter,
|
||||
# and must not error the query either.
|
||||
@@ -101,7 +101,7 @@ async def _ids_for(user_id: int, **parts) -> set[int]:
|
||||
@pytest.mark.asyncio
|
||||
async def test_repo_filter_finds_every_snippet_recorded_in_it(seeded):
|
||||
user_id, ids = seeded
|
||||
found = await _ids_for(user_id, repo="Scribe")
|
||||
found = await _ids_for(user_id, repo="Grimoire")
|
||||
assert found == {ids["nested"], ids["sibling"], ids["multi"]}
|
||||
assert ids["placeless"] not in found
|
||||
|
||||
@@ -124,9 +124,9 @@ async def test_path_prefix_reaches_into_subdirectories(seeded):
|
||||
@pytest.mark.asyncio
|
||||
async def test_parts_must_match_within_one_location(seeded):
|
||||
user_id, ids = seeded
|
||||
assert await _ids_for(user_id, repo="Scribe", path="src/a.py") == {ids["multi"]}
|
||||
assert await _ids_for(user_id, repo="Grimoire", path="src/a.py") == {ids["multi"]}
|
||||
# Same snippet, but repo and path belong to two different entries.
|
||||
assert await _ids_for(user_id, repo="Scribe", path="src/b.py") == set()
|
||||
assert await _ids_for(user_id, repo="Grimoire", path="src/b.py") == set()
|
||||
assert await _ids_for(user_id, repo="Portal", path="src/b.py") == {ids["multi"]}
|
||||
|
||||
|
||||
@@ -155,7 +155,7 @@ async def test_keyword_arm_applies_the_location_filter(seeded):
|
||||
"scribe.services.embeddings.semantic_search_notes",
|
||||
AsyncMock(return_value=[]),
|
||||
):
|
||||
assert await _ids_for(user_id, q="nested", repo="Scribe") == {ids["nested"]}
|
||||
assert await _ids_for(user_id, q="nested", repo="Grimoire") == {ids["nested"]}
|
||||
# Matches the query but not the place.
|
||||
assert await _ids_for(user_id, q="nested", repo="Portal") == set()
|
||||
|
||||
@@ -190,7 +190,7 @@ async def test_semantic_arm_applies_the_location_filter(seeded):
|
||||
assert await _ids_for(user_id, q="zzzznomatch", path="frontend/src") == {
|
||||
ids["nested"],
|
||||
}
|
||||
assert await _ids_for(user_id, q="zzzznomatch", repo="Scribe", path="src/b.py") == set()
|
||||
assert await _ids_for(user_id, q="zzzznomatch", repo="Grimoire", path="src/b.py") == set()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -212,13 +212,13 @@ async def test_sql_and_python_dialects_agree_on_the_same_rows(seeded):
|
||||
by_id = {n.id: n.data for n in rows}
|
||||
|
||||
cases = [
|
||||
{"repo": "Scribe"},
|
||||
{"repo": "Grimoire"},
|
||||
{"path": "frontend/src"},
|
||||
{"path": "frontend/src/lib/x.ts"},
|
||||
{"repo": "Scribe", "path": "src/a.py"},
|
||||
{"repo": "Scribe", "path": "src/b.py"},
|
||||
{"repo": "Grimoire", "path": "src/a.py"},
|
||||
{"repo": "Grimoire", "path": "src/b.py"},
|
||||
{"symbol": "beta"},
|
||||
{"repo": "Scribe", "path": "frontend", "symbol": "helper"},
|
||||
{"repo": "Grimoire", "path": "frontend", "symbol": "helper"},
|
||||
]
|
||||
for case in cases:
|
||||
sql_ids = await _ids_for(user_id, **case)
|
||||
|
||||
@@ -62,7 +62,7 @@ async def test_unknown_tool_keeps_the_upstream_error():
|
||||
#
|
||||
# SDK 2.x passes on only a ToolError's text; anything else is wrapped as
|
||||
# "Error executing tool X" and its message is kept on the server. ValueError is
|
||||
# how every Scribe tool refuses, so without this every refusal arrives bare.
|
||||
# how every Grimoire tool refuses, so without this every refusal arrives bare.
|
||||
|
||||
|
||||
def _raising_server() -> StrictArgsMCPServer:
|
||||
|
||||
@@ -43,7 +43,7 @@ async def test_creating_with_a_parent_passes_it_through():
|
||||
with patch("scribe.mcp.tools.design_systems.ds_svc") as svc:
|
||||
svc.create_design_system = AsyncMock(return_value=_fake_design_system())
|
||||
from scribe.mcp.tools.design_systems import create_design_system
|
||||
await create_design_system(title="Scribe", parent_id=4)
|
||||
await create_design_system(title="Grimoire", parent_id=4)
|
||||
assert svc.create_design_system.await_args.kwargs["parent_id"] == 4
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ async def test_create_raises_when_the_parent_is_not_writable():
|
||||
svc.create_design_system = AsyncMock(return_value=None)
|
||||
from scribe.mcp.tools.design_systems import create_design_system
|
||||
with pytest.raises(ValueError):
|
||||
await create_design_system(title="Scribe", parent_id=4)
|
||||
await create_design_system(title="Grimoire", parent_id=4)
|
||||
|
||||
|
||||
# --- the three-state parent -------------------------------------------------
|
||||
|
||||
@@ -8,7 +8,7 @@ tests/test_integration_rule_moments.py. These pin what is decided above it:
|
||||
fresh ones are recorded, each with its moment;
|
||||
- every door fails open;
|
||||
- the plugin is told which tools can reach anything, and nothing more;
|
||||
- every Scribe tool the shipped mappings name carries the attach, and the
|
||||
- every Grimoire tool the shipped mappings name carries the attach, and the
|
||||
hook and the route agree on every name between them.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
@@ -352,7 +352,7 @@ def test_an_install_with_nothing_mounted_sends_one_request_per_window(tmp_path):
|
||||
assert len(seen) == 1
|
||||
|
||||
|
||||
# ── the MCP door: every shipped Scribe action carries the attach ────────
|
||||
# ── the MCP door: every shipped Grimoire action carries the attach ────────
|
||||
|
||||
|
||||
def _attached_names(path: Path) -> dict[str, set[str]]:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""One definition of what SHIPS in the plugin, and the drift guards on it.
|
||||
|
||||
WHAT THIS IS ABOUT. Scribe publishes two artifacts. `plugin/` is not in the Docker image — installs fetch it from this
|
||||
WHAT THIS IS ABOUT. Grimoire publishes two artifacts. `plugin/` is not in the Docker image — installs fetch it from this
|
||||
repo through `.claude-plugin/marketplace.json`, so **a push IS the release**,
|
||||
with no build step in between. That makes "which files reach an install?" a
|
||||
question with real consequences, and it can be answered wrong two ways:
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
The mechanism this pins was read out of the installed Claude Code build, not
|
||||
out of the documentation, which describes a different one. Three facts decide
|
||||
whether the hook works at all, and all three are properties of what the shell
|
||||
writes rather than of anything Scribe runs:
|
||||
writes rather than of anything Grimoire runs:
|
||||
|
||||
* **Exit 0.** The handler keeps a hook's stdout only when it `succeeded`,
|
||||
which is `status === 0`. A non-zero exit sends the same bytes down the
|
||||
@@ -111,7 +111,7 @@ def test_the_instructions_name_what_has_to_survive():
|
||||
in the words a summarizer can act on."""
|
||||
said = _run(EVENT).stdout.lower()
|
||||
assert "id" in said and "title" in said
|
||||
for anchor in ("in progress", "scribe", "not yet recorded"):
|
||||
for anchor in ("in progress", "grimoire", "not yet recorded"):
|
||||
assert anchor in said, f"the instruction no longer mentions {anchor!r}"
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ WHY TWO DOORS AT ALL
|
||||
propose, offer three answers, and wait. That is right for a rule — the person
|
||||
a rule binds should have agreed to be bound.
|
||||
|
||||
A preference inverts it: it is a rule Scribe can and should update during
|
||||
A preference inverts it: it is a rule Grimoire can and should update during
|
||||
use. A preference that asks every time never drifts, and drifting is the whole feature. Reaching one through
|
||||
`create_rule(kind=..., when_to_apply="when the moment this fixture stands in for arises")` would mean reading it through the gate's prose, and
|
||||
the caller would hesitate over exactly the act this kind exists to make
|
||||
|
||||
@@ -5,12 +5,12 @@ not. `#3244` reads as complete to the writer and as homework to the reader,
|
||||
who has to look it up to know what their own conversation is about. Nobody
|
||||
knows what a note, task or milestone is by its id number.
|
||||
|
||||
PRODUCT, NOT A RULE. Every Scribe user hits this, so the fix is in
|
||||
PRODUCT, NOT A RULE. Every Grimoire user hits this, so the fix is in
|
||||
the surfaces the product ships — the skill that shapes how an agent writes,
|
||||
and the tool responses that hand a record back. A per-instance rule would fix
|
||||
it for one operator and leave the behaviour wrong for everyone else.
|
||||
|
||||
Scribe's duplicate gate already had the right shape — `id 412: "debounce
|
||||
Grimoire's duplicate gate already had the right shape — `id 412: "debounce
|
||||
helper"` — which is why these assert the CONVENTION reaches the other
|
||||
surfaces rather than inventing a new one.
|
||||
"""
|
||||
|
||||
@@ -59,7 +59,7 @@ def test_placement_comes_from_the_record():
|
||||
|
||||
|
||||
def test_the_shipped_shapes_assume_no_particular_domain():
|
||||
"""Scribe is domain-neutral: a home-infrastructure or writing project reads
|
||||
"""Grimoire is domain-neutral: a home-infrastructure or writing project reads
|
||||
these too. Software-specific evidence belongs in an operator's own
|
||||
preferences, never in the product default."""
|
||||
text = _text()
|
||||
|
||||
@@ -40,7 +40,7 @@ _SECRETS = [
|
||||
("vendor-prefixed token in a shell assignment",
|
||||
"TOK=flt_AAAABBBBCCCCDDDDEEEEFFFF\npython3 - <<'PY'",
|
||||
"flt_AAAABBBBCCCCDDDDEEEEFFFF"),
|
||||
("a Scribe fmcp_ key in an auth header",
|
||||
("a Grimoire fmcp_ key in an auth header",
|
||||
"curl -H 'Authorization: Bearer fmcp_ZZZZYYYYXXXXWWWWVVVV' https://x",
|
||||
"fmcp_ZZZZYYYYXXXXWWWWVVVV"),
|
||||
("a forge token in an export",
|
||||
|
||||
@@ -175,7 +175,7 @@ async def test_injected_menu_labels_the_record_kind():
|
||||
# Still title-first: the marker is metadata, not an excuse to carry bodies.
|
||||
assert "body" not in out["context"]
|
||||
# The header can't claim they're all notes when the markers say otherwise.
|
||||
assert "Scribe records" in lines[0]
|
||||
assert "Grimoire records" in lines[0]
|
||||
|
||||
|
||||
def test_list_shaped_services_use_the_shared_visibility_clause():
|
||||
|
||||
@@ -11,7 +11,7 @@ than a reimplementation of it. What they pin:
|
||||
* the marker is found from a subdirectory, the way git finds its root;
|
||||
* a bare integer works, because that is what a person writes by hand;
|
||||
* the marker BEATS a git remote, since someone put the file there on purpose;
|
||||
* an `instance` naming a different Scribe is refused — the case the field
|
||||
* an `instance` naming a different Grimoire is refused — the case the field
|
||||
exists for. A project id means nothing on its own: id 2 is a different
|
||||
project on every instance, so a marker that travels would otherwise scope
|
||||
the session to the wrong project, confidently and without a word.
|
||||
@@ -73,7 +73,7 @@ def test_the_marker_is_found_from_a_subdirectory(tmp_path):
|
||||
|
||||
def test_a_matching_instance_is_honoured(tmp_path):
|
||||
"""Host-only comparison, so http/https and a trailing slash are not a
|
||||
mismatch — only a genuinely different Scribe is."""
|
||||
mismatch — only a genuinely different Grimoire is."""
|
||||
_marker(tmp_path, instance="http://scribe.example.com/", project_id=2)
|
||||
assert _sh(f'scribe_scope_query "{tmp_path}"', tmp_path) == "project_id=2"
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@ def test_a_repo_binding_carries_the_branch_its_ledger_follows():
|
||||
"""Without `ref` a restored binding silently falls back to the
|
||||
default branch and the shape ledger starts accounting for a different
|
||||
tree — a wrong answer that looks like a working one."""
|
||||
b = SimpleNamespace(user_id=7, project_id=2, repo_key="Scribe", ref="dev")
|
||||
b = SimpleNamespace(user_id=7, project_id=2, repo_key="Grimoire", ref="dev")
|
||||
[row] = backup._repo_binding_rows([b])
|
||||
assert row["ref"] == "dev"
|
||||
|
||||
|
||||
@@ -126,7 +126,7 @@ def test_checking_a_folder_leaves_nothing_behind(tmp_path):
|
||||
(tmp_path / "scribe-20261014T020000Z.tar").write_bytes(b"x")
|
||||
config = BackupConfig(folder_path=str(tmp_path))
|
||||
said = asyncio.run(check_destination(config, None))
|
||||
assert "holds 1 Scribe backup" in said
|
||||
assert "holds 1 Grimoire backup" in said
|
||||
assert [p.name for p in tmp_path.iterdir()] == ["scribe-20261014T020000Z.tar"]
|
||||
|
||||
|
||||
@@ -176,4 +176,4 @@ def test_checking_s3_only_lists():
|
||||
|
||||
said = asyncio.run(check_destination(S3, "secret", transport=httpx.MockTransport(handler)))
|
||||
assert methods == ["GET"]
|
||||
assert "holds 1 Scribe backup" in said
|
||||
assert "holds 1 Grimoire backup" in said
|
||||
|
||||
@@ -241,7 +241,7 @@ async def test_same_location_is_a_duplicate_however_it_is_described():
|
||||
7, "primaryButton — something else entirely", body="x" * 400,
|
||||
project_id=2, is_task=False, note_type="snippet",
|
||||
code=".btn-primary { color: red; }",
|
||||
locations=[{"repo": "Scribe", "path": "a/b.css", "symbol": ".btn-primary"}],
|
||||
locations=[{"repo": "Grimoire", "path": "a/b.css", "symbol": ".btn-primary"}],
|
||||
)
|
||||
assert dup is not None
|
||||
assert dup.reason == "location"
|
||||
@@ -261,7 +261,7 @@ async def test_identical_code_is_a_duplicate_at_a_different_location():
|
||||
dup = await find_duplicate_note(
|
||||
7, "unionFind", body="x" * 400, project_id=2, is_task=False,
|
||||
note_type="snippet", code="def f():\n return 1",
|
||||
locations=[{"repo": "Scribe", "path": "z.py", "symbol": "f"}],
|
||||
locations=[{"repo": "Grimoire", "path": "z.py", "symbol": "f"}],
|
||||
)
|
||||
assert dup is not None
|
||||
assert dup.reason == "code"
|
||||
@@ -279,7 +279,7 @@ async def test_a_location_without_a_symbol_is_not_an_identity():
|
||||
dup = await find_duplicate_note(
|
||||
7, "Some recipe", body="x" * 400, project_id=2, is_task=False,
|
||||
note_type="snippet", code="",
|
||||
locations=[{"repo": "Scribe", "path": "a/b.css", "symbol": ""}],
|
||||
locations=[{"repo": "Grimoire", "path": "a/b.css", "symbol": ""}],
|
||||
)
|
||||
assert dup is None
|
||||
# Exactly one query — the title check. With no symbol and no code there is
|
||||
|
||||
@@ -21,7 +21,7 @@ async def test_create_with_a_parent_is_denied_without_write_on_that_parent():
|
||||
with patch("scribe.services.design_systems.access") as acc:
|
||||
acc.can_write_design_system = AsyncMock(return_value=False)
|
||||
from scribe.services.design_systems import create_design_system
|
||||
result = await create_design_system(user_id=1, title="Scribe", parent_id=7)
|
||||
result = await create_design_system(user_id=1, title="Grimoire", parent_id=7)
|
||||
assert result is None
|
||||
|
||||
|
||||
|
||||
@@ -126,7 +126,7 @@ async def test_build_session_context_includes_project_when_scoped():
|
||||
# No design system on the project -> no design block at all. An install with
|
||||
# none is the ordinary case, not a degraded one.
|
||||
assert "## Design system" not in out["context"]
|
||||
# Live state only: how to work with Scribe is the
|
||||
# Live state only: how to work with Grimoire is the
|
||||
# using-scribe skill's to say. A restated reflex here is a copy that drifts.
|
||||
assert 'content_type="rule"' not in out["context"]
|
||||
assert "Reflex:" not in out["context"]
|
||||
@@ -249,7 +249,7 @@ async def test_a_project_id_that_does_not_resolve_is_reported_not_dropped():
|
||||
assert "list_projects" in ctx
|
||||
# Not mistaken for the repo case, which has a different remedy.
|
||||
assert "## Repository not yet bound" not in ctx
|
||||
assert "No Scribe project is bound" not in ctx
|
||||
assert "No Grimoire project is bound" not in ctx
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -380,7 +380,7 @@ async def _autoinject(main_hits, reuse_hits, cfg=None, lesson_hits=None):
|
||||
@pytest.mark.asyncio
|
||||
async def test_a_snippet_takes_the_last_slot_when_none_won_on_score():
|
||||
"""The measured failure: a prompt asking for a helper returned three project
|
||||
records ABOUT building the retrieval system, and zero snippets. Scribe's own
|
||||
records ABOUT building the retrieval system, and zero snippets. Grimoire's own
|
||||
records are about software work, so they share vocabulary with any coding
|
||||
prompt while answering none of them."""
|
||||
main = [(0.66, fake_note(id=1, title="Step 3: title-first auto-inject", user_id=1)),
|
||||
|
||||
@@ -42,7 +42,7 @@ async def test_a_number_just_above_the_highest_id_is_a_guess():
|
||||
|
||||
|
||||
async def test_existing_ids_and_far_numbers_are_not_guesses():
|
||||
"""Below the max is a real record; far above it is not a Scribe id at all
|
||||
"""Below the max is a real record; far above it is not a Grimoire id at all
|
||||
(a PR, a forge issue) — neither may be refused."""
|
||||
far = 100 + GUESS_WINDOW + 1
|
||||
with patch.object(record_refs, "_max_note_id", AsyncMock(return_value=100)):
|
||||
|
||||
@@ -4,7 +4,7 @@ A record another user owns is that person's suggestion, not the operator's
|
||||
settled practice — and the two are indistinguishable unless every surface that
|
||||
hands one over says whose it is. These tests hold that line at the labelling
|
||||
helpers and at the process→skill manifest, which is the most consequential
|
||||
passive surface Scribe has (each entry becomes an auto-surfacing skill file).
|
||||
passive surface Grimoire has (each entry becomes an auto-surfacing skill file).
|
||||
"""
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
@@ -94,11 +94,11 @@ async def test_shared_process_skill_stub_never_claims_to_be_the_operators():
|
||||
|
||||
own, shared = out["processes"]
|
||||
|
||||
assert "operator's saved Scribe process" in own["description"]
|
||||
assert "operator's saved Grimoire process" in own["description"]
|
||||
assert "shared" not in own
|
||||
|
||||
assert shared["shared"] is True
|
||||
assert shared["owner"] == "alex"
|
||||
assert "operator's saved Scribe process" not in shared["description"]
|
||||
assert "operator's saved Grimoire process" not in shared["description"]
|
||||
assert "alex" in shared["description"]
|
||||
assert "go-ahead" in shared["description"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Tests for the drift check — does a snippet still match its source?
|
||||
|
||||
The check itself runs agent-side (Scribe has no checkout), so what's testable
|
||||
The check itself runs agent-side (Grimoire has no checkout), so what's testable
|
||||
here is everything around it: the verdict's shape, the rule that a verdict
|
||||
expires when the code it blessed is edited away, and the two dialects of the
|
||||
filter predicate — SQL for the browse/keyword arms, Python for the semantic
|
||||
|
||||
@@ -31,9 +31,9 @@ def _data(*locations: dict) -> dict:
|
||||
|
||||
def test_location_parts_keeps_only_what_was_asked_for():
|
||||
assert location_parts() == {}
|
||||
assert location_parts(repo="Scribe") == {"repo": "Scribe"}
|
||||
assert location_parts(repo=" Scribe ", path=" src/x.py ") == {
|
||||
"repo": "Scribe", "path": "src/x.py",
|
||||
assert location_parts(repo="Grimoire") == {"repo": "Grimoire"}
|
||||
assert location_parts(repo=" Grimoire ", path=" src/x.py ") == {
|
||||
"repo": "Grimoire", "path": "src/x.py",
|
||||
}
|
||||
|
||||
|
||||
@@ -54,16 +54,16 @@ def test_no_parts_matches_everything():
|
||||
|
||||
|
||||
def test_matches_exact_repo_path_and_symbol():
|
||||
data = _data({"repo": "Scribe", "path": "src/scribe/x.py", "symbol": "helper"})
|
||||
assert location_matches(data, {"repo": "Scribe"})
|
||||
assert location_matches(data, {"repo": "scribe"}) # repo names: case never distinguishes
|
||||
data = _data({"repo": "Grimoire", "path": "src/scribe/x.py", "symbol": "helper"})
|
||||
assert location_matches(data, {"repo": "Grimoire"})
|
||||
assert location_matches(data, {"repo": "grimoire"}) # repo names: case never distinguishes
|
||||
assert location_matches(data, {"path": "src/scribe/x.py"})
|
||||
assert location_matches(data, {"symbol": "helper"})
|
||||
assert location_matches(data, {"repo": "Scribe", "symbol": "helper"})
|
||||
assert location_matches(data, {"repo": "Grimoire", "symbol": "helper"})
|
||||
|
||||
|
||||
def test_path_matches_as_a_directory_prefix():
|
||||
data = _data({"repo": "Scribe", "path": "frontend/src/lib/x.ts", "symbol": ""})
|
||||
data = _data({"repo": "Grimoire", "path": "frontend/src/lib/x.ts", "symbol": ""})
|
||||
assert location_matches(data, {"path": "frontend/src"})
|
||||
assert location_matches(data, {"path": "frontend/src/"})
|
||||
assert location_matches(data, {"path": "frontend/src/lib/x.ts"})
|
||||
@@ -71,39 +71,39 @@ def test_path_matches_as_a_directory_prefix():
|
||||
|
||||
def test_path_prefix_stops_at_a_directory_boundary():
|
||||
"""`frontend/src` must not match `frontend/srcmap.ts` — a sibling, not a child."""
|
||||
data = _data({"repo": "Scribe", "path": "frontend/srcmap.ts", "symbol": ""})
|
||||
data = _data({"repo": "Grimoire", "path": "frontend/srcmap.ts", "symbol": ""})
|
||||
assert not location_matches(data, {"path": "frontend/src"})
|
||||
|
||||
|
||||
def test_parts_must_all_match_the_same_location():
|
||||
"""Repo A in one entry and path B in another is not "recorded at A/B"."""
|
||||
data = _data(
|
||||
{"repo": "Scribe", "path": "src/a.py", "symbol": ""},
|
||||
{"repo": "Grimoire", "path": "src/a.py", "symbol": ""},
|
||||
{"repo": "Portal", "path": "src/b.py", "symbol": ""},
|
||||
)
|
||||
assert location_matches(data, {"repo": "Scribe", "path": "src/a.py"})
|
||||
assert not location_matches(data, {"repo": "Scribe", "path": "src/b.py"})
|
||||
assert location_matches(data, {"repo": "Grimoire", "path": "src/a.py"})
|
||||
assert not location_matches(data, {"repo": "Grimoire", "path": "src/b.py"})
|
||||
|
||||
|
||||
def test_missing_or_empty_data_never_matches():
|
||||
assert not location_matches(None, {"repo": "Scribe"})
|
||||
assert not location_matches({}, {"repo": "Scribe"})
|
||||
assert not location_matches({"locations": []}, {"repo": "Scribe"})
|
||||
assert not location_matches({"name": "x"}, {"repo": "Scribe"})
|
||||
assert not location_matches(None, {"repo": "Grimoire"})
|
||||
assert not location_matches({}, {"repo": "Grimoire"})
|
||||
assert not location_matches({"locations": []}, {"repo": "Grimoire"})
|
||||
assert not location_matches({"name": "x"}, {"repo": "Grimoire"})
|
||||
|
||||
|
||||
def test_blank_recorded_part_does_not_match_a_requested_one():
|
||||
data = _data({"repo": "Scribe", "path": "", "symbol": ""})
|
||||
assert not location_matches(data, {"repo": "Scribe", "symbol": "helper"})
|
||||
data = _data({"repo": "Grimoire", "path": "", "symbol": ""})
|
||||
assert not location_matches(data, {"repo": "Grimoire", "symbol": "helper"})
|
||||
|
||||
|
||||
# --- location_jsonpath --------------------------------------------------------
|
||||
|
||||
def test_jsonpath_filters_within_one_locations_entry():
|
||||
expr = location_jsonpath({"repo": "Scribe", "symbol": "helper"})
|
||||
expr = location_jsonpath({"repo": "Grimoire", "symbol": "helper"})
|
||||
assert expr.startswith("$.locations[*] ? (")
|
||||
# repo: anchored, case-insensitive — mirrors location_matches.
|
||||
assert '@.repo like_regex "^Scribe$" flag "i"' in expr
|
||||
assert '@.repo like_regex "^Grimoire$" flag "i"' in expr
|
||||
assert '@.symbol == "helper"' in expr
|
||||
assert " && " in expr
|
||||
|
||||
@@ -144,7 +144,7 @@ def test_location_clause_renders_a_jsonpath_containment():
|
||||
|
||||
from scribe.services.knowledge import _location_clause
|
||||
|
||||
sql = str(_location_clause({"repo": "Scribe"}).compile(dialect=postgresql.dialect()))
|
||||
sql = str(_location_clause({"repo": "Grimoire"}).compile(dialect=postgresql.dialect()))
|
||||
assert "@?" in sql
|
||||
assert "data" in sql
|
||||
|
||||
@@ -178,9 +178,9 @@ async def test_mcp_tool_passes_locations_through_to_the_service():
|
||||
patch("scribe.services.access.label_shared_items",
|
||||
new=AsyncMock(return_value=[])):
|
||||
from scribe.mcp.tools.snippets import list_snippets
|
||||
await list_snippets(repo="Scribe", path="src/scribe", symbol="helper")
|
||||
await list_snippets(repo="Grimoire", path="src/scribe", symbol="helper")
|
||||
kwargs = m.await_args.kwargs
|
||||
assert kwargs["repo"] == "Scribe"
|
||||
assert kwargs["repo"] == "Grimoire"
|
||||
assert kwargs["path"] == "src/scribe"
|
||||
assert kwargs["symbol"] == "helper"
|
||||
|
||||
@@ -201,8 +201,8 @@ async def test_service_builds_the_filter_from_the_parts_given():
|
||||
with patch("scribe.services.knowledge.query_knowledge",
|
||||
new=AsyncMock(return_value=([], 0))) as m:
|
||||
from scribe.services.snippets import list_snippets
|
||||
await list_snippets(1, q="debounce", repo="Scribe", path=" src/ ")
|
||||
await list_snippets(1, q="debounce", repo="Grimoire", path=" src/ ")
|
||||
kwargs = m.await_args.kwargs
|
||||
assert kwargs["locations"] == {"repo": "Scribe", "path": "src/"}
|
||||
assert kwargs["locations"] == {"repo": "Grimoire", "path": "src/"}
|
||||
# Search and place compose — the location filter must not drop the query.
|
||||
assert kwargs["q"] == "debounce"
|
||||
|
||||
@@ -104,7 +104,7 @@ async def test_provenance_lives_and_dies_with_the_code_end_to_end(user_id):
|
||||
|
||||
note = await svc.create_snippet(
|
||||
user_id, name="prov_helper", code="def prov_helper():\n return 1\n",
|
||||
language="python", repo="Scribe", path="src/x.py", symbol="prov_helper",
|
||||
language="python", repo="Grimoire", path="src/x.py", symbol="prov_helper",
|
||||
commit_sha=SHA_A,
|
||||
)
|
||||
fields, view = await _fresh(user_id, note.id)
|
||||
@@ -140,7 +140,7 @@ async def test_verification_stamps_the_commit_and_ok_refreshes_provenance(user_i
|
||||
|
||||
note = await svc.create_snippet(
|
||||
user_id, name="prov_verify", code="def prov_verify():\n return 1\n",
|
||||
language="python", repo="Scribe", path="src/y.py", symbol="prov_verify",
|
||||
language="python", repo="Grimoire", path="src/y.py", symbol="prov_verify",
|
||||
commit_sha=SHA_A,
|
||||
)
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ def test_every_score_the_hook_can_write_reads_back(score) -> None:
|
||||
|
||||
|
||||
def test_the_real_reasons_from_the_poisoned_ledgers_parse() -> None:
|
||||
"""Verbatim from Portal and Scribe rows, so the parser is pinned to text
|
||||
"""Verbatim from Portal and Grimoire rows, so the parser is pinned to text
|
||||
that actually exists in the database rather than to text it generates."""
|
||||
assert stamp_score("hook: pulled #3283; payload resembles it (0.69)") == 0.69
|
||||
assert stamp_score("hook: pulled #3283; payload resembles it (0.77)") == 0.77
|
||||
@@ -93,7 +93,7 @@ def test_a_stamp_at_or_above_the_floor_is_not_listed() -> None:
|
||||
|
||||
@pytest.mark.parametrize("by", ["agent", "audit", "import", "mechanical"])
|
||||
def test_a_judgment_is_never_weak_however_it_would_have_scored(by) -> None:
|
||||
"""The measured case: 302 of Scribe's 334 rows under one canon were
|
||||
"""The measured case: 302 of Grimoire's 334 rows under one canon were
|
||||
`audit`-classified with no score at all. Listing those as weak would
|
||||
invite an agent to withdraw the only judgments in the ledger that were
|
||||
ever actually made by one."""
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""task_document — the document a TASK is embedded as, work logs included.
|
||||
|
||||
A work log is the richest record Scribe holds of *why* something is the way it
|
||||
A work log is the richest record Grimoire holds of *why* something is the way it
|
||||
is: prose written during the work, saying what was tried and ruled out. A log
|
||||
that is readable but not embedded is not findable, so "has anyone tried this
|
||||
approach?" — the question a log answers — cannot reach it.
|
||||
|
||||
@@ -10,7 +10,7 @@ almost nothing else does".
|
||||
|
||||
So the guidance is not decoration on this feature, it IS the feature's
|
||||
precondition, and product guidance lives in the tool docstrings and the skill
|
||||
rather than in a Scribe rule. That makes it exactly the kind of prose a later
|
||||
rather than in a Grimoire rule. That makes it exactly the kind of prose a later
|
||||
docstring tidy-up deletes without noticing what it was for.
|
||||
|
||||
WHAT THIS PINS, AND WHAT IT DOES NOT
|
||||
|
||||
@@ -1055,7 +1055,7 @@ def test_hook_stays_a_get_so_a_read_scoped_key_works():
|
||||
|
||||
|
||||
def test_hook_exits_silently_when_unconfigured():
|
||||
"""An install with no Scribe URL/token must produce no output at all."""
|
||||
"""An install with no Grimoire URL/token must produce no output at all."""
|
||||
out = subprocess.run(
|
||||
["bash", str(HOOK)],
|
||||
input=json.dumps({
|
||||
@@ -1137,7 +1137,7 @@ def _write_event(repo, session="s-nudge"):
|
||||
|
||||
|
||||
def test_hook_nudges_recording_when_copies_exist_but_nothing_is_recorded(tmp_path):
|
||||
"""The local arm proves duplication; when Scribe ANSWERS that it has
|
||||
"""The local arm proves duplication; when Grimoire ANSWERS that it has
|
||||
no record of it, the same context block must ask for create_snippet — the
|
||||
one moment the recording nudge is earned rather than noise."""
|
||||
with http_sink(b'{"context":"","note_ids":[],"sync_note_ids":[]}') as (port, seen):
|
||||
@@ -1172,7 +1172,7 @@ def test_hook_says_when_scribe_did_not_answer_once_per_outage(tmp_path):
|
||||
assert out.returncode == 0
|
||||
ctx = json.loads(out.stdout)["hookSpecificOutput"]["additionalContext"]
|
||||
assert "already defined" in ctx
|
||||
assert "Scribe did not answer the prior-art check for `b.py` within 5s" in ctx
|
||||
assert "Grimoire did not answer the prior-art check for `b.py` within 5s" in ctx
|
||||
assert "UNCHECKED" in ctx and "list_shapes" in ctx
|
||||
assert "None of those existing copies is recorded" not in ctx
|
||||
assert marker.is_file()
|
||||
@@ -1198,7 +1198,7 @@ def test_hook_says_when_scribe_did_not_answer_once_per_outage(tmp_path):
|
||||
out = subprocess.run(["bash", str(HOOK)], input=_write_event(repo, "s-out-2"),
|
||||
capture_output=True, text=True, env=env)
|
||||
ctx = json.loads(out.stdout)["hookSpecificOutput"]["additionalContext"]
|
||||
assert ctx.startswith("> Scribe did not answer")
|
||||
assert ctx.startswith("> Grimoire did not answer")
|
||||
|
||||
|
||||
def test_hook_stays_quiet_about_recording_when_nothing_is_duplicated(tmp_path):
|
||||
|
||||
Reference in New Issue
Block a user