Skip to content

feat(activity): canonicalize provider activity types - #2416

Merged
Asherlc merged 25 commits into
mainfrom
Asherlc/issue-2245-canonical-activity-types
Aug 3, 2026
Merged

Asherlc merged 25 commits into
mainfrom
Asherlc/issue-2245-canonical-activity-types

Conversation

@Asherlc

@Asherlc Asherlc commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Summary

  • replace the legacy activity-type storage contract with canonical type, provider type, and modality fields
  • preserve the raw provider activity type across every provider writer and server/mobile/web boundary
  • apply the atomic relation replacement as registered migration 0067_canonical_activity_types, preserving dependent views, constraints, indexes, triggers, grants, publication membership, and inbound foreign keys
  • update ClickHouse/dbt consumers and add migration, provider-sync, and web/mobile coverage

Validation

  • pnpm typecheck
  • pnpm exec biome check . --max-diagnostics=500
  • pnpm lint:migrations
  • pnpm lint:analytics-policy
  • focused unit tests: 92 passed
  • the pre-merge database migration integration test passed; current Docker integration rerun is blocked by the host's exhausted Compose address pools

Deployment note

The PostgreSQL replacement changes the fitness.activity relation OID. After applying migration 0067, resync the PeerDB raw activity mirror before relying on replicated activity analytics.

Fixes #2245

Summary by Sourcery

Introduce a canonical activity type and modality model across the stack, replacing the legacy activity_type contract and migrating storage, analytics, providers, and client consumers to the new schema.

New Features:

  • Add a canonical activity type and modality taxonomy with provider-type preservation for all activities.
  • Expose canonical activity type and modality fields through server APIs, MCP tools, and web/mobile contracts for filtering and visualization.

Enhancements:

  • Normalize legacy activity types into canonical types and modalities via a Postgres migration and ClickHouse/dbt backfill.
  • Update training, endurance, efficiency, cycling, hiking, and insights logic to rely on canonical types and modalities instead of provider-specific enums.
  • Simplify strength and climbing handling by treating legacy synonyms as a single canonical concept and adjusting downstream analytics and UI accordingly.

Tests:

  • Expand unit and integration tests across training helpers, database migrations, providers, repositories, and web/mobile components to cover canonical type and modality behavior.
  • Add ClickHouse/dbt read-model tests to verify canonical activity types, modality grouping, and indoor/virtual cycling speed suppression.

Summary by cubic

Canonicalizes activity types across the stack by replacing activity_type with canonical_type, provider_type, and modality. Updates analytics, APIs, and mobile/web to filter and display canonical types; unknown provider types default to other. Aligns with #2245.

  • New Features

    • Schema/docs: add canonical_activity_type and activity_modality; update docs/schema.*; add rollout plan; expand cspell.
    • Providers/writes: Garmin, Peloton, HealthKit, and manual recordings resolve ProviderActivityType via @dofek/training/activity-types.
    • Analytics/dbt: switch keys/filters to canonical_type; propagate provider_type/modality in source, deduped, and summary rows; simplify endurance sets to canonical types; hiking treats trail as running (modality='trail'); weekly healthspan counts strength by canonical_type.
    • Server/MCP: repositories, insights, predictions, PMC, power, cycling, hiking, and ClickHouse queries now use canonical_type; MCP get_activity_summary accepts canonical_types and groups by canonical_type; calendars return canonical_types; activity list/detail include modality.
    • Cycling advanced + UI: models/serving return typed modality; indoor/virtual cycling logic checks modality; VerticalAscentChart groups by modality.
    • Mobile/web: contracts rename activity_typecanonical_type; provider detail lists filter by canonical_type, provider_type, and modality.
    • Tests: expand coverage for canonical-type consumers across server, analytics, mobile, and ClickHouse; update ClickHouse deduped-activities expectations for canonical fields.
  • Migration

    • Adds 0068_canonical_activity_types with atomic table replacement preserving views, constraints, indexes, triggers, grants, publication membership, inbound FKs, and local-time fields in v_activity; relation OID changes.
    • Defaults unknown provider activity labels to canonical_type='other'; adds tests for fallback and migration edge cases.
    • Requires resync of the PeerDB raw activity mirror after deployment.

Written for commit 5226713. Summary will update on new commits.

Review in cubic

Copilot AI review requested due to automatic review settings August 2, 2026 19:46
@cursor

cursor Bot commented Aug 2, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@greptile-apps

greptile-apps Bot commented Aug 2, 2026

Copy link
Copy Markdown

Too many files changed for review. (316 files found, 100 file limit)

Bypass the limit by tagging @greptile-apps to review.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @Asherlc, your pull request is larger than the review limit of 150000 diff characters

@codereviewbot-ai

Copy link
Copy Markdown

🤖 Review skipped: Repository rate limit exceeded. Free accounts are limited to 2 reviews per 4 hours per repository. Upgrade to a paid plan for unlimited reviews.

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Important

Review skipped

Too many files!

This PR contains 316 files, which is 216 over the limit of 100.

To get a review, narrow the scope:
• coderabbit review --committed # exclude uncommitted changes
• coderabbit review --dir # limit to a subdirectory
• coderabbit review --base # compare against a closer base

Upgrade to a paid plan to raise the limit.

Usage-priced reviews support at most 300 files.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f90ff93d-a07b-4047-8c0f-e762b963399b

📥 Commits

Reviewing files that changed from the base of the PR and between 1259954 and 5226713.

