Files
Joel Salmon c4fc1cccd7 Phase 4: task-to-goal integration (Vision side)
Build the boundary the ImpactFlow core time-tracker plugs into. A task maps to
a foundation — one of the six stable profile elements (love, strength, mission,
vocation, short_term, long_term) — so the tracker can ask "which goal does this
build toward?" and post the answer back to Vision.

- Models + migration 006: task_mapping (one row per logged time entry).
- app/services/foundations.py: the six foundations + a pure, testable
  work-pattern aggregator (rollup) and a plain-language summary.
- app/routers/integration.py (user-scoped; tracker calls as the user or via
  X-API-Key): GET /foundations, POST/GET /task-mappings,
  GET /work-patterns?days=N (per-foundation minutes/share/neglected).
- Reminder engine now pulls from real work patterns: CheckinCoach takes an
  optional work-pattern summary (last 14 days) and reflects where time has gone
  against the person's own words — an observation, never a verdict.
- Frontend: dashboard.html (time per foundation + neglected); linked from
  profile.html.

Documented the core-tracker integration contract in the README. Phase 4
completes the Vision module's roadmap on the Discovery side; the core tracker
integrates by calling these endpoints.

Tests: 86 passing (added pure-aggregator, integration-endpoint, and
work-pattern-into-check-in tests; run in-container).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 21:24:58 -05:00

273 lines
11 KiB
Python

