feat(db): ORM models + Alembic migrations 0001–0003 — T09/T10/T11
SQLAlchemy ORM (src/activity_core/orm.py):
- ActivityDefinition, ActivityRun, TaskInstance mapped to Base.metadata
- Wired into migrations/env.py for autogenerate support
Migrations (chained 0001 → 0002 → 0003):
- 0001: activity_definitions (id, name, enabled, trigger_type,
trigger_config JSONB, context_sources JSONB, task_templates JSONB,
dedupe_key_strategy, version, created_at, updated_at)
- 0002: activity_runs (run_id, activity_id FK→activity_definitions,
scheduled_for, fired_at, context_snapshot JSONB, tasks_spawned,
version_used) + index on activity_id
- 0003: task_instances (id, run_id FK→activity_runs CASCADE,
type, params JSONB, status, created_at) + index on run_id
Apply with: ACTCORE_DB_URL=... alembic upgrade head
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 21:51:01 +00:00
|
|
|
"""SQLAlchemy ORM table definitions for activity-core.
|
|
|
|
|
|
|
|
|
|
These are the persistence-layer counterparts to the Pydantic domain models in
|
|
|
|
|
models.py. Alembic reads Base.metadata (imported via db.py) for autogenerate.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import uuid
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
|
|
|
|
|
from sqlalchemy import (
|
|
|
|
|
Boolean,
|
|
|
|
|
DateTime,
|
|
|
|
|
ForeignKey,
|
|
|
|
|
Integer,
|
feat(event-bridge): WP-0003a — domain model, rules module, event type registry
Implements phases 7–8 of the Event Bridge architecture (custodian-WP-0003a).
Domain model (T34, T40):
- Added RuleDef, InstructionDef, ActionDef to models.py
- Updated ActivityDefinition with rules/instructions fields (task_templates deprecated)
- Formalized EventEnvelope: id, type, version, timestamp, publisher, attributes
- Added from_nats_message() and from_webhook_payload() classmethods
Rules module (T35, T36, T37):
- src/activity_core/rules/ skeleton with boundary enforcement
- evaluate_condition() — sandboxed AST walker, whitelisted nodes only, never exec()
- execute_instruction() — LLM task generation with trusted_fields injection guard
- tests/rules/test_boundary.py verifies no cross-boundary imports
Infrastructure (T38, T39):
- Alembic migrations 0004 (task_spawn_log) and 0005 (event_types)
- IssueSink ABC + IssueCoreRestSink (REST) + NullSink (testing)
- TaskSpawnLog and EventType ORM models
Event type registry (T41, T42, T43):
- event_type_registry.py: file scanner, parser, DB sync, in-process lookup
- ACTIVITY_CURATOR_GATE env var (disabled|required) + approve endpoint
- Three org event type definitions: org.repo.registered, org.workstream.completed,
org.activity.run.completed
All 10 tests pass. Boundary test confirms rules/ isolation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 22:01:15 +02:00
|
|
|
String,
|
feat(db): ORM models + Alembic migrations 0001–0003 — T09/T10/T11
SQLAlchemy ORM (src/activity_core/orm.py):
- ActivityDefinition, ActivityRun, TaskInstance mapped to Base.metadata
- Wired into migrations/env.py for autogenerate support
Migrations (chained 0001 → 0002 → 0003):
- 0001: activity_definitions (id, name, enabled, trigger_type,
trigger_config JSONB, context_sources JSONB, task_templates JSONB,
dedupe_key_strategy, version, created_at, updated_at)
- 0002: activity_runs (run_id, activity_id FK→activity_definitions,
scheduled_for, fired_at, context_snapshot JSONB, tasks_spawned,
version_used) + index on activity_id
- 0003: task_instances (id, run_id FK→activity_runs CASCADE,
type, params JSONB, status, created_at) + index on run_id
Apply with: ACTCORE_DB_URL=... alembic upgrade head
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 21:51:01 +00:00
|
|
|
Text,
|
|
|
|
|
func,
|
|
|
|
|
)
|
|
|
|
|
from sqlalchemy.dialects.postgresql import JSONB, UUID
|
|
|
|
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
|
|
|
|
|
|
|
|
from activity_core.db import Base
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ActivityDefinition(Base):
|
|
|
|
|
__tablename__ = "activity_definitions"
|
|
|
|
|
|
|
|
|
|
id: Mapped[uuid.UUID] = mapped_column(
|
|
|
|
|
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
|
|
|
|
)
|
|
|
|
|
name: Mapped[str] = mapped_column(Text, nullable=False)
|
|
|
|
|
enabled: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
|
|
|
|
|
trigger_type: Mapped[str] = mapped_column(Text, nullable=False)
|
|
|
|
|
trigger_config: Mapped[dict] = mapped_column(JSONB, nullable=False)
|
|
|
|
|
context_sources: Mapped[list] = mapped_column(JSONB, nullable=False, default=list)
|
|
|
|
|
task_templates: Mapped[list] = mapped_column(JSONB, nullable=False, default=list)
|
2026-05-14 23:02:33 +02:00
|
|
|
rules_json: Mapped[list] = mapped_column(JSONB, nullable=False, default=list)
|
|
|
|
|
instructions_json: Mapped[list] = mapped_column(JSONB, nullable=False, default=list)
|
feat(db): ORM models + Alembic migrations 0001–0003 — T09/T10/T11
SQLAlchemy ORM (src/activity_core/orm.py):
- ActivityDefinition, ActivityRun, TaskInstance mapped to Base.metadata
- Wired into migrations/env.py for autogenerate support
Migrations (chained 0001 → 0002 → 0003):
- 0001: activity_definitions (id, name, enabled, trigger_type,
trigger_config JSONB, context_sources JSONB, task_templates JSONB,
dedupe_key_strategy, version, created_at, updated_at)
- 0002: activity_runs (run_id, activity_id FK→activity_definitions,
scheduled_for, fired_at, context_snapshot JSONB, tasks_spawned,
version_used) + index on activity_id
- 0003: task_instances (id, run_id FK→activity_runs CASCADE,
type, params JSONB, status, created_at) + index on run_id
Apply with: ACTCORE_DB_URL=... alembic upgrade head
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 21:51:01 +00:00
|
|
|
dedupe_key_strategy: Mapped[str] = mapped_column(
|
|
|
|
|
Text, nullable=False, default="skip"
|
|
|
|
|
)
|
|
|
|
|
version: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
|
|
|
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
|
|
|
DateTime(timezone=True), nullable=False, server_default=func.now()
|
|
|
|
|
)
|
|
|
|
|
updated_at: Mapped[datetime] = mapped_column(
|
|
|
|
|
DateTime(timezone=True),
|
|
|
|
|
nullable=False,
|
|
|
|
|
server_default=func.now(),
|
|
|
|
|
onupdate=func.now(),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ActivityRun(Base):
|
|
|
|
|
__tablename__ = "activity_runs"
|
|
|
|
|
|
|
|
|
|
run_id: Mapped[uuid.UUID] = mapped_column(
|
|
|
|
|
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
|
|
|
|
)
|
|
|
|
|
activity_id: Mapped[uuid.UUID] = mapped_column(
|
|
|
|
|
UUID(as_uuid=True),
|
|
|
|
|
ForeignKey("activity_definitions.id", ondelete="RESTRICT"),
|
|
|
|
|
nullable=False,
|
|
|
|
|
index=True,
|
|
|
|
|
)
|
|
|
|
|
scheduled_for: Mapped[datetime | None] = mapped_column(
|
|
|
|
|
DateTime(timezone=True), nullable=True
|
|
|
|
|
)
|
|
|
|
|
fired_at: Mapped[datetime] = mapped_column(
|
|
|
|
|
DateTime(timezone=True), nullable=False, server_default=func.now()
|
|
|
|
|
)
|
|
|
|
|
context_snapshot: Mapped[dict] = mapped_column(
|
|
|
|
|
JSONB, nullable=False, default=dict
|
|
|
|
|
)
|
|
|
|
|
tasks_spawned: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
|
|
|
version_used: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
|
|
|
|
|
|
|
|
|
feat(event-bridge): WP-0003a — domain model, rules module, event type registry
Implements phases 7–8 of the Event Bridge architecture (custodian-WP-0003a).
Domain model (T34, T40):
- Added RuleDef, InstructionDef, ActionDef to models.py
- Updated ActivityDefinition with rules/instructions fields (task_templates deprecated)
- Formalized EventEnvelope: id, type, version, timestamp, publisher, attributes
- Added from_nats_message() and from_webhook_payload() classmethods
Rules module (T35, T36, T37):
- src/activity_core/rules/ skeleton with boundary enforcement
- evaluate_condition() — sandboxed AST walker, whitelisted nodes only, never exec()
- execute_instruction() — LLM task generation with trusted_fields injection guard
- tests/rules/test_boundary.py verifies no cross-boundary imports
Infrastructure (T38, T39):
- Alembic migrations 0004 (task_spawn_log) and 0005 (event_types)
- IssueSink ABC + IssueCoreRestSink (REST) + NullSink (testing)
- TaskSpawnLog and EventType ORM models
Event type registry (T41, T42, T43):
- event_type_registry.py: file scanner, parser, DB sync, in-process lookup
- ACTIVITY_CURATOR_GATE env var (disabled|required) + approve endpoint
- Three org event type definitions: org.repo.registered, org.workstream.completed,
org.activity.run.completed
All 10 tests pass. Boundary test confirms rules/ isolation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 22:01:15 +02:00
|
|
|
class TaskSpawnLog(Base):
|
|
|
|
|
__tablename__ = "task_spawn_log"
|
|
|
|
|
|
|
|
|
|
id: Mapped[uuid.UUID] = mapped_column(
|
|
|
|
|
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
|
|
|
|
)
|
|
|
|
|
activity_def_id: Mapped[uuid.UUID] = mapped_column(
|
|
|
|
|
UUID(as_uuid=True),
|
|
|
|
|
ForeignKey("activity_definitions.id"),
|
|
|
|
|
nullable=False,
|
|
|
|
|
index=True,
|
|
|
|
|
)
|
|
|
|
|
source_type: Mapped[str] = mapped_column(String(20), nullable=False)
|
|
|
|
|
source_id: Mapped[str] = mapped_column(Text, nullable=False)
|
|
|
|
|
source_version: Mapped[str] = mapped_column(Text, nullable=False)
|
|
|
|
|
triggering_event_id: Mapped[str] = mapped_column(Text, nullable=False, index=True)
|
|
|
|
|
task_ref: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
|
|
|
condition_matched: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
|
|
|
prompt_hash: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
|
|
|
|
model: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
|
|
|
output_validated: Mapped[bool | None] = mapped_column(Boolean, nullable=True)
|
|
|
|
|
review_required: Mapped[bool | None] = mapped_column(Boolean, nullable=True)
|
|
|
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
|
|
|
DateTime(timezone=True), nullable=False, server_default=func.now()
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class EventType(Base):
|
|
|
|
|
__tablename__ = "event_types"
|
|
|
|
|
|
|
|
|
|
type_id: Mapped[str] = mapped_column(Text, primary_key=True)
|
|
|
|
|
version: Mapped[str] = mapped_column(Text, nullable=False)
|
|
|
|
|
publisher: Mapped[str] = mapped_column(Text, nullable=False)
|
|
|
|
|
governance: Mapped[str] = mapped_column(Text, nullable=False, default="publisher-declared")
|
|
|
|
|
status: Mapped[str] = mapped_column(Text, nullable=False, default="active")
|
|
|
|
|
attribute_schema: Mapped[dict] = mapped_column(JSONB, nullable=False)
|
|
|
|
|
raw_md: Mapped[str] = mapped_column(Text, nullable=False)
|
|
|
|
|
synced_at: Mapped[datetime] = mapped_column(
|
|
|
|
|
DateTime(timezone=True), nullable=False, server_default=func.now()
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
feat(db): ORM models + Alembic migrations 0001–0003 — T09/T10/T11
SQLAlchemy ORM (src/activity_core/orm.py):
- ActivityDefinition, ActivityRun, TaskInstance mapped to Base.metadata
- Wired into migrations/env.py for autogenerate support
Migrations (chained 0001 → 0002 → 0003):
- 0001: activity_definitions (id, name, enabled, trigger_type,
trigger_config JSONB, context_sources JSONB, task_templates JSONB,
dedupe_key_strategy, version, created_at, updated_at)
- 0002: activity_runs (run_id, activity_id FK→activity_definitions,
scheduled_for, fired_at, context_snapshot JSONB, tasks_spawned,
version_used) + index on activity_id
- 0003: task_instances (id, run_id FK→activity_runs CASCADE,
type, params JSONB, status, created_at) + index on run_id
Apply with: ACTCORE_DB_URL=... alembic upgrade head
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 21:51:01 +00:00
|
|
|
class TaskInstance(Base):
|
|
|
|
|
__tablename__ = "task_instances"
|
|
|
|
|
|
|
|
|
|
id: Mapped[uuid.UUID] = mapped_column(
|
|
|
|
|
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
|
|
|
|
)
|
|
|
|
|
run_id: Mapped[uuid.UUID] = mapped_column(
|
|
|
|
|
UUID(as_uuid=True),
|
|
|
|
|
ForeignKey("activity_runs.run_id", ondelete="CASCADE"),
|
|
|
|
|
nullable=False,
|
|
|
|
|
index=True,
|
|
|
|
|
)
|
|
|
|
|
type: Mapped[str] = mapped_column(Text, nullable=False)
|
|
|
|
|
params: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict)
|
|
|
|
|
status: Mapped[str] = mapped_column(Text, nullable=False, default="pending")
|
|
|
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
|
|
|
DateTime(timezone=True), nullable=False, server_default=func.now()
|
|
|
|
|
)
|
2026-08-03 19:22:50 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class OpsRun(Base):
|
|
|
|
|
"""Claimable automation run instance (ACT-ADR-005 / ACTIVITY-WP-0026)."""
|
|
|
|
|
|
|
|
|
|
__tablename__ = "ops_runs"
|
|
|
|
|
|
|
|
|
|
id: Mapped[uuid.UUID] = mapped_column(
|
|
|
|
|
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
|
|
|
|
)
|
|
|
|
|
activity_definition_id: Mapped[uuid.UUID] = mapped_column(
|
|
|
|
|
UUID(as_uuid=True),
|
|
|
|
|
ForeignKey("activity_definitions.id", ondelete="RESTRICT"),
|
|
|
|
|
nullable=False,
|
|
|
|
|
index=True,
|
|
|
|
|
)
|
|
|
|
|
idempotency_key: Mapped[str] = mapped_column(Text, nullable=False, unique=True)
|
|
|
|
|
target_repo: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
|
|
|
title: Mapped[str] = mapped_column(Text, nullable=False)
|
|
|
|
|
description: Mapped[str] = mapped_column(Text, nullable=False, default="")
|
|
|
|
|
labels: Mapped[list] = mapped_column(JSONB, nullable=False, default=list)
|
|
|
|
|
priority: Mapped[str] = mapped_column(Text, nullable=False, default="medium")
|
|
|
|
|
state: Mapped[str] = mapped_column(Text, nullable=False, default="open", index=True)
|
|
|
|
|
claim_owner: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
|
|
|
lease_until: Mapped[datetime | None] = mapped_column(
|
|
|
|
|
DateTime(timezone=True), nullable=True, index=True
|
|
|
|
|
)
|
|
|
|
|
attempt: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
|
|
|
source_type: Mapped[str] = mapped_column(Text, nullable=False)
|
|
|
|
|
source_id: Mapped[str] = mapped_column(Text, nullable=False)
|
|
|
|
|
triggering_event_id: Mapped[str] = mapped_column(Text, nullable=False, index=True)
|
|
|
|
|
approach_hint: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
|
|
|
result: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict)
|
|
|
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
|
|
|
DateTime(timezone=True), nullable=False, server_default=func.now()
|
|
|
|
|
)
|
|
|
|
|
updated_at: Mapped[datetime] = mapped_column(
|
|
|
|
|
DateTime(timezone=True),
|
|
|
|
|
nullable=False,
|
|
|
|
|
server_default=func.now(),
|
|
|
|
|
onupdate=func.now(),
|
|
|
|
|
)
|