📒 Files selected for processing (316)
  • analytics/models/read_models/activity_aerobic_efficiency.sql
  • analytics/models/read_models/activity_duplicate_matches.sql
  • analytics/models/read_models/activity_polarization_zones.sql
  • analytics/models/read_models/activity_power_curve.sql
  • analytics/models/read_models/activity_power_curve.sql.test.ts
  • analytics/models/read_models/activity_source_records.sql
  • analytics/models/read_models/activity_summary_rows.sql
  • analytics/models/read_models/activity_vo2max_estimate.sql
  • analytics/models/read_models/activity_vo2max_estimate.sql.test.ts
  • analytics/models/read_models/cycling_activity.sql
  • analytics/models/read_models/daily_endurance_load.sql
  • analytics/models/read_models/deduped_activities.sql
  • analytics/models/read_models/hiking_activity.sql
  • analytics/models/read_models/hiking_activity.sql.test.ts
  • analytics/models/read_models/read_model_microbatch.sql.test.ts
  • analytics/models/read_models/weekly_healthspan.sql
  • cspell.json
  • docs/schema.dbml
  • docs/schema.puml
  • docs/superpowers/plans/2026-07-29-canonical-activity-types.md
  • drizzle/0068_canonical_activity_types.sql
  • drizzle/_views/01_v_activity.sql
  • drizzle/meta/_journal.json
  • packages/garmin-connect/src/parsing.test.ts
  • packages/garmin-connect/src/parsing.ts
  • packages/garmin-connect/src/sports.test.ts
  • packages/garmin-connect/src/sports.ts
  • packages/mobile/app/(tabs)/strain.stories.tsx
  • packages/mobile/app/(tabs)/strain.test.tsx
  • packages/mobile/app/(tabs)/strain.tsx
  • packages/mobile/app/activity/[id].test.tsx
  • packages/mobile/app/activity/[id].tsx
  • packages/mobile/app/providers/[id].tsx
  • packages/mobile/components/ActivityCard.stories.tsx
  • packages/mobile/components/charts/VerticalAscentChart.stories.tsx
  • packages/mobile/components/charts/VerticalAscentChart.test.tsx
  • packages/mobile/components/charts/VerticalAscentChart.tsx
  • packages/mobile/types/api.ts
  • packages/peloton-client/src/parsing.test.ts
  • packages/peloton-client/src/parsing.ts
  • packages/server/src/contracts/mobile-dashboard-contracts.test.ts
  • packages/server/src/contracts/mobile-dashboard-contracts.ts
  • packages/server/src/export.integration.test.ts
  • packages/server/src/insights/data-join.ts
  • packages/server/src/insights/engine.test.ts
  • packages/server/src/insights/schemas.ts
  • packages/server/src/insights/types.ts
  • packages/server/src/lib/endurance-types.test.ts
  • packages/server/src/lib/endurance-types.ts
  • packages/server/src/lib/field-filters.test.ts
  • packages/server/src/mcp/route.test.ts
  • packages/server/src/mcp/tools.ts
  • packages/server/src/models/activity.test.ts
  • packages/server/src/models/activity.ts
  • packages/server/src/repositories/activities-calendar-repository.test.ts
  • packages/server/src/repositories/activities-calendar-repository.ts
  • packages/server/src/repositories/activity-recording-repository.ts
  • packages/server/src/repositories/activity-repository.integration.test.ts
  • packages/server/src/repositories/activity-repository.test.ts
  • packages/server/src/repositories/activity-repository.ts
  • packages/server/src/repositories/activity-visibility-consistency.integration.test.ts
  • packages/server/src/repositories/calendar-repository.test.ts
  • packages/server/src/repositories/calendar-repository.ts
  • packages/server/src/repositories/clickhouse-activity-sensor-analytics.ts
  • packages/server/src/repositories/clickhouse-activity-sensor-store.test.ts
  • packages/server/src/repositories/clickhouse-activity-sensor-store.ts
  • packages/server/src/repositories/climbing-repository.test.ts
  • packages/server/src/repositories/climbing-repository.ts
  • packages/server/src/repositories/climbing-training-log-repository.test.ts
  • packages/server/src/repositories/climbing-training-log-repository.ts
  • packages/server/src/repositories/correlation-repository.integration.test.ts
  • packages/server/src/repositories/correlation-repository.test.ts
  • packages/server/src/repositories/correlation-repository.ts
  • packages/server/src/repositories/cycling-advanced-models.ts
  • packages/server/src/repositories/cycling-advanced-repository.test.ts
  • packages/server/src/repositories/cycling-advanced-repository.ts
  • packages/server/src/repositories/cycling-analytics-repository.integration.test.ts
  • packages/server/src/repositories/cycling-analytics-repository.test.ts
  • packages/server/src/repositories/cycling-analytics-repository.ts
  • packages/server/src/repositories/derived-cardio-repository.integration.test.ts
  • packages/server/src/repositories/efficiency-repository.integration.test.ts
  • packages/server/src/repositories/efficiency-repository.test.ts
  • packages/server/src/repositories/efficiency-repository.ts
  • packages/server/src/repositories/health-kit-sync-repository.test.ts
  • packages/server/src/repositories/hiking-repository.test.ts
  • packages/server/src/repositories/hiking-repository.ts
  • packages/server/src/repositories/insights-repository.ts
  • packages/server/src/repositories/monthly-report-repository.integration.test.ts
  • packages/server/src/repositories/pmc-repository.test.ts
  • packages/server/src/repositories/pmc-repository.ts
  • packages/server/src/repositories/power-repository.test.ts
  • packages/server/src/repositories/power-repository.ts
  • packages/server/src/repositories/predictions-repository.test.ts
  • packages/server/src/repositories/predictions-repository.ts
  • packages/server/src/repositories/provider-detail-repository.integration.test.ts
  • packages/server/src/repositories/provider-detail-repository.test.ts
  • packages/server/src/repositories/provider-detail-repository.ts
  • packages/server/src/repositories/raw-activity-count.test.ts
  • packages/server/src/repositories/running-repository.ts
  • packages/server/src/repositories/strength-repository.integration.test.ts
  • packages/server/src/repositories/strength-repository.test.ts
  • packages/server/src/repositories/strength-repository.ts
  • packages/server/src/repositories/training-repository.test.ts
  • packages/server/src/repositories/training-repository.ts
  • packages/server/src/repositories/weekly-report-repository.integration.test.ts
  • packages/server/src/routers/activity-dedup.integration.test.ts
  • packages/server/src/routers/activity-power-curve-eligibility.integration.test.ts
  • packages/server/src/routers/activity-power-curve-read-model.integration.test.ts
  • packages/server/src/routers/activity.integration.test.ts
  • packages/server/src/routers/activity.test.ts
  • packages/server/src/routers/activity.ts
  • packages/server/src/routers/admin.test.ts
  • packages/server/src/routers/admin.ts
  • packages/server/src/routers/body-calendar-sleep-nutrition.test.ts
  • packages/server/src/routers/clickhouse-integration-test-helpers.ts
  • packages/server/src/routers/clickhouse-integration-test-models.ts
  • packages/server/src/routers/clickhouse-integration-test-read-models-a.ts
  • packages/server/src/routers/clickhouse-integration-test-read-models-b.ts
  • packages/server/src/routers/climbing.integration.test.ts
  • packages/server/src/routers/cycling-advanced-vertical-ascent.integration.test.ts
  • packages/server/src/routers/cycling-advanced.test.ts
  • packages/server/src/routers/cycling-advanced.ts
  • packages/server/src/routers/cycling.ts
  • packages/server/src/routers/duration-curves-data.integration.test.ts
  • packages/server/src/routers/efficiency.test.ts
  • packages/server/src/routers/health-kit-sync-processors.ts
  • packages/server/src/routers/health-kit-sync-schemas.ts
  • packages/server/src/routers/health-kit-sync.integration.test.ts
  • packages/server/src/routers/health-kit-sync.test.ts
  • packages/server/src/routers/healthspan.integration.test.ts
  • packages/server/src/routers/hiking-insights-life-events.test.ts
  • packages/server/src/routers/predictions.integration.test.ts
  • packages/server/src/routers/provider-detail.test.ts
  • packages/server/src/routers/router-data.integration.test.ts
  • packages/server/src/routers/router-logic.integration.test.ts
  • packages/server/src/routers/router.integration.test.ts
  • packages/server/src/routers/settings.integration.test.ts
  • packages/server/src/routers/sleep-consistency-across-endpoints.integration.test.ts
  • packages/server/src/routers/sleep-need.integration.test.ts
  • packages/server/src/routers/training-access-window.test.ts
  • packages/server/src/routers/trends-data.integration.test.ts
  • packages/server/src/routes/ingest-zos-health.ts
  • packages/server/src/services/mobile-training-tab.test.ts
  • packages/trainerroad-client/src/parsing.test.ts
  • packages/trainerroad-client/src/parsing.ts
  • packages/training/src/activity-icons.test.ts
  • packages/training/src/activity-icons.ts
  • packages/training/src/activity-types.test.ts
  • packages/training/src/activity-types.ts
  • packages/training/src/derived-cardio.test.ts
  • packages/training/src/derived-cardio.ts
  • packages/training/src/endurance-types.test.ts
  • packages/training/src/endurance-types.ts
  • packages/training/src/training.test.ts
  • packages/training/src/training.ts
  • packages/trainingpeaks-connect/src/parsing.test.ts
  • packages/trainingpeaks-connect/src/parsing.ts
  • packages/trainingpeaks-connect/src/sports.test.ts
  • packages/trainingpeaks-connect/src/sports.ts
  • packages/velohero-client/src/parsing.test.ts
  • packages/velohero-client/src/parsing.ts
  • packages/velohero-client/src/sports.ts
  • packages/web/src/components/ActivityCardContent.stories.tsx
  • packages/web/src/components/ActivityCardContent.test.tsx
  • packages/web/src/components/ActivityList.stories.tsx
  • packages/web/src/components/ActivityList.test.tsx
  • packages/web/src/components/ActivityList.tsx
  • packages/web/src/components/RecentActivitiesSection.stories.tsx
  • packages/web/src/components/RecentActivitiesSection.tsx
  • packages/web/src/components/TrainingInsightsPanel.stories.tsx
  • packages/web/src/components/TrainingInsightsPanel.test.tsx
  • packages/web/src/components/TrainingInsightsPanel.tsx
  • packages/web/src/components/VerticalAscentChart.stories.tsx
  • packages/web/src/components/VerticalAscentChart.test.tsx
  • packages/web/src/components/VerticalAscentChart.tsx
  • packages/web/src/lib/provider-detail-filter-options.test.ts
  • packages/web/src/lib/provider-detail-filter-options.ts
  • packages/web/src/pages/ActivityDetailPage.stories.tsx
  • packages/web/src/pages/ActivityDetailPage.test.tsx
  • packages/web/src/pages/ActivityDetailPage.tsx
  • packages/web/src/pages/AdminPage.tsx
  • packages/web/src/routes/training/climbing.test.tsx
  • packages/web/src/routes/training/climbing.tsx
  • packages/web/src/routes/training/range-plumbing.test-helper.tsx
  • packages/web/src/routes/training/running.test.tsx
  • packages/web/src/routes/training/running.tsx
  • packages/web/src/routes/training/strength.lazy.test.tsx
  • packages/web/src/routes/training/strength.lazy.tsx
  • packages/whoop-whoop/src/sports.ts
  • packages/xert-client/src/parsing.test.ts
  • packages/xert-client/src/parsing.ts
  • packages/zwift-client/src/parsing.test.ts
  • packages/zwift-client/src/parsing.ts
  • scripts/backfill-clickhouse.ts
  • scripts/backfill-provider-connections.integration.test.ts
  • scripts/backfill-ride-with-gps-track-points.test.ts
  • scripts/backfill-ride-with-gps-track-points.ts
  • scripts/seed-review-clickhouse.ts
  • scripts/seed/training.ts
  • scripts/seed/verification.integration.test.ts
  • src/db/activity-overlap-plan.integration.test.ts
  • src/db/activity-summary-canonical-ids.integration.test.ts
  • src/db/activity-type-classification.integration.test.ts
  • src/db/canonical-activity-types-migration.integration.test.ts
  • src/db/clickhouse-activity-summary.ts
  • src/db/clickhouse-metric-stream-bootstrap.ts
  • src/db/clickhouse-migrations/0069_canonical_activity_types.integration.test.ts
  • src/db/clickhouse-migrations/0069_canonical_activity_types.test.ts
  • src/db/clickhouse-migrations/0069_canonical_activity_types.ts
  • src/db/clickhouse-migrations/registry.test.ts
  • src/db/clickhouse-migrations/registry.ts
  • src/db/clickhouse-raw-tables.ts
  • src/db/clickhouse-read-models.ts
  • src/db/climbing-attempt-count-backfill.integration.test.ts
  • src/db/daily-body-measurement-read-model.integration.test.ts
  • src/db/deduped-activities-read-model.integration.test.ts
  • src/db/drizzle-schema.test.ts
  • src/db/provider-activity-absence.integration.test.ts
  • src/db/provider-activity-sync.integration.test.ts
  • src/db/provider-activity-sync.test.ts
  • src/db/provider-activity-sync.ts
  • src/db/record-local-time-context.integration.test.ts
  • src/db/schema/activity.ts
  • src/db/schema/enums.test.ts
  • src/db/schema/enums.ts
  • src/db/seed-dev-db.integration.test.ts
  • src/db/test-helpers.ts
  • src/db/tokens.integration.test.ts
  • src/fit/records.test.ts
  • src/fit/records.ts
  • src/jobs/enqueue-fit-file-import.test.ts
  • src/jobs/process-fit-file-import-job.test.ts
  • src/jobs/process-fit-file-import-job.ts
  • src/jobs/queues.test.ts
  • src/jobs/queues.ts
  • src/providers/amazfit-zepp.test.ts
  • src/providers/amazfit-zepp.ts
  • src/providers/apple-health/db-insertion.integration.test.ts
  • src/providers/apple-health/db-insertion.test.ts
  • src/providers/apple-health/db-insertion.ts
  • src/providers/apple-health/import.integration.test.ts
  • src/providers/apple-health/parsing.test.ts
  • src/providers/apple-health/streaming.test.ts
  • src/providers/apple-health/workouts.ts
  • src/providers/batch3-providers.test.ts
  • src/providers/concept2-sync.integration.test.ts
  • src/providers/concept2.test.ts
  • src/providers/concept2.ts
  • src/providers/coros-sync.integration.test.ts
  • src/providers/coros.test.ts
  • src/providers/coros.ts
  • src/providers/cycling-analytics-sync.integration.test.ts
  • src/providers/cycling-analytics.test.ts
  • src/providers/cycling-analytics.ts
  • src/providers/decathlon-sync.integration.test.ts
  • src/providers/decathlon.test.ts
  • src/providers/decathlon.ts
  • src/providers/fitbit-sync.integration.test.ts
  • src/providers/fitbit/parsers.ts
  • src/providers/fitbit/provider.test.ts
  • src/providers/garmin-connect-internal-sync.integration.test.ts
  • src/providers/garmin-dump.test.ts
  • src/providers/garmin-dump.ts
  • src/providers/garmin/sync-api-query.test.ts
  • src/providers/garmin/sync-api-query.ts
  • src/providers/garmin/sync-checkpoint.test.ts
  • src/providers/garmin/sync-checkpoint.ts
  • src/providers/garmin/sync-orchestrator.ts
  • src/providers/kaya/import.ts
  • src/providers/komoot-sync.integration.test.ts
  • src/providers/komoot.test.ts
  • src/providers/komoot.ts
  • src/providers/mapmyfitness-sync.integration.test.ts
  • src/providers/mapmyfitness.test.ts
  • src/providers/mapmyfitness.ts
  • src/providers/new-providers.test.ts
  • src/providers/oauth-providers.test.ts
  • src/providers/oura-sync.integration.test.ts
  • src/providers/oura.test.ts
  • src/providers/oura/parsing.ts
  • src/providers/peloton-sync-ext.integration.test.ts
  • src/providers/peloton-sync.integration.test.ts
  • src/providers/peloton.test.ts
  • src/providers/polar-sync.integration.test.ts
  • src/providers/polar/parsers.ts
  • src/providers/polar/types.ts
  • src/providers/ride-with-gps-ext.test.ts
  • src/providers/ride-with-gps-sync.integration.test.ts
  • src/providers/ride-with-gps.test.ts
  • src/providers/ride-with-gps.ts
  • src/providers/strava-sync.integration.test.ts
  • src/providers/strava.test.ts
  • src/providers/strava.ts
  • src/providers/strong-csv-sync.integration.test.ts
  • src/providers/strong-csv.ts
  • src/providers/suunto-sync.integration.test.ts
  • src/providers/suunto.test.ts
  • src/providers/suunto.ts
  • src/providers/trainerroad-sync.integration.test.ts
  • src/providers/wahoo-sync.integration.test.ts
  • src/providers/wahoo.test.ts
  • src/providers/wahoo/activity-persister.ts
  • src/providers/wahoo/parsers.ts
  • src/providers/wger-sync.integration.test.ts
  • src/providers/wger.test.ts
  • src/providers/wger.ts
  • src/providers/whoop-journal.test.ts
  • src/providers/whoop-parsing.test.ts
  • src/providers/whoop-sync.integration.test.ts
  • src/providers/whoop.test.ts
  • src/providers/whoop/parsing.ts
  • src/providers/whoop/sync-workouts.ts
  • src/providers/xert-sync.integration.test.ts
  • src/providers/xert.test.ts
  • src/providers/zwift-sync.integration.test.ts
  • src/providers/zwift.test.ts

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@sourcery-ai

