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:
2026-04-29 12:54:12 -04:00
parent 785756fb58
commit b57577352d
4 changed files with 307 additions and 0 deletions

View File

@@ -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."""

View File

@@ -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).

View File

@@ -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",