"""SQLAlchemy ORM models for the self-discovery module."""
from datetime import datetime
from typing import Optional
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, Text
from sqlalchemy.orm import Mapped, mapped_column
from app.database import Base
class User(Base):
"""A signed-in human (Google OAuth) or the synthetic admin record used
by the X-API-Key dual-auth path for the MCP server."""
__tablename__ = "users"
id: Mapped[str] = mapped_column(String, primary_key=True)
email: Mapped[str] = mapped_column(String, unique=True, nullable=False)
display_name: Mapped[str] = mapped_column(String, nullable=False)
avatar_url: Mapped[Optional[str]] = mapped_column(String, nullable=True)
# Google's "sub" claim. Null only for the synthetic API-key admin user.
google_id: Mapped[Optional[str]] = mapped_column(
String, unique=True, nullable=True
)
role: Mapped[str] = mapped_column(String, nullable=False, default="user")
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False)
last_login_at: Mapped[Optional[datetime]] = mapped_column(
DateTime, nullable=True
)
class RefreshToken(Base):
"""One row per issued refresh token. token_hash stores a SHA-256 of the
raw token so a DB leak can't be replayed back at the auth endpoint."""
__tablename__ = "refresh_tokens"
id: Mapped[str] = mapped_column(String, primary_key=True)
user_id: Mapped[str] = mapped_column(
String, ForeignKey("users.id"), nullable=False, index=True
)
token_hash: Mapped[str] = mapped_column(
String, unique=True, nullable=False
)
device: Mapped[Optional[str]] = mapped_column(String, nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False)
expires_at: Mapped[datetime] = mapped_column(DateTime, nullable=False)
revoked_at: Mapped[Optional[datetime]] = mapped_column(
DateTime, nullable=True
)
class ActivityLog(Base):
"""Append-only audit trail. Pruned to 90 days on app startup."""
__tablename__ = "activity_log"
id: Mapped[str] = mapped_column(String, primary_key=True)
user_id: Mapped[str] = mapped_column(
String, ForeignKey("users.id"), nullable=False, index=True
)
action: Mapped[str] = mapped_column(String, nullable=False)
resource: Mapped[str] = mapped_column(String, nullable=False)
resource_id: Mapped[Optional[str]] = mapped_column(String, nullable=True)
metadata_json: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
source: Mapped[str] = mapped_column(String, nullable=False, default="web")
ip_address: Mapped[Optional[str]] = mapped_column(String, nullable=True)
user_agent: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
created_at: Mapped[datetime] = mapped_column(
DateTime, nullable=False, index=True
)
class DiscoveryConversation(Base):
"""A single self-discovery conversation: the five narrative responses."""
__tablename__ = "discovery_conversation"
id: Mapped[str] = mapped_column(String, primary_key=True)
user_id: Mapped[str] = mapped_column(
String, ForeignKey("users.id"), nullable=False, index=True
)
started_at: Mapped[datetime] = mapped_column(DateTime, nullable=False)
completed_at: Mapped[Optional[datetime]] = mapped_column(
DateTime, nullable=True
)
prompt_alive: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
prompt_friction: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
prompt_pull: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
prompt_recognition: Mapped[Optional[str]] = mapped_column(
Text, nullable=True
)
prompt_future: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
# Phase 1 goal-articulation prompts: the person's own near- and long-term
# goals, in their own words.
prompt_goals_short: Mapped[Optional[str]] = mapped_column(
Text, nullable=True
)
prompt_goals_long: Mapped[Optional[str]] = mapped_column(
Text, nullable=True
)
class DiscoveryProfile(Base):
"""The extracted enneagram + Ikigai profile for a conversation."""
__tablename__ = "discovery_profile"
id: Mapped[str] = mapped_column(String, primary_key=True)
user_id: Mapped[str] = mapped_column(
String, ForeignKey("users.id"), nullable=False, index=True
)
conversation_id: Mapped[str] = mapped_column(
String, ForeignKey("discovery_conversation.id"), nullable=False
)
generated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False)
triad: Mapped[Optional[str]] = mapped_column(String, nullable=True)
probable_type: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
wing: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
instinctual_variant: Mapped[Optional[str]] = mapped_column(
String, nullable=True
)
instinctual_stack: Mapped[Optional[str]] = mapped_column(
String, nullable=True
)
love_summary: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
strength_summary: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
mission_summary: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
vocation_summary: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
overlap_narrative: Mapped[Optional[str]] = mapped_column(
Text, nullable=True
)
# AI-articulated goals: the person's own stated goals, clarified and
# connected to their Ikigai/enneagram pattern (mirror, never prescription).
short_term_goals: Mapped[Optional[str]] = mapped_column(
Text, nullable=True
)
long_term_goals: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
confidence_json: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
locked: Mapped[bool] = mapped_column(
Boolean, nullable=False, default=False
)
class ReflectionMessage(Base):
"""One turn in the Phase 2 AI-coach reflection loop. The coach mirrors the
profile back; the person reacts; iterate until they affirm (lock)."""
__tablename__ = "reflection_message"
id: Mapped[str] = mapped_column(String, primary_key=True)
profile_id: Mapped[str] = mapped_column(
String, ForeignKey("discovery_profile.id"), nullable=False, index=True
)
user_id: Mapped[str] = mapped_column(
String, ForeignKey("users.id"), nullable=False, index=True
)
# "coach" (AI mirror) or "person" (the human).
role: Mapped[str] = mapped_column(String, nullable=False)
content: Mapped[str] = mapped_column(Text, nullable=False)
# Monotonic order within a profile's reflection thread.
sequence: Mapped[int] = mapped_column(Integer, nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False)
class ProfileRevision(Base):
"""A snapshot of a profile's editable prose at a point in time, so edits
and iterations are captured rather than overwritten. source is one of
'extraction' (initial), 'reflection' (AI-coach loop), 'manual_edit'."""
__tablename__ = "profile_revision"
id: Mapped[str] = mapped_column(String, primary_key=True)
profile_id: Mapped[str] = mapped_column(
String, ForeignKey("discovery_profile.id"), nullable=False, index=True
)
user_id: Mapped[str] = mapped_column(
String, ForeignKey("users.id"), nullable=False, index=True
)
source: Mapped[str] = mapped_column(String, nullable=False)
# JSON snapshot of the seven editable prose fields at this revision.
fields_json: Mapped[str] = mapped_column(Text, nullable=False)
note: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False)
class CoachingPreferences(Base):
"""Phase 3: how this person wants to be coached. Auto-generated from their
Enneagram/Ikigai profile, then overridable. One row per user."""
__tablename__ = "coaching_preferences"
id: Mapped[str] = mapped_column(String, primary_key=True)
user_id: Mapped[str] = mapped_column(
String, ForeignKey("users.id"), unique=True, nullable=False, index=True
)
# The profile the defaults were derived from (provenance).
profile_id: Mapped[Optional[str]] = mapped_column(
String, ForeignKey("discovery_profile.id"), nullable=True
)
coaching_frequency: Mapped[str] = mapped_column(String, nullable=False)
coaching_style: Mapped[str] = mapped_column(String, nullable=False)
misalignment_threshold: Mapped[str] = mapped_column(String, nullable=False)
friction_tolerance: Mapped[str] = mapped_column(String, nullable=False)
prefer_questions_over_directives: Mapped[bool] = mapped_column(
Boolean, nullable=False, default=True
)
time_of_day_preference: Mapped[str] = mapped_column(String, nullable=False)
# True while still using auto-derived defaults; False once the user edits.
auto_generated: Mapped[bool] = mapped_column(
Boolean, nullable=False, default=True
)
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False)
updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False)
class CoachingCheckin(Base):
"""Phase 3: a periodic coaching check-in. The body quotes the person's own
words and asks whether their stated direction still feels valid (mirror,
not compass). The person's answer is recorded in still_valid."""
__tablename__ = "coaching_checkin"
id: Mapped[str] = mapped_column(String, primary_key=True)
user_id: Mapped[str] = mapped_column(
String, ForeignKey("users.id"), nullable=False, index=True
)
profile_id: Mapped[str] = mapped_column(
String, ForeignKey("discovery_profile.id"), nullable=False
)
body: Mapped[str] = mapped_column(Text, nullable=False)
created_at: Mapped[datetime] = mapped_column(
DateTime, nullable=False, index=True
)
# The person's self-assessment: is their direction still valid?
still_valid: Mapped[Optional[bool]] = mapped_column(Boolean, nullable=True)
response_note: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
acknowledged_at: Mapped[Optional[datetime]] = mapped_column(
DateTime, nullable=True
)
class TaskMapping(Base):
"""Phase 4: a logged unit of work from the ImpactFlow core time-tracker,
mapped to the profile foundation it builds toward. One row per time entry;
the work-pattern aggregation rolls these up per foundation."""
__tablename__ = "task_mapping"
id: Mapped[str] = mapped_column(String, primary_key=True)
user_id: Mapped[str] = mapped_column(
String, ForeignKey("users.id"), nullable=False, index=True
)
# Opaque id of the task in the core tracker (not an FK; external system).
external_task_id: Mapped[str] = mapped_column(String, nullable=False)
task_label: Mapped[Optional[str]] = mapped_column(String, nullable=True)
# One of foundations.FOUNDATIONS: love | strength | mission | vocation |
# short_term | long_term.
foundation: Mapped[str] = mapped_column(String, nullable=False, index=True)
minutes: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
occurred_at: Mapped[datetime] = mapped_column(
DateTime, nullable=False, index=True
)
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False)