sourcery-ai Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

This PR replaces the legacy single activity_type string with a structured canonical_type, provider_type, and modality across Postgres, ClickHouse/dbt, server repositories/routers, provider parsers, training utilities, and web/mobile clients, ensuring synonyms and subtypes are normalized while preserving raw provider labels and modality semantics.

Sequence diagram for provider activity canonicalization and persistence

sequenceDiagram
  participant Provider as StravaProvider
  participant Mapper as createActivityTypeMapper
  participant Type as resolveProviderActivityType
  participant Upsert as upsertProviderActivity
  participant DB as fitness_activity

  Provider->>Mapper: mapStravaActivityType(sportType)
  Mapper->>Type: resolveProviderActivityType(providerType, legacyType)
  Type-->>Mapper: ProviderActivityType
  Mapper-->>Provider: ProviderActivityType

  Provider->>Upsert: upsertProviderActivity(activityType: ProviderActivityType, ...)
  Upsert->>DB: insert activity (canonicalType, providerType, modality)
  Upsert->>DB: onConflictDoUpdate(canonicalType, providerType, modality)
Loading

File-Level Changes

Change Details Files
Introduce canonical activity type and modality classification and use it in shared training utilities.
  • Add a new activity-types module defining canonical types, modalities, legacy mappings, and provider classification helpers.
  • Update training utilities such as strength and cycling type lists, activity type mappers, cadence label/unit, and weekly volume collapsing to operate on canonical types and modality instead of legacy subtypes.
  • Adjust endurance-type helpers to treat indoor/virtual cycling as modalities, and update activity icon category resolution to use canonical categories.
