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
+52 -52
View File
@@ -5,38 +5,38 @@ from pathlib import Path
from quart import Quart, g, jsonify, make_response, request, send_from_directory
from scribe.config import Config
from scribe.routes.admin import admin_bp
from scribe.routes.api import api
from scribe.routes.auth import auth_bp
from scribe.routes.export import export_bp
from scribe.routes.notes import notes_bp
from scribe.routes.milestones import milestones_bp
from scribe.routes.task_logs import task_logs_bp
from scribe.routes.projects import projects_bp
from scribe.routes.retrieval import retrieval_bp
from scribe.routes.settings import settings_bp
from scribe.routes.tasks import tasks_bp
from scribe.routes.groups import groups_bp
from scribe.routes.shares import shares_bp
from scribe.routes.in_app_notifications import notifications_bp
from scribe.routes.users import users_bp
from scribe.routes.api_keys import api_keys_bp
from scribe.routes.search import search_bp
from scribe.routes.knowledge import knowledge_bp
from scribe.routes.rulebooks import rulebooks_bp
from scribe.routes.plugin import plugin_bp
from scribe.routes.design_systems import design_systems_bp
from scribe.routes.trash import trash_bp
from scribe.routes.dashboard import dashboard_bp
from scribe.routes.systems import systems_bp
from scribe.routes.canonical_systems import canonical_systems_bp
from scribe.routes.platforms import platforms_bp
from scribe.routes.family import family_bp
from scribe.routes.lessons import lessons_bp
from scribe.routes.snippets import snippets_bp
from scribe.routes.webhooks import webhooks_bp
from scribe.mcp import mount_mcp
from grimoire.config import Config
from grimoire.routes.admin import admin_bp
from grimoire.routes.api import api
from grimoire.routes.auth import auth_bp
from grimoire.routes.export import export_bp
from grimoire.routes.notes import notes_bp
from grimoire.routes.milestones import milestones_bp
from grimoire.routes.task_logs import task_logs_bp
from grimoire.routes.projects import projects_bp
from grimoire.routes.retrieval import retrieval_bp
from grimoire.routes.settings import settings_bp
from grimoire.routes.tasks import tasks_bp
from grimoire.routes.groups import groups_bp
from grimoire.routes.shares import shares_bp
from grimoire.routes.in_app_notifications import notifications_bp
from grimoire.routes.users import users_bp
from grimoire.routes.api_keys import api_keys_bp
from grimoire.routes.search import search_bp
from grimoire.routes.knowledge import knowledge_bp
from grimoire.routes.rulebooks import rulebooks_bp
from grimoire.routes.plugin import plugin_bp
from grimoire.routes.design_systems import design_systems_bp
from grimoire.routes.trash import trash_bp
from grimoire.routes.dashboard import dashboard_bp
from grimoire.routes.systems import systems_bp
from grimoire.routes.canonical_systems import canonical_systems_bp
from grimoire.routes.platforms import platforms_bp
from grimoire.routes.family import family_bp
from grimoire.routes.lessons import lessons_bp
from grimoire.routes.snippets import snippets_bp
from grimoire.routes.webhooks import webhooks_bp
from grimoire.mcp import mount_mcp
STATIC_DIR = Path(__file__).parent / "static"
logger = logging.getLogger(__name__)
@@ -131,7 +131,7 @@ def create_app() -> Quart:
# Log usage for API requests (skip logs endpoint to avoid recursion)
if request.path.startswith("/api/") and not request.path.startswith("/api/admin/logs"):
try:
from scribe.services.logging import log_usage
from grimoire.services.logging import log_usage
user = getattr(g, "user", None)
await log_usage(
@@ -173,13 +173,13 @@ def create_app() -> Quart:
# `create_app`, which runs before either is true.
_startup_finished = asyncio.Event()
from scribe.services.auth import start_auth_token_retention_loop
from scribe.services.embeddings import (
from grimoire.services.auth import start_auth_token_retention_loop
from grimoire.services.embeddings import (
backfill_milestone_embeddings, backfill_note_embeddings,
backfill_rule_embeddings, backfill_system_embeddings,
)
from scribe.services.logging import start_log_retention_loop
from scribe.services.notifications import start_notification_loop
from grimoire.services.logging import start_log_retention_loop
from grimoire.services.notifications import start_notification_loop
start_log_retention_loop()
start_notification_loop()
@@ -200,7 +200,7 @@ def create_app() -> Quart:
# answer this exists to stop giving. Failure is logged and swallowed —
# provenance is worth a lot and never worth refusing to boot.
try:
from scribe.services.retrieval_tuning import record_release_defaults
from grimoire.services.retrieval_tuning import record_release_defaults
moved = await record_release_defaults()
for row in moved:
if not row["baseline"]:
@@ -255,7 +255,7 @@ def create_app() -> Quart:
# row would read as "no snippet here" rather than as a gap. Separate
# try block so neither backfill can skip the other.
try:
from scribe.services.snippets import backfill_snippet_data
from grimoire.services.snippets import backfill_snippet_data
await backfill_snippet_data()
except Exception:
logger.warning("Snippet data backfill failed", exc_info=True)
@@ -272,17 +272,17 @@ def create_app() -> Quart:
# living on as a coroutine nobody will ever wake.
try:
# Recurrence scheduler (recurring-task spawn every 15m)
from scribe.services.recurrence_scheduler import start_recurrence_scheduler
from grimoire.services.recurrence_scheduler import start_recurrence_scheduler
start_recurrence_scheduler(asyncio.get_running_loop())
# Version-pinning scheduler (daily auto-pin scan at 03:00 UTC)
from scribe.services.version_pinning_scheduler import (
from grimoire.services.version_pinning_scheduler import (
start_version_pinning_scheduler,
)
start_version_pinning_scheduler(asyncio.get_running_loop())
# Trash retention scheduler (daily expired-trash purge at 03:30 UTC)
from scribe.services.trash_scheduler import start_trash_scheduler
from grimoire.services.trash_scheduler import start_trash_scheduler
start_trash_scheduler(asyncio.get_running_loop())
# DB maintenance scheduler (daily targeted VACUUM ANALYZE, default
@@ -291,7 +291,7 @@ def create_app() -> Quart:
# comment above `_STARTUP_READ_TIMEOUT`. Anything added here that
# touches the database needs the same treatment: a lifespan hook
# that does not return is a worker that never serves.
from scribe.services.db_maintenance_scheduler import (
from grimoire.services.db_maintenance_scheduler import (
get_maintenance_hour,
start_db_maintenance_scheduler,
)
@@ -301,13 +301,13 @@ def create_app() -> Quart:
# Scheduled backups. An hourly tick that reads its config when it
# fires, so starting it reads nothing here.
from scribe.services.backup_job import start_backup_scheduler
from grimoire.services.backup_job import start_backup_scheduler
start_backup_scheduler(asyncio.get_running_loop())
# Diagnostic instrumentation — heartbeat, signal handlers, asyncio
# exception hook. Cheap (~1 log line/min), high diagnostic value when
# the app crashes mysteriously. See services/diagnostics.py.
from scribe.services.diagnostics import start_diagnostics
from grimoire.services.diagnostics import start_diagnostics
start_diagnostics(asyncio.get_running_loop())
finally:
# STARTUP IS OVER — release the backfill. Everything above
@@ -318,21 +318,21 @@ def create_app() -> Quart:
@app.after_serving
async def shutdown():
from scribe.services.recurrence_scheduler import stop_recurrence_scheduler
from grimoire.services.recurrence_scheduler import stop_recurrence_scheduler
stop_recurrence_scheduler()
from scribe.services.version_pinning_scheduler import (
from grimoire.services.version_pinning_scheduler import (
stop_version_pinning_scheduler,
)
stop_version_pinning_scheduler()
from scribe.services.trash_scheduler import stop_trash_scheduler
from grimoire.services.trash_scheduler import stop_trash_scheduler
stop_trash_scheduler()
from scribe.services.db_maintenance_scheduler import (
from grimoire.services.db_maintenance_scheduler import (
stop_db_maintenance_scheduler,
)
stop_db_maintenance_scheduler()
from scribe.services.backup_job import stop_backup_scheduler
from grimoire.services.backup_job import stop_backup_scheduler
stop_backup_scheduler()
from scribe.services.diagnostics import stop_diagnostics
from grimoire.services.diagnostics import stop_diagnostics
stop_diagnostics()
@app.route("/")
@@ -382,7 +382,7 @@ def create_app() -> Quart:
logger.exception("Internal server error on %s %s", request.method, request.path)
try:
from scribe.services.logging import log_error
from grimoire.services.logging import log_error
user = getattr(g, "user", None)
await log_error(
+2 -2
View File
@@ -2,8 +2,8 @@ import functools
from quart import g, jsonify, request, session
from scribe.services.auth import get_user_by_id
from scribe.services.api_keys import lookup_key
from grimoire.services.auth import get_user_by_id
from grimoire.services.api_keys import lookup_key
def _check_auth(f, required_role: str | None = None):
@@ -20,7 +20,7 @@ class Config:
DATABASE_URL: str = _read_secret(
"DATABASE_URL",
"DATABASE_URL_FILE",
"postgresql+asyncpg://scribe:scribe@localhost:5432/scribe",
"postgresql+asyncpg://grimoire:grimoire@localhost:5432/grimoire",
)
SECRET_KEY: str = _read_secret("SECRET_KEY", "SECRET_KEY_FILE", "dev-secret-change-me")
SECURE_COOKIES: bool = os.environ.get("SECURE_COOKIES", "").lower() in ("1", "true", "yes")
@@ -3,6 +3,6 @@
Auth uses the existing `api_keys` table: a Bearer token in the Authorization
header resolves to a user, and every tool call acts on that user's data only.
"""
from scribe.mcp.server import build_mcp_server, mount_mcp
from grimoire.mcp.server import build_mcp_server, mount_mcp
__all__ = ["build_mcp_server", "mount_mcp"]
@@ -1,7 +1,7 @@
"""Per-request MCP context.
The ASGI middleware (mcp/server.py) populates `_user_id_ctx` from
scope['scribe_user_id'] before dispatching to MCPServer tool handlers.
scope['grimoire_user_id'] before dispatching to MCPServer tool handlers.
Tool functions then call `current_user_id()` to retrieve it.
Tools must never fall back to a default user — `current_user_id()` raises
@@ -9,7 +9,7 @@ if called outside a properly-authed MCP request.
"""
from contextvars import ContextVar
_user_id_ctx: ContextVar[int | None] = ContextVar("scribe_mcp_user_id", default=None)
_user_id_ctx: ContextVar[int | None] = ContextVar("grimoire_mcp_user_id", default=None)
def current_user_id() -> int:
@@ -1,7 +1,7 @@
"""MCP-side Bearer token resolution. Reuses the existing api_keys infrastructure."""
from __future__ import annotations
from scribe.services.api_keys import lookup_key
from grimoire.services.api_keys import lookup_key
@@ -354,7 +354,7 @@ def build_mcp_server() -> MCPServer:
here: since SDK v2 they belong to the ASGI app, built in `mount_mcp`.
"""
mcp = StrictArgsMCPServer("scribe", instructions=_INSTRUCTIONS.strip())
from scribe.mcp.tools import register_all
from grimoire.mcp.tools import register_all
register_all(mcp)
return mcp
@@ -364,7 +364,7 @@ def mount_mcp(app: Quart) -> None:
A small ASGI middleware between Quart and the MCPServer sub-app validates the
Bearer token against the api_keys table. Authenticated requests have their
user_id attached to the ASGI scope under "scribe_user_id" for tool handlers
user_id attached to the ASGI scope under "grimoire_user_id" for tool handlers
to read.
The SDK's streamable_http session manager owns a task group that must be
@@ -373,7 +373,7 @@ def mount_mcp(app: Quart) -> None:
inside Quart, we hook the session manager's `run()` async context manager
into Quart's serving lifecycle (before_serving / after_serving).
"""
from scribe.mcp.auth import resolve_bearer
from grimoire.mcp.auth import resolve_bearer
mcp = build_mcp_server()
# stateless_http=True: don't hand the client a persistent Mcp-Session-Id.
@@ -427,7 +427,7 @@ def mount_mcp(app: Quart) -> None:
"status": 401,
"headers": [
(b"content-type", b"application/json"),
(b"www-authenticate", b'Bearer realm="scribe-mcp"'),
(b"www-authenticate", b'Bearer realm="grimoire-mcp"'),
],
})
await send({
@@ -456,8 +456,8 @@ def mount_mcp(app: Quart) -> None:
})
return
scope["scribe_user_id"] = user_id
from scribe.mcp._context import _user_id_ctx
scope["grimoire_user_id"] = user_id
from grimoire.mcp._context import _user_id_ctx
token = _user_id_ctx.set(user_id)
try:
await mcp_asgi(scope, receive, send)
@@ -4,7 +4,7 @@ Each tool module exposes a `register(mcp)` function that attaches its tools
to an MCPServer instance. `register_all(mcp)` is the single entry point called
from `mcp.server.build_mcp_server`.
"""
from scribe.mcp.tools import (
from grimoire.mcp.tools import (
design_systems, family, lessons, milestones, notes, processes, projects, recent, repos,
moments, platforms, retrieval_review, retrieval_tuning,
wide_net,
@@ -19,10 +19,10 @@ Sentinels, matching the milestone/task tool conventions:
"""
from __future__ import annotations
from scribe.mcp._context import current_user_id
from scribe.services import design_systems as ds_svc
from scribe.services.design_systems import DesignSystemCycle
from scribe.services.design_starter_roles import (
from grimoire.mcp._context import current_user_id
from grimoire.services import design_systems as ds_svc
from grimoire.services.design_systems import DesignSystemCycle
from grimoire.services.design_starter_roles import (
ALL_GROUPS,
DEFAULT_TOKEN_PREFIX,
describe_groups,
@@ -6,9 +6,9 @@ and enforced by the service, and every decision is logged with its reason.
"""
from __future__ import annotations
from scribe.mcp._context import current_user_id
from scribe.services import family as family_svc
from scribe.services import family_adoption as adoption_svc
from grimoire.mcp._context import current_user_id
from grimoire.services import family as family_svc
from grimoire.services import family_adoption as adoption_svc
async def list_family_ideas(
@@ -9,17 +9,17 @@ would be something the writer had to know to include.
"""
from __future__ import annotations
from scribe.mcp._context import current_user_id
from scribe.services import access as access_svc
from scribe.services import dedup as dedup_svc
from scribe.services import knowledge as knowledge_svc
from scribe.services import lesson_rules as lesson_rules_svc
from scribe.services import lessons as lessons_svc
from scribe.services import systems as systems_svc
from scribe.services import trash as trash_svc
from scribe.mcp.tools import systems as systems_tools
from scribe.services.note_usage import attach_usage, record_pulled
from scribe.services import moment_delivery
from grimoire.mcp._context import current_user_id
from grimoire.services import access as access_svc
from grimoire.services import dedup as dedup_svc
from grimoire.services import knowledge as knowledge_svc
from grimoire.services import lesson_rules as lesson_rules_svc
from grimoire.services import lessons as lessons_svc
from grimoire.services import systems as systems_svc
from grimoire.services import trash as trash_svc
from grimoire.mcp.tools import systems as systems_tools
from grimoire.services.note_usage import attach_usage, record_pulled
from grimoire.services import moment_delivery
# The payload shape lives in the service (`lesson_to_dict`), shared with the
@@ -11,16 +11,16 @@ Sentinels:
"""
from __future__ import annotations
from scribe.mcp._context import current_user_id
from scribe.services import dedup as dedup_svc
from scribe.services import family as family_svc
from scribe.services import milestones as milestones_svc
from scribe.services import notes as notes_svc
from scribe.services import task_logs as task_logs_svc
from scribe.services import rulebooks as rulebooks_svc
from scribe.services import trash as trash_svc
from scribe.services.record_refs import refuse_guessed_ids
from scribe.services import moment_delivery
from grimoire.mcp._context import current_user_id
from grimoire.services import dedup as dedup_svc
from grimoire.services import family as family_svc
from grimoire.services import milestones as milestones_svc
from grimoire.services import notes as notes_svc
from grimoire.services import task_logs as task_logs_svc
from grimoire.services import rulebooks as rulebooks_svc
from grimoire.services import trash as trash_svc
from grimoire.services.record_refs import refuse_guessed_ids
from grimoire.services import moment_delivery
async def list_milestones(project_id: int) -> dict:
@@ -12,11 +12,11 @@ operator's yes.
"""
from __future__ import annotations
from scribe.mcp._context import current_user_id
from scribe.services import moment_actions as actions_svc
from scribe.services import moments as moments_svc
from scribe.services import reply_shapes as shapes_svc
from scribe.services import rule_moment_judgments as judgments_svc
from grimoire.mcp._context import current_user_id
from grimoire.services import moment_actions as actions_svc
from grimoire.services import moments as moments_svc
from grimoire.services import reply_shapes as shapes_svc
from grimoire.services import rule_moment_judgments as judgments_svc
async def list_moments() -> dict:
@@ -13,18 +13,18 @@ Sentinel conventions (inherited from existing fable-mcp tools):
"""
from __future__ import annotations
from scribe.mcp._context import current_user_id
from scribe.mcp.tools import systems as systems_tools
from scribe.services import access as access_svc
from scribe.services import dedup as dedup_svc
from scribe.services import family as family_svc
from scribe.services import notes as notes_svc
from scribe.services import supersession as supersession_svc
from scribe.services import systems as systems_svc
from scribe.services import trash as trash_svc
from scribe.services.note_usage import record_pulled
from scribe.services.record_refs import refuse_guessed_ids
from scribe.services import moment_delivery
from grimoire.mcp._context import current_user_id
from grimoire.mcp.tools import systems as systems_tools
from grimoire.services import access as access_svc
from grimoire.services import dedup as dedup_svc
from grimoire.services import family as family_svc
from grimoire.services import notes as notes_svc
from grimoire.services import supersession as supersession_svc
from grimoire.services import systems as systems_svc
from grimoire.services import trash as trash_svc
from grimoire.services.note_usage import record_pulled
from grimoire.services.record_refs import refuse_guessed_ids
from grimoire.services import moment_delivery
async def list_notes(
@@ -5,8 +5,8 @@ platforms decide which family ideas reach it.
"""
from __future__ import annotations
from scribe.mcp._context import current_user_id
from scribe.services import platforms as platforms_svc
from grimoire.mcp._context import current_user_id
from grimoire.services import platforms as platforms_svc
async def list_platforms(project_id: int = 0) -> dict:
@@ -6,17 +6,17 @@ get_process is the fire mechanism: it returns the full prompt for Claude to run.
"""
from __future__ import annotations
from scribe.mcp._context import current_user_id
from scribe.services import access as access_svc
from scribe.services import dedup as dedup_svc
from scribe.services import knowledge as knowledge_svc
from scribe.services import moment_actions
from scribe.services import moments as moments_svc
from scribe.services import notes as notes_svc
from scribe.services import systems as systems_svc
from scribe.services import trash as trash_svc
from scribe.services.note_usage import record_pulled
from scribe.services import moment_delivery
from grimoire.mcp._context import current_user_id
from grimoire.services import access as access_svc
from grimoire.services import dedup as dedup_svc
from grimoire.services import knowledge as knowledge_svc
from grimoire.services import moment_actions
from grimoire.services import moments as moments_svc
from grimoire.services import notes as notes_svc
from grimoire.services import systems as systems_svc
from grimoire.services import trash as trash_svc
from grimoire.services.note_usage import record_pulled
from grimoire.services import moment_delivery
async def list_processes(
@@ -9,22 +9,22 @@ from __future__ import annotations
import logging
from scribe.mcp._context import current_user_id
from scribe.mcp.tools import systems as systems_tools
from scribe.services import coverage as coverage_svc
from scribe.services import design_systems as design_systems_svc
from scribe.services import family_adoption as family_adoption_svc
from scribe.services import inception as inception_svc
from scribe.services import milestones as milestones_svc
from scribe.services import notes as notes_svc
from scribe.services import platforms as platforms_svc
from scribe.services import projects as projects_svc
from scribe.services import reply_shapes as reply_shapes_svc
from scribe.services import rulebooks as rulebooks_svc
from scribe.services import systems as systems_svc
from scribe.services import trash as trash_svc
from scribe.services.background import spawn
from scribe.services.note_usage import record_surfaced
from grimoire.mcp._context import current_user_id
from grimoire.mcp.tools import systems as systems_tools
from grimoire.services import coverage as coverage_svc
from grimoire.services import design_systems as design_systems_svc
from grimoire.services import family_adoption as family_adoption_svc
from grimoire.services import inception as inception_svc
from grimoire.services import milestones as milestones_svc
from grimoire.services import notes as notes_svc
from grimoire.services import platforms as platforms_svc
from grimoire.services import projects as projects_svc
from grimoire.services import reply_shapes as reply_shapes_svc
from grimoire.services import rulebooks as rulebooks_svc
from grimoire.services import systems as systems_svc
from grimoire.services import trash as trash_svc
from grimoire.services.background import spawn
from grimoire.services.note_usage import record_surfaced
logger = logging.getLogger(__name__)
@@ -13,10 +13,10 @@ from datetime import datetime, timedelta, timezone
from sqlalchemy import select
from scribe.mcp._context import current_user_id
from scribe.models import async_session
from scribe.models.note import Note
from scribe.models.project import Project
from grimoire.mcp._context import current_user_id
from grimoire.models import async_session
from grimoire.models.note import Note
from grimoire.models.project import Project
async def get_recent(days: int = 7, limit: int = 25) -> dict:
@@ -8,9 +8,9 @@ it survives re-clones and ssh/https URL differences.
"""
from __future__ import annotations
from scribe.mcp._context import current_user_id
from scribe.services import projects as projects_svc
from scribe.services import repo_bindings as repo_bindings_svc
from grimoire.mcp._context import current_user_id
from grimoire.services import projects as projects_svc
from grimoire.services import repo_bindings as repo_bindings_svc
async def bind_repo(repo_url: str, project_id: int, ref: str = "") -> dict:
@@ -4,8 +4,8 @@
not. These two say whether the line RELATED, which is a reading only someone
looking at the query beside the line can do.
"""
from scribe.mcp._context import current_user_id
from scribe.services import retrieval_review as review_svc
from grimoire.mcp._context import current_user_id
from grimoire.services import retrieval_review as review_svc
async def menus_to_review(
@@ -4,9 +4,9 @@ The write half of the loop `retrieval_telemetry` opens. That tool says what a
bar did; these say what the bar IS, and let it be changed with the argument
attached.
"""
from scribe.mcp._context import current_user_id
from scribe.services import retrieval_migration as migration_svc
from scribe.services import retrieval_tuning as tuning_svc
from grimoire.mcp._context import current_user_id
from grimoire.services import retrieval_migration as migration_svc
from grimoire.services import retrieval_tuning as tuning_svc
async def retrieval_surfaces() -> dict:
@@ -10,15 +10,15 @@ preview-style warning.
"""
from __future__ import annotations
from scribe.mcp._context import current_user_id
from scribe.services import dedup as dedup_svc
from scribe.services import moments as moments_svc
from scribe.services import rulebooks as rulebooks_svc
from scribe.services import trash as trash_svc
from scribe.services.rule_usage import (
from grimoire.mcp._context import current_user_id
from grimoire.services import dedup as dedup_svc
from grimoire.services import moments as moments_svc
from grimoire.services import rulebooks as rulebooks_svc
from grimoire.services import trash as trash_svc
from grimoire.services.rule_usage import (
record_rule_outcome, record_rule_pulled,
)
from scribe.services import moment_delivery
from grimoire.services import moment_delivery
# ── Rulebook CRUD ───────────────────────────────────────────────────────
@@ -9,16 +9,16 @@ from __future__ import annotations
import time
from scribe.mcp._context import current_user_id
from scribe.services.access import owner_names_for
from scribe.services.knowledge import content_type_filters
from scribe.services.text import MATCHED_PASSAGE, excerpt_fields
from scribe.services.embeddings import (
from grimoire.mcp._context import current_user_id
from grimoire.services.access import owner_names_for
from grimoire.services.knowledge import content_type_filters
from grimoire.services.text import MATCHED_PASSAGE, excerpt_fields
from grimoire.services.embeddings import (
DEFAULT_SIMILARITY_THRESHOLD, semantic_search_milestones, semantic_search_notes,
semantic_search_rules, semantic_search_systems,
)
from scribe.services import rulebooks as rulebooks_svc
from scribe.services.retrieval_telemetry import record_retrieval, retrieval_summary
from grimoire.services import rulebooks as rulebooks_svc
from grimoire.services.retrieval_telemetry import record_retrieval, retrieval_summary
# A matched chunk is at most _CHUNK_CHAR_BUDGET (1400) characters, and it is
@@ -109,7 +109,7 @@ async def _search_milestones(uid: int, q: str, limit: int, project_id: int) -> d
chunks = report.get("best_chunk") or {}
progress: dict[int, dict] = {}
if raw:
from scribe.services import milestones as milestones_svc
from grimoire.services import milestones as milestones_svc
for pid in {m.project_id for _s, m in raw}:
for row in await milestones_svc.get_project_milestone_summary(uid, pid):
@@ -9,9 +9,9 @@ turn that the sync has not seen yet.
"""
from __future__ import annotations
from scribe.mcp._context import current_user_id
from scribe.services import coverage as coverage_svc
from scribe.services import shape_ledger as shape_ledger_svc
from grimoire.mcp._context import current_user_id
from grimoire.services import coverage as coverage_svc
from grimoire.services import shape_ledger as shape_ledger_svc
async def classify_shapes(
@@ -10,15 +10,15 @@ on create, System association passthrough).
"""
from __future__ import annotations
from scribe.mcp._context import current_user_id
from scribe.mcp.tools import systems as systems_tools
from scribe.services import access as access_svc
from scribe.services import dedup as dedup_svc
from scribe.services import family as family_svc
from scribe.services import snippets as snippets_svc
from scribe.services.note_usage import attach_usage, record_pulled
from scribe.services import systems as systems_svc
from scribe.services import moment_delivery
from grimoire.mcp._context import current_user_id
from grimoire.mcp.tools import systems as systems_tools
from grimoire.services import access as access_svc
from grimoire.services import dedup as dedup_svc
from grimoire.services import family as family_svc
from grimoire.services import snippets as snippets_svc
from grimoire.services.note_usage import attach_usage, record_pulled
from grimoire.services import systems as systems_svc
from grimoire.services import moment_delivery
async def list_snippets(
@@ -288,7 +288,7 @@ async def get_snippet(snippet_id: int, project_id: int = 0) -> dict:
# The structured consumer map: ledger rows judged against this
# snippet. Attached only when non-empty — and never for projects
# the caller can't read.
from scribe.services import shape_ledger as shape_ledger_svc
from grimoire.services import shape_ledger as shape_ledger_svc
consumers = await shape_ledger_svc.snippet_consumers(uid, int(note.id))
if consumers["instances"]:
@@ -13,13 +13,13 @@ Sentinels (match the milestone/task tool conventions):
"""
from __future__ import annotations
from scribe.mcp._context import current_user_id
from scribe.services import canonical_systems as canonical_systems_svc
from scribe.services import milestones as milestones_svc
from scribe.services import notes as notes_svc
from scribe.services import systems as systems_svc
from scribe.services import system_usage as system_usage_svc
from scribe.services.system_usage import record_system_pulled
from grimoire.mcp._context import current_user_id
from grimoire.services import canonical_systems as canonical_systems_svc
from grimoire.services import milestones as milestones_svc
from grimoire.services import notes as notes_svc
from grimoire.services import systems as systems_svc
from grimoire.services import system_usage as system_usage_svc
from grimoire.services.system_usage import record_system_pulled
# Below this, a project is young enough that the mild "which area is this
# about?" question stays proportionate; at or above it, a zero-Systems project
@@ -8,9 +8,9 @@ from __future__ import annotations
from sqlalchemy import select
from scribe.mcp._context import current_user_id
from scribe.models import async_session
from scribe.models.note import Note
from grimoire.mcp._context import current_user_id
from grimoire.models import async_session
from grimoire.models.note import Note
def _aggregate_tag_counts(tag_lists) -> dict[str, int]:
@@ -18,30 +18,30 @@ Sentinels (preserved from existing fable-mcp):
"""
from __future__ import annotations
from scribe.mcp._context import current_user_id
from scribe.mcp.tools import systems as systems_tools
from scribe.services import access as access_svc
from scribe.services import dedup as dedup_svc
from scribe.services import family as family_svc
from scribe.services import family_adoption as family_adoption_svc
from scribe.services import milestones as milestones_svc
from scribe.services import notes as notes_svc
from grimoire.mcp._context import current_user_id
from grimoire.mcp.tools import systems as systems_tools
from grimoire.services import access as access_svc
from grimoire.services import dedup as dedup_svc
from grimoire.services import family as family_svc
from grimoire.services import family_adoption as family_adoption_svc
from grimoire.services import milestones as milestones_svc
from grimoire.services import notes as notes_svc
# Imported by NAME, not reached through notes_svc: minted_kind is pure
# validation, not a service call, and a test that stubs the service module to
# avoid the database would otherwise stub the validation too — turning a
# guard into a MagicMock that approves anything.
from scribe.services.notes import minted_kind
from scribe.services import placement as placement_svc
from scribe.services import planning as planning_svc
from scribe.services import record_batch as batch_svc
from scribe.services import rulebooks as rulebooks_svc
from scribe.services import systems as systems_svc
from scribe.services import task_logs as task_logs_svc
from scribe.services import trash as trash_svc
from scribe.services.note_usage import record_pulled
from scribe.services.record_refs import refuse_guessed_ids
from scribe.services.text import elide
from scribe.services import moment_delivery
from grimoire.services.notes import minted_kind
from grimoire.services import placement as placement_svc
from grimoire.services import planning as planning_svc
from grimoire.services import record_batch as batch_svc
from grimoire.services import rulebooks as rulebooks_svc
from grimoire.services import systems as systems_svc
from grimoire.services import task_logs as task_logs_svc
from grimoire.services import trash as trash_svc
from grimoire.services.note_usage import record_pulled
from grimoire.services.record_refs import refuse_guessed_ids
from grimoire.services.text import elide
from grimoire.services import moment_delivery
# A work log entry is prose, often long — the discipline asks for what was
@@ -6,8 +6,8 @@ the deleted_batch_id that a delete operation returns.
"""
from __future__ import annotations
from scribe.mcp._context import current_user_id
from scribe.services import trash as trash_svc
from grimoire.mcp._context import current_user_id
from grimoire.services import trash as trash_svc
async def list_trash() -> dict:
@@ -35,9 +35,9 @@ from __future__ import annotations
import textwrap
import time
from scribe.mcp._context import current_user_id
from scribe.services.embeddings import semantic_search_rules
from scribe.services.retrieval_telemetry import record_retrieval
from grimoire.mcp._context import current_user_id
from grimoire.services.embeddings import semantic_search_rules
from grimoire.services.retrieval_telemetry import record_retrieval
# The telemetry `source`, and a PULL — never added to AMBIENT_SOURCES (those
# are deliveries nobody chose) and never confused with a push arm. Keeping it
@@ -1,7 +1,7 @@
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.orm import DeclarativeBase
from scribe.config import Config
from grimoire.config import Config
# Named rather than inlined so the deadline below is READABLE. SQLAlchemy
# captures `connect_args` in a closure and merges it at connect time, so an
@@ -44,53 +44,53 @@ class Base(DeclarativeBase):
pass
from scribe.models.base import CreatedAtMixin, TimestampMixin # noqa: E402, F401
from grimoire.models.base import CreatedAtMixin, TimestampMixin # noqa: E402, F401
from scribe.models.note import Note, TaskPriority, TaskStatus # noqa: E402, F401
from scribe.models.setting import Setting # noqa: E402, F401
from scribe.models.user import User # noqa: E402, F401
from scribe.models.app_log import AppLog # noqa: E402, F401
from scribe.models.password_reset import PasswordResetToken # noqa: E402, F401
from scribe.models.invitation import InvitationToken # noqa: E402, F401
from scribe.models.embedding import ( # noqa: E402, F401
from grimoire.models.note import Note, TaskPriority, TaskStatus # noqa: E402, F401
from grimoire.models.setting import Setting # noqa: E402, F401
from grimoire.models.user import User # noqa: E402, F401
from grimoire.models.app_log import AppLog # noqa: E402, F401
from grimoire.models.password_reset import PasswordResetToken # noqa: E402, F401
from grimoire.models.invitation import InvitationToken # noqa: E402, F401
from grimoire.models.embedding import ( # noqa: E402, F401
MilestoneEmbedding, NoteEmbedding, RuleEmbedding, SystemEmbedding,
)
from scribe.models.retrieval_log import RetrievalLog # noqa: E402, F401
from scribe.models.retrieval_tuning import RetrievalTuningEvent # noqa: E402, F401
from scribe.models.note_usage import NoteUsageEvent # noqa: E402, F401
from scribe.models.rule_usage import RuleUsageEvent # noqa: E402, F401
from scribe.models.system_usage import SystemUsageEvent # noqa: E402, F401
from scribe.models.retrieval_judgment import RetrievalJudgment # noqa: E402, F401
from scribe.models.moment_mapping import MomentMapping # noqa: E402, F401
from scribe.models.project import Project # noqa: E402, F401
from scribe.models.milestone import Milestone # noqa: E402, F401
from scribe.models.task_log import TaskLog # noqa: E402, F401
from scribe.models.note_draft import NoteDraft # noqa: E402, F401
from scribe.models.note_version import NoteVersion # noqa: E402, F401
from scribe.models.rule_version import RuleVersion # noqa: E402, F401
from scribe.models.note_supersession import NoteSupersession # noqa: E402, F401
from scribe.models.group import Group, GroupMembership # noqa: E402, F401
from scribe.models.share import NoteShare, ProjectShare # noqa: E402, F401
from scribe.models.notification import Notification # noqa: E402, F401
from scribe.models.api_key import ApiKey # noqa: E402, F401
from grimoire.models.retrieval_log import RetrievalLog # noqa: E402, F401
from grimoire.models.retrieval_tuning import RetrievalTuningEvent # noqa: E402, F401
from grimoire.models.note_usage import NoteUsageEvent # noqa: E402, F401
from grimoire.models.rule_usage import RuleUsageEvent # noqa: E402, F401
from grimoire.models.system_usage import SystemUsageEvent # noqa: E402, F401
from grimoire.models.retrieval_judgment import RetrievalJudgment # noqa: E402, F401
from grimoire.models.moment_mapping import MomentMapping # noqa: E402, F401
from grimoire.models.project import Project # noqa: E402, F401
from grimoire.models.milestone import Milestone # noqa: E402, F401
from grimoire.models.task_log import TaskLog # noqa: E402, F401
from grimoire.models.note_draft import NoteDraft # noqa: E402, F401
from grimoire.models.note_version import NoteVersion # noqa: E402, F401
from grimoire.models.rule_version import RuleVersion # noqa: E402, F401
from grimoire.models.note_supersession import NoteSupersession # noqa: E402, F401
from grimoire.models.group import Group, GroupMembership # noqa: E402, F401
from grimoire.models.share import NoteShare, ProjectShare # noqa: E402, F401
from grimoire.models.notification import Notification # noqa: E402, F401
from grimoire.models.api_key import ApiKey # noqa: E402, F401
# Imported before rulebook: rule_systems foreign-keys canonical_systems.
from scribe.models.canonical_system import CanonicalSystem # noqa: E402, F401
from scribe.models.rulebook import ( # noqa: E402, F401
from grimoire.models.canonical_system import CanonicalSystem # noqa: E402, F401
from grimoire.models.rulebook import ( # noqa: E402, F401
Rulebook, RulebookTopic, Rule, RuleRelation, rule_moments, rule_systems,
)
# After notes and rules: it foreign-keys both.
from scribe.models.lesson_rule_link import LessonNoRule, LessonRuleLink # noqa: E402, F401
from grimoire.models.lesson_rule_link import LessonNoRule, LessonRuleLink # noqa: E402, F401
# After rules: it foreign-keys them.
from scribe.models.rule_moment_judgment import RuleMomentJudgment # noqa: E402, F401
from scribe.models.repo_binding import RepoBinding # noqa: E402, F401
from scribe.models.forge_connection import ForgeConnection # noqa: E402, F401
from scribe.models.code_shape import CodeShape, CodeShapeConsumer, CodeShapeEvent, CodeShapeUse # noqa: E402, F401
from scribe.models.system import System, RecordSystem # noqa: E402, F401
from scribe.models.design_system import DesignSystem, DesignToken # noqa: E402, F401
from grimoire.models.rule_moment_judgment import RuleMomentJudgment # noqa: E402, F401
from grimoire.models.repo_binding import RepoBinding # noqa: E402, F401
from grimoire.models.forge_connection import ForgeConnection # noqa: E402, F401
from grimoire.models.code_shape import CodeShape, CodeShapeConsumer, CodeShapeEvent, CodeShapeUse # noqa: E402, F401
from grimoire.models.system import System, RecordSystem # noqa: E402, F401
from grimoire.models.design_system import DesignSystem, DesignToken # noqa: E402, F401
# After notes, projects and rulebook topics: family canon foreign-keys all
# three.
from scribe.models.family import ( # noqa: E402, F401
from grimoire.models.family import ( # noqa: E402, F401
FamilyAdoption, FamilyDecision, FamilyIdea, FamilyIdeaPlatform,
FamilyIdeaReference, Platform, ProjectPlatform,
)
@@ -3,8 +3,8 @@ from datetime import datetime
from sqlalchemy import DateTime, ForeignKey, Index, Integer, Text
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import CreatedAtMixin, iso
from grimoire.models import Base
from grimoire.models.base import CreatedAtMixin, iso
class ApiKey(Base, CreatedAtMixin):
@@ -3,8 +3,8 @@ from datetime import datetime, timezone
from sqlalchemy import DateTime, Float, Index, Integer, Text
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import iso
from grimoire.models import Base
from grimoire.models.base import iso
class AppLog(Base):
@@ -1,8 +1,8 @@
from sqlalchemy import Index, Integer, Text, text
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import SoftDeleteMixin, TimestampMixin, iso
from grimoire.models import Base
from grimoire.models.base import SoftDeleteMixin, TimestampMixin, iso
class CanonicalSystem(Base, TimestampMixin, SoftDeleteMixin):
@@ -12,8 +12,8 @@ from sqlalchemy import (
)
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import TimestampMixin, iso
from grimoire.models import Base
from grimoire.models.base import TimestampMixin, iso
# The classification vocabulary. `unclassified` is the default and
# THE todo state; every other status is a judgment, stamped with who made it —
@@ -19,8 +19,8 @@ from sqlalchemy import BigInteger, ForeignKey, Index, Integer, Text, text
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import SoftDeleteMixin, TimestampMixin, iso
from grimoire.models import Base
from grimoire.models.base import SoftDeleteMixin, TimestampMixin, iso
class DesignSystem(Base, TimestampMixin, SoftDeleteMixin):
@@ -4,7 +4,7 @@ from pgvector.sqlalchemy import Vector
from sqlalchemy import BigInteger, DateTime, ForeignKey, Integer, Text
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from grimoire.models import Base
# bge-small-en-v1.5 produces 384-dim unit-normalized vectors. The column is a
# native pgvector `vector(384)` (see migration 0067) so similarity is ranked in
@@ -41,8 +41,8 @@ from sqlalchemy import (
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import CreatedAtMixin, SoftDeleteMixin, TimestampMixin, iso
from grimoire.models import Base
from grimoire.models.base import CreatedAtMixin, SoftDeleteMixin, TimestampMixin, iso
# Each tuple is the whitelist its CHECK constraint enforces (migration 0120).
# A new value means DROP + ADD CONSTRAINT in the same migration.
@@ -1,8 +1,8 @@
from sqlalchemy import ForeignKey, Integer, Text, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import TimestampMixin, iso
from grimoire.models import Base
from grimoire.models.base import TimestampMixin, iso
class ForgeConnection(Base, TimestampMixin):
@@ -3,8 +3,8 @@ from datetime import datetime, timezone
from sqlalchemy import ForeignKey, Integer, Text, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column, relationship
from scribe.models import Base
from scribe.models.base import CreatedAtMixin, TimestampMixin, iso
from grimoire.models import Base
from grimoire.models.base import CreatedAtMixin, TimestampMixin, iso
class Group(Base, TimestampMixin):
@@ -3,8 +3,8 @@ from datetime import datetime, timezone
from sqlalchemy import Boolean, DateTime, ForeignKey, Index, Text
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import CreatedAtMixin
from grimoire.models import Base
from grimoire.models.base import CreatedAtMixin
class InvitationToken(Base, CreatedAtMixin):
@@ -4,8 +4,8 @@ from sqlalchemy import BigInteger, DateTime, ForeignKey, Integer, Text, UniqueCo
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import CreatedAtMixin, iso
from grimoire.models import Base
from grimoire.models.base import CreatedAtMixin, iso
SUGGESTED = "suggested"
CONFIRMED = "confirmed"
@@ -1,8 +1,8 @@
from sqlalchemy import ForeignKey, Integer, Text
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import SoftDeleteMixin, TimestampMixin, iso
from grimoire.models import Base
from grimoire.models.base import SoftDeleteMixin, TimestampMixin, iso
class Milestone(Base, TimestampMixin, SoftDeleteMixin):
@@ -1,8 +1,8 @@
from sqlalchemy import BigInteger, ForeignKey, Integer, Text, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import CreatedAtMixin, iso
from grimoire.models import Base
from grimoire.models.base import CreatedAtMixin, iso
class MomentMapping(Base, CreatedAtMixin):
@@ -5,8 +5,8 @@ from sqlalchemy import Date, DateTime, ForeignKey, Index, Integer, Text
from sqlalchemy.dialects.postgresql import ARRAY, JSONB
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import SoftDeleteMixin, TimestampMixin, iso
from grimoire.models import Base
from grimoire.models.base import SoftDeleteMixin, TimestampMixin, iso
class TaskStatus(str, enum.Enum):
@@ -212,6 +212,6 @@ class Note(Base, TimestampMixin, SoftDeleteMixin):
def _claim_state(note: "Note") -> dict | None:
from scribe.services.task_claims import claim_state
from grimoire.services.task_claims import claim_state
return claim_state(note)
@@ -1,8 +1,8 @@
from sqlalchemy import ForeignKey, Integer, Text
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import TimestampMixin, iso
from grimoire.models import Base
from grimoire.models.base import TimestampMixin, iso
class NoteDraft(Base, TimestampMixin):
@@ -1,8 +1,8 @@
from sqlalchemy import ForeignKey, Index, Integer, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import CreatedAtMixin, iso
from grimoire.models import Base
from grimoire.models.base import CreatedAtMixin, iso
class NoteSupersession(Base, CreatedAtMixin):
@@ -1,8 +1,8 @@
from sqlalchemy import Index, Integer, Text
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import CreatedAtMixin, iso
from grimoire.models import Base
from grimoire.models.base import CreatedAtMixin, iso
SURFACED = "surfaced"
PULLED = "pulled"
@@ -1,8 +1,8 @@
from sqlalchemy import ARRAY, ForeignKey, Integer, Text
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import CreatedAtMixin, iso
from grimoire.models import Base
from grimoire.models.base import CreatedAtMixin, iso
class NoteVersion(Base, CreatedAtMixin):
@@ -4,8 +4,8 @@ from sqlalchemy import DateTime, ForeignKey, Integer, Text
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import CreatedAtMixin, iso
from grimoire.models import Base
from grimoire.models.base import CreatedAtMixin, iso
class Notification(Base, CreatedAtMixin):
@@ -3,8 +3,8 @@ from datetime import datetime, timezone
from sqlalchemy import Boolean, DateTime, ForeignKey, Index, Text
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import CreatedAtMixin
from grimoire.models import Base
from grimoire.models.base import CreatedAtMixin
class PasswordResetToken(Base, CreatedAtMixin):
@@ -2,8 +2,8 @@ import enum
from sqlalchemy import BigInteger, ForeignKey, Integer, Text
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import SoftDeleteMixin, TimestampMixin, iso
from grimoire.models import Base
from grimoire.models.base import SoftDeleteMixin, TimestampMixin, iso
class ProjectStatus(str, enum.Enum):
@@ -1,8 +1,8 @@
from sqlalchemy import ForeignKey, Integer, Text, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import TimestampMixin, iso
from grimoire.models import Base
from grimoire.models.base import TimestampMixin, iso
class RepoBinding(Base, TimestampMixin):
@@ -1,8 +1,8 @@
from sqlalchemy import BigInteger, Boolean, Float, Index, Integer, Text, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import CreatedAtMixin, iso
from grimoire.models import Base
from grimoire.models.base import CreatedAtMixin, iso
ON_POINT = "on_point"
ADJACENT = "adjacent"
@@ -4,8 +4,8 @@ from sqlalchemy import Boolean, DateTime, Float, Index, Integer, Text
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import iso
from grimoire.models import Base
from grimoire.models.base import iso
class RetrievalLog(Base):
@@ -3,7 +3,7 @@ from datetime import datetime, timezone
from sqlalchemy import DateTime, Float, Index, Integer, Text
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from grimoire.models import Base
class RetrievalTuningEvent(Base):
@@ -4,9 +4,9 @@ from sqlalchemy import BigInteger, DateTime, ForeignKey, Text, UniqueConstraint
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import CreatedAtMixin, iso
from scribe.models.lesson_rule_link import CONFIRMED, LINK_STATES, REJECTED, SUGGESTED
from grimoire.models import Base
from grimoire.models.base import CreatedAtMixin, iso
from grimoire.models.lesson_rule_link import CONFIRMED, LINK_STATES, REJECTED, SUGGESTED
# The same three states as a lesson's link to a rule, for the same reasons —
# re-exported so a reader of this table need not know where they were first
@@ -1,8 +1,8 @@
from sqlalchemy import BigInteger, Index, Text
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import CreatedAtMixin, iso
from grimoire.models import Base
from grimoire.models.base import CreatedAtMixin, iso
SURFACED = "surfaced"
PULLED = "pulled"
@@ -3,8 +3,8 @@ from datetime import datetime
from sqlalchemy import BigInteger, ForeignKey, Text
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import CreatedAtMixin, iso
from grimoire.models import Base
from grimoire.models.base import CreatedAtMixin, iso
class RuleVersion(Base, CreatedAtMixin):
@@ -6,8 +6,8 @@ from sqlalchemy import (
)
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import CreatedAtMixin, SoftDeleteMixin, TimestampMixin, iso
from grimoire.models import Base
from grimoire.models.base import CreatedAtMixin, SoftDeleteMixin, TimestampMixin, iso
class Rulebook(Base, TimestampMixin, SoftDeleteMixin):
@@ -1,7 +1,7 @@
from sqlalchemy import ForeignKey, Integer, Text
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from grimoire.models import Base
class Setting(Base):
@@ -1,8 +1,8 @@
from sqlalchemy import CheckConstraint, ForeignKey, Integer, Text
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import TimestampMixin, iso
from grimoire.models import Base
from grimoire.models.base import TimestampMixin, iso
class ProjectShare(Base, TimestampMixin):
@@ -2,8 +2,8 @@ from sqlalchemy import ForeignKey, Index, Integer, Text, UniqueConstraint, text
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import CreatedAtMixin, SoftDeleteMixin, TimestampMixin, iso
from grimoire.models import Base
from grimoire.models.base import CreatedAtMixin, SoftDeleteMixin, TimestampMixin, iso
class System(Base, TimestampMixin, SoftDeleteMixin):
@@ -1,8 +1,8 @@
from sqlalchemy import BigInteger, Index, Text
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import CreatedAtMixin, iso
from grimoire.models import Base
from grimoire.models.base import CreatedAtMixin, iso
SURFACED = "surfaced"
PULLED = "pulled"
@@ -1,8 +1,8 @@
from sqlalchemy import ForeignKey, Integer, Text
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import TimestampMixin, iso
from grimoire.models import Base
from grimoire.models.base import TimestampMixin, iso
class TaskLog(Base, TimestampMixin):
@@ -1,8 +1,8 @@
from sqlalchemy import Index, Integer, Text
from sqlalchemy.orm import Mapped, mapped_column
from scribe.models import Base
from scribe.models.base import CreatedAtMixin, iso
from grimoire.models import Base
from grimoire.models.base import CreatedAtMixin, iso
class User(Base, CreatedAtMixin):
@@ -1,6 +1,6 @@
"""Restore a Grimoire backup into an empty instance, from the command line.
python -m scribe.restore /backups/scribe-20261015T020000Z.tar
python -m grimoire.restore /backups/scribe-20261015T020000Z.tar
This is how a fresh instance is brought back after losing the old one. Run it
inside the Grimoire container, which already has the database settings and has
@@ -32,7 +32,7 @@ from sqlalchemy import func, select
def load_backup(path: Path) -> dict:
"""The payload in `path`, whichever of the two backup formats it is."""
from scribe.services.backup_file import is_archive, read_archive
from grimoire.services.backup_file import is_archive, read_archive
with open(path, "rb") as f:
if is_archive(f):
@@ -41,17 +41,17 @@ def load_backup(path: Path) -> dict:
async def _account_count() -> int:
from scribe.models import async_session
from scribe.models.user import User
from grimoire.models import async_session
from grimoire.models.user import User
async with async_session() as session:
return (await session.execute(select(func.count()).select_from(User))).scalar_one()
async def run(path: Path) -> int:
from scribe.models import engine
from scribe.services.backup import restore_full_backup
from scribe.services.backup_file import ArchiveError
from grimoire.models import engine
from grimoire.services.backup import restore_full_backup
from grimoire.services.backup_file import ArchiveError
try:
try:
@@ -83,7 +83,7 @@ async def run(path: Path) -> int:
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(
prog="python -m scribe.restore",
prog="python -m grimoire.restore",
description="Restore a Grimoire backup (.tar or .json) into an empty instance.",
)
parser.add_argument("file", type=Path, help="the backup file")
@@ -3,8 +3,8 @@ import json
from quart import Blueprint, Response, g, jsonify, request
from scribe.auth import admin_required, login_required, get_current_user_id
from scribe.services.auth import (
from grimoire.auth import admin_required, login_required, get_current_user_id
from grimoire.services.auth import (
create_invitation,
delete_user,
is_registration_open,
@@ -13,15 +13,15 @@ from scribe.services.auth import (
revoke_invitation,
set_registration_open,
)
from scribe.services.backup import (
from grimoire.services.backup import (
export_full_backup,
export_user_backup,
restore_full_backup,
)
from scribe.services.email import SMTP_SETTING_KEYS, get_base_url, get_smtp_config, is_smtp_configured, send_test_email
from scribe.services.logging import get_logs, get_log_stats, log_audit
from scribe.services.notifications import send_invitation_email
from scribe.services.settings import (
from grimoire.services.email import SMTP_SETTING_KEYS, get_base_url, get_smtp_config, is_smtp_configured, send_test_email
from grimoire.services.logging import get_logs, get_log_stats, log_audit
from grimoire.services.notifications import send_invitation_email
from grimoire.services.settings import (
SECRET_MASK,
get_admin_setting,
set_admin_setting,
@@ -61,14 +61,14 @@ async def backup():
async def restore():
"""Restore from an uploaded backup: the JSON download (application/json)
or the scheduled backup's tar (sent as the raw body). The upload is held
to the app's request-size limit; `python -m scribe.restore` has none, and
to the app's request-size limit; `python -m grimoire.restore` has none, and
is the way in for a bigger file or a fresh instance."""
if request.mimetype == "application/json":
data = await request.get_json()
else:
import io
from scribe.services.backup_file import ArchiveError, read_archive
from grimoire.services.backup_file import ArchiveError, read_archive
try:
data = read_archive(io.BytesIO(await request.get_data()))
@@ -180,8 +180,8 @@ async def test_smtp():
@admin_bp.route("/forge-webhook", methods=["GET"])
@admin_required
async def get_forge_webhook_settings():
from scribe.config import Config
from scribe.routes.webhooks import FORGE_WEBHOOK_SECRET_KEY
from grimoire.config import Config
from grimoire.routes.webhooks import FORGE_WEBHOOK_SECRET_KEY
webhook_secret = (
await get_admin_setting(FORGE_WEBHOOK_SECRET_KEY, "")
@@ -197,7 +197,7 @@ async def get_forge_webhook_settings():
@admin_bp.route("/forge-webhook", methods=["PUT"])
@admin_required
async def update_forge_webhook_settings():
from scribe.routes.webhooks import FORGE_WEBHOOK_SECRET_KEY
from grimoire.routes.webhooks import FORGE_WEBHOOK_SECRET_KEY
data = await request.get_json() or {}
uid = get_current_user_id()
@@ -270,8 +270,8 @@ async def update_base_url():
@admin_required
async def get_db_maintenance():
"""Current DB-maintenance config + the last run's summary."""
from scribe.services.db_maintenance import get_last_run
from scribe.services.db_maintenance_scheduler import (
from grimoire.services.db_maintenance import get_last_run
from grimoire.services.db_maintenance_scheduler import (
get_maintenance_hour,
is_maintenance_enabled,
)
@@ -286,7 +286,7 @@ async def get_db_maintenance():
@admin_required
async def get_db_maintenance_health():
"""Read-only per-table bloat/health stats from Postgres + total DB size."""
from scribe.services.db_maintenance import get_table_health
from grimoire.services.db_maintenance import get_table_health
return jsonify(await get_table_health())
@@ -294,7 +294,7 @@ async def get_db_maintenance_health():
@admin_required
async def update_db_maintenance():
"""Set whether scheduled maintenance runs and at what UTC hour."""
from scribe.services.db_maintenance_scheduler import reschedule_db_maintenance
from grimoire.services.db_maintenance_scheduler import reschedule_db_maintenance
data = await request.get_json() or {}
enabled = bool(data.get("enabled", True))
try:
@@ -319,7 +319,7 @@ async def update_db_maintenance():
@admin_required
async def run_db_maintenance_now():
"""Run a VACUUM (ANALYZE) sweep immediately and return its summary."""
from scribe.services.db_maintenance import run_maintenance
from grimoire.services.db_maintenance import run_maintenance
uid = get_current_user_id()
await log_audit(
"db_maintenance_run", user_id=uid, username=g.user.username,
@@ -342,8 +342,8 @@ _BACKUP_CHECK_TIMEOUT = 45.0
async def _backups_payload() -> dict:
from datetime import datetime, timezone
from scribe.services import backup_destination as dest
from scribe.services import backup_job as job
from grimoire.services import backup_destination as dest
from grimoire.services import backup_job as job
config = await dest.load_config()
secret, stored = await dest.load_secret()
@@ -373,7 +373,7 @@ async def get_backups():
async def update_backups():
"""Change the destination or schedule. `s3_secret` replaces the stored
secret when it is sent and isn't the mask; `clear_s3_secret` removes it."""
from scribe.services import backup_destination as dest
from grimoire.services import backup_destination as dest
data = await request.get_json() or {}
try:
@@ -403,7 +403,7 @@ async def check_backups_destination():
"""Ask the destination whether it would take a backup, without sending
one. The body may carry unsaved form values, so a destination can be
tested before it is saved; the stored secret is used unless one is sent."""
from scribe.services import backup_destination as dest
from grimoire.services import backup_destination as dest
data = await request.get_json() or {}
try:
@@ -431,7 +431,7 @@ async def check_backups_destination():
async def run_backup_now():
"""Start a backup now. It runs in the background; the page follows it
through GET /backups (`running`, then the newest run)."""
from scribe.services import backup_job as job
from grimoire.services import backup_job as job
try:
job.start_backup_now()
@@ -1,7 +1,7 @@
from quart import Blueprint, jsonify, request
from scribe.auth import login_required, get_current_user_id
from scribe.services.api_keys import create_api_key, list_api_keys, revoke_api_key
from grimoire.auth import login_required, get_current_user_id
from grimoire.services.api_keys import create_api_key, list_api_keys, revoke_api_key
api_keys_bp = Blueprint("api_keys", __name__, url_prefix="/api/api-keys")
@@ -6,10 +6,10 @@ import secrets
from quart import Blueprint, g, jsonify, redirect, request, session
from scribe.auth import login_required, get_current_user_id
from scribe.config import Config
from scribe.rate_limit import is_rate_limited
from scribe.services.auth import (
from grimoire.auth import login_required, get_current_user_id
from grimoire.config import Config
from grimoire.rate_limit import is_rate_limited
from grimoire.services.auth import (
authenticate,
change_password,
create_password_reset_token,
@@ -26,14 +26,14 @@ from scribe.services.auth import (
validate_invitation_token,
verify_password,
)
from scribe.services.logging import log_audit
from scribe.services.notifications import (
from grimoire.services.logging import log_audit
from grimoire.services.notifications import (
notify_security_event,
send_password_reset_email,
send_password_reset_success_email,
)
from scribe.services.email import get_base_url, is_smtp_configured
from scribe.services.oauth import (
from grimoire.services.email import get_base_url, is_smtp_configured
from grimoire.services.oauth import (
build_auth_url,
exchange_code,
get_userinfo,
@@ -13,10 +13,10 @@ import logging
from quart import Blueprint, jsonify, request
from scribe.auth import admin_required, get_current_user_id, login_required
from scribe.routes.utils import not_found
from scribe.services import canonical_systems as canonical_svc
from scribe.services.projects import get_project_for_user
from grimoire.auth import admin_required, get_current_user_id, login_required
from grimoire.routes.utils import not_found
from grimoire.services import canonical_systems as canonical_svc
from grimoire.services.projects import get_project_for_user
logger = logging.getLogger(__name__)
@@ -1,8 +1,8 @@
"""Dashboard REST endpoint — the aggregated landing payload."""
from quart import Blueprint, g, jsonify
from scribe.auth import login_required
from scribe.services.dashboard import build_dashboard
from grimoire.auth import login_required
from grimoire.services.dashboard import build_dashboard
dashboard_bp = Blueprint("dashboard", __name__, url_prefix="/api/dashboard")
@@ -17,14 +17,14 @@ one and returns None for the other:
"""
from quart import Blueprint, jsonify, request
from scribe.auth import get_current_user_id, login_required
from scribe.routes.utils import not_found
from scribe.services import design_systems as ds_svc
from scribe.services.design_starter_roles import (
from grimoire.auth import get_current_user_id, login_required
from grimoire.routes.utils import not_found
from grimoire.services import design_systems as ds_svc
from grimoire.services.design_starter_roles import (
DEFAULT_TOKEN_PREFIX,
describe_groups,
)
from scribe.services.design_systems import DesignSystemCycle
from grimoire.services.design_systems import DesignSystemCycle
design_systems_bp = Blueprint("design_systems", __name__, url_prefix="/api")
@@ -6,9 +6,9 @@ from datetime import datetime, timezone
from quart import Blueprint, Response, request
from scribe.auth import login_required, get_current_user_id
from scribe.models import async_session
from scribe.models.note import Note
from grimoire.auth import login_required, get_current_user_id
from grimoire.models import async_session
from grimoire.models.note import Note
from sqlalchemy import select
export_bp = Blueprint("export", __name__)
@@ -83,7 +83,7 @@ async def export_data():
status=200,
headers={
"Content-Type": "application/json; charset=utf-8",
"Content-Disposition": f'attachment; filename="scribe-{stamp}.json"',
"Content-Disposition": f'attachment; filename="grimoire-{stamp}.json"',
},
)
@@ -112,6 +112,6 @@ async def export_data():
status=200,
headers={
"Content-Type": "application/zip",
"Content-Disposition": f'attachment; filename="scribe-{stamp}.zip"',
"Content-Disposition": f'attachment; filename="grimoire-{stamp}.zip"',
},
)
@@ -11,10 +11,10 @@ import logging
from quart import Blueprint, jsonify, request
from scribe.auth import get_current_user_id, login_required
from scribe.routes.utils import not_found
from scribe.services import family as family_svc
from scribe.services import family_adoption as adoption_svc
from grimoire.auth import get_current_user_id, login_required
from grimoire.routes.utils import not_found
from grimoire.services import family as family_svc
from grimoire.services import family_adoption as adoption_svc
logger = logging.getLogger(__name__)
@@ -2,9 +2,9 @@ import asyncio
from quart import Blueprint, g, jsonify, request
from scribe.auth import get_current_user_id, login_required
from scribe.services import groups as group_svc
from scribe.services.notifications import notify_group_added
from grimoire.auth import get_current_user_id, login_required
from grimoire.services import groups as group_svc
from grimoire.services.notifications import notify_group_added
groups_bp = Blueprint("groups", __name__, url_prefix="/api/groups")
@@ -1,7 +1,7 @@
from quart import Blueprint, jsonify, request
from scribe.auth import get_current_user_id, login_required
from scribe.services.notifications import (
from grimoire.auth import get_current_user_id, login_required
from grimoire.services.notifications import (
list_in_app_notifications,
mark_all_notifications_read,
mark_notification_read,
@@ -3,11 +3,11 @@ import logging
from quart import Blueprint, jsonify, request
from scribe.auth import get_current_user_id, login_required
from scribe.routes.utils import parse_pagination
from scribe.services.access import label_shared_items
from scribe.services.knowledge import FACET_TYPES
from scribe.services.note_usage import attach_usage
from grimoire.auth import get_current_user_id, login_required
from grimoire.routes.utils import parse_pagination
from grimoire.services.access import label_shared_items
from grimoire.services.knowledge import FACET_TYPES
from grimoire.services.note_usage import attach_usage
logger = logging.getLogger(__name__)
@@ -51,7 +51,7 @@ async def list_knowledge():
limit, offset = parse_pagination(default_limit=24, max_limit=100)
page = max(1, int(request.args.get("page", 1)))
from scribe.services.knowledge import query_knowledge
from grimoire.services.knowledge import query_knowledge
items, total = await query_knowledge(
user_id=uid,
note_type=note_type,
@@ -109,7 +109,7 @@ async def list_knowledge_ids():
if note_type and note_type not in _VALID_TYPES:
return jsonify({"error": "Invalid type"}), 400
from scribe.services.knowledge import query_knowledge_ids
from grimoire.services.knowledge import query_knowledge_ids
ids, total = await query_knowledge_ids(
user_id=uid, note_type=note_type, tags=tags,
sort=sort, q=q, limit=limit, offset=offset,
@@ -135,7 +135,7 @@ async def get_knowledge_batch():
if len(ids) > 100:
return jsonify({"error": "Too many IDs (max 100)"}), 400
from scribe.services.knowledge import get_knowledge_by_ids
from grimoire.services.knowledge import get_knowledge_by_ids
items = await get_knowledge_by_ids(uid, ids)
# The scrolling feed hydrates its cards here, not from the list route, so the
# ownership markers have to be applied on this path too.
@@ -149,7 +149,7 @@ async def list_knowledge_tags():
uid = get_current_user_id()
note_type = request.args.get("type", "").strip().lower() or None
from scribe.services.knowledge import get_knowledge_tags
from grimoire.services.knowledge import get_knowledge_tags
tags = await get_knowledge_tags(uid, note_type=note_type)
return jsonify({"tags": tags})
@@ -162,6 +162,6 @@ async def get_knowledge_counts():
tags_raw = request.args.get("tags", "").strip()
tags = [t.strip() for t in tags_raw.split(",") if t.strip()] if tags_raw else None
from scribe.services.knowledge import get_knowledge_counts as _counts
from grimoire.services.knowledge import get_knowledge_counts as _counts
counts = await _counts(uid, tags=tags)
return jsonify(counts)
@@ -22,20 +22,20 @@ import logging
from quart import Blueprint, jsonify, request
from scribe.auth import get_current_user_id, login_required
from scribe.routes.utils import not_found, parse_pagination
from scribe.services import dedup as dedup_svc
from scribe.services import knowledge as knowledge_svc
from scribe.services import lesson_rules as lesson_rules_svc
from scribe.services import lessons as lessons_svc
from scribe.services import systems as systems_svc
from scribe.services import trash as trash_svc
from scribe.services.access import (
from grimoire.auth import get_current_user_id, login_required
from grimoire.routes.utils import not_found, parse_pagination
from grimoire.services import dedup as dedup_svc
from grimoire.services import knowledge as knowledge_svc
from grimoire.services import lesson_rules as lesson_rules_svc
from grimoire.services import lessons as lessons_svc
from grimoire.services import systems as systems_svc
from grimoire.services import trash as trash_svc
from grimoire.services.access import (
can_write_note,
describe_provenance,
label_shared_items,
)
from scribe.services.note_usage import attach_usage, record_pulled
from grimoire.services.note_usage import attach_usage, record_pulled
logger = logging.getLogger(__name__)
@@ -3,10 +3,10 @@ import logging
from quart import Blueprint, jsonify, request
from scribe.auth import login_required, get_current_user_id
from scribe.routes.utils import not_found, parse_pagination
from scribe.services.access import can_write_project
from scribe.services.milestones import (
from grimoire.auth import login_required, get_current_user_id
from grimoire.routes.utils import not_found, parse_pagination
from grimoire.services.access import can_write_project
from grimoire.services.milestones import (
create_milestone,
delete_milestone,
get_milestone_in_project,
@@ -14,8 +14,8 @@ from scribe.services.milestones import (
list_milestones,
update_milestone,
)
from scribe.services.notes import list_notes
from scribe.services.projects import get_project_for_user
from grimoire.services.notes import list_notes
from grimoire.services.projects import get_project_for_user
logger = logging.getLogger(__name__)
@@ -114,7 +114,7 @@ async def delete_milestone_route(project_id: int, milestone_id: int):
milestone = await get_milestone_in_project(project_id, milestone_id)
if milestone is None:
return not_found("Milestone")
from scribe.services.trash import delete as trash_delete
from grimoire.services.trash import delete as trash_delete
batch = await trash_delete(milestone.user_id, "milestone", milestone_id)
if batch is None:
return not_found("Milestone")
@@ -4,10 +4,10 @@ import re
from quart import Blueprint, jsonify, request
from scribe.auth import login_required, get_current_user_id
from scribe.routes.utils import not_found, parse_iso_date, parse_pagination
from scribe.services.access import can_write_note
from scribe.services.notes import (
from grimoire.auth import login_required, get_current_user_id
from grimoire.routes.utils import not_found, parse_iso_date, parse_pagination
from grimoire.services.access import can_write_note
from grimoire.services.notes import (
build_note_graph,
convert_note_to_task,
convert_task_to_note,
@@ -24,13 +24,13 @@ from scribe.services.notes import (
update_note,
verification_row,
)
from scribe.services.note_drafts import upsert_draft, get_draft, delete_draft
from scribe.services import dedup as dedup_svc
from scribe.services import supersession as supersession_svc
from scribe.services import systems as systems_svc
from scribe.services.note_usage import record_pulled
from grimoire.services.note_drafts import upsert_draft, get_draft, delete_draft
from grimoire.services import dedup as dedup_svc
from grimoire.services import supersession as supersession_svc
from grimoire.services import systems as systems_svc
from grimoire.services.note_usage import record_pulled
from scribe.services.note_versions import list_versions, get_version
from grimoire.services.note_versions import list_versions, get_version
logger = logging.getLogger(__name__)
@@ -95,7 +95,7 @@ async def create_note_route():
project_id = data.get("project_id")
if project_id is None and data.get("project"):
from scribe.services.projects import get_project_by_title as _gpbt
from grimoire.services.projects import get_project_by_title as _gpbt
proj = await _gpbt(uid, data["project"])
if proj:
project_id = proj.id
@@ -318,7 +318,7 @@ async def delete_note_route(note_id: int):
note_obj, _ = result
if not await can_write_note(uid, note_id):
return jsonify({"error": "Permission denied"}), 403
from scribe.services.trash import delete as trash_delete
from grimoire.services.trash import delete as trash_delete
batch = await trash_delete(note_obj.user_id, "note", note_id)
if batch is None:
return not_found("Note")
@@ -481,7 +481,7 @@ async def pin_version_route(note_id: int, version_id: int):
label = data.get("label")
if label is not None and not isinstance(label, str):
return jsonify({"error": "label must be a string or null"}), 400
from scribe.services.version_pinning import pin_version
from grimoire.services.version_pinning import pin_version
try:
version = await pin_version(uid, note_id, version_id, label=label)
except ValueError as e:
@@ -498,7 +498,7 @@ async def pin_version_route(note_id: int, version_id: int):
async def unpin_version_route(note_id: int, version_id: int):
"""Downgrade a manually-pinned version back to rolling."""
uid = get_current_user_id()
from scribe.services.version_pinning import unpin_version
from grimoire.services.version_pinning import unpin_version
version = await unpin_version(uid, note_id, version_id)
if version is None:
return not_found("Version")
@@ -13,9 +13,9 @@ import logging
from quart import Blueprint, jsonify, request
from scribe.auth import admin_required, get_current_user_id, login_required
from scribe.routes.utils import not_found
from scribe.services import platforms as platforms_svc
from grimoire.auth import admin_required, get_current_user_id, login_required
from grimoire.routes.utils import not_found
from grimoire.services import platforms as platforms_svc
logger = logging.getLogger(__name__)
@@ -10,19 +10,19 @@ from __future__ import annotations
from quart import Blueprint, g, jsonify, request
from scribe.auth import admin_required, get_current_user_id, login_required
from scribe.config import Config
from scribe.services import lesson_rules as lesson_rules_svc
from scribe.services import moment_delivery as moment_delivery_svc
from scribe.services import plugin_context as plugin_ctx_svc
from scribe.services import reply_shapes as reply_shapes_svc
from scribe.services import repo_bindings as repo_bindings_svc
from scribe.services import report_check as report_check_svc
from scribe.services import rule_moment_judgments as judgments_svc
from scribe.services import shape_check as shape_check_svc
from scribe.services import task_claims as task_claims_svc
from scribe.services.embeddings import memoized_query_embeddings
from scribe.services.settings import get_admin_setting, set_setting
from grimoire.auth import admin_required, get_current_user_id, login_required
from grimoire.config import Config
from grimoire.services import lesson_rules as lesson_rules_svc
from grimoire.services import moment_delivery as moment_delivery_svc
from grimoire.services import plugin_context as plugin_ctx_svc
from grimoire.services import reply_shapes as reply_shapes_svc
from grimoire.services import repo_bindings as repo_bindings_svc
from grimoire.services import report_check as report_check_svc
from grimoire.services import rule_moment_judgments as judgments_svc
from grimoire.services import shape_check as shape_check_svc
from grimoire.services import task_claims as task_claims_svc
from grimoire.services.embeddings import memoized_query_embeddings
from grimoire.services.settings import get_admin_setting, set_setting
plugin_bp = Blueprint("plugin", __name__, url_prefix="/api/plugin")
@@ -564,9 +564,9 @@ async def shape_check():
body: dict = {"status": "ok", "unjudged": []}
if not project_id or not written:
return jsonify(body)
from scribe.services import access
from scribe.services import coverage as coverage_svc
from scribe.services import shape_ledger as shape_ledger_svc
from grimoire.services import access
from grimoire.services import coverage as coverage_svc
from grimoire.services import shape_ledger as shape_ledger_svc
if not await access.can_read_project(g.user.id, project_id):
return jsonify(body)
@@ -3,12 +3,12 @@ import logging
from quart import Blueprint, g, jsonify, request
from scribe.auth import login_required, get_current_user_id
from scribe.routes.utils import not_found, parse_pagination
from scribe.services import inception as inception_svc
from scribe.services.milestones import list_milestones
from scribe.services.notes import list_notes
from scribe.services.projects import (
from grimoire.auth import login_required, get_current_user_id
from grimoire.routes.utils import not_found, parse_pagination
from grimoire.services import inception as inception_svc
from grimoire.services.milestones import list_milestones
from grimoire.services.notes import list_notes
from grimoire.services.projects import (
create_project,
delete_project,
get_project,
@@ -159,7 +159,7 @@ async def update_project_route(project_id: int):
@login_required
async def delete_project_route(project_id: int):
uid = get_current_user_id()
from scribe.services.trash import delete as trash_delete
from grimoire.services.trash import delete as trash_delete
batch = await trash_delete(uid, "project", project_id)
if batch is None:
return not_found("Project")
@@ -175,8 +175,8 @@ async def get_coverage_route(project_id: int):
sense; `coverage` is null until something has computed it (a webhook
push or an explicit refresh).
"""
from scribe.services.coverage import cached_coverage
from scribe.services.forge import get_forges
from grimoire.services.coverage import cached_coverage
from grimoire.services.forge import get_forges
uid = get_current_user_id()
result = await get_project_for_user(uid, project_id)
@@ -200,8 +200,8 @@ async def refresh_coverage_route(project_id: int):
Synchronous on purpose: the caller is a person who just clicked Refresh
and wants the new number, and the forge timeout bounds the wait.
"""
from scribe.services.coverage import refresh_coverage
from scribe.services.forge import ForgeError, get_forges
from grimoire.services.coverage import refresh_coverage
from grimoire.services.forge import ForgeError, get_forges
uid = get_current_user_id()
result = await get_project_for_user(uid, project_id)
@@ -237,7 +237,7 @@ async def set_project_forge_route(project_id: int):
service enforces that, so a collaborator's token can never end up serving
someone else's project.
"""
from scribe.services.forge_connections import set_project_pin
from grimoire.services.forge_connections import set_project_pin
uid = get_current_user_id()
result = await get_project_for_user(uid, project_id)
@@ -17,14 +17,14 @@ from datetime import datetime, timedelta, timezone
from quart import Blueprint, jsonify, request
from scribe.auth import get_current_user_id, login_required
from scribe.services import moment_actions as moment_actions_svc
from scribe.services import moments as moments_svc
from scribe.services import reply_shapes as shapes_svc
from scribe.services import rule_moment_judgments as judgments_svc
from scribe.services import rulebooks as rulebooks_svc
from scribe.services.retrieval_telemetry import moment_usage
from scribe.services.retrieval_tuning import set_dial, current_settings, tuning_history
from grimoire.auth import get_current_user_id, login_required
from grimoire.services import moment_actions as moment_actions_svc
from grimoire.services import moments as moments_svc
from grimoire.services import reply_shapes as shapes_svc
from grimoire.services import rule_moment_judgments as judgments_svc
from grimoire.services import rulebooks as rulebooks_svc
from grimoire.services.retrieval_telemetry import moment_usage
from grimoire.services.retrieval_tuning import set_dial, current_settings, tuning_history
logger = logging.getLogger(__name__)
@@ -7,11 +7,11 @@ from __future__ import annotations
from quart import Blueprint, jsonify, request
from scribe.auth import get_current_user_id, login_required
import scribe.services.moments as moments_svc
import scribe.services.rulebooks as rulebooks_svc
from scribe.services.trash import delete as trash_delete
from scribe.services.rule_usage import (
from grimoire.auth import get_current_user_id, login_required
import grimoire.services.moments as moments_svc
import grimoire.services.rulebooks as rulebooks_svc
from grimoire.services.trash import delete as trash_delete
from grimoire.services.rule_usage import (
empty_rule_usage, record_rule_pulled, usage_for_rules,
)
@@ -2,14 +2,14 @@ import time
from quart import Blueprint, jsonify, request
from scribe.auth import login_required, get_current_user_id
from scribe.services.access import owner_names_for
from scribe.services.embeddings import (
from grimoire.auth import login_required, get_current_user_id
from grimoire.services.access import owner_names_for
from grimoire.services.embeddings import (
INTERACTIVE_SEARCH_THRESHOLD as _REST_SEARCH_THRESHOLD,
)
from scribe.services.embeddings import semantic_search_notes
from scribe.services.knowledge import content_type_filters
from scribe.services.retrieval_telemetry import record_retrieval
from grimoire.services.embeddings import semantic_search_notes
from grimoire.services.knowledge import content_type_filters
from grimoire.services.retrieval_telemetry import record_retrieval
# The interactive floor lives in embeddings.py, shared with Browse search and
# the list views' semantic `q` — one number, one rationale.
@@ -3,11 +3,11 @@ import logging
from quart import Blueprint, jsonify, request
from scribe.auth import login_required, get_current_user_id
from scribe.config import Config
from scribe.services.retrieval_surfaces import dial_for_key, get_surface
from scribe.services.retrieval_tuning import set_dial
from scribe.services.settings import (
from grimoire.auth import login_required, get_current_user_id
from grimoire.config import Config
from grimoire.services.retrieval_surfaces import dial_for_key, get_surface
from grimoire.services.retrieval_tuning import set_dial
from grimoire.services.settings import (
SECRET_MASK, delete_setting, get_all_settings, get_setting, set_settings_batch,
)
@@ -120,8 +120,8 @@ async def test_search():
@settings_bp.route("/forge-connections", methods=["GET"])
@login_required
async def list_forge_connections_route():
from scribe.services.forge import FORGE_KINDS
from scribe.services.forge_connections import connection_token, list_connections
from grimoire.services.forge import FORGE_KINDS
from grimoire.services.forge_connections import connection_token, list_connections
uid = get_current_user_id()
rows = await list_connections(uid)
@@ -139,7 +139,7 @@ async def list_forge_connections_route():
@settings_bp.route("/forge-connections", methods=["POST"])
@login_required
async def create_forge_connection_route():
from scribe.services.forge_connections import create_connection
from grimoire.services.forge_connections import create_connection
uid = get_current_user_id()
data = await request.get_json() or {}
@@ -158,7 +158,7 @@ async def create_forge_connection_route():
@settings_bp.route("/forge-connections/<int:connection_id>", methods=["PUT"])
@login_required
async def update_forge_connection_route(connection_id: int):
from scribe.services.forge_connections import update_connection
from grimoire.services.forge_connections import update_connection
uid = get_current_user_id()
data = await request.get_json() or {}
@@ -184,7 +184,7 @@ async def update_forge_connection_route(connection_id: int):
@settings_bp.route("/forge-connections/<int:connection_id>", methods=["DELETE"])
@login_required
async def delete_forge_connection_route(connection_id: int):
from scribe.services.forge_connections import delete_connection
from grimoire.services.forge_connections import delete_connection
uid = get_current_user_id()
if not await delete_connection(uid, connection_id):
@@ -198,8 +198,8 @@ async def test_forge_connection_route(connection_id: int):
"""Probe the SAVED connection: reachability and token acceptance in one
press, so a misconfiguration is visible now rather than as silent
fallbacks later, per keyring row."""
from scribe.services.forge import ForgeError, build_adapter
from scribe.services.forge_connections import connection_token, get_connection
from grimoire.services.forge import ForgeError, build_adapter
from grimoire.services.forge_connections import connection_token, get_connection
uid = get_current_user_id()
row = await get_connection(uid, connection_id)
@@ -2,11 +2,11 @@ import asyncio
from quart import Blueprint, jsonify, request
from scribe.auth import get_current_user_id, login_required
from scribe.services import sharing as share_svc
from scribe.services.access import can_admin_project, can_write_note
from scribe.services.notifications import notify_note_shared, notify_project_shared
from scribe.services.sharing import list_shared_with_me
from grimoire.auth import get_current_user_id, login_required
from grimoire.services import sharing as share_svc
from grimoire.services.access import can_admin_project, can_write_note
from grimoire.services.notifications import notify_note_shared, notify_project_shared
from grimoire.services.sharing import list_shared_with_me
shares_bp = Blueprint("shares", __name__)
@@ -14,18 +14,18 @@ import logging
from quart import Blueprint, jsonify, request
from scribe.auth import get_current_user_id, login_required
from scribe.routes.utils import not_found, parse_pagination
from scribe.services import dedup as dedup_svc
from scribe.services import snippets as snippets_svc
from scribe.services import systems as systems_svc
from scribe.services.note_usage import attach_usage, record_pulled
from scribe.services.access import (
from grimoire.auth import get_current_user_id, login_required
from grimoire.routes.utils import not_found, parse_pagination
from grimoire.services import dedup as dedup_svc
from grimoire.services import snippets as snippets_svc
from grimoire.services import systems as systems_svc
from grimoire.services.note_usage import attach_usage, record_pulled
from grimoire.services.access import (
can_write_note,
describe_provenance,
label_shared_items,
)
from scribe.services.notes import get_note_for_user
from grimoire.services.notes import get_note_for_user
logger = logging.getLogger(__name__)
@@ -5,12 +5,12 @@ import logging
from quart import Blueprint, jsonify, request
from scribe.auth import login_required, get_current_user_id
from scribe.routes.utils import not_found
from scribe.services import systems as systems_svc
from scribe.services.access import can_write_project
from scribe.services.projects import get_project_for_user
from scribe.services import system_usage as system_usage_svc
from grimoire.auth import login_required, get_current_user_id
from grimoire.routes.utils import not_found
from grimoire.services import systems as systems_svc
from grimoire.services.access import can_write_project
from grimoire.services.projects import get_project_for_user
from grimoire.services import system_usage as system_usage_svc
logger = logging.getLogger(__name__)
@@ -3,8 +3,8 @@ import logging
from quart import Blueprint, jsonify, request
from scribe.auth import get_current_user_id, login_required
from scribe.services.task_logs import create_log, delete_log, list_logs, update_log
from grimoire.auth import get_current_user_id, login_required
from grimoire.services.task_logs import create_log, delete_log, list_logs, update_log
logger = logging.getLogger(__name__)
@@ -49,7 +49,7 @@ async def update_log_route(task_id: int, log_id: int):
if content is not None:
content = content.strip() or None
from scribe.services.task_logs import _UNSET
from grimoire.services.task_logs import _UNSET
duration_minutes = data.get("duration_minutes", _UNSET)
log = await update_log(uid, log_id, content=content, duration_minutes=duration_minutes)
@@ -2,22 +2,22 @@ from datetime import date
from quart import Blueprint, jsonify, request
from scribe.auth import login_required, get_current_user_id
from scribe.models.note import TaskPriority, TaskStatus
from scribe.routes.utils import not_found, parse_iso_date, parse_pagination
from scribe.services.access import can_write_note
from scribe.services import placement as placement_svc
from scribe.services import systems as systems_svc
from scribe.services.notes import (
from grimoire.auth import login_required, get_current_user_id
from grimoire.models.note import TaskPriority, TaskStatus
from grimoire.routes.utils import not_found, parse_iso_date, parse_pagination
from grimoire.services.access import can_write_note
from grimoire.services import placement as placement_svc
from grimoire.services import systems as systems_svc
from grimoire.services.notes import (
create_note,
get_note_for_user,
list_notes,
update_note,
)
from scribe.services.notes import minted_kind as notes_minted_kind
from scribe.services.note_usage import record_pulled
from scribe.services.planning import start_planning as svc_start_planning
from scribe.services.recurrence import calculate_next_due, validate_recurrence_rule
from grimoire.services.notes import minted_kind as notes_minted_kind
from grimoire.services.note_usage import record_pulled
from grimoire.services.planning import start_planning as svc_start_planning
from grimoire.services.recurrence import calculate_next_due, validate_recurrence_rule
tasks_bp = Blueprint("tasks", __name__, url_prefix="/api/tasks")
@@ -127,7 +127,7 @@ async def create_task_route():
project_id = data.get("project_id")
if project_id is None and data.get("project"):
from scribe.services.projects import get_project_by_title as _gpbt
from grimoire.services.projects import get_project_by_title as _gpbt
proj = await _gpbt(uid, data["project"])
if proj:
project_id = proj.id
@@ -329,7 +329,7 @@ async def delete_task_route(task_id: int):
task_note, _ = result
if not await can_write_note(uid, task_id):
return jsonify({"error": "Permission denied"}), 403
from scribe.services.trash import delete as trash_delete
from grimoire.services.trash import delete as trash_delete
batch = await trash_delete(task_note.user_id, "task", task_id)
if batch is None:
return not_found("Task")
@@ -3,8 +3,8 @@ from __future__ import annotations
from quart import Blueprint, jsonify
from scribe.auth import get_current_user_id, login_required
import scribe.services.trash as trash_svc
from grimoire.auth import get_current_user_id, login_required
import grimoire.services.trash as trash_svc
trash_bp = Blueprint("trash", __name__, url_prefix="/api/trash")
@@ -1,9 +1,9 @@
from quart import Blueprint, jsonify, request
from sqlalchemy import or_, select
from scribe.auth import get_current_user_id, login_required
from scribe.models import async_session
from scribe.models.user import User
from grimoire.auth import get_current_user_id, login_required
from grimoire.models import async_session
from grimoire.models.user import User
users_bp = Blueprint("users", __name__, url_prefix="/api/users")

Some files were not shown because too many files have changed in this diff Show More