feat(inception): projects.inception record + project_rulebook_exclusions — migration 0085 with legacy backfill; backup v10 (#2879, milestone 297 step 1)
CI & Build / Python lint (push) Successful in 6s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / TypeScript typecheck (push) Successful in 42s
CI & Build / integration (push) Successful in 53s
CI & Build / Python tests (push) Successful in 1m36s
CI & Build / Build & push image (push) Successful in 36s
CI & Build / Python lint (push) Successful in 6s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / TypeScript typecheck (push) Successful in 42s
CI & Build / integration (push) Successful in 53s
CI & Build / Python tests (push) Successful in 1m36s
CI & Build / Build & push image (push) Successful in 36s
A project's inheritance becomes a decision, not a default (milestone 297).
- projects.inception (JSONB, NULL = undecided): {decided_at, decided_by, via
mcp|ui|legacy, choices {exclude_always_on_rulebooks, subscribe_rulebooks,
design_system_id, seed_systems}}; on to_dict.
- project_rulebook_exclusions: a project's opt-out of a whole always-on
rulebook — the sibling of the rule/topic suppressions, CASCADE both ways.
- services/inception.py (first cut): the vocabulary, validate_inception
(pure, all-or-nothing), normalize_choices, is_decided. Effects come in
step 3.
- Migration 0085 backfills every existing project via="legacy" with its
current standing (no exclusions, its subscriptions, its design_system_id,
no seed) so the ask fires only for projects created after this ships.
- Backup v10: rulebook_exclusions section; project rows carry inception and
design_system_id, restored in a post-pass once rulebooks/design systems are
mapped (design_system_id was not restored before — fixed in passing).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
"""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}
|
||||
Reference in New Issue
Block a user