packages/training/src/activity-types.ts
packages/training/src/activity-types.test.ts
packages/training/src/training.ts
packages/training/src/endurance-types.ts
packages/training/src/activity-icons.ts
packages/training/src/derived-cardio.ts
packages/training/src/endurance-types.test.ts
packages/training/src/training.test.ts
packages/training/src/activity-icons.test.ts
Replace fitness.activity.activity_type with canonical_type, provider_type, and modality in Postgres and adjust all schema, views, and migrations.
  • Define new canonical_activity_type and activity_modality enums and add canonical_type, provider_type, modality columns to fitness.activity with appropriate constraints, indexes, and comments.
  • Implement migration 0067_canonical_activity_types to backfill canonical_type and modality from the legacy enum, preserve provider_type, rebuild dependent views, constraints, triggers, grants, and publication memberships, and drop the old activity_type.
  • Add integration tests to validate the atomic replacement and classification behavior and ensure inbound foreign keys, v_activity, clickhouse views, and publication state remain consistent.
src/db/schema/enums.ts
src/db/schema/activity.ts
drizzle/0067_canonical_activity_types.sql
drizzle/_views/01_v_activity.sql
drizzle/meta/_journal.json
src/db/canonical-activity-types-migration.integration.test.ts
src/db/activity-type-classification.integration.test.ts
Normalize activity types in ClickHouse and dbt read models to canonical_type and modality, and propagate provider_type where needed.
  • Introduce a ClickHouse migration 0063_canonical_activity_types that renames activity_type to canonical_type, backfills canonical_type and modality using the legacy map, and adds provider_type columns in source and analytics tables.
  • Update ClickHouse bootstrap schemas and analytics read_models (activity_summary_rows, cycling_activity, hiking_activity, aerobic efficiency/decoupling, polarization zones, vo2max, daily_endurance_load, deduped_activities, etc.) to use canonical_type for filtering/grouping and modality for indoor/virtual and trail distinctions.
  • Adjust ClickHouse v_activity and v_activity_members views, and ensure healthspan, pmc, cycling advanced, training, and efficiency repositories query by canonical_type and pass through modality.
src/db/clickhouse-migrations/0063_canonical_activity_types.ts
src/db/clickhouse-migrations/0063_canonical_activity_types.test.ts
src/db/clickhouse-migrations/0063_canonical_activity_types.integration.test.ts
src/db/clickhouse-read-models.ts
src/db/clickhouse-raw-tables.ts
analytics/models/read_models/activity_summary_rows.sql
analytics/models/read_models/cycling_activity.sql
analytics/models/read_models/hiking_activity.sql
analytics/models/read_models/activity_aerobic_efficiency.sql
analytics/models/read_models/activity_polarization_zones.sql
analytics/models/read_models/activity_vo2max_estimate.sql
analytics/models/read_models/daily_endurance_load.sql
analytics/models/read_models/deduped_activities.sql
analytics/models/read_models/activity_power_curve.sql
analytics/models/read_models/activity_duplicate_matches.sql
analytics/models/read_models/read_model_microbatch.sql.test.ts
Update provider parsers and sync paths to produce structured ProviderActivityType objects carrying canonicalType, providerType, and modality.
  • Change all provider-specific type mappers (Strava, Wahoo, Polar, Garmin, Oura, RideWithGPS, Zwift, Peloton, MapMyFitness, Suunto, Coros, Concept2, Decathlon, Xert, VeloHero, TrainerRoad, Komoot, Amazfit/Zep, Fitbit, HealthKit, WHOOP) to return ProviderActivityType instead of plain strings.
  • Ensure indoor/virtual cycling, trail running, open-water swimming, functional fitness, hand cycling, wheelchair activities, and other subtypes set the appropriate modality while mapping to the correct canonical type.
  • Propagate the new structured activityType through parse* functions, sync jobs, and metric-stream builders, and adjust speed suppression logic to look at modality (e.g., omit speed for indoor/virtual cycling).
src/providers/training.ts (mappers)
src/providers/strava.ts
src/providers/wahoo.ts
src/providers/polar/parsers.ts
src/providers/garmin-dump.ts
src/providers/garmin/sync-orchestrator.ts
src/providers/oura/parsing.ts
src/providers/ride-with-gps.ts
src/providers/zwift.ts
src/providers/peloton.ts
src/providers/mapmyfitness.ts
src/providers/suunto.ts
src/providers/coros.ts
src/providers/concept2.ts
src/providers/decathlon.ts
src/providers/xert.ts
packages/velohero-client/src/sports.ts
packages/velohero-client/src/parsing.ts
packages/trainingpeaks-connect/src/sports.ts
packages/trainingpeaks-connect/src/parsing.ts
src/providers/amazfit-zepp.ts
src/providers/fitbit/parsers.ts
src/providers/apple-health/parsing.ts
src/providers/apple-health/workouts.ts
src/providers/whoop/parsing.ts
src/providers/whoop/sync-workouts.ts
Adapt server repositories, routers, MCP tools, and export logic to the canonical activity type contract.
  • Change ActivityRepository, ActivitiesCalendarRepository, EfficiencyRepository, CyclingAnalyticsRepository, CyclingAdvancedRepository, HikingRepository, PredictionsRepository, TrainingRepository, CalendarRepository, PowerRepository, PmcRepository, WeeklyReportRepository, MonthlyReportRepository, ClimbingRepository, and ActivityRecordingRepository to query and expose canonical_type (and modality where needed) instead of legacy activity_type.
  • Update MCP tools (get_activity_summary, activity_search) and mobile/web training/strain dashboards to use canonical types, adjust grouping keys, and format labels using canonicalType.
  • Ensure provider-activity sync (upsert/find) stores canonicalType, providerType, and modality, and maintains uniqueness predicates based on canonicalType and time rather than legacy strings.
packages/server/src/models/activity.ts
packages/server/src/repositories/activity-repository.ts
packages/server/src/repositories/activities-calendar-repository.ts
packages/server/src/repositories/efficiency-repository.ts
packages/server/src/repositories/cycling-analytics-repository.ts
packages/server/src/repositories/cycling-advanced-repository.ts
packages/server/src/repositories/hiking-repository.ts
packages/server/src/repositories/predictions-repository.ts
packages/server/src/repositories/training-repository.ts
packages/server/src/repositories/calendar-repository.ts
packages/server/src/repositories/power-repository.ts
packages/server/src/repositories/pmc-repository.ts
packages/server/src/repositories/weekly-report-repository.ts
packages/server/src/repositories/monthly-report-repository.ts
packages/server/src/repositories/climbing-repository.ts
packages/server/src/mcp/tools.ts
src/db/provider-activity-sync.ts
src/db/provider-activity-sync.test.ts
src/db/provider-activity-absence.integration.test.ts
src/jobs/process-fit-file-import-job.ts
src/jobs/enqueue-fit-file-import.test.ts
packages/server/src/routers/activity.ts
packages/server/src/routers/cycling.ts
packages/server/src/routers/training.ts
packages/server/src/routers/efficiency.ts
packages/server/src/routers/clickhouse-integration-test-read-models-a.ts
packages/server/src/routers/clickhouse-integration-test-read-models-b.ts
docs/superpowers/plans/2026-07-29-canonical-activity-types.md
Update web and mobile UI components and stories to render and filter using canonical activity types and modality.
  • Change ActivityList, ActivityCard, ActivityDetailPage, VerticalAscentChart, TrainingInsightsPanel, Running and Strength training routes, and recent-activity sections to use canonical_type and modality for labels/icons and grouping.
  • Ensure climbing and cycling advanced views treat synonyms as one canonical concept but still show relevant modality (e.g., mountain vs road vs gravel) in ascent charts.
  • Update mobile screens (Strain, Training tab, ActivityDetailScreen, VerticalAscentChart, dashboards) to consume canonical_type, modality, and updated contracts from the server/MCP.
