Files
bvandeusenandClaude Opus 5.5 0f5ef0be63
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
rename(547): the server is grimoire — package, image, DB defaults, backups
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>
2026-10-22 10:50:45 -04:00

91 lines
4.3 KiB
Python

"""Project inception — the record's shape.
The WHY a project inherits what it does lives on projects.inception. Pure
validation is pinned here; the effects are the integration tests.
Rules are not an inception choice: a rule in a rulebook is global, so there is
nothing to choose.
"""
from grimoire.models.project import Project
from grimoire.services.inception import (
CHOICE_KEYS, INCEPTION_VIAS, is_decided, normalize_choices, validate_inception,
)
def test_project_carries_an_inception_record_and_to_dict_shows_it():
assert "inception" in Project.__table__.c
assert Project.__table__.c.inception.nullable # NULL = undecided
p = Project(user_id=1, title="x", inception=None)
assert p.to_dict()["inception"] is None and not is_decided(p)
p.inception = {"via": "mcp", "decided_at": "2026-08-22T00:00:00+00:00", "decided_by": 1,
"choices": normalize_choices({"seed_systems": True})}
assert is_decided(p) and p.to_dict()["inception"]["via"] == "mcp"
assert INCEPTION_VIAS == ("mcp", "ui", "legacy")
def test_validate_inception_pins_the_choice_vocabulary():
assert CHOICE_KEYS == ("design_system_id", "seed_systems", "platforms")
assert validate_inception({}) is None
assert validate_inception({"design_system_id": 3, "seed_systems": True}) is None
assert validate_inception({"design_system_id": None}) is None
assert "must be an object" in validate_inception([])
assert "unknown inception choice" in validate_inception({"repo": "x"})
# The retired exclusion key is now an UNKNOWN key rather than a typed one,
# which is the right error: a caller still passing it is asking for a
# choice that no longer exists, and silently ignoring it would let them
# believe a rulebook had been declined.
assert "unknown inception choice" in validate_inception(
{"exclude_always_on_rulebooks": [1]})
# Same for a rulebook subscription choice.
assert "unknown inception choice" in validate_inception({"subscribe_rulebooks": [2]})
assert "positive id or null" in validate_inception({"design_system_id": 0})
assert "positive id or null" in validate_inception({"design_system_id": True})
assert "true or false" in validate_inception({"seed_systems": "yes"})
def test_validate_inception_takes_platforms_as_slugs_or_unstated():
"""Slugs, not ids: an inception record outlives a restore, and a slug is
what both sides of a restore agree on. None is "not answered", which is
different from [] — "none of them"."""
assert validate_inception({"platforms": ["android-app", "go"]}) is None
assert validate_inception({"platforms": []}) is None
assert validate_inception({"platforms": None}) is None
assert "platform slugs" in validate_inception({"platforms": "android-app"})
assert "platform slugs" in validate_inception({"platforms": [3]})
assert "platform slugs" in validate_inception({"platforms": [" "]})
def test_normalize_choices_is_canonical_and_complete():
out = normalize_choices({"design_system_id": 4})
assert out == {"design_system_id": 4, "seed_systems": False, "platforms": None}
assert normalize_choices(None) == {
"design_system_id": None, "seed_systems": False, "platforms": None,
}
# Sorted and de-duplicated, so two equal answers record identically.
assert normalize_choices({"platforms": ["go", " android-app", "go"]})["platforms"] == [
"android-app", "go",
]
def test_standard_systems_vocabulary_reads_the_catalog_not_a_constant():
"""The vocabulary is the global catalog table, not a module constant: a
constant cannot be a foreign key, so nothing outside a
project could reference an area. The seed and the bootstrap ask must both
read the table, or the list they show and the list they mint diverge."""
import inspect
from grimoire.services import systems as systems_svc
assert not hasattr(systems_svc, "STANDARD_SYSTEMS"), (
"the constant is gone — the catalog table is the single source"
)
source = inspect.getsource(systems_svc.seed_standard_systems)
assert "list_canonical_systems" in source
assert "canonical_id=entry.id" in source, (
"a seeded System must be mapped as it is created, or a project born "
"from the standard set still needs a reconciliation pass"
)