feat(family): platforms declared at inception and detected from bound repos (milestone 463 step 2, #4988)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 21s
CI & Build / TypeScript typecheck (push) Successful in 1m10s
CI & Build / integration (push) Successful in 1m48s
CI & Build / Python tests (push) Successful in 2m40s
CI & Build / Build & push image (push) Failing after 44s
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 21s
CI & Build / TypeScript typecheck (push) Successful in 1m10s
CI & Build / integration (push) Successful in 1m48s
CI & Build / Python tests (push) Successful in 2m40s
CI & Build / Build & push image (push) Failing after 44s
A project's platforms decide which family ideas reach it. This step makes membership answerable from every door: - services/platforms.py: the global catalog (writes are admin-only and duplicate-gated by slug); pure marker detection; and membership reads and writes. Detection only ADDS, and only where nobody has answered. It never overrides a declared or rejected row and never removes one. - coverage: the archive scan now carries every path, and the refresh runs detection fail-open. - inception: a platforms choice (slugs, or null for unanswered). The list is the whole answer: members left out of it become rejected. - MCP: list_platforms and set_project_platforms; enter_project and get_project carry the project's platforms. - REST: /api/platforms (admin writes) and /api/projects/<id>/platforms. - UI: a platforms checklist on the inception card, a Family tab on ProjectView, and a Platforms admin tab in Settings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+21
-3
@@ -27,7 +27,7 @@ def test_project_carries_an_inception_record_and_to_dict_shows_it():
|
||||
|
||||
|
||||
def test_validate_inception_pins_the_choice_vocabulary():
|
||||
assert CHOICE_KEYS == ("design_system_id", "seed_systems")
|
||||
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
|
||||
@@ -46,10 +46,28 @@ def test_validate_inception_pins_the_choice_vocabulary():
|
||||
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}
|
||||
assert normalize_choices(None) == {"design_system_id": None, "seed_systems": False}
|
||||
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():
|
||||
|
||||
Reference in New Issue
Block a user