packages/web/src/components/ActivityList.tsx
packages/web/src/components/ActivityCardContent.tsx
packages/web/src/components/VerticalAscentChart.tsx
packages/web/src/components/TrainingInsightsPanel.tsx
packages/web/src/routes/training/running.tsx
packages/web/src/routes/training/strength.lazy.tsx
packages/web/src/routes/training/climbing.tsx
packages/web/src/pages/ActivityDetailPage.tsx
packages/web/src/components/ActivityList.test.tsx
packages/web/src/components/VerticalAscentChart.test.tsx
packages/web/src/routes/training/running.test.tsx
packages/web/src/routes/training/strength.lazy.test.tsx
packages/web/src/routes/training/climbing.test.tsx
packages/mobile/components/charts/VerticalAscentChart.tsx
packages/mobile/components/charts/VerticalAscentChart.test.tsx
packages/mobile/app/(tabs)/strain.tsx
packages/mobile/app/(tabs)/strain.test.tsx
packages/mobile/app/activity/[id].tsx
packages/mobile/app/activity/[id].test.tsx
packages/mobile/app/(tabs)/strain.stories.tsx
packages/mobile/components/ActivityCard.stories.tsx
packages/mobile/types/api.ts

Assessment against linked issues

Issue Objective Addressed Explanation
#2245 Introduce a canonical activity type enum with a synonym-to-canonical mapping, keep the provider’s raw type in a separate field, and represent modality (indoor/virtual/road/mountain/etc.) as an orthogonal dimension.
#2245 Migrate the PostgreSQL schema from a single legacy activity_type column to canonical_type, provider_type, and activity_modality, backfilling all existing rows via a complete mapping and removing the old activity_type storage.
#2245 Update all activity readers and analytics consumers (ClickHouse/dbt models, repositories, MCP tools, web and mobile clients, tests) to group and filter on canonical_type (using modality where needed), and add tests ensuring every legacy enum value maps to exactly one canonical type and that synonyms aggregate into single buckets.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Storybook previews for 5748c055 are ready:

This comment updates automatically on each PR push.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Canonicalize provider activity types into canonical type + provider type + modality

✨ Enhancement 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Replace legacy activity_type storage with canonical_type, provider_type, and modality.
• Preserve raw upstream provider activity type end-to-end for analytics and UI.
• Migrate Postgres (atomic relation replacement, migration 0067) and ClickHouse/dbt consumers, plus
 provider-sync, server, web, and mobile call sites, with new test coverage.
Diagram

graph TD
  P{{"Providers"}} --> SYNC["provider-activity-sync.ts"] --> PG[("Postgres: fitness.activity")]
  PG --> V["drizzle view: v_activity"] --> API["server repos/contracts"] --> UI["web/mobile"]
  PG --> CH[("ClickHouse mirror")]
  CH --> DBT["dbt read models"]

  subgraph Legend
    direction LR
    _ext{{"External"}} ~~~ _svc["Service/Module"] ~~~ _db[("Database")]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Additive migration + dual-write (no relation replacement)
  • ➕ Avoids Postgres relation OID change (less replication disruption)
  • ➕ Can stage rollout with read-from-new/write-both and backfill in-place
  • ➖ Longer window of mixed contracts and higher code complexity (dual paths)
  • ➖ Harder to guarantee PeerDB never observes partially migrated rows
2. Replace enums with lookup table (activity_type dimension)
  • ➕ Easier to evolve taxonomy without enum migrations
  • ➕ Allows richer metadata and versioning of classifications
  • ➖ More joins and migration complexity; larger architectural shift than needed
  • ➖ Doesn't directly solve preserving raw provider label unless still stored separately
3. Infer modality at read-time instead of storing it
  • ➕ Reduces storage surface area and migration complexity
  • ➕ Lets classification rules evolve without rewriting rows
  • ➖ Breaks existing consumers that need stable modality semantics
  • ➖ Harder to audit/debug historical classification and guarantee consistency across systems

Recommendation: The PR's approach (explicit canonical_type + verbatim provider_type + stored modality, with atomic relation replacement) is the most robust for preventing partial-contract observation during replication and for keeping consumer semantics stable. The key tradeoff—relation OID change requiring PeerDB resync—is acceptable given the migration's guarantee of preserving dependent objects (views, constraints, indexes, triggers, grants, publications, inbound FKs) and the broad consumer cutover included here.

Files changed (26) +2404 / -321

Enhancement (15) +604 / -99
activity-types.tsIntroduce canonical activity types, modalities, and legacy classification mapping +428/-0

Introduce canonical activity types, modalities, and legacy classification mapping

• Defines canonical activity type and modality vocabularies, an exhaustive legacy-to-(canonical, modality) mapping, and helpers to resolve provider activity types while preserving the raw provider label.

packages/training/src/activity-types.ts

activity.tsUpdate activity table contract to store canonical_type, provider_type, modality +5/-2

Update activity table contract to store canonical_type, provider_type, modality

• Replaces the activityType column with canonicalType, providerType, and modality columns on fitness.activity. Keeps modality nullable for legacy rows and non-modal activities.

src/db/schema/activity.ts

01_v_activity.sqlUpdate v_activity dedup view to use canonical fields +17/-11

Update v_activity dedup view to use canonical fields

• Replaces activity_type comparisons with canonical_type and propagates provider_type and modality through the merged/canonical activity selection.

drizzle/_views/01_v_activity.sql

provider-activity-sync.tsUpsert provider activities via ProviderActivityType and store split fields +45/-13

Upsert provider activities via ProviderActivityType and store split fields

• Changes the ingestion/upsert contract to accept a structured activityType (canonicalType/providerType/modality) and normalizes it into stored columns for inserts and conflict updates. Updates identity matching to use canonical_type.

src/db/provider-activity-sync.ts

clickhouse-read-models.tsUpdate ClickHouse read models to select canonical_type/provider_type/modality +16/-8

Update ClickHouse read models to select canonical_type/provider_type/modality

• Replaces activity_type usage with canonical_type and includes provider_type/modality in the dedup/merge pipeline for ClickHouse-backed read models.

src/db/clickhouse-read-models.ts

activity_summary_rows.sqlCut over activity summary read model to canonical_type/provider_type/modality +25/-9

Cut over activity summary read model to canonical_type/provider_type/modality

• Selects canonical_type/provider_type/modality and updates indoor/virtual suppression rules to depend on modality rather than legacy cycling subtype strings.

analytics/models/read_models/activity_summary_rows.sql

activity_source_records.sqlExpose provider_type alongside canonical_type in source records model +9/-3

Expose provider_type alongside canonical_type in source records model

• Updates the model to carry canonical typing and the preserved upstream provider type for downstream analytics/auditing.

analytics/models/read_models/activity_source_records.sql

activity_power_curve.sqlUpdate power curve model to use canonical typing columns +4/-4

Update power curve model to use canonical typing columns

• Replaces legacy activity_type references with canonical_type and aligns joins/filters with the new schema contract.

analytics/models/read_models/activity_power_curve.sql

activity_vo2max_estimate.sqlSimplify VO2max model typing logic around canonical fields +4/-14

Simplify VO2max model typing logic around canonical fields

• Removes legacy subtype-specific branching and uses canonical_type/modality-based logic where needed.

analytics/models/read_models/activity_vo2max_estimate.sql

activity.tsServe activityType from canonical_type and add modality to detail DTOs +9/-2

Serve activityType from canonical_type and add modality to detail DTOs

• Updates Activity model row shape to read canonical_type instead of activity_type and includes modality in the exposed ActivityDetail payload.

packages/server/src/models/activity.ts

activity-repository.tsUpdate activity repository queries/filters to canonical_type + modality +14/-9

Update activity repository queries/filters to canonical_type + modality

• Renames selected/validated columns to canonical_type and threads modality through list/detail rows. Updates activity type filters and query matching to target canonical_type.

