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:
@@ -23,6 +23,7 @@ from scribe.services import design_systems as design_systems_svc
|
||||
from scribe.services import inception as inception_svc
|
||||
from scribe.services import milestones as milestones_svc
|
||||
from scribe.services import notes as notes_svc
|
||||
from scribe.services import platforms as platforms_svc
|
||||
from scribe.services import projects as projects_svc
|
||||
from scribe.services import rulebooks as rulebooks_svc
|
||||
from scribe.services import systems as systems_svc
|
||||
@@ -125,9 +126,15 @@ async def enter_project(project_id: int) -> dict:
|
||||
create it with create_system rather than leaving the area unmodelled. Read
|
||||
a subsystem's accumulated records with list_system_records. Each is id and name; get_system has the charter.
|
||||
|
||||
`platforms` (milestone 463) is what the project is built on or ships as
|
||||
— Android app, container image, Go, … — each with how it is known
|
||||
(`declared` by a person, `detected` from a bound repo). They decide which
|
||||
family ideas reach this project. An empty list on a project that plainly
|
||||
ships something is worth correcting with set_project_platforms.
|
||||
|
||||
`inception` (milestone 297) appears ONLY when the project is yours and
|
||||
nobody has decided what it inherits: it carries the current defaults
|
||||
(design system, Systems), what to ask the
|
||||
(design system, Systems, platforms), what to ask the
|
||||
operator — once — and the decide_project_inception call that answers it;
|
||||
it repeats on every enter until a decision is recorded.
|
||||
|
||||
@@ -188,6 +195,9 @@ async def enter_project(project_id: int) -> dict:
|
||||
# three days of the feature landing (#2546's audit). Untagged writes now
|
||||
# also ask with the vocabulary listed; this copy lets the first write tag.
|
||||
systems = await systems_svc.list_systems(uid, project_id)
|
||||
platforms = platforms_svc.members(
|
||||
await platforms_svc.project_platforms(uid, project_id) or []
|
||||
)
|
||||
|
||||
# The arrival-moment half of the bootstrap ask (#2683): session start is
|
||||
# when the agent has just read the project map and is not yet deep in a
|
||||
@@ -258,6 +268,7 @@ async def enter_project(project_id: int) -> dict:
|
||||
},
|
||||
"pattern_coverage": coverage_svc.coverage_line(coverage) if coverage else None,
|
||||
"systems": [{"id": s.id, "name": s.name} for s in systems],
|
||||
"platforms": platforms,
|
||||
"design_system": design_system,
|
||||
"milestone_summary": milestone_summary,
|
||||
**rulebooks_svc.rules_payload(
|
||||
@@ -302,12 +313,15 @@ async def get_project(project_id: int) -> dict:
|
||||
rules (project_rules), and applicable_rules: the
|
||||
global rules tagged to an area this project works in. Every other global
|
||||
rule applies too and arrives by retrieval when the work matches it.
|
||||
`platforms` is every platform the project has an answer for, with its
|
||||
state — rejected ones included, unlike enter_project's brief list.
|
||||
"""
|
||||
uid = current_user_id()
|
||||
project = await projects_svc.get_project(uid, project_id)
|
||||
if project is None:
|
||||
raise ValueError(f"project {project_id} not found")
|
||||
data = project.to_dict()
|
||||
data["platforms"] = await platforms_svc.project_platforms(uid, project_id) or []
|
||||
rows = await milestones_svc.get_project_milestone_summary(uid, project_id)
|
||||
data["milestone_summary"], _ = milestones_svc.brief_milestone_summary(rows)
|
||||
applicable = await rulebooks_svc.get_applicable_rules(
|
||||
@@ -317,17 +331,21 @@ async def get_project(project_id: int) -> dict:
|
||||
return data
|
||||
|
||||
|
||||
def _inception_choices(design_system_id, seed_systems) -> dict | None:
|
||||
def _inception_choices(design_system_id, seed_systems, platforms=None) -> dict | None:
|
||||
"""The tool args → an inception choices object, or None when no inception
|
||||
arg was given at all (a bare create stays undecided and enter_project
|
||||
asks). design_system_id: 0 = not stated, -1 = explicitly none, n = that
|
||||
system."""
|
||||
if not design_system_id and seed_systems is None:
|
||||
system. platforms: None = not stated (memberships untouched), a list =
|
||||
the whole answer."""
|
||||
if not design_system_id and seed_systems is None and platforms is None:
|
||||
return None
|
||||
return {
|
||||
choices = {
|
||||
"design_system_id": None if design_system_id in (0, -1) else design_system_id,
|
||||
"seed_systems": bool(seed_systems),
|
||||
}
|
||||
if platforms is not None:
|
||||
choices["platforms"] = list(platforms)
|
||||
return choices
|
||||
|
||||
|
||||
async def create_project(
|
||||
@@ -338,11 +356,12 @@ async def create_project(
|
||||
color: str = "",
|
||||
design_system_id: int = 0,
|
||||
seed_systems: bool | None = None,
|
||||
platforms: list[str] | None = None,
|
||||
) -> dict:
|
||||
"""Create a new project in Scribe — and decide what it inherits.
|
||||
|
||||
A project's inheritance is a decision, not a default (milestone 297):
|
||||
before calling, ask the operator the two inception questions and pass
|
||||
before calling, ask the operator the three inception questions and pass
|
||||
the answers; a project created without either is UNDECIDED and
|
||||
enter_project will ask until decide_project_inception records it.
|
||||
Defaults if nobody decides: no design system, no Systems. Rules are not
|
||||
@@ -359,6 +378,9 @@ async def create_project(
|
||||
(list_design_systems); -1 = explicitly none; 0 = not stated.
|
||||
seed_systems: true mints the standard starter Systems (CI & Release,
|
||||
Auth & Access, …) so records can be tagged from day one.
|
||||
platforms: slugs of what the project is built on or ships as
|
||||
(list_platforms) — they decide which family ideas reach it. The
|
||||
list is the whole answer; omit it to leave the question open.
|
||||
"""
|
||||
uid = current_user_id()
|
||||
project = await projects_svc.create_project(
|
||||
@@ -370,7 +392,7 @@ async def create_project(
|
||||
color=color or None,
|
||||
)
|
||||
data = project.to_dict()
|
||||
choices = _inception_choices(design_system_id, seed_systems)
|
||||
choices = _inception_choices(design_system_id, seed_systems, platforms)
|
||||
if choices is not None:
|
||||
decided = await inception_svc.decide(uid, project.id, choices=choices, via="mcp")
|
||||
data["inception"] = decided["inception"]
|
||||
@@ -388,6 +410,7 @@ async def decide_project_inception(
|
||||
project_id: int,
|
||||
design_system_id: int = 0,
|
||||
seed_systems: bool | None = None,
|
||||
platforms: list[str] | None = None,
|
||||
) -> dict:
|
||||
"""Record what a project inherits — answer enter_project's `inception` ask,
|
||||
or re-decide later (milestone 297).
|
||||
@@ -400,10 +423,10 @@ async def decide_project_inception(
|
||||
|
||||
Args: as create_project's inception args. Passing nothing records a
|
||||
decision to take nothing (no design system, no seed) — a valid answer,
|
||||
stated.
|
||||
stated — and leaves the project's platforms as they are.
|
||||
"""
|
||||
uid = current_user_id()
|
||||
choices = _inception_choices(design_system_id, seed_systems) or {}
|
||||
choices = _inception_choices(design_system_id, seed_systems, platforms) or {}
|
||||
decided = await inception_svc.decide(uid, project_id, choices=choices, via="mcp")
|
||||
return {"project_id": project_id, **decided}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user