feat(family): platforms, family ideas, the adoption ledger and its decision log (milestone 463 step 1, #4987)
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>
This commit is contained in:
2026-10-06 09:33:27 -04:00
co-authored by Claude Opus 5.5
parent ccbccb025c
commit e68ccc5884
7 changed files with 1474 additions and 4 deletions
+46 -3
View File
@@ -27,7 +27,7 @@ def test_backup_version_is_current():
(Named for the number it asserted until v10, which is exactly the drift a
name-carrying-a-value invites; it now says what it checks.)"""
assert backup.BACKUP_VERSION == 24
assert backup.BACKUP_VERSION == 25
def _exportable_note(**over):
@@ -125,11 +125,25 @@ def test_a_repo_binding_carries_the_branch_its_ledger_follows():
assert row["ref"] == "dev"
class _AnySlug(dict):
"""A slug map in which every platform id resolves. The stand-in's
platform_id is an arbitrary integer, and the guard is about which columns
travel, not about what a missing platform does — that would make the
import builder skip a row the guard needs it to build."""
def get(self, key, default=None):
return "platform-x"
# The table -> (model, row helper) registry the column guard walks. Kept here
# rather than in the service because it exists only to be introspected: the
# product code already knows these pairings by calling them.
def _column_guard_targets():
from scribe.models.canonical_system import CanonicalSystem
from scribe.models.family import (
FamilyAdoption, FamilyDecision, FamilyIdea, FamilyIdeaPlatform,
FamilyIdeaReference, Platform, ProjectPlatform,
)
from scribe.models.code_shape import CodeShape, CodeShapeEvent, CodeShapeUse
from scribe.models.lesson_rule_link import LessonNoRule, LessonRuleLink
from scribe.models.rule_moment_judgment import RuleMomentJudgment
@@ -187,6 +201,18 @@ def _column_guard_targets():
"code_shapes": (CodeShape, backup._code_shape_rows),
"code_shape_events": (CodeShapeEvent, backup._code_shape_event_rows),
"code_shape_uses": (CodeShapeUse, backup._code_shape_use_rows),
"platforms": (Platform, backup._platform_rows),
"project_platforms": (
ProjectPlatform, lambda rows: backup._project_platform_rows(rows, _AnySlug()),
),
"family_ideas": (FamilyIdea, backup._family_idea_rows),
"family_idea_platforms": (
FamilyIdeaPlatform,
lambda rows: backup._family_idea_platform_rows(rows, _AnySlug()),
),
"family_idea_references": (FamilyIdeaReference, backup._family_idea_reference_rows),
"family_adoptions": (FamilyAdoption, backup._family_adoption_rows),
"family_decisions": (FamilyDecision, backup._family_decision_rows),
}
@@ -306,6 +332,13 @@ def _import_guard_targets():
"code_shapes": backup._build_code_shape,
"code_shape_events": backup._build_code_shape_event,
"code_shape_uses": backup._build_code_shape_use,
"platforms": backup._build_platform,
"project_platforms": backup._build_project_platform,
"family_ideas": backup._build_family_idea,
"family_idea_platforms": backup._build_family_idea_platform,
"family_idea_references": backup._build_family_idea_reference,
"family_adoptions": backup._build_family_adoption,
"family_decisions": backup._build_family_decision,
}
return {
table: (model, helper, builders[table])
@@ -324,7 +357,8 @@ def _everything_maps(row: dict) -> "backup._Maps":
maps = backup._Maps()
ids = {v for v in row.values() if isinstance(v, int)} | {0, 1}
for name in ("users", "projects", "milestones", "notes", "rulebooks",
"topics", "rules", "systems", "design_systems", "shapes"):
"topics", "rules", "systems", "design_systems", "shapes",
"decisions"):
getattr(maps, name).update({i: i + 1000 for i in ids})
# `canonical_slug` only, never `slug`: a canonical_systems row is built
# exactly when its slug is NOT already known to the destination, so
@@ -332,6 +366,11 @@ def _everything_maps(row: dict) -> "backup._Maps":
slug = row.get("canonical_slug")
if slug:
maps.canonical_by_slug[slug] = 7
# `platform_slug` only, never `slug`, for the same reason: a platforms row
# is built exactly when its slug is unknown to the destination.
platform_slug = row.get("platform_slug")
if platform_slug:
maps.platform_by_slug[platform_slug] = 8
return maps
@@ -556,7 +595,11 @@ async def test_export_full_backup_contains_every_declared_section():
# v20: whether an area's rulings were read once shown.
"system_usage_events",
# v23: proposals and judgments about a rule's moments.
"rule_moment_judgments"):
"rule_moment_judgments",
# v25: family canon (milestone 463).
"platforms", "project_platforms", "family_ideas",
"family_idea_platforms", "family_idea_references",
"family_adoptions", "family_decisions"):
assert key in out, f"missing export section: {key}"
assert out[key] == []