packages/server/src/repositories/activity-repository.ts

mobile-dashboard-contracts.tsUpdate mobile training/recovery contracts to canonical_type and modality +9/-8

Update mobile training/recovery contracts to canonical_type and modality

• Renames activity fields in Zod schemas from activity_type to canonical_type and adds modality where surfaced in training fixtures and derived computations.

packages/server/src/contracts/mobile-dashboard-contracts.ts

api.tsAlign mobile API schemas with canonical_type field names +2/-2

Align mobile API schemas with canonical_type field names

• Renames activity_type to canonical_type in shared activity row and weekly volume schemas to match server contract changes.

packages/mobile/types/api.ts

ActivityList.tsxRender activity list using canonical_type +4/-4

Render activity list using canonical_type

• Renames activity row field and ensures icons/labels derive from canonical_type for consistent UI grouping.

packages/web/src/components/ActivityList.tsx

strava.tsPersist raw Strava sport_type as provider_type and separate modality +13/-10

Persist raw Strava sport_type as provider_type and separate modality

• Changes Strava parsing to return a structured ProviderActivityType via resolveProviderActivityType and updates indoor-cycling suppression to depend on modality rather than legacy type strings.

src/providers/strava.ts

Bug fix (2) +4 / -9
ActivityDetailPage.tsxSimplify strength/climbing detection to canonical concepts +2/-4

Simplify strength/climbing detection to canonical concepts

• Removes legacy subtype sets and treats strength/climbing as single canonical types for conditional UI sections.

packages/web/src/pages/ActivityDetailPage.tsx

[id].tsxSimplify strength/climbing detection to canonical concepts +2/-5

Simplify strength/climbing detection to canonical concepts

• Removes legacy subtype sets and uses canonical strength/climbing checks for activity detail rendering logic.

packages/mobile/app/activity/[id].tsx

Refactor (2) +39 / -57
training.tsRefactor training utilities to use canonical types and modality +32/-42

Refactor training utilities to use canonical types and modality

• Updates activity type mappers to return ProviderActivityType objects and switches subtype/group logic (e.g., vertical ascent grouping, weekly volume collapsing) to use canonical_type and modality.

packages/training/src/training.ts

endurance-types.tsSwitch indoor-cycling logic from type-based to modality-based +7/-15

Switch indoor-cycling logic from type-based to modality-based

• Replaces isIndoorCycling(activityType) with isIndoorCyclingModality(modality) so speed/distance suppression works with canonical types + modality.

packages/training/src/endurance-types.ts

Tests (2) +236 / -13
activity-types.test.tsAdd exhaustive tests for activity type classification and provider preservation +226/-0

Add exhaustive tests for activity type classification and provider preservation

• Adds unit coverage to ensure legacy mappings resolve to expected canonical/modality outputs and provider type strings are validated/preserved.

packages/training/src/activity-types.test.ts

enums.test.tsUpdate enum tests for canonical activity types and modalities +10/-13

Update enum tests for canonical activity types and modalities

• Adjusts schema enum assertions to validate the new canonical activity type and modality enums instead of the legacy activity_type enum.

src/db/schema/enums.test.ts

Documentation (1) +370 / -0
2026-07-29-canonical-activity-types.mdDocument TDD rollout plan and legacy→canonical decision table +370/-0

Document TDD rollout plan and legacy→canonical decision table

• Adds a detailed migration plan, including exhaustive legacy mapping, rollout steps, and validation notes for canonical activity typing.

docs/superpowers/plans/2026-07-29-canonical-activity-types.md

Other (4) +1151 / -143
enums.tsReplace legacy activity_type enum with canonical_type and modality enums +7/-143

Replace legacy activity_type enum with canonical_type and modality enums

• Removes the large legacy activity_type enum and introduces canonical_activity_type and activity_modality enums sourced from the training package constants.

src/db/schema/enums.ts

0067_canonical_activity_types.sqlAtomic Postgres migration replacing fitness.activity while preserving dependencies +917/-0

Atomic Postgres migration replacing fitness.activity while preserving dependencies

• Adds canonical enums and replaces fitness.activity atomically to avoid partial-contract replication, while capturing/recreating dependent views, constraints, indexes, triggers, grants, publication membership, and inbound foreign keys. Includes backfill rules for provider_type and modality from legacy values.

drizzle/0067_canonical_activity_types.sql

_journal.jsonRegister migration 0067_canonical_activity_types +7/-0

Register migration 0067_canonical_activity_types

• Adds migration metadata entry to Drizzle journal for the canonical activity type cutover.

drizzle/meta/_journal.json

0063_canonical_activity_types.tsAdd ClickHouse migration to rename/backfill canonical fields across tables +220/-0

Add ClickHouse migration to rename/backfill canonical fields across tables

• Introduces a ClickHouse migration that renames legacy columns, adds provider_type and modality, and backfills canonical_type/modality via a generated CASE expression based on the legacy mapping. Validates non-empty canonical/provider fields post-mutation.

src/db/clickhouse-migrations/0063_canonical_activity_types.ts

@qodo-code-review

qodo-code-review Bot commented Aug 2, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 300 rules

Grey Divider


Action required

1. Stale activity_type inserts ✓ Resolved 🐞 Bug ≡ Correctness
Description
ClimbingTrainingLogRepository still inserts into fitness.activity.activity_type, but this PR
replaces that column with canonical_type/provider_type/modality. Those INSERTs will fail with an
undefined-column error and prevent manual finger-loading/climbing sessions from being logged.
Code

src/db/schema/activity.ts[R211-213]

+    canonicalType: canonicalActivityTypeEnum("canonical_type").notNull(),
+    providerType: text("provider_type").notNull(),
+    modality: activityModalityEnum("modality"),
Relevance

●●● Strong

Repo expects updating climbing-training-log SQL with schema changes; similar repository SQL
correctness fixes accepted in #2285.

PR-#2285

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The activity table schema now defines canonical_type and provider_type (required) and no longer
defines activity_type, while ClimbingTrainingLogRepository still issues INSERTs referencing
activity_type, which will fail once the migration is applied.

src/db/schema/activity.ts[199-214]
packages/server/src/repositories/climbing-training-log-repository.ts[175-191]
packages/server/src/repositories/climbing-training-log-repository.ts[247-263]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
This PR changes the `fitness.activity` storage contract from `activity_type` to `canonical_type`, `provider_type`, and `modality`. However, `ClimbingTrainingLogRepository` still executes raw SQL that inserts into `activity_type`, which will error at runtime after migration 0067.

## Issue Context
`fitness.activity` now requires `canonical_type` and `provider_type` (both NOT NULL). Manual activity logging paths should construct these fields (e.g., via `resolveProviderActivityType(...)` or `resolveRawProviderActivityType(...)`) and insert them instead of `activity_type`.

