forge-openclaw-plugin 0.2.67 → 0.2.69

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/dist/assets/{board-DFNV9VAZ.js → board-BfqxFNiQ.js} +1 -1
  2. package/dist/assets/index-BfLQnCNZ.js +91 -0
  3. package/dist/assets/index-DIapFz9v.css +1 -0
  4. package/dist/assets/{motion-CXdn34ih.js → motion-C0ALlgho.js} +1 -1
  5. package/dist/assets/{table-CEq3bTDv.js → table-WcMjnJll.js} +1 -1
  6. package/dist/assets/{ui-g7FaEglG.js → ui-B5I-3U91.js} +1 -1
  7. package/dist/assets/vendor-B-Lq_OG3.css +1 -0
  8. package/dist/assets/vendor-C56o26_3.js +2163 -0
  9. package/dist/index.html +8 -8
  10. package/dist/server/server/migrations/060_psyche_devrage_metrics.sql +40 -0
  11. package/dist/server/server/migrations/061_health_workout_raw_evidence.sql +95 -0
  12. package/dist/server/server/src/app.js +94 -12
  13. package/dist/server/server/src/health-workout-analytics.js +572 -0
  14. package/dist/server/server/src/health.js +116 -3
  15. package/dist/server/server/src/openapi.js +238 -0
  16. package/dist/server/server/src/psyche-types.js +90 -0
  17. package/dist/server/server/src/services/devrage.js +412 -0
  18. package/dist/server/server/src/services/psyche.js +3 -0
  19. package/dist/server/src/lib/api.js +13 -0
  20. package/openclaw.plugin.json +1 -1
  21. package/package.json +1 -1
  22. package/server/migrations/060_psyche_devrage_metrics.sql +40 -0
  23. package/server/migrations/061_health_workout_raw_evidence.sql +95 -0
  24. package/skills/forge-openclaw/SKILL.md +25 -1
  25. package/skills/forge-openclaw/entity_conversation_playbooks.md +46 -14
  26. package/dist/assets/index-B9IVt8VN.js +0 -90
  27. package/dist/assets/index-DJlo9Tsp.css +0 -1
  28. package/dist/assets/vendor-BcOHGipZ.js +0 -1341
  29. package/dist/assets/vendor-DT3pnAKJ.css +0 -1
@@ -3,6 +3,7 @@ import { z } from "zod";
3
3
  import { getDatabase, runInTransaction } from "./db.js";
4
4
  import { HttpError } from "./errors.js";
5
5
  import { buildWorkoutSessionPersistenceSeed, buildWorkoutSessionPresentation, workoutActivityDescriptorSchema, workoutDetailsSchema } from "./health-workout-adapters.js";
6
+ import { getHealthZoneProfile, getStoredWorkoutAnalytics, getWorkoutRawEvidence, healthZoneProfilePatchSchema, recomputeAndStoreWorkoutAnalytics, upsertWorkoutRoutePoints, upsertWorkoutTimeSeries, workoutCaptureQualitySchema, workoutRoutePointSchema, workoutTimeSeriesSampleSchema, patchHealthZoneProfile } from "./health-workout-analytics.js";
6
7
  import { getMovementMobileBootstrap, ingestMovementSync, movementSyncPayloadSchema } from "./movement.js";
7
8
  import { ingestScreenTimeSync, screenTimeSyncPayloadSchema } from "./screen-time.js";
8
9
  import { recordActivityEvent } from "./repositories/activity-events.js";
@@ -245,6 +246,10 @@ export const mobileHealthSyncSchema = z.object({
245
246
  sourceProductType: z.string().trim().optional(),
246
247
  activity: workoutActivityDescriptorSchema.optional(),
247
248
  details: workoutDetailsSchema.optional(),
249
+ timeSeriesSamples: z.array(workoutTimeSeriesSampleSchema).default([]),
250
+ routePoints: z.array(workoutRoutePointSchema).default([]),
251
+ captureQuality: workoutCaptureQualitySchema.optional(),
252
+ syncCursor: z.record(z.string(), z.unknown()).default({}),
248
253
  links: z.array(healthLinkSchema).default([]),
249
254
  annotations: workoutAnnotationSchema.partial().default({})
250
255
  }))
