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:
@@ -136,6 +136,16 @@ def _apply_migrations(conn: sqlite3.Connection) -> None:
|
||||
"CREATE INDEX IF NOT EXISTS idx_memories_valid_until ON memories(valid_until)"
|
||||
)
|
||||
|
||||
# Phase 5 (Engineering V1): when a memory graduates to an entity, we
|
||||
# keep the memory row as an immutable historical pointer. The forward
|
||||
# pointer lets downstream code follow "what did this memory become?"
|
||||
# without having to join through source_refs.
|
||||
if not _column_exists(conn, "memories", "graduated_to_entity_id"):
|
||||
conn.execute("ALTER TABLE memories ADD COLUMN graduated_to_entity_id TEXT")
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_memories_graduated ON memories(graduated_to_entity_id)"
|
||||
)
|
||||
|
||||
# Phase 4 (Robustness V1): append-only audit log for memory mutations.
|
||||
# Every create/update/promote/reject/supersede/invalidate/reinforce/expire/
|
||||
# auto_promote writes one row here. before/after are JSON snapshots of the
|
||||
@@ -160,6 +170,60 @@ def _apply_migrations(conn: sqlite3.Connection) -> None:
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_memory_audit_timestamp ON memory_audit(timestamp)")
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_memory_audit_action ON memory_audit(action)")
|
||||
|
||||
# Phase 5 (Engineering V1): entity_kind discriminator lets one audit
|
||||
# table serve both memories AND entities. Default "memory" keeps existing
|
||||
# rows correct; entity mutations write entity_kind="entity".
|
||||
if not _column_exists(conn, "memory_audit", "entity_kind"):
|
||||
conn.execute("ALTER TABLE memory_audit ADD COLUMN entity_kind TEXT DEFAULT 'memory'")
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_memory_audit_entity_kind ON memory_audit(entity_kind)"
|
||||
)
|
||||
|
||||
# Phase 5: conflicts + conflict_members tables per conflict-model.md.
|
||||
# A conflict is "two or more active rows claiming the same slot with
|
||||
# incompatible values". slot_kind + slot_key identify the logical slot
|
||||
# (e.g., "component.material" for some component id). Members point
|
||||
# back to the conflicting rows (memory or entity) with layer trust so
|
||||
# resolution can pick the highest-trust winner.
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS conflicts (
|
||||
id TEXT PRIMARY KEY,
|
||||
slot_kind TEXT NOT NULL,
|
||||
slot_key TEXT NOT NULL,
|
||||
project TEXT DEFAULT '',
|
||||
status TEXT DEFAULT 'open',
|
||||
resolution TEXT DEFAULT '',
|
||||
resolved_at DATETIME,
|
||||
detected_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
note TEXT DEFAULT ''
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS conflict_members (
|
||||
id TEXT PRIMARY KEY,
|
||||
conflict_id TEXT NOT NULL REFERENCES conflicts(id) ON DELETE CASCADE,
|
||||
member_kind TEXT NOT NULL,
|
||||
member_id TEXT NOT NULL,
|
||||
member_layer_trust INTEGER DEFAULT 0,
|
||||
value_snapshot TEXT DEFAULT ''
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_conflicts_status ON conflicts(status)")
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_conflicts_project ON conflicts(project)")
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_conflicts_slot ON conflicts(slot_kind, slot_key)"
|
||||
)
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_conflict_members_conflict ON conflict_members(conflict_id)"
|
||||
)
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_conflict_members_member ON conflict_members(member_kind, member_id)"
|
||||
)
|
||||
|
||||
# Phase 9 Commit A: capture loop columns on the interactions table.
|
||||
# The original schema only carried prompt + project_id + a context_pack
|
||||
# JSON blob. To make interactions a real audit trail of what AtoCore fed
|
||||
|
||||
Reference in New Issue
Block a user