catence 0.2.1-beta.4 → 0.2.1-beta.6
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.
- package/dist/contracts/release.d.ts +1 -1
- package/dist/contracts/release.js +1 -1
- package/dist/elt/application/management.d.ts +7 -0
- package/dist/elt/application/management.js +30 -0
- package/dist/elt/normalization/normalizers.js +60 -0
- package/dist/interfaces/cli/commands.js +5 -1
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.js +1 -1
- package/package.json +1 -1
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Keep these values synchronized with release/manifest.json; release checks
|
|
4
4
|
* deliberately reject drift before a package can be published.
|
|
5
5
|
*/
|
|
6
|
-
export declare const CATENCE_RUNTIME_VERSION = "0.2.1-beta.
|
|
6
|
+
export declare const CATENCE_RUNTIME_VERSION = "0.2.1-beta.6";
|
|
7
7
|
export declare const CATENCE_PROTOCOL_VERSION = 1;
|
|
8
8
|
export type CatenceRuntimeCapabilities = {
|
|
9
9
|
mcp: true;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Keep these values synchronized with release/manifest.json; release checks
|
|
4
4
|
* deliberately reject drift before a package can be published.
|
|
5
5
|
*/
|
|
6
|
-
export const CATENCE_RUNTIME_VERSION = '0.2.1-beta.
|
|
6
|
+
export const CATENCE_RUNTIME_VERSION = '0.2.1-beta.6';
|
|
7
7
|
export const CATENCE_PROTOCOL_VERSION = 1;
|
|
8
8
|
export function catenceRuntimeHealth() {
|
|
9
9
|
return {
|
|
@@ -37,4 +37,11 @@ export declare function resolveExtractionErrors(paths: CatencePaths, opts?: {
|
|
|
37
37
|
* previously mis-parsed nutrition_days/nutrition_items from stored payloads.
|
|
38
38
|
*/
|
|
39
39
|
export declare function reimportNutrition(paths: CatencePaths): Promise<Record<string, unknown>>;
|
|
40
|
+
/**
|
|
41
|
+
* Re-run normalization over already-captured Garmin weigh-in entities without
|
|
42
|
+
* contacting any provider. Stores synced before beta.5 staged weigh_ins
|
|
43
|
+
* payloads but never projected them to daily_metrics, so this backfills
|
|
44
|
+
* weight_kg from stored payloads. Idempotent upserts make it safe to re-run.
|
|
45
|
+
*/
|
|
46
|
+
export declare function reimportWeight(paths: CatencePaths): Promise<Record<string, unknown>>;
|
|
40
47
|
export {};
|
|
@@ -722,6 +722,36 @@ export async function reimportNutrition(paths) {
|
|
|
722
722
|
await database.close();
|
|
723
723
|
}
|
|
724
724
|
}
|
|
725
|
+
/**
|
|
726
|
+
* Re-run normalization over already-captured Garmin weigh-in entities without
|
|
727
|
+
* contacting any provider. Stores synced before beta.5 staged weigh_ins
|
|
728
|
+
* payloads but never projected them to daily_metrics, so this backfills
|
|
729
|
+
* weight_kg from stored payloads. Idempotent upserts make it safe to re-run.
|
|
730
|
+
*/
|
|
731
|
+
export async function reimportWeight(paths) {
|
|
732
|
+
if (!existsSync(paths.database))
|
|
733
|
+
throw new Error('No Catence database exists yet. Complete a sync before re-importing weight.');
|
|
734
|
+
const database = await CatenceDatabase.open(paths);
|
|
735
|
+
try {
|
|
736
|
+
const rows = await database.rows(`SELECT provider, entity_type, remote_id, parent_remote_id, occurred_on, source_updated_at, raw_object_hash,
|
|
737
|
+
cast(payload_json AS VARCHAR) AS payload_json, cast(extension_json AS VARCHAR) AS extension_json
|
|
738
|
+
FROM source_entities
|
|
739
|
+
WHERE provider = 'garmin' AND entity_type = 'body_composition'
|
|
740
|
+
ORDER BY occurred_on, remote_id`);
|
|
741
|
+
for (const row of rows) {
|
|
742
|
+
await importSourceEntity(database, {
|
|
743
|
+
kind: 'source_entity', schemaVersion: 1, provider: row.provider, entityType: row.entity_type,
|
|
744
|
+
remoteId: row.remote_id, parentRemoteId: row.parent_remote_id, occurredOn: row.occurred_on, sourceUpdatedAt: row.source_updated_at,
|
|
745
|
+
rawObjectHash: row.raw_object_hash, payload: parseJsonObject(row.payload_json), extension: parseJsonObject(row.extension_json),
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
const weights = (await database.rows(`SELECT count(*) AS count FROM daily_metrics WHERE metric_name = 'weight_kg'`))[0]?.count ?? 0;
|
|
749
|
+
return { reimportedEntities: rows.length, weightObservations: Number(weights) };
|
|
750
|
+
}
|
|
751
|
+
finally {
|
|
752
|
+
await database.close();
|
|
753
|
+
}
|
|
754
|
+
}
|
|
725
755
|
function parseJsonObject(value) {
|
|
726
756
|
if (!value)
|
|
727
757
|
return {};
|
|
@@ -271,6 +271,8 @@ export async function importSourceEntity(database, entity) {
|
|
|
271
271
|
}
|
|
272
272
|
if (entity.entityType === 'wellness' || entity.entityType === 'daily_health')
|
|
273
273
|
await importDailyMetrics(database, entity.provider, entity.remoteId, entity.occurredOn, payload, rawHash);
|
|
274
|
+
if (entity.provider === 'garmin' && entity.entityType === 'body_composition')
|
|
275
|
+
await importGarminWeight(database, entity.occurredOn, payload, rawHash);
|
|
274
276
|
if (entity.entityType === 'nutrition_day' || entity.entityType === 'nutrition_log')
|
|
275
277
|
await importNutrition(database, entity.provider, entity.occurredOn, entity.remoteId, payload, rawHash);
|
|
276
278
|
if (entity.provider === 'garmin' && entity.entityType === 'training_metric')
|
|
@@ -968,6 +970,64 @@ async function importDailyMetrics(database, provider, remoteId, date, payload, r
|
|
|
968
970
|
if (provider === 'garmin')
|
|
969
971
|
await importGarminDailyDetails(database, remoteId, metricDate, payload, rawHash);
|
|
970
972
|
}
|
|
973
|
+
/**
|
|
974
|
+
* Weight-only projection for Garmin weigh-ins. The range endpoint returns
|
|
975
|
+
* `{dailyWeightSummaries: [{summaryDate, allWeightMetrics: [{weight, ...}]}]}`
|
|
976
|
+
* with weight in grams, while dayview returns `{dateWeightList: [...]}`.
|
|
977
|
+
* The worker stages a whole range window as one `body_composition` entity,
|
|
978
|
+
* so dates are derived per entry here. Multiple weigh-ins on one day resolve
|
|
979
|
+
* to the latest timestamp. Body-composition fields (fat, muscle, ...) are
|
|
980
|
+
* deliberately ignored.
|
|
981
|
+
*/
|
|
982
|
+
function weightKgFromValue(value) {
|
|
983
|
+
if (value === null)
|
|
984
|
+
return null;
|
|
985
|
+
return value > 1000 ? value / 1000 : value;
|
|
986
|
+
}
|
|
987
|
+
function weightEntryDate(entry, fallbackDay) {
|
|
988
|
+
// Prefer the parent summaryDate (Garmin's local day) over the entry's UTC
|
|
989
|
+
// timestampGMT, which can fall on a different UTC date.
|
|
990
|
+
return fallbackDay
|
|
991
|
+
?? datePart(firstString(entry, ['summaryDate', 'calendarDate', 'date']))
|
|
992
|
+
?? datePart(timestamp(entry.timestampGMT));
|
|
993
|
+
}
|
|
994
|
+
function collectGarminWeightEntries(payload, fallbackDay) {
|
|
995
|
+
const entries = [];
|
|
996
|
+
const pushEntry = (entry, fallback) => {
|
|
997
|
+
const kg = weightKgFromValue(firstNumber(entry, ['weight', 'weightValue', 'weightInKg']));
|
|
998
|
+
if (kg === null)
|
|
999
|
+
return;
|
|
1000
|
+
const day = weightEntryDate(entry, fallback ?? fallbackDay);
|
|
1001
|
+
if (!day)
|
|
1002
|
+
return;
|
|
1003
|
+
const ts = typeof entry.timestampGMT === 'number' && Number.isFinite(entry.timestampGMT) ? entry.timestampGMT : 0;
|
|
1004
|
+
entries.push({ date: day, kg, ts });
|
|
1005
|
+
};
|
|
1006
|
+
for (const summary of objectArray(payload.dailyWeightSummaries)) {
|
|
1007
|
+
const day = datePart(firstString(summary, ['summaryDate', 'calendarDate', 'date'])) ?? fallbackDay;
|
|
1008
|
+
for (const metric of objectArray(summary.allWeightMetrics))
|
|
1009
|
+
pushEntry(metric, day);
|
|
1010
|
+
}
|
|
1011
|
+
for (const key of ['dateWeightList', 'allWeightMetrics']) {
|
|
1012
|
+
for (const entry of objectArray(payload[key]))
|
|
1013
|
+
pushEntry(entry, fallbackDay);
|
|
1014
|
+
}
|
|
1015
|
+
if (firstNumber(payload, ['weight', 'weightValue', 'weightInKg']) !== null)
|
|
1016
|
+
pushEntry(payload, fallbackDay);
|
|
1017
|
+
return entries;
|
|
1018
|
+
}
|
|
1019
|
+
async function importGarminWeight(database, date, payload, rawHash) {
|
|
1020
|
+
const fallbackDay = datePart(date);
|
|
1021
|
+
const latestByDay = new Map();
|
|
1022
|
+
for (const entry of collectGarminWeightEntries(payload, fallbackDay)) {
|
|
1023
|
+
const current = latestByDay.get(entry.date);
|
|
1024
|
+
if (!current || entry.ts >= current.ts)
|
|
1025
|
+
latestByDay.set(entry.date, { kg: entry.kg, ts: entry.ts });
|
|
1026
|
+
}
|
|
1027
|
+
for (const [metricDate, { kg }] of latestByDay) {
|
|
1028
|
+
await upsertDailyMetric(database, 'garmin', metricDate, 'weight_kg', kg, null, 'kg', rawHash);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
971
1031
|
async function importNutrition(database, provider, date, remoteId, payload, rawHash) {
|
|
972
1032
|
const nutritionDate = datePart(date) ?? datePart(firstString(payload, ['id', 'date', 'calendarDate'])) ?? null;
|
|
973
1033
|
if (!nutritionDate)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { createInterface } from 'node:readline/promises';
|
|
3
3
|
import { stdin as input, stdout as output } from 'node:process';
|
|
4
4
|
import { Command } from 'commander';
|
|
5
|
-
import { addAthlete, athleteStorePaths, CATENCE_RUNTIME_VERSION, CatenceDatabase, channelForVersion, createDemoStore, dataStatus, defaultCatalogHome, disconnectStravaAccount, fetchNpmDistTags, fetchPypiReleases, hasCommand, importStagedFile, initializeCatalog, isGlobalNpmInstall, linkActivity, loadCatalog, npmGlobalPrefix, planSelfUpdate, readInstalledConsoleVersion, rebuildRetrievalIndex, reimportNutrition, resolveExtractionErrors, resolveAthlete, resolveCatalogPaths, retryDataSync, runSelfUpdateCommand, setAthleteSecret, syncData, syncProgress, connectStrava, connectStravaWithCallback, unlinkActivity, } from '../../runtime/index.js';
|
|
5
|
+
import { addAthlete, athleteStorePaths, CATENCE_RUNTIME_VERSION, CatenceDatabase, channelForVersion, createDemoStore, dataStatus, defaultCatalogHome, disconnectStravaAccount, fetchNpmDistTags, fetchPypiReleases, hasCommand, importStagedFile, initializeCatalog, isGlobalNpmInstall, linkActivity, loadCatalog, npmGlobalPrefix, planSelfUpdate, readInstalledConsoleVersion, rebuildRetrievalIndex, reimportNutrition, reimportWeight, resolveExtractionErrors, resolveAthlete, resolveCatalogPaths, retryDataSync, runSelfUpdateCommand, setAthleteSecret, syncData, syncProgress, connectStrava, connectStravaWithCallback, unlinkActivity, } from '../../runtime/index.js';
|
|
6
6
|
const program = new Command()
|
|
7
7
|
.name('catence-data')
|
|
8
8
|
.description('Athlete-scoped Catence data setup, extraction, and normalization.')
|
|
@@ -256,6 +256,10 @@ program.command('reimport-nutrition').description('Re-run normalization over cap
|
|
|
256
256
|
const { paths } = await currentAthlete();
|
|
257
257
|
process.stdout.write(`${JSON.stringify(await reimportNutrition(paths), null, 2)}\n`);
|
|
258
258
|
});
|
|
259
|
+
program.command('reimport-weight').description('Backfill weight_kg from captured Garmin weigh-in entities without contacting providers.').action(async () => {
|
|
260
|
+
const { paths } = await currentAthlete();
|
|
261
|
+
process.stdout.write(`${JSON.stringify(await reimportWeight(paths), null, 2)}\n`);
|
|
262
|
+
});
|
|
259
263
|
program
|
|
260
264
|
.command('import')
|
|
261
265
|
.description('Import a staged JSONL file (for example a swim-length backfill) into one athlete’s data store.')
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -29,7 +29,7 @@ export { WELLNESS_METRICS, WellnessService } from '../core/query/wellness.js';
|
|
|
29
29
|
export { jsonSafe, ReadOnlyRepository } from '../core/query/repository.js';
|
|
30
30
|
export { queryReadOnlyData } from '../core/query/sql-guard.js';
|
|
31
31
|
export { createDemoStore, demoStoreMetadata } from '../elt/application/demo.js';
|
|
32
|
-
export { connectStrava, connectStravaWithCallback, dataStatus, disconnectStravaAccount, initializeDataStore, importStagedFile, linkActivity, rebuildRetrievalIndex, reimportNutrition, resolveExtractionErrors, retryDataSync, syncData, syncProgress, unlinkActivity, } from '../elt/application/management.js';
|
|
32
|
+
export { connectStrava, connectStravaWithCallback, dataStatus, disconnectStravaAccount, initializeDataStore, importStagedFile, linkActivity, rebuildRetrievalIndex, reimportNutrition, reimportWeight, resolveExtractionErrors, retryDataSync, syncData, syncProgress, unlinkActivity, } from '../elt/application/management.js';
|
|
33
33
|
export type { ProviderChoice } from '../elt/application/management.js';
|
|
34
34
|
export { DETACHED_SYNC_PROVIDERS, DetachedSyncBusyError, startDetachedSync, } from '../elt/application/detached-sync.js';
|
|
35
35
|
export type { DetachedSyncHandle, DetachedSyncProvider, DetachedSyncRequest, DetachedSyncSpawner } from '../elt/application/detached-sync.js';
|
package/dist/runtime/index.js
CHANGED
|
@@ -16,7 +16,7 @@ export { WELLNESS_METRICS, WellnessService } from '../core/query/wellness.js';
|
|
|
16
16
|
export { jsonSafe, ReadOnlyRepository } from '../core/query/repository.js';
|
|
17
17
|
export { queryReadOnlyData } from '../core/query/sql-guard.js';
|
|
18
18
|
export { createDemoStore, demoStoreMetadata } from '../elt/application/demo.js';
|
|
19
|
-
export { connectStrava, connectStravaWithCallback, dataStatus, disconnectStravaAccount, initializeDataStore, importStagedFile, linkActivity, rebuildRetrievalIndex, reimportNutrition, resolveExtractionErrors, retryDataSync, syncData, syncProgress, unlinkActivity, } from '../elt/application/management.js';
|
|
19
|
+
export { connectStrava, connectStravaWithCallback, dataStatus, disconnectStravaAccount, initializeDataStore, importStagedFile, linkActivity, rebuildRetrievalIndex, reimportNutrition, reimportWeight, resolveExtractionErrors, retryDataSync, syncData, syncProgress, unlinkActivity, } from '../elt/application/management.js';
|
|
20
20
|
export { DETACHED_SYNC_PROVIDERS, DetachedSyncBusyError, startDetachedSync, } from '../elt/application/detached-sync.js';
|
|
21
21
|
export { buildOpenCodeGoConsoleProfiles, classifyOpenCodeGoModel, fetchOpenCodeGoModelIds, mergeOpenCodeGoConsoleProfiles, OPENCODE_GO_API_BASE_ENV, OPENCODE_GO_API_KEY_ENV, OPENCODE_GO_DEFAULT_BASE_URL, OPENCODE_GO_MESSAGES_API_BASE_ENV, } from '../core/runtime/opencode-go.js';
|
|
22
22
|
export { CatenceDatabase, openReadOnlyRepository, ReadOnlyDatabaseError } from '../elt/storage/database.js';
|
package/package.json
CHANGED