CI & Build / TypeScript typecheck (push) Successful in 1m11s
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 25s
CI & Build / integration (push) Successful in 1m50s
CI & Build / Python tests (push) Successful in 2m41s
CI & Build / Build & push image (push) Successful in 1m8s
When one project solves something every project on the same platform will meet, that solution becomes family canon and every other project on the platform answers it. This is the storage for that. - platforms: a global catalog in the canonical_systems shape, seeded with generic technology names and the file markers step 2's detection reads. - project_platforms: declared / detected / rejected. A rejected row is kept so detection cannot re-add what a person said no to. - family_ideas: a note's family state. No new record type; any note, snippet or lesson becomes an idea. A canon idea must state when it applies (CHECK). - family_idea_platforms: the only scope source. A linked rule topic takes its scope from the idea, so the two cannot disagree. - family_idea_references: reference implementations, explicit not inferred. - family_adoptions: one answer per (project, idea). Variant and exempt require a reason (CHECK). Recheck is derived from the two canon versions, never stored. - family_decisions: the append-only log, with a required reason and the earlier decisions each one followed. The agent decides with no approval step, so precedent is what keeps its calls consistent. Backup v25 carries all seven: platforms by slug, precedent ids remapped through the decision map. Both column guards cover the new tables, and a real-Postgres test exercises the CHECKs and the restore remaps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
70 lines
2.9 KiB
Python
70 lines
2.9 KiB
Python
"""Family canon's schema — the parts that need no database (milestone 463 step 1).
|
|
|
|
The constraints themselves, against real Postgres, are in
|
|
tests/test_integration_family_canon.py. These pin that the migration and the
|
|
model agree on every whitelist (rule 36: a value one side accepts and the
|
|
other refuses only fails at INSERT time, which CI's unit lane never reaches),
|
|
and that the seed ships nothing an install other than this one would find
|
|
foreign (rule 115).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
from pathlib import Path
|
|
|
|
from scribe.models import family
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
def _migration():
|
|
path = ROOT / "alembic" / "versions" / "0120_family_canon.py"
|
|
spec = importlib.util.spec_from_file_location("m0120", path)
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
def test_the_migration_checks_and_the_model_agree():
|
|
m = _migration()
|
|
assert tuple(m._MEMBERSHIP) == family.MEMBERSHIP_STATES
|
|
assert tuple(m._IDEA_STATUSES) == family.IDEA_STATUSES
|
|
assert tuple(m._ADOPTION_STATUSES) == family.ADOPTION_STATUSES
|
|
assert tuple(m._REASONED) == family.REASONED_STATUSES
|
|
assert tuple(m._ACTIONS) == family.DECISION_ACTIONS
|
|
assert tuple(m._DECIDERS) == family.DECIDERS
|
|
|
|
|
|
def test_the_departures_that_need_a_reason_are_adoption_states():
|
|
assert set(family.REASONED_STATUSES) <= set(family.ADOPTION_STATUSES)
|
|
|
|
|
|
def test_the_seed_is_generic_and_unique():
|
|
seed = _migration()._SEED
|
|
slugs = [slug for _, slug, _, _ in seed]
|
|
assert len(slugs) == len(set(slugs)), "a duplicate slug breaks the live-unique index"
|
|
for name, slug, description, markers in seed:
|
|
assert name and slug and description
|
|
assert slug == slug.lower() and " " not in slug
|
|
# A list, possibly empty — never None. Detection reads it directly.
|
|
assert isinstance(markers, list)
|
|
|
|
|
|
def test_the_column_defaults_match_the_first_state_of_each_lifecycle():
|
|
"""A row created with no state given starts where its lifecycle starts.
|
|
An idea is a candidate until promoted; an answer is unassessed until
|
|
someone assesses it — never silently `adopted`."""
|
|
assert family.FamilyIdea.__table__.c.status.server_default.arg == "candidate"
|
|
assert family.FamilyAdoption.__table__.c.status.server_default.arg == "unassessed"
|
|
assert family.ProjectPlatform.__table__.c.state.server_default.arg == "declared"
|
|
|
|
|
|
def test_a_canon_ideas_scope_lives_on_the_idea_and_nowhere_else():
|
|
"""One scope source. A rule topic is linked FROM the idea and carries no
|
|
platforms of its own, so the two cannot disagree about who a standard
|
|
reaches (#4221's two-sources-of-truth shape)."""
|
|
from scribe.models.rulebook import RulebookTopic
|
|
|
|
assert "topic_id" in family.FamilyIdea.__table__.c
|
|
assert not {c.name for c in RulebookTopic.__table__.c} & {"platform_id", "platform_ids"}
|