Phase 5: iteration & polish (visualizations, goal history, smart tagging)

Final roadmap phase. No DB migration — it reads data already captured.

- Goal-evolution history: GET /discovery/profile/me/goal-history derives a
  per-goal timeline from the profile_revision snapshots (pure aggregator in
  app/services/profile_history.py).
- Smart tagging: POST /discovery/integration/suggest-foundation suggests which
  foundation a task builds toward + rationale/confidence (FoundationTagger,
  app/services/tagging.py). Suggestion only; the person confirms by posting the
  task mapping.
- Deeper goal-refinement: the reflect loop accepts an optional focus ("goals")
  that steers the coach toward sharpening goals — still mirror, not compass.
- Visualizations: visuals.html renders an Ikigai Venn and an Enneagram diagram
  (plain-language callouts, not the raw type number) plus the goal-evolution
  timeline; linked from profile.html.

Tests: 99 passing (added pure goal-history tests, goal-history + suggest
endpoint tests, reflect-focus passthrough; run in-container). README updated.

This completes the ImpactFlow Vision roadmap (Phases 1-5) on the Discovery side.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joel Salmon
2026-06-16 21:37:33 -05:00
parent c4fc1cccd7
commit b9d7b0e22b
14 changed files with 798 additions and 9 deletions
+40
View File
@@ -9,6 +9,7 @@ up to feed the coaching reminder engine and the goal dashboard.
All routes are user-scoped via the dual-auth dependency; the tracker calls as
the user (forwarded session/JWT) or, service-to-service, with ``X-API-Key``.
"""
import os
import uuid
from datetime import datetime, timedelta, timezone
@@ -25,6 +26,7 @@ from app.services.foundations import (
FOUNDATIONS,
rollup,
)
from app.services.tagging import FoundationTagger, TaggingError
router = APIRouter(prefix="/discovery/integration", tags=["integration"])
@@ -141,6 +143,44 @@ async def work_patterns_for(
return rollup(entries)
@router.post(
"/suggest-foundation", response_model=schemas.SuggestFoundationOut
)
async def suggest_foundation(
payload: schemas.SuggestFoundationRequest,
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
"""Phase 5 smart tagging: suggest which foundation a task builds toward.
The person confirms by posting the task mapping. Suggestion only — nothing
is stored here."""
label = payload.task_label.strip()
if not label:
raise HTTPException(status_code=400, detail="task_label is required")
profile = await _latest_profile(db, user.id)
if profile is None:
raise HTTPException(status_code=404, detail="No profile for this user")
foundations_text = {
key: getattr(profile, field, None)
for key, field in FOUNDATION_TO_FIELD.items()
}
api_key = os.getenv("ANTHROPIC_API_KEY")
model = os.getenv("ANTHROPIC_MODEL", "claude-sonnet-4-6")
try:
tagger = FoundationTagger(api_key=api_key, model=model)
result = await tagger.suggest(label, foundations_text)
except TaggingError as exc:
raise HTTPException(status_code=502, detail=str(exc)) from exc
return schemas.SuggestFoundationOut(
foundation=result["foundation"],
label=FOUNDATIONS[result["foundation"]],
rationale=result["rationale"],
confidence=result["confidence"],
)
@router.get("/work-patterns", response_model=schemas.WorkPatternsOut)
async def get_work_patterns(
days: int = Query(30, ge=1, le=365),