feat: Phase 5A — Engineering V1 foundation
First slice of the Engineering V1 sprint. Lays the schema + lifecycle
plumbing so the 10 canonical queries, memory graduation, and conflict
detection can land cleanly on top.
Schema (src/atocore/models/database.py):
- conflicts + conflict_members tables per conflict-model.md (with 5
indexes on status/project/slot/members)
- memory_audit.entity_kind discriminator — same audit table serves
both memories ("memory") and entities ("entity"); unified history
without duplicating infrastructure
- memories.graduated_to_entity_id forward pointer for graduated
memories (M → E transition preserves the memory as historical
pointer)
Memory (src/atocore/memory/service.py):
- MEMORY_STATUSES gains "graduated" — memory-entity graduation flow
ready to wire in Phase 5F
Engineering service (src/atocore/engineering/service.py):
- RELATIONSHIP_TYPES organized into 4 families per ontology-v1.md:
+ Structural: contains, part_of, interfaces_with
+ Intent: satisfies, constrained_by, affected_by_decision,
based_on_assumption (new), supersedes
+ Validation: analyzed_by, validated_by, supports (new),
conflicts_with (new), depends_on
+ Provenance: described_by, updated_by_session (new),
evidenced_by (new), summarized_in (new)
- create_entity + create_relationship now call resolve_project_name()
on write (canonicalization contract per doc)
- Both accept actor= parameter for audit provenance
- _audit_entity() helper uses shared memory_audit table with
entity_kind="entity" — one observability layer for everything
- promote_entity / reject_entity_candidate / supersede_entity —
mirror the memory lifecycle exactly (same pattern, same naming)
- get_entity_audit() reads from the shared table filtered by
entity_kind
API (src/atocore/api/routes.py):
- POST /entities/{id}/promote (candidate → active)
- POST /entities/{id}/reject (candidate → invalid)
- GET /entities/{id}/audit (full history for one entity)
- POST /entities passes actor="api-http" through
Tests: 317 → 326 (9 new):
- test_entity_project_canonicalization (p04 → p04-gigabit)
- test_promote_entity_candidate_to_active
- test_reject_entity_candidate
- test_promote_active_entity_noop (only candidates promote)
- test_entity_audit_log_captures_lifecycle (before/after snapshots)
- test_new_relationship_types_available (6 new types present)
- test_conflicts_tables_exist
- test_memory_audit_has_entity_kind
- test_graduated_status_accepted
What's next (5B-5I, deferred): entity triage UI tab, core structure
queries, the 3 killer queries, memory graduation script, conflict
detection, MCP + context pack integration. See plan file.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1286,6 +1286,7 @@ def api_create_entity(req: EntityCreateRequest) -> dict:
|
||||
status=req.status,
|
||||
confidence=req.confidence,
|
||||
source_refs=req.source_refs,
|
||||
actor="api-http",
|
||||
)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
@@ -1326,6 +1327,34 @@ def api_list_entities(
|
||||
}
|
||||
|
||||
|
||||
@router.post("/entities/{entity_id}/promote")
|
||||
def api_promote_entity(entity_id: str) -> dict:
|
||||
"""Promote a candidate entity to active (Phase 5 Engineering V1)."""
|
||||
from atocore.engineering.service import promote_entity
|
||||
success = promote_entity(entity_id, actor="api-http")
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail=f"Entity not found or not a candidate: {entity_id}")
|
||||
return {"status": "promoted", "id": entity_id}
|
||||
|
||||
|
||||
@router.post("/entities/{entity_id}/reject")
|
||||
def api_reject_entity(entity_id: str) -> dict:
|
||||
"""Reject a candidate entity (Phase 5)."""
|
||||
from atocore.engineering.service import reject_entity_candidate
|
||||
success = reject_entity_candidate(entity_id, actor="api-http")
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail=f"Entity not found or not a candidate: {entity_id}")
|
||||
return {"status": "rejected", "id": entity_id}
|
||||
|
||||
|
||||
@router.get("/entities/{entity_id}/audit")
|
||||
def api_entity_audit(entity_id: str, limit: int = 100) -> dict:
|
||||
"""Return the audit history for a specific entity."""
|
||||
from atocore.engineering.service import get_entity_audit
|
||||
entries = get_entity_audit(entity_id, limit=limit)
|
||||
return {"entity_id": entity_id, "entries": entries, "count": len(entries)}
|
||||
|
||||
|
||||
@router.get("/entities/{entity_id}")
|
||||
def api_get_entity(entity_id: str) -> dict:
|
||||
"""Get an entity with its relationships and related entities."""
|
||||
|
||||
Reference in New Issue
Block a user