feat: Phase 5B-5D — 10 canonical engineering queries + triage UI

The graph becomes useful. Before this commit, entities sat in the DB
as data with no narrative. After: the director can ask "what am I
forgetting?" and get a structured answer in milliseconds.

New module (src/atocore/engineering/queries.py, 360 lines):

Structure queries (Q-001/004/005/008/013):
- system_map(project): full subsystem → component tree + orphans +
  materials joined per component
- decisions_affecting(project, subsystem_id?): decisions linked via
  AFFECTED_BY_DECISION, scoped to a subsystem or whole project
- requirements_for(component_id): Q-005 forward trace
- recent_changes(project, since, limit): Q-013 via memory_audit join
  (reuses the Phase 4 audit infrastructure — entity_kind='entity')

The 3 killer queries (the real value):
- orphan_requirements(project): requirements with NO inbound SATISFIES
  edge. "What do I claim the system must do that nothing actually
  claims to handle?" Q-006.
- risky_decisions(project): decisions whose BASED_ON_ASSUMPTION edge
  points to an assumption with status in ('superseded','invalid') OR
  properties.flagged=True. Finds cascading risk from shaky premises. Q-009.
- unsupported_claims(project): ValidationClaim entities with no inbound
  SUPPORTS edge — asserted but no Result to back them. Q-011.
- all_gaps(project): runs all three in one call for dashboards.

History + impact (Q-016/017):
- impact_analysis(entity_id, max_depth=3): BFS over outbound edges.
  "What's downstream of this if I change it?"
- evidence_chain(entity_id): inbound SUPPORTS/EVIDENCED_BY/DESCRIBED_BY/
  VALIDATED_BY/ANALYZED_BY. "How do I know this is true?"

API (src/atocore/api/routes.py) exposes 10 endpoints:
- GET /engineering/projects/{p}/systems
- GET /engineering/decisions?project=&subsystem=
- GET /engineering/components/{id}/requirements
- GET /engineering/changes?project=&since=&limit=
- GET /engineering/gaps/orphan-requirements?project=
- GET /engineering/gaps/risky-decisions?project=
- GET /engineering/gaps/unsupported-claims?project=
- GET /engineering/gaps?project=  (combined)
- GET /engineering/impact?entity=&max_depth=
- GET /engineering/evidence?entity=

Mirror integration (src/atocore/engineering/mirror.py):
- New _gaps_section() renders at top of every project page
- If any gap non-empty: shows up-to-10 per category with names + context
- Clean project: " No gaps detected" — signals everything is traced

Triage UI (src/atocore/engineering/triage_ui.py):
- /admin/triage now shows BOTH memory candidates AND entity candidates
- Entity cards: name, type, project, confidence, source provenance,
  Promote/Reject buttons, link to wiki entity page
- Entity promote/reject via fetch to /entities/{id}/promote|reject
- One triage UI for the whole pipeline — consistent muscle memory

Tests: 326 → 341 (15 new, all in test_engineering_queries.py):
- System map structure + orphan detection + material joins
- Killer queries: positive + negative cases (empty when clean)
- Decisions query: project-wide and subsystem-scoped
- Impact analysis walks outbound BFS
- Evidence chain walks inbound provenance

No regressions. All 10 daily queries from the plan are now live and
answering real questions against the graph.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-17 07:18:46 -04:00
parent 07664bd743
commit 53b71639ad
5 changed files with 947 additions and 13 deletions

View File

@@ -1327,6 +1327,86 @@ def api_list_entities(
}
# --- Phase 5 Engineering V1: The 10 canonical queries ---
@router.get("/engineering/projects/{project_name}/systems")
def api_system_map(project_name: str) -> dict:
"""Q-001 + Q-004: subsystem/component tree for a project."""
from atocore.engineering.queries import system_map
return system_map(project_name)
@router.get("/engineering/decisions")
def api_decisions_affecting(
project: str,
subsystem: str | None = None,
) -> dict:
"""Q-008: decisions affecting a subsystem (or the whole project)."""
from atocore.engineering.queries import decisions_affecting
return decisions_affecting(project, subsystem_id=subsystem)
@router.get("/engineering/components/{component_id}/requirements")
def api_requirements_for_component(component_id: str) -> dict:
"""Q-005: requirements that a component satisfies."""
from atocore.engineering.queries import requirements_for
return requirements_for(component_id)
@router.get("/engineering/changes")
def api_recent_engineering_changes(
project: str,
since: str | None = None,
limit: int = 50,
) -> dict:
"""Q-013: entity changes in project since timestamp."""
from atocore.engineering.queries import recent_changes
return recent_changes(project, since=since, limit=limit)
@router.get("/engineering/gaps/orphan-requirements")
def api_orphan_requirements(project: str) -> dict:
"""Q-006 (killer): requirements with no SATISFIES edge."""
from atocore.engineering.queries import orphan_requirements
return orphan_requirements(project)
@router.get("/engineering/gaps/risky-decisions")
def api_risky_decisions(project: str) -> dict:
"""Q-009 (killer): decisions resting on flagged/superseded assumptions."""
from atocore.engineering.queries import risky_decisions
return risky_decisions(project)
@router.get("/engineering/gaps/unsupported-claims")
def api_unsupported_claims(project: str) -> dict:
"""Q-011 (killer): validation claims with no SUPPORTS edge."""
from atocore.engineering.queries import unsupported_claims
return unsupported_claims(project)
@router.get("/engineering/gaps")
def api_all_gaps(project: str) -> dict:
"""Combined Q-006 + Q-009 + Q-011 for a project."""
from atocore.engineering.queries import all_gaps
return all_gaps(project)
@router.get("/engineering/impact")
def api_impact_analysis(entity: str, max_depth: int = 3) -> dict:
"""Q-016: transitive outbound impact of changing an entity."""
from atocore.engineering.queries import impact_analysis
return impact_analysis(entity, max_depth=max_depth)
@router.get("/engineering/evidence")
def api_evidence_chain(entity: str) -> dict:
"""Q-017: inbound evidence chain for an entity."""
from atocore.engineering.queries import evidence_chain
return evidence_chain(entity)
@router.post("/entities/{entity_id}/promote")
def api_promote_entity(entity_id: str) -> dict:
"""Promote a candidate entity to active (Phase 5 Engineering V1)."""