@@ -788,6 +793,7 @@ function mapSleepRawLog(row) {
788
793
  function mapWorkoutSession(row) {
789
794
  const provenance = safeJsonParse(row.provenance_json, {});
790
795
  const derived = safeJsonParse(row.derived_json, {});
796
+ const analytics = getStoredWorkoutAnalytics(row);
791
797
  const presentation = buildWorkoutSessionPresentation({
792
798
  source: row.source,
793
799
  sourceType: row.source_type,
@@ -833,6 +839,7 @@ function mapWorkoutSession(row) {
833
839
  annotations: safeJsonParse(row.annotations_json, {}),
834
840
  provenance,
835
841
  derived,
842
+ analytics,
836
843
  generatedFromHabitId: row.generated_from_habit_id,
837
844
  generatedFromCheckInId: row.generated_from_check_in_id,
838
845
  reconciliationStatus: row.reconciliation_status,
@@ -1352,6 +1359,28 @@ export function getWorkoutSessionById(workoutId) {
1352
1359
  .get(workoutId);
1353
1360
  return row ? mapWorkoutSession(row) : undefined;
1354
1361
  }
1362
+ export function getWorkoutSessionDetailById(workoutId, resolution = "adaptive") {
1363
+ const row = getDatabase()
1364
+ .prepare(`SELECT * FROM health_workout_sessions WHERE id = ?`)
1365
+ .get(workoutId);
1366
+ if (!row) {
1367
+ return undefined;
1368
+ }
1369
+ const workout = mapWorkoutSession(row);
1370
+ return {
1371
+ workout,
1372
+ analytics: getStoredWorkoutAnalytics(row),
1373
+ evidence: getWorkoutRawEvidence(row, resolution),
1374
+ zoneProfile: getHealthZoneProfile(row.user_id)
1375
+ };
1376
+ }
1377
+ export { healthZoneProfilePatchSchema };
1378
+ export function getHealthZoneProfileForUser(userId) {
1379
+ return getHealthZoneProfile(userId);
1380
+ }
1381
+ export function patchHealthZoneProfileForUser(userId, patch) {
1382
+ return patchHealthZoneProfile(userId, patch);
1383
+ }
1355
1384
  function listPairingRows(userIds) {
1356
1385
  const params = [];
1357
1386
  const where = userIds && userIds.length > 0
@@ -2072,6 +2101,7 @@ function insertOrUpdateWorkoutSession(pairing, input) {
2072
2101
  ? Number((input.distanceMeters / input.exerciseMinutes).toFixed(2))
2073
2102
  : null
2074
2103
  }), matchedGenerated.generated_from_habit_id ? "merged" : "standalone", now, matchedGenerated.id);
2104
+ persistWorkoutEvidenceForInput(matchedGenerated.id, pairing.user_id, input);
2075
2105
  return {
2076
2106
  mode: matchedGenerated.generated_from_habit_id || matchedGenerated.source !== "apple_health"
2077
2107
  ? "merged"
@@ -2105,8 +2135,43 @@ function insertOrUpdateWorkoutSession(pairing, input) {
2105
2135
  ? Number((input.distanceMeters / input.exerciseMinutes).toFixed(2))
2106
2136
  : null
2107
2137
  }), now, now);
2138
+ persistWorkoutEvidenceForInput(id, pairing.user_id, input);
2108
2139
  return { mode: "created", id };
2109
2140
  }
2141
+ function persistWorkoutEvidenceForInput(workoutId, userId, input) {
2142
+ if (input.timeSeriesSamples.length > 0) {
2143
+ upsertWorkoutTimeSeries({
2144
+ workoutId,
2145
+ userId,
2146
+ samples: input.timeSeriesSamples
2147
+ });
2148
+ }
2149
+ if (input.routePoints.length > 0) {
2150
+ upsertWorkoutRoutePoints({
2151
+ workoutId,
2152
+ userId,
2153
+ points: input.routePoints
2154
+ });
2155
+ }
2156
+ const row = getDatabase()
2157
+ .prepare(`SELECT * FROM health_workout_sessions WHERE id = ?`)
2158
+ .get(workoutId);
2159
+ if (row) {
2160
+ recomputeAndStoreWorkoutAnalytics(row);
2161
+ if (input.captureQuality) {
2162
+ const derived = safeJsonParse(row.derived_json, {});
2163
+ getDatabase()
2164
+ .prepare(`UPDATE health_workout_sessions
2165
+ SET derived_json = ?, updated_at = ?
2166
+ WHERE id = ?`)
2167
+ .run(JSON.stringify({
2168
+ ...derived,
2169
+ captureQuality: input.captureQuality,
2170
+ syncCursor: input.syncCursor
2171
+ }), nowIso(), workoutId);
2172
+ }
2173
+ }
2174
+ }
2110
2175
  function clusterSleepRowsByGap(rows) {
2111
2176
  const clusters = [];
2112
2177
  let currentCluster = [];
@@ -2920,6 +2985,42 @@ export function getFitnessViewData(userIds) {
2920
2985
  workoutTypeBreakdown.set(session.workoutType, current);
2921
2986
  }
2922
2987
  const orderedWorkoutTypes = [...workoutTypeBreakdown.entries()].sort((left, right) => right[1].totalMinutes - left[1].totalMinutes);
2988
+ const zoneTotals = new Map();
2989
+ let totalZoneSeconds = 0;
2990
+ let heartRateCoverageSum = 0;
2991
+ let heartRateCoverageCount = 0;
2992
+ let totalTrainingLoad = 0;
2993
+ let routeWorkoutCount = 0;
2994
+ for (const session of recent) {
2995
+ const analytics = session.analytics;
2996
+ for (const zone of analytics?.zoneDurations ?? []) {
2997
+ const current = zoneTotals.get(zone.key) ?? {
2998
+ label: zone.label,
2999
+ seconds: 0
3000
+ };
3001
+ current.seconds += zone.seconds;
3002
+ totalZoneSeconds += zone.seconds;
3003
+ zoneTotals.set(zone.key, current);
3004
+ }
3005
+ if (typeof analytics?.dataQuality?.sampleCoverage === "number") {
3006
+ heartRateCoverageSum += analytics.dataQuality.sampleCoverage;
3007
+ heartRateCoverageCount += 1;
3008
+ }
3009
+ if (typeof analytics?.load?.trimp === "number") {
3010
+ totalTrainingLoad += analytics.load.trimp;
3011
+ }
3012
+ if (analytics?.routeSummary?.hasRoute) {
3013
+ routeWorkoutCount += 1;
3014
+ }
3015
+ }
3016
+ const zoneMix = [...zoneTotals.entries()].map(([key, value]) => ({
3017
+ key,
3018
+ label: value.label,
3019
+ seconds: Math.round(value.seconds),
3020
+ percentage: totalZoneSeconds > 0
3021
+ ? Number((value.seconds / totalZoneSeconds).toFixed(4))
3022
+ : 0
3023
+ }));
2923
3024
  return {
2924
3025
  summary: {
2925
3026
  workoutCount: weekly.length,
@@ -2940,7 +3041,13 @@ export function getFitnessViewData(userIds) {
2940
3041
  topWorkoutType: orderedWorkoutTypes[0]?.[0] ?? null,
2941
3042
  topWorkoutTypeLabel: recent.find((session) => session.workoutType === orderedWorkoutTypes[0]?.[0])
2942
3043
  ?.workoutTypeLabel ?? null,
2943
- streakDays: Array.from(new Set(weekly.map((session) => dayKey(session.startedAt)))).length
3044
+ streakDays: Array.from(new Set(weekly.map((session) => dayKey(session.startedAt)))).length,
3045
+ averageHeartRateCoverage: heartRateCoverageCount > 0
3046
+ ? Number((heartRateCoverageSum / heartRateCoverageCount).toFixed(3))
3047
+ : 0,
3048
+ totalTrainingLoad: Number(totalTrainingLoad.toFixed(1)),
3049
+ routeWorkoutCount,
3050
+ zoneMix
2944
3051
  },
2945
3052
  weeklyTrend: weekly
2946
3053
  .map((session) => ({
@@ -2951,7 +3058,12 @@ export function getFitnessViewData(userIds) {
2951
3058
  activityFamily: session.activityFamily,
2952
3059
  activityFamilyLabel: session.activityFamilyLabel,
2953
3060
  durationMinutes: Math.round(session.durationSeconds / 60),
2954
- energyKcal: Math.round(session.totalEnergyKcal ?? session.activeEnergyKcal ?? 0)
3061
+ energyKcal: Math.round(session.totalEnergyKcal ?? session.activeEnergyKcal ?? 0),
3062
+ zoneDurations: session.analytics
3063
+ ?.zoneDurations ?? [],
3064
+ trainingLoad: (session.analytics
3065
+ ?.load?.trimp ?? null),
3066
+ heartRateCoverage: (session.analytics?.dataQuality?.sampleCoverage ?? 0)
2955
3067
  }))
2956
3068
  .reverse(),
2957
3069
  typeBreakdown: orderedWorkoutTypes.map(([workoutType, metrics]) => ({
@@ -3482,6 +3594,7 @@ export function deleteWorkoutSession(workoutId, activity) {
3482
3594
  if (!current) {
3483
3595
  return undefined;
3484
3596
  }
3597
+ const deletedWorkout = mapWorkoutSession(current);
3485
3598
  getDatabase()
3486
3599
  .prepare(`DELETE FROM health_workout_sessions WHERE id = ?`)
3487
3600
  .run(workoutId);
@@ -3499,7 +3612,7 @@ export function deleteWorkoutSession(workoutId, activity) {
3499
3612
  startedAt: current.started_at
3500
3613
  }
3501
3614
  });
3502
- return mapWorkoutSession(current);
3615
+ return deletedWorkout;
3503
3616
  }
3504
3617
  export function updateWorkoutMetadata(workoutId, patch, activity) {
3505
3618
  const parsed = updateWorkoutMetadataSchema.parse(patch);
@@ -4645,6 +4645,224 @@ export function buildOpenApiDocument() {
4645
4645
  updatedAt: { type: "string", format: "date-time" }
4646
4646
  }
4647
4647
  };
4648
+ const devrageMetricPayload = {
4649
+ type: "object",
4650
+ additionalProperties: false,
4651
+ required: [
4652
+ "generatedAt",
4653
+ "hasData",
4654
+ "latestDateKey",
4655
+ "rawSwearCount",
4656
+ "swearingMessagePercent",
4657
+ "conversationsScanned",
4658
+ "messagesScanned",
4659
+ "messagesWithSwears",
4660
+ "dailyAverage",
4661
+ "weeklyAverage",
4662
+ "history",
4663
+ "sync"
4664
+ ],
4665
+ properties: {
4666
+ generatedAt: { type: "string", format: "date-time" },
4667
+ hasData: { type: "boolean" },
4668
+ latestDateKey: nullable({ type: "string" }),
4669
+ rawSwearCount: { type: "number" },
4670
+ swearingMessagePercent: { type: "number" },
4671
+ conversationsScanned: { type: "integer" },
4672
+ messagesScanned: { type: "integer" },
4673
+ messagesWithSwears: { type: "integer" },
4674
+ dailyAverage: {
4675
+ type: "object",
4676
+ additionalProperties: false,
4677
+ required: ["rawSwearCount", "swearingMessagePercent"],
4678
+ properties: {
4679
+ rawSwearCount: { type: "number" },
4680
+ swearingMessagePercent: { type: "number" }
4681
+ }
4682
+ },
4683
+ weeklyAverage: {
4684
+ type: "object",
4685
+ additionalProperties: false,
4686
+ required: ["rawSwearCount", "swearingMessagePercent"],
4687
+ properties: {
4688
+ rawSwearCount: { type: "number" },
4689
+ swearingMessagePercent: { type: "number" }
4690
+ }
4691
+ },
4692
+ history: arrayOf({
4693
+ type: "object",
4694
+ additionalProperties: false,
4695
+ required: [
4696
+ "dateKey",
4697
+ "rawSwearCount",
4698
+ "swearingMessagePercent",
4699
+ "conversationsScanned",
4700
+ "messagesScanned",
4701
+ "messagesWithSwears"
4702
+ ],
4703
+ properties: {
4704
+ dateKey: { type: "string" },
4705
+ rawSwearCount: { type: "number" },
4706
+ swearingMessagePercent: { type: "number" },
4707
+ conversationsScanned: { type: "integer" },
4708
+ messagesScanned: { type: "integer" },
4709
+ messagesWithSwears: { type: "integer" }
4710
+ }
4711
+ }),
4712
+ sync: {
4713
+ type: "object",
4714
+ additionalProperties: false,
4715
+ required: ["fullSyncCompletedAt", "lastDailySyncAt", "lastSyncedDateKey"],
4716
+ properties: {
4717
+ fullSyncCompletedAt: nullable({ type: "string", format: "date-time" }),
4718
+ lastDailySyncAt: nullable({ type: "string", format: "date-time" }),
4719
+ lastSyncedDateKey: nullable({ type: "string" })
4720
+ }
4721
+ }
4722
+ }
4723
+ };
4724
+ const dailyMetricDayRecord = {
4725
+ type: "object",
4726
+ additionalProperties: false,
4727
+ required: [
4728
+ "dateKey",
4729
+ "average",
4730
+ "minimum",
4731
+ "maximum",
4732
+ "latest",
4733
+ "total",
4734
+ "sampleCount",
4735
+ "latestSampleAt"
4736
+ ],
4737
+ properties: {
4738
+ dateKey: { type: "string" },
4739
+ average: nullable({ type: "number" }),
4740
+ minimum: nullable({ type: "number" }),
4741
+ maximum: nullable({ type: "number" }),
4742
+ latest: nullable({ type: "number" }),
4743
+ total: nullable({ type: "number" }),
4744
+ sampleCount: { type: "integer" },
4745
+ latestSampleAt: nullable({ type: "string", format: "date-time" })
4746
+ }
4747
+ };
4748
+ const dailyMetricRecord = {
4749
+ type: "object",
4750
+ additionalProperties: false,
4751
+ required: [
4752
+ "metric",
4753
+ "label",
4754
+ "category",
4755
+ "unit",
4756
+ "aggregation",
4757
+ "latestValue",
4758
+ "latestDateKey",
4759
+ "baselineValue",
4760
+ "deltaValue",
4761
+ "coverageDays",
4762
+ "days"
4763
+ ],
4764
+ properties: {
4765
+ metric: { type: "string" },
4766
+ label: { type: "string" },
4767
+ category: { type: "string" },
4768
+ unit: { type: "string" },
4769
+ aggregation: { type: "string", enum: ["discrete", "cumulative"] },
4770
+ latestValue: nullable({ type: "number" }),
4771
+ latestDateKey: nullable({ type: "string" }),
4772
+ baselineValue: nullable({ type: "number" }),
4773
+ deltaValue: nullable({ type: "number" }),
4774
+ coverageDays: { type: "integer" },
4775
+ days: arrayOf(dailyMetricDayRecord)
4776
+ }
4777
+ };
4778
+ const psycheMetricsViewData = {
4779
+ type: "object",
4780
+ additionalProperties: false,
4781
+ required: ["summary", "context", "metrics"],
4782
+ properties: {
4783
+ summary: {
4784
+ type: "object",
4785
+ additionalProperties: false,
4786
+ required: [
4787
+ "hasData",
4788
+ "trackedDays",
4789
+ "metricCount",
4790
+ "latestDateKey",
4791
+ "latestMetricCount",
4792
+ "categoryBreakdown"
4793
+ ],
4794
+ properties: {
4795
+ hasData: { type: "boolean" },
4796
+ trackedDays: { type: "integer" },
4797
+ metricCount: { type: "integer" },
4798
+ latestDateKey: nullable({ type: "string" }),
4799
+ latestMetricCount: { type: "integer" },
4800
+ categoryBreakdown: arrayOf({
4801
+ type: "object",
4802
+ additionalProperties: false,
4803
+ required: ["category", "metricCount", "coverageDays"],
4804
+ properties: {
4805
+ category: { type: "string" },
4806
+ metricCount: { type: "integer" },
4807
+ coverageDays: { type: "integer" }
4808
+ }
4809
+ })
4810
+ }
4811
+ },
4812
+ context: {
4813
+ type: "object",
4814
+ additionalProperties: false,
4815
+ required: [
4816
+ "generatedAt",
4817
+ "conversationsScanned",
4818
+ "sourceCount",
4819
+ "messagesScanned",
4820
+ "messagesWithSwears",
4821
+ "totalSwears",
4822
+ "dailyAverage",
4823
+ "weeklyAverage",
4824
+ "sync"
4825
+ ],
4826
+ properties: {
4827
+ generatedAt: { type: "string", format: "date-time" },
4828
+ conversationsScanned: { type: "integer" },
4829
+ sourceCount: { type: "integer" },
4830
+ messagesScanned: { type: "integer" },
4831
+ messagesWithSwears: { type: "integer" },
4832
+ totalSwears: { type: "number" },
4833
+ dailyAverage: {
4834
+ type: "object",
4835
+ additionalProperties: false,
4836
+ required: ["rawSwearCount", "swearingMessagePercent"],
4837
+ properties: {
4838
+ rawSwearCount: { type: "number" },
4839
+ swearingMessagePercent: { type: "number" }
4840
+ }
4841
+ },
4842
+ weeklyAverage: {
4843
+ type: "object",
4844
+ additionalProperties: false,
4845
+ required: ["rawSwearCount", "swearingMessagePercent"],
4846
+ properties: {
4847
+ rawSwearCount: { type: "number" },
4848
+ swearingMessagePercent: { type: "number" }
4849
+ }
4850
+ },
4851
+ sync: {
4852
+ type: "object",
4853
+ additionalProperties: false,
4854
+ required: ["fullSyncCompletedAt", "lastDailySyncAt", "lastSyncedDateKey"],
4855
+ properties: {
4856
+ fullSyncCompletedAt: nullable({ type: "string", format: "date-time" }),
4857
+ lastDailySyncAt: nullable({ type: "string", format: "date-time" }),
4858
+ lastSyncedDateKey: nullable({ type: "string" })
4859
+ }
4860
+ }
4861
+ }
4862
+ },
4863
+ metrics: arrayOf(dailyMetricRecord)
4864
+ }
4865
+ };
4648
4866
  const psycheOverviewPayload = {
4649
4867
  type: "object",
4650
4868
  additionalProperties: false,
@@ -4657,6 +4875,7 @@ export function buildOpenApiDocument() {
4657
4875
  "beliefs",
4658
4876
  "modes",
4659
4877
  "schemaPressure",
4878
+ "devrageMetric",
4660
4879
  "reports",
4661
4880
  "openInsights",
4662
4881
  "openNotes",
@@ -4680,6 +4899,7 @@ export function buildOpenApiDocument() {
4680
4899
  activationCount: { type: "integer" }
4681
4900
  }
4682
4901
  }),
4902
+ devrageMetric: devrageMetricPayload,
4683
4903
  reports: arrayOf({ $ref: "#/components/schemas/TriggerReport" }),
4684
4904
  openInsights: { type: "integer" },
4685
4905
  openNotes: { type: "integer" },
@@ -4992,6 +5212,7 @@ export function buildOpenApiDocument() {
4992
5212
  WorkoutSession: workoutSession,
4993
5213
  SleepViewData: sleepViewData,
4994
5214
  FitnessViewData: fitnessViewData,
5215
+ PsycheMetricsViewData: psycheMetricsViewData,
4995
5216
  PsycheOverviewPayload: psycheOverviewPayload,
4996
5217
  Insight: insight,
4997
5218
  InsightFeedback: insightFeedback,
@@ -7044,6 +7265,23 @@ export function buildOpenApiDocument() {
7044
7265
  }
7045
7266
  }
7046
7267
  },
7268
+ "/api/v1/psyche/metrics": {
7269
+ get: {
7270
+ summary: "Get daily Psyche metric history",
7271
+ responses: {
7272
+ "200": jsonResponse({
7273
+ type: "object",
7274
+ required: ["metrics"],
7275
+ properties: {
7276
+ metrics: {
7277
+ $ref: "#/components/schemas/PsycheMetricsViewData"
7278
+ }
7279
+ }
7280
+ }, "Psyche metrics view"),
7281
+ default: { $ref: "#/components/responses/Error" }
7282
+ }
7283
+ }
7284
+ },
7047
7285
  "/api/v1/psyche/values": {
7048
7286
  get: {
7049
7287
  summary: "List ACT-style values",
@@ -244,6 +244,95 @@ export const schemaPressureEntrySchema = z.object({
244
244
  title: nonEmptyTrimmedString,
245
245
  activationCount: z.number().int().nonnegative()
246
246
  });
247
+ export const devrageMetricPayloadSchema = z.object({
248
+ generatedAt: z.string(),
249
+ hasData: z.boolean(),
250
+ latestDateKey: z.string().nullable(),
251
+ rawSwearCount: z.number().nonnegative(),
252
+ swearingMessagePercent: z.number().nonnegative(),
253
+ conversationsScanned: z.number().int().nonnegative(),
254
+ messagesScanned: z.number().int().nonnegative(),
255
+ messagesWithSwears: z.number().int().nonnegative(),
256
+ dailyAverage: z.object({
257
+ rawSwearCount: z.number().nonnegative(),
258
+ swearingMessagePercent: z.number().nonnegative()
259
+ }),
260
+ weeklyAverage: z.object({
261
+ rawSwearCount: z.number().nonnegative(),
262
+ swearingMessagePercent: z.number().nonnegative()
263
+ }),
264
+ history: z.array(z.object({
265
+ dateKey: z.string(),
266
+ rawSwearCount: z.number().nonnegative(),
267
+ swearingMessagePercent: z.number().nonnegative(),
268
+ conversationsScanned: z.number().int().nonnegative(),
269
+ messagesScanned: z.number().int().nonnegative(),
270
+ messagesWithSwears: z.number().int().nonnegative()
271
+ })),
272
+ sync: z.object({
273
+ fullSyncCompletedAt: z.string().nullable(),
274
+ lastDailySyncAt: z.string().nullable(),
275
+ lastSyncedDateKey: z.string().nullable()
276
+ })
277
+ });
278
+ export const psycheMetricDayRecordSchema = z.object({
279
+ dateKey: z.string(),
280
+ average: z.number().nullable(),
281
+ minimum: z.number().nullable(),
282
+ maximum: z.number().nullable(),
283
+ latest: z.number().nullable(),
284
+ total: z.number().nullable(),
285
+ sampleCount: z.number().int().nonnegative(),
286
+ latestSampleAt: z.string().nullable()
287
+ });
288
+ export const psycheMetricsViewDataSchema = z.object({
289
+ summary: z.object({
290
+ hasData: z.boolean(),
291
+ trackedDays: z.number().int().nonnegative(),
292
+ metricCount: z.number().int().nonnegative(),
293
+ latestDateKey: z.string().nullable(),
294
+ latestMetricCount: z.number().int().nonnegative(),
295
+ categoryBreakdown: z.array(z.object({
296
+ category: z.string(),
297
+ metricCount: z.number().int().nonnegative(),
298
+ coverageDays: z.number().int().nonnegative()
299
+ }))
300
+ }),
301
+ context: z.object({
302
+ generatedAt: z.string(),
303
+ conversationsScanned: z.number().int().nonnegative(),
304
+ sourceCount: z.number().int().nonnegative(),
305
+ messagesScanned: z.number().int().nonnegative(),
306
+ messagesWithSwears: z.number().int().nonnegative(),
307
+ totalSwears: z.number().nonnegative(),
308
+ dailyAverage: z.object({
309
+ rawSwearCount: z.number().nonnegative(),
310
+ swearingMessagePercent: z.number().nonnegative()
311
+ }),
312
+ weeklyAverage: z.object({
313
+ rawSwearCount: z.number().nonnegative(),
314
+ swearingMessagePercent: z.number().nonnegative()
315
+ }),
316
+ sync: z.object({
317
+ fullSyncCompletedAt: z.string().nullable(),
318
+ lastDailySyncAt: z.string().nullable(),
319
+ lastSyncedDateKey: z.string().nullable()
320
+ })
321
+ }),
322
+ metrics: z.array(z.object({
323
+ metric: z.string(),
324
+ label: z.string(),
325
+ category: z.string(),
326
+ unit: z.string(),
327
+ aggregation: z.enum(["discrete", "cumulative"]),
328
+ latestValue: z.number().nullable(),
329
+ latestDateKey: z.string().nullable(),
330
+ baselineValue: z.number().nullable(),
331
+ deltaValue: z.number().nullable(),
332
+ coverageDays: z.number().int().nonnegative(),
333
+ days: z.array(psycheMetricDayRecordSchema)
334
+ }))
335
+ });
247
336
  export const psycheOverviewPayloadSchema = z.object({
248
337
  generatedAt: z.string(),
249
338
  domain: domainSchema,
@@ -254,6 +343,7 @@ export const psycheOverviewPayloadSchema = z.object({
254
343
  modes: z.array(modeProfileSchema),
255
344
  reports: z.array(triggerReportSchema),
256
345
  schemaPressure: z.array(schemaPressureEntrySchema),
346
+ devrageMetric: devrageMetricPayloadSchema,
257
347
  openInsights: z.number().int().nonnegative(),
258
348
  openNotes: z.number().int().nonnegative(),
259
349
  committedActions: z.array(trimmedString)