## Fix Focus Areas
- packages/server/src/repositories/climbing-training-log-repository.ts[175-191]
- packages/server/src/repositories/climbing-training-log-repository.ts[247-263]
- src/db/schema/activity.ts[199-214]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Schema diagrams not regenerated ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The PR changes src/db/schema/* (e.g., replaces activity_type with
canonical_type/provider_type/modality) but the generated diagram artifacts in
docs/schema.dbml and docs/schema.puml still reflect the old activity_type column. This
violates the requirement to keep schema diagram outputs in sync with schema changes and can mislead
developers and reviewers.
Code

src/db/schema/activity.ts[R210-213]

    externalId: text("external_id").notNull(),
-    activityType: activityTypeEnum("activity_type").notNull(),
+    canonicalType: canonicalActivityTypeEnum("canonical_type").notNull(),
+    providerType: text("provider_type").notNull(),
+    modality: activityModalityEnum("modality"),
Relevance

●●● Strong

Schema diagram sync is enforced; updates to docs/schema.puml were accepted when schema changed
(#1075, #2285).

PR-#1075
PR-#2285

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 722019 requires updating both docs/schema.dbml and docs/schema.puml whenever
src/db/schema/** changes. The PR updates the activity schema to use canonicalType,
providerType, and modality, but the docs diagrams still show activity_type, indicating the
diagram artifacts were not regenerated for this change set.

Rule 722019: Regenerate DB schema diagrams when schema files change
src/db/schema/activity.ts[210-214]
docs/schema.dbml[413-420]
docs/schema.puml[130-137]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Schema files under `src/db/schema/` were changed, but the generated schema diagram artifacts (`docs/schema.dbml` and `docs/schema.puml`) were not updated and still show the pre-change `activity_type` column.

## Issue Context
The repo includes `scripts/generate-schema-diagram.ts` which generates `docs/schema.dbml` and `docs/schema.puml` from the Drizzle schema.

## Fix Focus Areas
- src/db/schema/activity.ts[210-213]
- docs/schema.dbml[413-440]
- docs/schema.puml[130-152]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. client.query() used for SQL ✓ Resolved 📘 Rule violation ≡ Correctness
Description
New DB integration test code executes raw SQL via pg client.query() instead of the typed
executeWithSchema() wrapper, bypassing runtime row-shape validation. This can allow silent schema
drift in test assertions and violates the required SQL execution pattern.
Code

src/db/canonical-activity-types-migration.integration.test.ts[R413-416]

+      const identityBefore = await client.query<IdentityRow>(`
+        SELECT
+          'fitness.activity'::regclass::oid::integer AS relation_oid,
+          attribute.attnum::integer AS provider_attnum
Relevance

●●● Strong

Team repeatedly required executeWithSchema/Zod validation for raw SQL in tests (accepted in #2219,
#2297).

PR-#2219
PR-#2297

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 722086 requires raw SQL execution to be performed through a typed
executeWithSchema() wrapper with schema validation. The new test file performs catalog queries
using await client.query<...>( with raw SQL strings, which bypasses the required wrapper and its
runtime validation.

Rule 722086: Execute raw SQL via typed executeWithSchema wrapper
src/db/canonical-activity-types-migration.integration.test.ts[413-420]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new integration test executes raw SQL via `pg`'s `client.query()` instead of the required typed `executeWithSchema()` wrapper.

## Issue Context
Compliance requires raw SQL execution to go through `executeWithSchema()` (with a Zod schema) to validate result row shapes at runtime.

## Fix Focus Areas
- src/db/canonical-activity-types-migration.integration.test.ts[413-440]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Exact identity ignores providerType ✓ Resolved 🐞 Bug ≡ Correctness
Description
findUniqueProviderActivityByExactIdentity now treats (providerId, userId, canonicalType, startedAt,
endedAt) as the exact provider identity, ignoring the newly stored provider_type and modality. This
widens matching vs legacy subtype-based identity and can return an incorrect activity id (or become
ambiguous and return undefined) when multiple provider activity subtypes share the same canonical
type and timestamps.
Code

src/db/provider-activity-sync.ts[R71-74]

        FROM fitness.activity
        WHERE provider_id = ${identity.providerId}
          AND user_id = ${identity.userId}
-          AND activity_type = ${identity.activityType}
+          AND canonical_type = ${identity.canonicalType}
Relevance

●● Moderate

Only prior “exact identity” implementation matched by activity_type; no history on adding
provider_type/modality to identity lookup.

PR-#1673

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The exact-identity lookup filters on canonical_type and is used to reuse an existing activity during
garmin-dump FIT imports; the new schema clearly supports a more specific identity via
provider_type/modality, but those fields are not part of the lookup.

src/db/provider-activity-sync.ts[52-82]
src/jobs/process-fit-file-import-job.ts[392-402]
src/db/schema/activity.ts[199-214]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`findUniqueProviderActivityByExactIdentity()` is documented as using an “exact provider identity”, but it currently filters on `canonical_type` only. Under the new storage contract, `provider_type` (raw provider activity type) and `modality` exist specifically to preserve provider specificity; ignoring them makes this identity coarser than the data available.

## Issue Context
This lookup is used in the FIT import path for garmin-dump when no activity summary is present, so a false/ambiguous match can cause the import to reuse the wrong activity row or fail to dedupe.

## Fix Focus Areas
- src/db/provider-activity-sync.ts[52-82]
- src/jobs/process-fit-file-import-job.ts[392-402]
- src/db/schema/activity.ts[199-214]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment thread src/db/schema/activity.ts
Comment thread src/db/canonical-activity-types-migration.integration.test.ts Outdated
Comment thread src/db/schema/activity.ts
Comment thread src/db/provider-activity-sync.ts
@codereviewbot-ai

Copy link
Copy Markdown

🤖 Review skipped: Repository rate limit exceeded. Free accounts are limited to 2 reviews per 4 hours per repository. Upgrade to a paid plan for unlimited reviews.

1 similar comment
@codereviewbot-ai

Copy link
Copy Markdown

🤖 Review skipped: Repository rate limit exceeded. Free accounts are limited to 2 reviews per 4 hours per repository. Upgrade to a paid plan for unlimited reviews.

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Mobile Preview

Scan to open on device:

QR code for dofek://preview/pr-2416

Channel pr-2416
Deep Link dofek://preview/pr-2416
Commit 5748c05

To test on device:

  1. Build and install the preview client: PREVIEW_CHANNEL=pr-2416 pnpm expo prebuild --clean -p ios
  2. Or tap deep link on an existing preview build: dofek://preview/pr-2416

Each PR gets its own channel. Build a preview client with PREVIEW_CHANNEL=pr-{N} to test.

@codereviewbot-ai

Copy link
Copy Markdown

🤖 Review skipped: Repository rate limit exceeded. Free accounts are limited to 2 reviews per 4 hours per repository. Upgrade to a paid plan for unlimited reviews.

1 similar comment
@codereviewbot-ai

Copy link
Copy Markdown

🤖 Review skipped: Repository rate limit exceeded. Free accounts are limited to 2 reviews per 4 hours per repository. Upgrade to a paid plan for unlimited reviews.

@codereviewbot-ai

Copy link
Copy Markdown

🤖 Review skipped: Repository rate limit exceeded. Free accounts are limited to 2 reviews per 4 hours per repository. Upgrade to a paid plan for unlimited reviews.

@codereviewbot-ai

Copy link
Copy Markdown

🤖 Review skipped: Repository rate limit exceeded. Free accounts are limited to 2 reviews per 4 hours per repository. Upgrade to a paid plan for unlimited reviews.

@codereviewbot-ai

Copy link
Copy Markdown

🤖 Review skipped: Repository rate limit exceeded. Free accounts are limited to 2 reviews per 4 hours per repository. Upgrade to a paid plan for unlimited reviews.

@codereviewbot-ai

Copy link
Copy Markdown

🤖 Review skipped: Repository rate limit exceeded. Free accounts are limited to 2 reviews per 4 hours per repository. Upgrade to a paid plan for unlimited reviews.

@codereviewbot-ai

codereviewbot-ai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Pull Request Review Summary

Overall, this PR provides a thorough refactoring transitioning the activity schema from a single legacy activity_type column to canonical_type, provider_type, and modality.

Findings

  1. Migration Safety (drizzle/0068_canonical_activity_types.sql): The CASE statement generating canonical_type during activity_next creation lacks an ELSE 'other' fallback branch. If any existing activity row has NULL or an unmapped type string, canonical_type will evaluate to NULL and break the subsequent SET NOT NULL constraint alteration. (See inline comment)
  2. Existing Audit Findings: Note that prior bot review comments flagged missing schema diagram updates (docs/schema.dbml, docs/schema.puml), stale activity_type inserts in ClimbingTrainingLogRepository, and exact identity search logic in findUniqueProviderActivityByExactIdentity. Ensure those items are addressed prior to merging.

🤖 Reviewed by codereviewbot.ai - Catch bugs before your team does.

Comment thread drizzle/0068_canonical_activity_types.sql
@codereviewbot-ai

Copy link
Copy Markdown

🤖 Review skipped: Repository rate limit exceeded. Free accounts are limited to 2 reviews per 4 hours per repository. Upgrade to a paid plan for unlimited reviews.

@codereviewbot-ai

Copy link
Copy Markdown

🤖 Review skipped: Repository rate limit exceeded. Free accounts are limited to 2 reviews per 4 hours per repository. Upgrade to a paid plan for unlimited reviews.

@codereviewbot-ai

Copy link
Copy Markdown

🤖 Review skipped: Repository rate limit exceeded. Free accounts are limited to 2 reviews per 4 hours per repository. Upgrade to a paid plan for unlimited reviews.

@codereviewbot-ai

Copy link
Copy Markdown

🤖 Review skipped: Repository rate limit exceeded. Free accounts are limited to 2 reviews per 4 hours per repository. Upgrade to a paid plan for unlimited reviews.

@codereviewbot-ai

Copy link
Copy Markdown

🤖 Review skipped: Repository rate limit exceeded. Free accounts are limited to 2 reviews per 4 hours per repository. Upgrade to a paid plan for unlimited reviews.

@codereviewbot-ai

Copy link
Copy Markdown

🤖 Review skipped: Repository rate limit exceeded. Free accounts are limited to 2 reviews per 4 hours per repository. Upgrade to a paid plan for unlimited reviews.

@codereviewbot-ai

Copy link
Copy Markdown

🤖 Review skipped: Repository rate limit exceeded. Free accounts are limited to 2 reviews per 4 hours per repository. Upgrade to a paid plan for unlimited reviews.

@codereviewbot-ai

Copy link
Copy Markdown

🤖 Review skipped: Repository rate limit exceeded. Free accounts are limited to 2 reviews per 4 hours per repository. Upgrade to a paid plan for unlimited reviews.

@Asherlc
Asherlc merged commit 63852d7 into main Aug 3, 2026
115 checks passed
@Asherlc
Asherlc deleted the Asherlc/issue-2245-canonical-activity-types branch August 3, 2026 06:38
Asherlc added a commit that referenced this pull request Aug 4, 2026
* fix(clickhouse): repair stale canonical activity type views

ClickHouse stores the query text a view is created with, and
`standardViewHeader()` emits `CREATE VIEW IF NOT EXISTS`. Migration 0069
renamed `activity_type` to `canonical_type` on `postgres_fitness.activity`
and the analytics serving tables while PR #2416 updated the matching view
bodies, but neither view was dropped first, so the recreate was a no-op
against the existing production objects. `analytics.activity_summary` and
`analytics.v_activity` kept selecting a column that no longer exists, and
every server read of the summary view failed with UNKNOWN_IDENTIFIER.

Migration 0071 drops and recreates the three affected views from the
current builders. It then backfills the `provider_type` and `modality`
columns that 0069 added without values: the dbt models that own those
tables pick dirty keys from `_peerdb_synced_at`, which a mutation does not
advance, so historical rows would have stayed null indefinitely. The
backfill copies provenance from the replicated activity rows through a
temporary Join-engine lookup and `joinGet`, bounded to live rows that are
still missing it, and drops the lookup when it finishes.

Extracts the duplicated column-existence probe into a shared
`hasClickHouseColumn` helper and the migration test client into a local
`test-helpers.ts`.

Fixes DOFEK-SERVER-53

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MeXWX6Z7h5uWtVByYo26zd

* test(clickhouse): name the missing-replica activity fixture in words

cspell rejected "unreplicated". Rename the fixture and its assertion to
describe the same case with dictionary words rather than widening the
project dictionary for one coined term.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MeXWX6Z7h5uWtVByYo26zd

* refactor(clickhouse): validate column introspection rows with zod

Parse the system.columns response with a Zod schema instead of trusting a
TypeScript interface across the ClickHouse runtime boundary. The count comes
back as a string for 64-bit integers, so the schema admits both.

Also records why provider_type alone marks a row as un-backfilled: a null
modality is a legitimate resting state for many canonical types, so widening
the filter would rewrite correct rows on every run without converging.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MeXWX6Z7h5uWtVByYo26zd

* fix(deps): clear high advisories and align the mobile gesture peer

Both checks failed on the base branch and tripped fail-fast, cancelling the
unit, integration, and E2E jobs, so nothing in this PR was being exercised by
CI.

pnpm audit --prod --audit-level=high reported three high advisories, all in
transitive dependencies the workspace already pins. Advance those pins to the
patched releases rather than lowering the audit level: brace-expansion to
5.0.9 (GHSA-rgw5-rvv9-x895), fast-uri to 3.1.5 (GHSA-7p8r-x3mc-p8w7), and
ip-address to 10.4.0 (GHSA-mwp4-54f8-5fhr). The fast-uri selector moves with
its advisory range, and staying inside each existing major keeps the parents'
ranges satisfied.

expo install --check required react-native-gesture-handler ~3.1.0 against a
2.32.0 pin. No workspace source imports the package — it is a peer of
expo-router and react-navigation — so the major bump carries no API surface
of ours.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MeXWX6Z7h5uWtVByYo26zd

* docs(incident): record the OTA healthcheck outage as unresolved

Adds the two CI failures fixed alongside this change, including the pnpm 10+
detail that overrides live in pnpm-workspace.yaml rather than package.json,
and records the Publish Mobile Preview OTA healthcheck 404 as an unresolved
production service state rather than omitting it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MeXWX6Z7h5uWtVByYo26zd

* fix(deploy): pass EXPO_APP_ID to the OTA update service

expo-open-ota reads EXPO_APP_ID at startup and logs "EXPO_APP_ID not set"
without it, leaving dofek_ota at 0/1 so ota.dofek.asherlc.com/hc answers 404
and the preview publish workflow fails its healthcheck before reaching
eoas publish.

The variable was already present in Infisical, but the ota service in
deploy/stack.yml never referenced ${EXPO_APP_ID}, so it was never interpolated
into the container. The dotenv template renders every secret at the project
root and docker stack deploy runs with that dotenv loaded, so naming the
variable is all that was missing.

Also adds EXPO_APP_ID to REQUIRED_DEPLOY_KEYS so an absent value fails the
deploy with a named key rather than surfacing later as a healthcheck 404,
closing the fail-fast follow-up left open by the earlier staging incident.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MeXWX6Z7h5uWtVByYo26zd

* fix(mobile): align Expo SDK 57 dependency pins

Expo's published SDK 57 native-modules map now expects
react-native-gesture-handler ~2.32.0, reversing the ~3.1.0 requirement
that motivated yesterday's bump in PR #2420. `pnpm expo install --check`
validates against Expo's live version map rather than anything in the
repo, so it now fails on main's head too, with the exact same
dependency set that passed CI this morning.

Root cause: expo, expo-linking, expo-modules-core, expo-router, and
expo-updates patch versions and react-native-gesture-handler's expected
range all changed upstream between this morning's green run and now.

Fix: bump the five expo packages to their now-expected patch versions
and revert react-native-gesture-handler to 2.32.0, matching
https://api.expo.dev/v2/sdks/57.0.0/native-modules. Verified locally
with `pnpm expo install --check` (passes) and `pnpm test:mobile`
(176 files, 1476 tests pass).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JvC38Y81GUpfMTy2a9YkCu

* docs(incident): disambiguate the fail-fast CI failure count

CodeRabbit flagged that "Two CI failures" read ambiguously against
the separately-described OTA workflow failure later in the same
entry. Scope the count to what it actually describes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JvC38Y81GUpfMTy2a9YkCu

* fix(deploy): fail closed on the OTA JWT and signing key secrets

CodeRabbit flagged that OTA_JWT_SECRET, OTA_PRIVATE_KEY_B64, and
OTA_PUBLIC_KEY_B64 rendered as empty strings rather than failing the
deploy when absent. This was previously declined pending confirmation
that all three are actually populated in Infisical -- the repository
owner has now confirmed that.

Add :? guards to the three interpolations in deploy/stack.yml and add
them to REQUIRED_DEPLOY_KEYS in scripts/validate-deploy-env.ts, so a
missing value fails the deploy with a named key instead of starting
the OTA container misconfigured.

Validated: `docker stack config -c deploy/stack.yml` renders the ota
service correctly with all three vars set, and fails with a named
"OTA_JWT_SECRET is required" error when one is unset. Unit tests pass
(7/7 in validate-deploy-env.test.ts). tsc --noEmit clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JvC38Y81GUpfMTy2a9YkCu

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Canonical activity types: normalize synonyms, keep provider type as a separate field

2 participants