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:
@@ -293,16 +293,109 @@ _TRIAGE_CSS = """
|
||||
"""
|
||||
|
||||
|
||||
def _render_entity_card(entity) -> str:
|
||||
"""Phase 5: entity candidate card with promote/reject."""
|
||||
eid = _escape(entity.id)
|
||||
name = _escape(entity.name)
|
||||
etype = _escape(entity.entity_type)
|
||||
project = _escape(entity.project or "(global)")
|
||||
desc = _escape(entity.description or "")
|
||||
conf = f"{entity.confidence:.2f}"
|
||||
src_refs = entity.source_refs or []
|
||||
source_display = _escape(", ".join(src_refs[:3])) if src_refs else "(no provenance)"
|
||||
|
||||
return f"""
|
||||
<div class="cand cand-entity" id="ecand-{eid}" data-entity-id="{eid}">
|
||||
<div class="cand-head">
|
||||
<span class="cand-type entity-type">[entity · {etype}]</span>
|
||||
<span class="cand-project">{project}</span>
|
||||
<span class="cand-meta">conf {conf} · src: {source_display}</span>
|
||||
</div>
|
||||
<div class="cand-body">
|
||||
<div class="entity-name">{name}</div>
|
||||
<div class="entity-desc">{desc}</div>
|
||||
</div>
|
||||
<div class="cand-actions">
|
||||
<button class="btn-entity-promote" data-entity-id="{eid}" title="Promote entity (Y)">✅ Promote Entity</button>
|
||||
<button class="btn-entity-reject" data-entity-id="{eid}" title="Reject entity (N)">❌ Reject</button>
|
||||
<a class="btn-link" href="/wiki/entities/{eid}">View in wiki →</a>
|
||||
</div>
|
||||
<div class="cand-status" id="estatus-{eid}"></div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
|
||||
_ENTITY_TRIAGE_SCRIPT = """
|
||||
<script>
|
||||
async function entityPromote(id) {
|
||||
const st = document.getElementById('estatus-' + id);
|
||||
st.textContent = 'Promoting…';
|
||||
st.className = 'cand-status ok';
|
||||
const r = await fetch('/entities/' + encodeURIComponent(id) + '/promote', {method:'POST'});
|
||||
if (r.ok) {
|
||||
st.textContent = '✅ Entity promoted';
|
||||
setTimeout(() => {
|
||||
const card = document.getElementById('ecand-' + id);
|
||||
if (card) { card.style.opacity = '0'; setTimeout(() => card.remove(), 300); }
|
||||
}, 400);
|
||||
} else st.textContent = '❌ ' + r.status;
|
||||
}
|
||||
async function entityReject(id) {
|
||||
const st = document.getElementById('estatus-' + id);
|
||||
st.textContent = 'Rejecting…';
|
||||
st.className = 'cand-status ok';
|
||||
const r = await fetch('/entities/' + encodeURIComponent(id) + '/reject', {method:'POST'});
|
||||
if (r.ok) {
|
||||
st.textContent = '❌ Entity rejected';
|
||||
setTimeout(() => {
|
||||
const card = document.getElementById('ecand-' + id);
|
||||
if (card) { card.style.opacity = '0'; setTimeout(() => card.remove(), 300); }
|
||||
}, 400);
|
||||
} else st.textContent = '❌ ' + r.status;
|
||||
}
|
||||
document.addEventListener('click', (e) => {
|
||||
const eid = e.target.dataset?.entityId;
|
||||
if (!eid) return;
|
||||
if (e.target.classList.contains('btn-entity-promote')) entityPromote(eid);
|
||||
else if (e.target.classList.contains('btn-entity-reject')) entityReject(eid);
|
||||
});
|
||||
</script>
|
||||
"""
|
||||
|
||||
_ENTITY_TRIAGE_CSS = """
|
||||
<style>
|
||||
.cand-entity { border-left: 3px solid #059669; }
|
||||
.entity-type { background: #059669; color: white; padding: 0.1rem 0.5rem; border-radius: 3px; font-size: 0.75rem; }
|
||||
.entity-name { font-size: 1.15rem; font-weight: 600; margin-bottom: 0.3rem; }
|
||||
.entity-desc { opacity: 0.85; font-size: 0.95rem; }
|
||||
.btn-entity-promote { background: #059669; color: white; border-color: #059669; }
|
||||
.btn-entity-reject:hover { background: #dc2626; color: white; border-color: #dc2626; }
|
||||
.btn-link { padding: 0.4rem 0.9rem; text-decoration: none; color: var(--accent); border: 1px solid var(--border); border-radius: 4px; font-size: 0.88rem; }
|
||||
.btn-link:hover { background: var(--hover); }
|
||||
.section-break { border-top: 2px solid var(--border); margin: 2rem 0 1rem 0; padding-top: 1rem; }
|
||||
</style>
|
||||
"""
|
||||
|
||||
|
||||
def render_triage_page(limit: int = 100) -> str:
|
||||
"""Render the full triage page with all pending candidates."""
|
||||
"""Render the full triage page with pending memory + entity candidates."""
|
||||
from atocore.engineering.service import get_entities
|
||||
|
||||
try:
|
||||
candidates = get_memories(status="candidate", limit=limit)
|
||||
mem_candidates = get_memories(status="candidate", limit=limit)
|
||||
except Exception as e:
|
||||
body = f"<p>Error loading candidates: {_escape(str(e))}</p>"
|
||||
body = f"<p>Error loading memory candidates: {_escape(str(e))}</p>"
|
||||
return render_html("Triage — AtoCore", body, breadcrumbs=[("Wiki", "/wiki"), ("Triage", "")])
|
||||
|
||||
if not candidates:
|
||||
body = _TRIAGE_CSS + """
|
||||
try:
|
||||
entity_candidates = get_entities(status="candidate", limit=limit)
|
||||
except Exception as e:
|
||||
entity_candidates = []
|
||||
|
||||
total = len(mem_candidates) + len(entity_candidates)
|
||||
|
||||
if total == 0:
|
||||
body = _TRIAGE_CSS + _ENTITY_TRIAGE_CSS + """
|
||||
<div class="triage-header">
|
||||
<h1>Triage Queue</h1>
|
||||
</div>
|
||||
@@ -313,15 +406,34 @@ def render_triage_page(limit: int = 100) -> str:
|
||||
"""
|
||||
return render_html("Triage — AtoCore", body, breadcrumbs=[("Wiki", "/wiki"), ("Triage", "")])
|
||||
|
||||
cards_html = "".join(_render_candidate_card(c) for c in candidates)
|
||||
# Memory cards
|
||||
mem_cards = "".join(_render_candidate_card(c) for c in mem_candidates)
|
||||
|
||||
body = _TRIAGE_CSS + f"""
|
||||
# Entity cards
|
||||
ent_cards_html = ""
|
||||
if entity_candidates:
|
||||
ent_cards = "".join(_render_entity_card(e) for e in entity_candidates)
|
||||
ent_cards_html = f"""
|
||||
<div class="section-break">
|
||||
<h2>🔧 Entity Candidates ({len(entity_candidates)})</h2>
|
||||
<p class="auto-triage-msg">
|
||||
Typed graph entries awaiting review. Promoting an entity connects it to
|
||||
the engineering knowledge graph (subsystems, requirements, decisions, etc.).
|
||||
</p>
|
||||
</div>
|
||||
{ent_cards}
|
||||
"""
|
||||
|
||||
body = _TRIAGE_CSS + _ENTITY_TRIAGE_CSS + f"""
|
||||
<div class="triage-header">
|
||||
<h1>Triage Queue</h1>
|
||||
<span class="count"><span id="cand-count">{len(candidates)}</span> pending</span>
|
||||
<span class="count">
|
||||
<span id="cand-count">{len(mem_candidates)}</span> memory ·
|
||||
{len(entity_candidates)} entity
|
||||
</span>
|
||||
</div>
|
||||
<div class="triage-help">
|
||||
Review candidate memories the auto-triage wasn't sure about. Edit the content
|
||||
Review candidates the auto-triage wasn't sure about. Edit the content
|
||||
if needed, then promote or reject. Shortcuts: <kbd>Y</kbd> promote · <kbd>N</kbd>
|
||||
reject · <kbd>E</kbd> edit · <kbd>S</kbd> scroll to next.
|
||||
</div>
|
||||
@@ -330,12 +442,14 @@ def render_triage_page(limit: int = 100) -> str:
|
||||
🤖 Auto-process queue
|
||||
</button>
|
||||
<span id="auto-triage-status" class="auto-triage-msg">
|
||||
Sends the full queue through LLM triage on the host. Promotes durable facts,
|
||||
rejects noise, leaves only ambiguous items here for you.
|
||||
Sends the full memory queue through LLM triage on the host. Entity candidates
|
||||
stay for manual review (types + relationships matter too much to auto-decide).
|
||||
</span>
|
||||
</div>
|
||||
{cards_html}
|
||||
""" + _TRIAGE_SCRIPT
|
||||
<h2>📝 Memory Candidates ({len(mem_candidates)})</h2>
|
||||
{mem_cards}
|
||||
{ent_cards_html}
|
||||
""" + _TRIAGE_SCRIPT + _ENTITY_TRIAGE_SCRIPT
|
||||
|
||||
return render_html(
|
||||
"Triage — AtoCore",
|
||||
|
||||
Reference in New Issue
Block a user