feat(engineering): V1-A — Q-001 subsystem-scoped + pillar query integration
Per docs/plans/engineering-v1-completion-plan.md Phase V1-A. Scope is
deliberately tight: a single shape fix on Q-001 and an integration test
that proves the four pillar queries (Q-001 subsystem-scoped, Q-005,
Q-006, Q-017) work end-to-end against one seed graph on
p05-interferometer.
The shape fix:
- New subsystem_contents() in src/atocore/engineering/queries.py.
Returns {subsystem, contains: [{id, entity_type, name, status}]}
by walking inbound part_of edges (the inverse of CONTAINS), filtered
to active children.
- New route GET /entities/Subsystem/{subsystem_id}?expand=contains
matching the spec invocation in
docs/architecture/engineering-query-catalog.md Q-001 verbatim.
400 on unsupported expand value, 404 on missing/wrong-type id.
- Aliased under /v1.
The existing project-wide /engineering/projects/{name}/systems route
stays as-is for Q-004 (full project tree). The two queries answer
different operator questions and both belong in V1.
V1-A acceptance test (test_v1a_pillar_queries_run_end_to_end_against_
single_seed):
- Q-001 subsystem-scoped: Optics returns Primary Mirror + Diverger Lens
- Q-005 requirements_for: Primary Mirror has its single satisfied req
- Q-006 orphan_requirements: the orphan surfaces, the satisfied does not
- Q-017 evidence_chain: supported claim has FEA result via 'supports';
unsupported claim has no 'supports' edge
If this single test fails, V1-A is not done.
Tests: 596 -> 602 (+6). Per the plan: "~4 tests"; the +2 are basic
helper-function negatives (missing id, wrong type) kept separate from
the route and integration tests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2393,6 +2393,41 @@ def api_entity_audit(entity_id: str, limit: int = 100) -> dict:
|
||||
return {"entity_id": entity_id, "entries": entries, "count": len(entries)}
|
||||
|
||||
|
||||
@router.get("/entities/Subsystem/{subsystem_id}")
|
||||
def api_subsystem_contents(subsystem_id: str, expand: str = "contains") -> dict:
|
||||
"""Q-001 (subsystem-scoped variant) — the spec-shaped invocation.
|
||||
|
||||
Per ``docs/architecture/engineering-query-catalog.md`` Q-001:
|
||||
``GET /entities/Subsystem/<id>?expand=contains``
|
||||
→ ``{ subsystem, contains: [{ id, type, name, status }] }``
|
||||
|
||||
Distinct from the project-wide tree at
|
||||
``GET /engineering/projects/{name}/systems`` (Q-004), which stays
|
||||
as-is. V1-A only adds this single shape fix; broader query catalog
|
||||
closure is V1-C.
|
||||
|
||||
``expand`` is currently restricted to ``"contains"``; other expand
|
||||
facets (Q-007 ``constraints``, Q-008 ``decisions``) land in V1-C.
|
||||
"""
|
||||
if expand != "contains":
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=(
|
||||
f"unsupported expand={expand!r} for /entities/Subsystem; "
|
||||
"V1-A supports only 'contains'."
|
||||
),
|
||||
)
|
||||
from atocore.engineering.queries import subsystem_contents
|
||||
|
||||
result = subsystem_contents(subsystem_id)
|
||||
if result is None:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Subsystem not found or not a subsystem: {subsystem_id}",
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/entities/{entity_id}")
|
||||
def api_get_entity(entity_id: str) -> dict:
|
||||
"""Get an entity with its relationships and related entities."""
|
||||
|
||||
@@ -118,6 +118,66 @@ def system_map(project: str) -> dict:
|
||||
return out
|
||||
|
||||
|
||||
def subsystem_contents(subsystem_id: str) -> dict | None:
|
||||
"""Q-001 subsystem-scoped variant: a single subsystem and its
|
||||
direct ``CONTAINS`` children.
|
||||
|
||||
Spec: ``GET /entities/Subsystem/<id>?expand=contains`` per
|
||||
``docs/architecture/engineering-query-catalog.md`` Q-001.
|
||||
|
||||
Differs from :func:`system_map` (Q-004) which returns the
|
||||
project-wide tree. The subsystem-scoped form is what individual
|
||||
operator queries actually need: "what's inside this one subsystem?"
|
||||
rather than "show me the whole project."
|
||||
|
||||
The relationship walk uses inbound ``part_of`` edges (the inverse
|
||||
of ``CONTAINS``) so both child Components and child Subsystems
|
||||
surface uniformly. Filters to active children only — superseded
|
||||
or invalid rows do not belong in a "current contents" answer.
|
||||
|
||||
Returns:
|
||||
``{"subsystem": {id, name, project, status, description},
|
||||
"contains": [{id, entity_type, name, status}]}``
|
||||
or ``None`` when the entity does not exist or is not a subsystem.
|
||||
"""
|
||||
with get_connection() as conn:
|
||||
ss = conn.execute(
|
||||
"SELECT * FROM entities WHERE id = ?",
|
||||
(subsystem_id,),
|
||||
).fetchone()
|
||||
if ss is None or ss["entity_type"] != "subsystem":
|
||||
return None
|
||||
rows = conn.execute(
|
||||
"SELECT e.id, e.entity_type, e.name, e.status "
|
||||
"FROM relationships r "
|
||||
"JOIN entities e ON e.id = r.source_entity_id "
|
||||
"WHERE r.relationship_type = 'part_of' "
|
||||
"AND r.target_entity_id = ? "
|
||||
"AND e.status = 'active' "
|
||||
"ORDER BY e.entity_type, e.name",
|
||||
(subsystem_id,),
|
||||
).fetchall()
|
||||
|
||||
return {
|
||||
"subsystem": {
|
||||
"id": ss["id"],
|
||||
"name": ss["name"],
|
||||
"project": ss["project"],
|
||||
"status": ss["status"],
|
||||
"description": ss["description"] or "",
|
||||
},
|
||||
"contains": [
|
||||
{
|
||||
"id": r["id"],
|
||||
"entity_type": r["entity_type"],
|
||||
"name": r["name"],
|
||||
"status": r["status"],
|
||||
}
|
||||
for r in rows
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def decisions_affecting(project: str, subsystem_id: str | None = None) -> dict:
|
||||
"""Q-008: decisions that affect a subsystem (or whole project).
|
||||
|
||||
|
||||
@@ -66,6 +66,8 @@ _V1_PUBLIC_PATHS = {
|
||||
"/entities/{entity_id}/invalidate",
|
||||
"/entities/{entity_id}/supersede",
|
||||
"/entities/{entity_id}/audit",
|
||||
# V1-A: Q-001 subsystem-scoped variant per engineering-query-catalog
|
||||
"/entities/Subsystem/{subsystem_id}",
|
||||
"/relationships",
|
||||
"/ingest",
|
||||
"/ingest/sources",
|
||||
|
||||
Reference in New Issue
Block a user