CI & Build / Plugin hooks (push) Successful in 10s
CI & Build / TypeScript typecheck (push) Successful in 39s
CI & Build / Python lint (push) Successful in 4s
CI & Build / integration (push) Successful in 30s
CI & Build / Python tests (push) Failing after 56s
CI & Build / Build & push image (push) Skipped
The eight standard area names already existed — as STANDARD_SYSTEMS, a tuple in
services/systems.py that milestone 297 seeds at inception. A constant cannot be
a foreign key, so nothing outside a project could reference an area: systems.
project_id is NOT NULL, and a rule that spans projects would have to chain
itself to one project's row. And because the list only ever applied on the
inception-seed path, three spellings of one area reached this instance anyway
(CI & runners / CI and Release / CI & release).
- canonical_systems: global, no user_id — a shared project inherits the
vocabulary instead of re-earning it. Migration 0087 seeds the same eight.
- systems.canonical_id: nullable, SET NULL. Association only — no System is
renamed and record_systems is untouched, so no record's tags move.
- canonical_slug folds &/and, case and punctuation, so spelling variants map
mechanically and a real difference ("CI & runners") becomes a proposal a
human confirms. propose_mappings reports; set_system_canonical is the only
writer.
- seed_standard_systems now reads the catalog and maps as it mints, so a
project born standard never needs a reconciliation pass.
- Catalog writes are admin-only; reads are open — a global list anyone can
extend stops being shared.
- backup: carried by SLUG, not id (ids are per-install). Restore reuses the
target's own rows and only creates entries an admin added on the source; an
unknown slug restores unmapped rather than failing.
Rule 22: STANDARD_SYSTEMS is removed, not deprecated. Rule 115: nothing seeded
names an app, repo or house convention. Design in note 3026.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
79 lines
4.1 KiB
Python
79 lines
4.1 KiB
Python
"""Project inception (milestone 297) — step 1: the record's shape.
|
|
|
|
The WHY a project inherits what it does lives on projects.inception; the
|
|
opt-out of an always-on rulebook is its own association table. Pure
|
|
validation is pinned here; the effects are step 3's integration tests.
|
|
"""
|
|
from scribe.models import Base
|
|
from scribe.models.project import Project
|
|
from scribe.models.rulebook import project_rulebook_exclusions
|
|
from scribe.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_exclusions_table_is_the_suppressions_sibling():
|
|
t = Base.metadata.tables["project_rulebook_exclusions"]
|
|
assert project_rulebook_exclusions is t
|
|
assert {c.name for c in t.primary_key.columns} == {"project_id", "rulebook_id"}
|
|
fks = {fk.column.table.name: fk.ondelete for c in t.columns for fk in c.foreign_keys}
|
|
assert fks == {"projects": "CASCADE", "rulebooks": "CASCADE"}
|
|
|
|
|
|
def test_validate_inception_pins_the_choice_vocabulary():
|
|
assert CHOICE_KEYS == ("exclude_always_on_rulebooks", "subscribe_rulebooks", "design_system_id", "seed_systems")
|
|
assert validate_inception({}) is None
|
|
assert validate_inception({"exclude_always_on_rulebooks": [1], "subscribe_rulebooks": [2],
|
|
"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"})
|
|
assert "list of rulebook ids" in validate_inception({"exclude_always_on_rulebooks": "1"})
|
|
assert "list of rulebook ids" in validate_inception({"subscribe_rulebooks": [0]})
|
|
assert "list of rulebook ids" in validate_inception({"subscribe_rulebooks": [True]})
|
|
assert "both excluded and subscribed" in validate_inception(
|
|
{"exclude_always_on_rulebooks": [1, 2], "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_normalize_choices_is_canonical_and_complete():
|
|
out = normalize_choices({"subscribe_rulebooks": [3, 1, 3], "exclude_always_on_rulebooks": [2]})
|
|
assert out == {"exclude_always_on_rulebooks": [2], "subscribe_rulebooks": [1, 3],
|
|
"design_system_id": None, "seed_systems": False}
|
|
assert normalize_choices(None) == {"exclude_always_on_rulebooks": [], "subscribe_rulebooks": [],
|
|
"design_system_id": None, "seed_systems": False}
|
|
|
|
|
|
def test_standard_systems_vocabulary_reads_the_catalog_not_a_constant():
|
|
"""The vocabulary moved from a module constant to the global catalog table
|
|
(milestone 307): 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 scribe.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"
|
|
)
|
|
|