@opendatalabs/personal-server-ts-core 1.33.8 → 1.34.0
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/additions/budget.d.ts +28 -0
- package/dist/additions/budget.d.ts.map +1 -0
- package/dist/additions/budget.js +119 -0
- package/dist/additions/budget.js.map +1 -0
- package/dist/additions/durable.d.ts +64 -0
- package/dist/additions/durable.d.ts.map +1 -0
- package/dist/additions/durable.js +591 -0
- package/dist/additions/durable.js.map +1 -0
- package/dist/additions/first-added.d.ts +32 -48
- package/dist/additions/first-added.d.ts.map +1 -1
- package/dist/additions/first-added.js +68 -123
- package/dist/additions/first-added.js.map +1 -1
- package/dist/additions/index.d.ts +3 -0
- package/dist/additions/index.d.ts.map +1 -1
- package/dist/additions/index.js +3 -0
- package/dist/additions/index.js.map +1 -1
- package/dist/additions/ledger-store.js +1 -1
- package/dist/additions/record-rules.d.ts.map +1 -1
- package/dist/additions/record-rules.js +7 -0
- package/dist/additions/record-rules.js.map +1 -1
- package/dist/additions/summary.d.ts +88 -1
- package/dist/additions/summary.d.ts.map +1 -1
- package/dist/additions/summary.js +173 -8
- package/dist/additions/summary.js.map +1 -1
- package/dist/additions/version-inventory.d.ts +80 -0
- package/dist/additions/version-inventory.d.ts.map +1 -0
- package/dist/additions/version-inventory.js +232 -0
- package/dist/additions/version-inventory.js.map +1 -0
- package/dist/api/index.d.ts +70 -2
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +506 -79
- package/dist/api/index.js.map +1 -1
- package/dist/auth/index.d.ts +1 -1
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +1 -1
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/request.d.ts +6 -0
- package/dist/auth/request.d.ts.map +1 -1
- package/dist/auth/request.js +66 -4
- package/dist/auth/request.js.map +1 -1
- package/dist/contracts/data.d.ts +54 -8
- package/dist/contracts/data.d.ts.map +1 -1
- package/dist/contracts/data.js +178 -35
- package/dist/contracts/data.js.map +1 -1
- package/dist/contracts/index.d.ts +1 -1
- package/dist/contracts/index.d.ts.map +1 -1
- package/dist/derivatives/registration.d.ts +0 -5
- package/dist/derivatives/registration.d.ts.map +1 -1
- package/dist/derivatives/registration.js +2 -1
- package/dist/derivatives/registration.js.map +1 -1
- package/dist/errors/pdpp-catalog.d.ts +36 -0
- package/dist/errors/pdpp-catalog.d.ts.map +1 -0
- package/dist/errors/pdpp-catalog.js +86 -0
- package/dist/errors/pdpp-catalog.js.map +1 -0
- package/dist/grants/index.d.ts +2 -0
- package/dist/grants/index.d.ts.map +1 -1
- package/dist/grants/index.js +4 -0
- package/dist/grants/index.js.map +1 -1
- package/dist/grants/pdpp-binding-store.d.ts +89 -0
- package/dist/grants/pdpp-binding-store.d.ts.map +1 -0
- package/dist/grants/pdpp-binding-store.js +112 -0
- package/dist/grants/pdpp-binding-store.js.map +1 -0
- package/dist/grants/pdpp-binding.d.ts +169 -0
- package/dist/grants/pdpp-binding.d.ts.map +1 -0
- package/dist/grants/pdpp-binding.js +236 -0
- package/dist/grants/pdpp-binding.js.map +1 -0
- package/dist/legacy-projection/bindings.d.ts +28 -1
- package/dist/legacy-projection/bindings.d.ts.map +1 -1
- package/dist/legacy-projection/bindings.js +622 -189
- package/dist/legacy-projection/bindings.js.map +1 -1
- package/dist/legacy-projection/claude-source-record.d.ts +22 -0
- package/dist/legacy-projection/claude-source-record.d.ts.map +1 -0
- package/dist/legacy-projection/claude-source-record.js +360 -0
- package/dist/legacy-projection/claude-source-record.js.map +1 -0
- package/dist/legacy-projection/declared-text.d.ts +41 -0
- package/dist/legacy-projection/declared-text.d.ts.map +1 -0
- package/dist/legacy-projection/declared-text.js +199 -0
- package/dist/legacy-projection/declared-text.js.map +1 -0
- package/dist/legacy-projection/github-browser-bindings.d.ts.map +1 -1
- package/dist/legacy-projection/github-browser-bindings.js +89 -5
- package/dist/legacy-projection/github-browser-bindings.js.map +1 -1
- package/dist/legacy-projection/github-legacy-fields.d.ts +6 -0
- package/dist/legacy-projection/github-legacy-fields.d.ts.map +1 -0
- package/dist/legacy-projection/github-legacy-fields.js +202 -0
- package/dist/legacy-projection/github-legacy-fields.js.map +1 -0
- package/dist/legacy-projection/index.d.ts +3 -1
- package/dist/legacy-projection/index.d.ts.map +1 -1
- package/dist/legacy-projection/index.js +49 -1
- package/dist/legacy-projection/index.js.map +1 -1
- package/dist/legacy-projection/oura-browser-bindings.d.ts.map +1 -1
- package/dist/legacy-projection/oura-browser-bindings.js +43 -41
- package/dist/legacy-projection/oura-browser-bindings.js.map +1 -1
- package/dist/legacy-projection/types.d.ts +38 -2
- package/dist/legacy-projection/types.d.ts.map +1 -1
- package/dist/mcp/resources.d.ts.map +1 -1
- package/dist/mcp/resources.js +3 -0
- package/dist/mcp/resources.js.map +1 -1
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +11 -2
- package/dist/mcp/tools.js.map +1 -1
- package/dist/pdpp/approval.d.ts +271 -0
- package/dist/pdpp/approval.d.ts.map +1 -0
- package/dist/pdpp/approval.js +467 -0
- package/dist/pdpp/approval.js.map +1 -0
- package/dist/pdpp/client-identity.d.ts +88 -0
- package/dist/pdpp/client-identity.d.ts.map +1 -0
- package/dist/pdpp/client-identity.js +196 -0
- package/dist/pdpp/client-identity.js.map +1 -0
- package/dist/pdpp/client-metadata.d.ts +95 -0
- package/dist/pdpp/client-metadata.d.ts.map +1 -0
- package/dist/pdpp/client-metadata.js +96 -0
- package/dist/pdpp/client-metadata.js.map +1 -0
- package/dist/pdpp/commitments.d.ts +75 -0
- package/dist/pdpp/commitments.d.ts.map +1 -0
- package/dist/pdpp/commitments.js +113 -0
- package/dist/pdpp/commitments.js.map +1 -0
- package/dist/pdpp/declaration.d.ts +118 -0
- package/dist/pdpp/declaration.d.ts.map +1 -0
- package/dist/pdpp/declaration.js +836 -0
- package/dist/pdpp/declaration.js.map +1 -0
- package/dist/pdpp/index.d.ts +37 -0
- package/dist/pdpp/index.d.ts.map +1 -0
- package/dist/pdpp/index.js +23 -0
- package/dist/pdpp/index.js.map +1 -0
- package/dist/pdpp/issuance.d.ts +126 -0
- package/dist/pdpp/issuance.d.ts.map +1 -0
- package/dist/pdpp/issuance.js +180 -0
- package/dist/pdpp/issuance.js.map +1 -0
- package/dist/pdpp/pkce.d.ts +53 -0
- package/dist/pdpp/pkce.d.ts.map +1 -0
- package/dist/pdpp/pkce.js +109 -0
- package/dist/pdpp/pkce.js.map +1 -0
- package/dist/pdpp/redirect.d.ts +60 -0
- package/dist/pdpp/redirect.d.ts.map +1 -0
- package/dist/pdpp/redirect.js +125 -0
- package/dist/pdpp/redirect.js.map +1 -0
- package/dist/pdpp/resolve.d.ts +109 -0
- package/dist/pdpp/resolve.d.ts.map +1 -0
- package/dist/pdpp/resolve.js +393 -0
- package/dist/pdpp/resolve.js.map +1 -0
- package/dist/pdpp/review.d.ts +280 -0
- package/dist/pdpp/review.d.ts.map +1 -0
- package/dist/pdpp/review.js +205 -0
- package/dist/pdpp/review.js.map +1 -0
- package/dist/pdpp/selection.d.ts +50 -0
- package/dist/pdpp/selection.d.ts.map +1 -0
- package/dist/pdpp/selection.js +442 -0
- package/dist/pdpp/selection.js.map +1 -0
- package/dist/pdpp/store.d.ts +228 -0
- package/dist/pdpp/store.d.ts.map +1 -0
- package/dist/pdpp/store.js +542 -0
- package/dist/pdpp/store.js.map +1 -0
- package/dist/pdpp/tokens.d.ts +181 -0
- package/dist/pdpp/tokens.d.ts.map +1 -0
- package/dist/pdpp/tokens.js +419 -0
- package/dist/pdpp/tokens.js.map +1 -0
- package/dist/pdpp/types.d.ts +441 -0
- package/dist/pdpp/types.d.ts.map +1 -0
- package/dist/pdpp/types.js +61 -0
- package/dist/pdpp/types.js.map +1 -0
- package/dist/pdpp-version.d.ts +28 -0
- package/dist/pdpp-version.d.ts.map +1 -0
- package/dist/pdpp-version.js +28 -0
- package/dist/pdpp-version.js.map +1 -0
- package/dist/policy/data-read.d.ts +24 -0
- package/dist/policy/data-read.d.ts.map +1 -1
- package/dist/policy/data-read.js +60 -3
- package/dist/policy/data-read.js.map +1 -1
- package/dist/policy/data-write.d.ts.map +1 -1
- package/dist/policy/data-write.js +3 -0
- package/dist/policy/data-write.js.map +1 -1
- package/dist/policy/index.d.ts +1 -0
- package/dist/policy/index.d.ts.map +1 -1
- package/dist/policy/index.js +1 -0
- package/dist/policy/index.js.map +1 -1
- package/dist/policy/reserved-scopes.d.ts +3 -0
- package/dist/policy/reserved-scopes.d.ts.map +1 -0
- package/dist/policy/reserved-scopes.js +5 -0
- package/dist/policy/reserved-scopes.js.map +1 -0
- package/dist/ports/index.d.ts +63 -0
- package/dist/ports/index.d.ts.map +1 -1
- package/dist/ports/index.js.map +1 -1
- package/dist/ports/pdpp-auth.d.ts +129 -0
- package/dist/ports/pdpp-auth.d.ts.map +1 -0
- package/dist/ports/pdpp-auth.js +60 -0
- package/dist/ports/pdpp-auth.js.map +1 -0
- package/dist/ports/pdpp-auth.test-utils.d.ts +9 -0
- package/dist/ports/pdpp-auth.test-utils.d.ts.map +1 -0
- package/dist/ports/pdpp-auth.test-utils.js +18 -0
- package/dist/ports/pdpp-auth.test-utils.js.map +1 -0
- package/dist/schemas/server-config.d.ts +44 -0
- package/dist/schemas/server-config.d.ts.map +1 -1
- package/dist/schemas/server-config.js +91 -0
- package/dist/schemas/server-config.js.map +1 -1
- package/dist/signing/request-signer.d.ts +2 -0
- package/dist/signing/request-signer.d.ts.map +1 -1
- package/dist/signing/request-signer.js +1 -0
- package/dist/signing/request-signer.js.map +1 -1
- package/dist/storage/adapters/interface.d.ts +3 -0
- package/dist/storage/adapters/interface.d.ts.map +1 -1
- package/dist/storage/adapters/vana-audience.d.ts +12 -0
- package/dist/storage/adapters/vana-audience.d.ts.map +1 -0
- package/dist/storage/adapters/vana-audience.js +72 -0
- package/dist/storage/adapters/vana-audience.js.map +1 -0
- package/dist/storage/adapters/vana.d.ts +2 -0
- package/dist/storage/adapters/vana.d.ts.map +1 -1
- package/dist/storage/adapters/vana.js +53 -20
- package/dist/storage/adapters/vana.js.map +1 -1
- package/dist/storage/canonical-legacy-projection.d.ts +262 -0
- package/dist/storage/canonical-legacy-projection.d.ts.map +1 -0
- package/dist/storage/canonical-legacy-projection.js +761 -0
- package/dist/storage/canonical-legacy-projection.js.map +1 -0
- package/dist/storage/index/manager.d.ts +29 -2
- package/dist/storage/index/manager.d.ts.map +1 -1
- package/dist/storage/index/schema.d.ts +1 -1
- package/dist/storage/index/schema.js +1 -1
- package/dist/storage/index/types.d.ts +7 -1
- package/dist/storage/index/types.d.ts.map +1 -1
- package/dist/storage/legacy-projection.d.ts +4 -3
- package/dist/storage/legacy-projection.d.ts.map +1 -1
- package/dist/storage/legacy-projection.js +145 -42
- package/dist/storage/legacy-projection.js.map +1 -1
- package/dist/storage/legacy-scope-coverage-exceptions.d.ts +8 -0
- package/dist/storage/legacy-scope-coverage-exceptions.d.ts.map +1 -0
- package/dist/storage/legacy-scope-coverage-exceptions.js +10 -0
- package/dist/storage/legacy-scope-coverage-exceptions.js.map +1 -0
- package/dist/storage/legacy-scope-coverage.d.ts +53 -0
- package/dist/storage/legacy-scope-coverage.d.ts.map +1 -0
- package/dist/storage/legacy-scope-coverage.js +403 -0
- package/dist/storage/legacy-scope-coverage.js.map +1 -0
- package/dist/storage/legacy-scope-entity-keys.d.ts +4 -0
- package/dist/storage/legacy-scope-entity-keys.d.ts.map +1 -0
- package/dist/storage/legacy-scope-entity-keys.js +92 -0
- package/dist/storage/legacy-scope-entity-keys.js.map +1 -0
- package/dist/storage/legacy-scope-view.d.ts +154 -0
- package/dist/storage/legacy-scope-view.d.ts.map +1 -0
- package/dist/storage/legacy-scope-view.js +735 -0
- package/dist/storage/legacy-scope-view.js.map +1 -0
- package/dist/storage/pdpp-records/completion.d.ts +91 -0
- package/dist/storage/pdpp-records/completion.d.ts.map +1 -0
- package/dist/storage/pdpp-records/completion.js +170 -0
- package/dist/storage/pdpp-records/completion.js.map +1 -0
- package/dist/storage/pdpp-records/cursor.d.ts +41 -0
- package/dist/storage/pdpp-records/cursor.d.ts.map +1 -0
- package/dist/storage/pdpp-records/cursor.js +49 -0
- package/dist/storage/pdpp-records/cursor.js.map +1 -0
- package/dist/storage/pdpp-records/declaration-witness.d.ts +58 -0
- package/dist/storage/pdpp-records/declaration-witness.d.ts.map +1 -0
- package/dist/storage/pdpp-records/declaration-witness.js +230 -0
- package/dist/storage/pdpp-records/declaration-witness.js.map +1 -0
- package/dist/storage/pdpp-records/enforcement.d.ts +52 -0
- package/dist/storage/pdpp-records/enforcement.d.ts.map +1 -0
- package/dist/storage/pdpp-records/enforcement.js +116 -0
- package/dist/storage/pdpp-records/enforcement.js.map +1 -0
- package/dist/storage/pdpp-records/index.d.ts +5 -0
- package/dist/storage/pdpp-records/index.d.ts.map +1 -0
- package/dist/storage/pdpp-records/index.js +5 -0
- package/dist/storage/pdpp-records/index.js.map +1 -0
- package/dist/storage/pdpp-records/ingest-plan.d.ts +39 -0
- package/dist/storage/pdpp-records/ingest-plan.d.ts.map +1 -0
- package/dist/storage/pdpp-records/ingest-plan.js +103 -0
- package/dist/storage/pdpp-records/ingest-plan.js.map +1 -0
- package/dist/storage/pdpp-records/interface.d.ts +104 -0
- package/dist/storage/pdpp-records/interface.d.ts.map +1 -0
- package/dist/storage/pdpp-records/interface.js +2 -0
- package/dist/storage/pdpp-records/interface.js.map +1 -0
- package/dist/storage/pdpp-records/memory-store.d.ts +9 -0
- package/dist/storage/pdpp-records/memory-store.d.ts.map +1 -0
- package/dist/storage/pdpp-records/memory-store.js +466 -0
- package/dist/storage/pdpp-records/memory-store.js.map +1 -0
- package/dist/storage/pdpp-records/portable.d.ts +9 -0
- package/dist/storage/pdpp-records/portable.d.ts.map +1 -0
- package/dist/storage/pdpp-records/portable.js +8 -0
- package/dist/storage/pdpp-records/portable.js.map +1 -0
- package/dist/storage/pdpp-records/read-port.d.ts +44 -0
- package/dist/storage/pdpp-records/read-port.d.ts.map +1 -0
- package/dist/storage/pdpp-records/read-port.js +22 -0
- package/dist/storage/pdpp-records/read-port.js.map +1 -0
- package/dist/storage/pdpp-records/record-key.d.ts +52 -0
- package/dist/storage/pdpp-records/record-key.d.ts.map +1 -0
- package/dist/storage/pdpp-records/record-key.js +97 -0
- package/dist/storage/pdpp-records/record-key.js.map +1 -0
- package/dist/storage/pdpp-records/schema.d.ts +41 -0
- package/dist/storage/pdpp-records/schema.d.ts.map +1 -0
- package/dist/storage/pdpp-records/schema.js +153 -0
- package/dist/storage/pdpp-records/schema.js.map +1 -0
- package/dist/storage/pdpp-records/stream-declaration.d.ts +88 -0
- package/dist/storage/pdpp-records/stream-declaration.d.ts.map +1 -0
- package/dist/storage/pdpp-records/stream-declaration.js +57 -0
- package/dist/storage/pdpp-records/stream-declaration.js.map +1 -0
- package/dist/storage/pdpp-records/types.d.ts +138 -0
- package/dist/storage/pdpp-records/types.d.ts.map +1 -0
- package/dist/storage/pdpp-records/types.js +38 -0
- package/dist/storage/pdpp-records/types.js.map +1 -0
- package/dist/sync/data-point-feed.d.ts +3 -0
- package/dist/sync/data-point-feed.d.ts.map +1 -1
- package/dist/sync/data-point-feed.js +17 -1
- package/dist/sync/data-point-feed.js.map +1 -1
- package/dist/sync/delete-data-port.d.ts +2 -0
- package/dist/sync/delete-data-port.d.ts.map +1 -1
- package/dist/sync/delete-data-port.js +1 -1
- package/dist/sync/delete-data-port.js.map +1 -1
- package/dist/sync/engine/sync-manager.d.ts.map +1 -1
- package/dist/sync/engine/sync-manager.js +5 -2
- package/dist/sync/engine/sync-manager.js.map +1 -1
- package/dist/sync/index.d.ts +3 -1
- package/dist/sync/index.d.ts.map +1 -1
- package/dist/sync/index.js +4 -0
- package/dist/sync/index.js.map +1 -1
- package/dist/sync/pdpp-import.d.ts +192 -0
- package/dist/sync/pdpp-import.d.ts.map +1 -0
- package/dist/sync/pdpp-import.js +441 -0
- package/dist/sync/pdpp-import.js.map +1 -0
- package/dist/sync/scope-deletions.d.ts +10 -0
- package/dist/sync/scope-deletions.d.ts.map +1 -1
- package/dist/sync/scope-deletions.js +40 -7
- package/dist/sync/scope-deletions.js.map +1 -1
- package/dist/sync/workers/download.d.ts +14 -0
- package/dist/sync/workers/download.d.ts.map +1 -1
- package/dist/sync/workers/download.js +149 -13
- package/dist/sync/workers/download.js.map +1 -1
- package/dist/sync/workers/upload.d.ts +2 -0
- package/dist/sync/workers/upload.d.ts.map +1 -1
- package/dist/sync/workers/upload.js +7 -0
- package/dist/sync/workers/upload.js.map +1 -1
- package/dist/test-utils/memory-storage.d.ts +1 -1
- package/dist/test-utils/memory-storage.d.ts.map +1 -1
- package/dist/test-utils/memory-storage.js +11 -5
- package/dist/test-utils/memory-storage.js.map +1 -1
- package/package.json +54 -4
|
@@ -32,6 +32,58 @@ function ifDefined(record, sourceField, targetField) {
|
|
|
32
32
|
? {}
|
|
33
33
|
: { [targetField]: record[sourceField] };
|
|
34
34
|
}
|
|
35
|
+
export function ouraInteger(value) {
|
|
36
|
+
return typeof value === "number" && Number.isFinite(value)
|
|
37
|
+
? Math.round(value)
|
|
38
|
+
: value;
|
|
39
|
+
}
|
|
40
|
+
function ifDefinedOuraInteger(record, sourceField, targetField) {
|
|
41
|
+
return record[sourceField] === undefined
|
|
42
|
+
? {}
|
|
43
|
+
: { [targetField]: ouraInteger(record[sourceField]) };
|
|
44
|
+
}
|
|
45
|
+
export function ouraContributors(value, integerFields) {
|
|
46
|
+
if (!isObjectRow(value))
|
|
47
|
+
return undefined;
|
|
48
|
+
const contributors = { ...value };
|
|
49
|
+
for (const field of integerFields) {
|
|
50
|
+
if (Object.hasOwn(contributors, field)) {
|
|
51
|
+
contributors[field] = ouraInteger(contributors[field]);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return contributors;
|
|
55
|
+
}
|
|
56
|
+
export function withOuraContributors(value, integerFields) {
|
|
57
|
+
const contributors = ouraContributors(value, integerFields);
|
|
58
|
+
return contributors ? { contributors } : {};
|
|
59
|
+
}
|
|
60
|
+
const OURA_ACTIVITY_CONTRIBUTOR_INTEGERS = [
|
|
61
|
+
"meet_daily_targets",
|
|
62
|
+
"move_every_hour",
|
|
63
|
+
"recovery_time",
|
|
64
|
+
"stay_active",
|
|
65
|
+
"training_frequency",
|
|
66
|
+
"training_volume",
|
|
67
|
+
];
|
|
68
|
+
const OURA_READINESS_CONTRIBUTOR_INTEGERS = [
|
|
69
|
+
"activity_balance",
|
|
70
|
+
"body_temperature",
|
|
71
|
+
"hrv_balance",
|
|
72
|
+
"previous_day_activity",
|
|
73
|
+
"previous_night",
|
|
74
|
+
"recovery_index",
|
|
75
|
+
"resting_heart_rate",
|
|
76
|
+
"sleep_balance",
|
|
77
|
+
];
|
|
78
|
+
export const OURA_SLEEP_CONTRIBUTOR_INTEGERS = [
|
|
79
|
+
"deep_sleep",
|
|
80
|
+
"efficiency",
|
|
81
|
+
"latency",
|
|
82
|
+
"rem_sleep",
|
|
83
|
+
"restfulness",
|
|
84
|
+
"timing",
|
|
85
|
+
"total_sleep",
|
|
86
|
+
];
|
|
35
87
|
function missingStream(scope, expectedStream) {
|
|
36
88
|
return {
|
|
37
89
|
ok: false,
|
|
@@ -56,6 +108,85 @@ function missingStream(scope, expectedStream) {
|
|
|
56
108
|
function projectionTime(options) {
|
|
57
109
|
return options.now ?? new Date().toISOString();
|
|
58
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* Counts source rows a binding leaves out and turns them into one
|
|
113
|
+
* `records_dropped` diagnostic per (stream, reason). Each row is counted
|
|
114
|
+
* once, under the first reason it failed, so counts sum to distinct rows.
|
|
115
|
+
*/
|
|
116
|
+
class DroppedRows {
|
|
117
|
+
scope;
|
|
118
|
+
counts = new Map();
|
|
119
|
+
constructor(scope) {
|
|
120
|
+
this.scope = scope;
|
|
121
|
+
}
|
|
122
|
+
add(stream, reason, count = 1) {
|
|
123
|
+
if (count <= 0)
|
|
124
|
+
return;
|
|
125
|
+
const key = JSON.stringify([stream, reason]);
|
|
126
|
+
this.counts.set(key, (this.counts.get(key) ?? 0) + count);
|
|
127
|
+
}
|
|
128
|
+
diagnostics() {
|
|
129
|
+
return [...this.counts].map(([key, count]) => {
|
|
130
|
+
const [stream, reason] = JSON.parse(key);
|
|
131
|
+
return {
|
|
132
|
+
kind: "records_dropped",
|
|
133
|
+
scope: this.scope,
|
|
134
|
+
stream,
|
|
135
|
+
count,
|
|
136
|
+
reasons: [reason],
|
|
137
|
+
};
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/** `{ diagnostics }` when there are any, else nothing, for spreading into a result. */
|
|
142
|
+
function withDiagnostics(diagnostics) {
|
|
143
|
+
return diagnostics.length > 0 ? { diagnostics } : {};
|
|
144
|
+
}
|
|
145
|
+
export const OURA_BAD_DAY = "Oura day is missing or not a YYYY-MM-DD date";
|
|
146
|
+
export const OURA_BAD_SCORE = "Oura score is absent or does not round to an integer from 0 to 100";
|
|
147
|
+
/** A real calendar date in the legacy `format: date` shape. */
|
|
148
|
+
export function isCalendarDay(value) {
|
|
149
|
+
if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
const parsed = new Date(`${value}T00:00:00.000Z`);
|
|
153
|
+
return (!Number.isNaN(parsed.getTime()) && parsed.toISOString().startsWith(value));
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* The legacy Oura daily schemas require `score`, typed integer 0-100 or null.
|
|
157
|
+
* OpenPet decodes it as `*int`, so null is valid while fractional numbers are
|
|
158
|
+
* not. Rounding happens at the projection boundary.
|
|
159
|
+
*/
|
|
160
|
+
function isOuraScore(value) {
|
|
161
|
+
if (value === null)
|
|
162
|
+
return true;
|
|
163
|
+
return (typeof value === "number" &&
|
|
164
|
+
Number.isFinite(value) &&
|
|
165
|
+
Math.round(value) >= 0 &&
|
|
166
|
+
Math.round(value) <= 100);
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Oura rows the legacy schema cannot hold are left out and counted, never
|
|
170
|
+
* emitted with an impossible day or without the required score. A null score
|
|
171
|
+
* is valid under the frozen legacy schema.
|
|
172
|
+
* `scoreField` names the source field that becomes the legacy `score`
|
|
173
|
+
* (per row, or undefined for a row that produces no daily score). The
|
|
174
|
+
* caller decides what an all-dropped stream means.
|
|
175
|
+
*/
|
|
176
|
+
export function partitionOuraRows(scope, stream, rows, scoreField) {
|
|
177
|
+
const dropped = new DroppedRows(scope);
|
|
178
|
+
const valid = rows.filter((row) => {
|
|
179
|
+
const field = scoreField(row);
|
|
180
|
+
if (!isCalendarDay(row.day))
|
|
181
|
+
dropped.add(stream, OURA_BAD_DAY);
|
|
182
|
+
else if (field !== undefined && !isOuraScore(row[field]))
|
|
183
|
+
dropped.add(stream, OURA_BAD_SCORE);
|
|
184
|
+
else
|
|
185
|
+
return true;
|
|
186
|
+
return false;
|
|
187
|
+
});
|
|
188
|
+
return { rows: valid, diagnostics: dropped.diagnostics() };
|
|
189
|
+
}
|
|
59
190
|
function requireStreams(scope, options, expectedStreams) {
|
|
60
191
|
for (const stream of expectedStreams) {
|
|
61
192
|
if (!options.fetchedStreams.includes(stream)) {
|
|
@@ -189,6 +320,7 @@ const youtubeSubscriptions = {
|
|
|
189
320
|
"handle",
|
|
190
321
|
"avatar_url",
|
|
191
322
|
"subscriber_count",
|
|
323
|
+
"subscriber_count_text",
|
|
192
324
|
"description",
|
|
193
325
|
"is_verified",
|
|
194
326
|
"notifications",
|
|
@@ -197,6 +329,7 @@ const youtubeSubscriptions = {
|
|
|
197
329
|
primaryKey: { subscriptions: ["id"] },
|
|
198
330
|
lossy: [
|
|
199
331
|
"Rows without legacy-required channelTitle/channelUrl or required notification booleans are skipped and reported.",
|
|
332
|
+
"subscriberCountText is null when the page rendered no count text; the connector omits subscriber_count_text then, and legacy's own default is null.",
|
|
200
333
|
],
|
|
201
334
|
provenance: youtubeProvenance,
|
|
202
335
|
project(records, options) {
|
|
@@ -232,7 +365,7 @@ const youtubeSubscriptions = {
|
|
|
232
365
|
handle: youtubeStringOrNull(row.handle),
|
|
233
366
|
channelId: youtubeStringOrNull(row.channel_id),
|
|
234
367
|
avatarUrl: youtubeStringOrNull(row.avatar_url),
|
|
235
|
-
subscriberCountText: null,
|
|
368
|
+
subscriberCountText: youtubeRequiredString(row.subscriber_count_text) ?? null,
|
|
236
369
|
subscriberCount: typeof row.subscriber_count === "number"
|
|
237
370
|
? row.subscriber_count
|
|
238
371
|
: null,
|
|
@@ -426,18 +559,20 @@ const youtubeHistory = {
|
|
|
426
559
|
watch_history: [
|
|
427
560
|
"position",
|
|
428
561
|
"watched_date",
|
|
562
|
+
"watched_date_label",
|
|
429
563
|
"video_id",
|
|
430
564
|
"video_url",
|
|
431
565
|
"video_title",
|
|
432
566
|
"channel_title",
|
|
433
567
|
"channel_url",
|
|
434
568
|
"view_count",
|
|
569
|
+
"views_text",
|
|
435
570
|
"description",
|
|
436
571
|
],
|
|
437
572
|
},
|
|
438
573
|
primaryKey: { watch_history: ["id"] },
|
|
439
574
|
lossy: [
|
|
440
|
-
"Preserves page order by position and caps output at 50 entries.
|
|
575
|
+
"Preserves page order by position and caps output at 50 entries. watchedAt carries watched_date (YYYY-MM-DD, null unchanged) at day precision only; no timestamp is inferred. watchedAtText carries watched_date_label (the browser section header as shown, as legacy emitted it); it falls back to watched_date only when the label is absent or empty. views_text is preserved when present; otherwise integer view_count is rendered as '<count> views'. Rows without required videoUrl are skipped and reported. Missing positions fail because page order cannot be preserved.",
|
|
441
576
|
],
|
|
442
577
|
provenance: youtubeProvenance,
|
|
443
578
|
project(records, options) {
|
|
@@ -464,14 +599,17 @@ const youtubeHistory = {
|
|
|
464
599
|
return [];
|
|
465
600
|
}
|
|
466
601
|
const viewCount = typeof row.view_count === "number" ? row.view_count : null;
|
|
602
|
+
const viewsText = youtubeRequiredString(row.views_text);
|
|
467
603
|
return [
|
|
468
604
|
{
|
|
469
|
-
|
|
605
|
+
watchedAt: youtubeStringOrNull(row.watched_date),
|
|
606
|
+
watchedAtText: youtubeRequiredString(row.watched_date_label) ??
|
|
607
|
+
youtubeStringOrNull(row.watched_date),
|
|
470
608
|
videoId: youtubeStringOrNull(row.video_id),
|
|
471
609
|
videoUrl,
|
|
472
610
|
videoTitle: youtubeStringOrNull(row.video_title),
|
|
473
611
|
channelTitle: youtubeStringOrNull(row.channel_title),
|
|
474
|
-
views: viewCount === null ? null : `${viewCount} views
|
|
612
|
+
views: viewsText ?? (viewCount === null ? null : `${viewCount} views`),
|
|
475
613
|
description: youtubeStringOrNull(row.description),
|
|
476
614
|
_position: row.position,
|
|
477
615
|
},
|
|
@@ -588,15 +726,14 @@ const instagramProfile = {
|
|
|
588
726
|
"external_url",
|
|
589
727
|
"follower_count",
|
|
590
728
|
"following_count",
|
|
729
|
+
"post_count",
|
|
591
730
|
"is_private",
|
|
592
731
|
"is_verified",
|
|
593
732
|
"is_business",
|
|
594
733
|
],
|
|
595
734
|
},
|
|
596
735
|
primaryKey: { profile: ["id"] },
|
|
597
|
-
lossy: [
|
|
598
|
-
"media_count (no PDPP equivalent; post_count is a different concept — Instagram's media_count includes non-post media types the posts stream does not cover)",
|
|
599
|
-
],
|
|
736
|
+
lossy: [],
|
|
600
737
|
provenance: [
|
|
601
738
|
{
|
|
602
739
|
kind: "oci",
|
|
@@ -646,21 +783,21 @@ const instagramProfile = {
|
|
|
646
783
|
const payload = {
|
|
647
784
|
username,
|
|
648
785
|
full_name: fullName,
|
|
786
|
+
bio: typeof p.bio === "string" ? p.bio : "",
|
|
649
787
|
};
|
|
650
|
-
if (typeof p.bio === "string")
|
|
651
|
-
payload.bio = p.bio;
|
|
652
788
|
if (typeof p.profile_pic_url === "string") {
|
|
653
789
|
payload.profile_pic_url = p.profile_pic_url;
|
|
654
790
|
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
}
|
|
791
|
+
payload.external_url =
|
|
792
|
+
typeof p.external_url === "string" ? p.external_url : "";
|
|
658
793
|
if (typeof p.follower_count === "number") {
|
|
659
794
|
payload.follower_count = p.follower_count;
|
|
660
795
|
}
|
|
661
796
|
if (typeof p.following_count === "number") {
|
|
662
797
|
payload.following_count = p.following_count;
|
|
663
798
|
}
|
|
799
|
+
if (typeof p.post_count === "number")
|
|
800
|
+
payload.media_count = p.post_count;
|
|
664
801
|
if (typeof p.is_private === "boolean")
|
|
665
802
|
payload.is_private = p.is_private;
|
|
666
803
|
if (typeof p.is_verified === "boolean")
|
|
@@ -693,6 +830,7 @@ const instagramPosts = {
|
|
|
693
830
|
"media_url is only available for images and the first image in a carousel; video posts and later carousel images are absent from the artifact's parser output",
|
|
694
831
|
"who_liked.profile_pic_url is omitted because the post_likes stream does not declare it",
|
|
695
832
|
"who_liked.pk and who_liked.id both use post_likes.user_id because the source exposes one liker identity field",
|
|
833
|
+
"likers without post_id, user_id, or username, or without a matching post, are dropped and counted",
|
|
696
834
|
],
|
|
697
835
|
provenance: [
|
|
698
836
|
{
|
|
@@ -711,11 +849,18 @@ const instagramPosts = {
|
|
|
711
849
|
return guard;
|
|
712
850
|
}
|
|
713
851
|
const sourcePosts = byStream(records, "posts");
|
|
852
|
+
const postIds = new Set(sourcePosts.map((post) => post.id));
|
|
853
|
+
const dropped = new DroppedRows("instagram.posts");
|
|
714
854
|
const likesByPost = new Map();
|
|
715
855
|
for (const liker of byStream(records, "post_likes")) {
|
|
716
856
|
if (typeof liker.post_id !== "string" ||
|
|
717
857
|
typeof liker.user_id !== "string" ||
|
|
718
858
|
typeof liker.username !== "string") {
|
|
859
|
+
dropped.add("post_likes", "Liker lacks post_id, user_id, or username");
|
|
860
|
+
continue;
|
|
861
|
+
}
|
|
862
|
+
if (!postIds.has(liker.post_id)) {
|
|
863
|
+
dropped.add("post_likes", "Liker has no matching post");
|
|
719
864
|
continue;
|
|
720
865
|
}
|
|
721
866
|
const whoLiked = likesByPost.get(liker.post_id) ?? [];
|
|
@@ -744,6 +889,7 @@ const instagramPosts = {
|
|
|
744
889
|
return {
|
|
745
890
|
ok: true,
|
|
746
891
|
payload: { posts },
|
|
892
|
+
...withDiagnostics(dropped.diagnostics()),
|
|
747
893
|
};
|
|
748
894
|
},
|
|
749
895
|
};
|
|
@@ -768,7 +914,7 @@ const instagramFollowing = {
|
|
|
768
914
|
},
|
|
769
915
|
primaryKey: { following: ["id"] },
|
|
770
916
|
lossy: [
|
|
771
|
-
"rows without a non-empty string username are
|
|
917
|
+
"rows without a non-empty string username are dropped and counted because the legacy account schema requires username",
|
|
772
918
|
],
|
|
773
919
|
provenance: [
|
|
774
920
|
{
|
|
@@ -782,9 +928,12 @@ const instagramFollowing = {
|
|
|
782
928
|
const guard = requireStreams("instagram.following", options, ["following"]);
|
|
783
929
|
if (guard)
|
|
784
930
|
return guard;
|
|
931
|
+
const dropped = new DroppedRows("instagram.following");
|
|
785
932
|
const accounts = byStream(records, "following").flatMap((r) => {
|
|
786
|
-
if (typeof r.username !== "string" || !r.username)
|
|
933
|
+
if (typeof r.username !== "string" || !r.username) {
|
|
934
|
+
dropped.add("following", "Account lacks a nonempty username");
|
|
787
935
|
return [];
|
|
936
|
+
}
|
|
788
937
|
const account = {
|
|
789
938
|
username: r.username,
|
|
790
939
|
full_name: typeof r.full_name === "string" ? r.full_name : "",
|
|
@@ -798,7 +947,11 @@ const instagramFollowing = {
|
|
|
798
947
|
account.pk = r.id;
|
|
799
948
|
return [account];
|
|
800
949
|
});
|
|
801
|
-
return {
|
|
950
|
+
return {
|
|
951
|
+
ok: true,
|
|
952
|
+
payload: { accounts, total: accounts.length },
|
|
953
|
+
...withDiagnostics(dropped.diagnostics()),
|
|
954
|
+
};
|
|
802
955
|
},
|
|
803
956
|
};
|
|
804
957
|
const instagramAds = {
|
|
@@ -810,7 +963,7 @@ const instagramAds = {
|
|
|
810
963
|
primaryKey: { ads: ["id"] },
|
|
811
964
|
lossy: [
|
|
812
965
|
"ads rows with a null description omit the optional legacy category description",
|
|
813
|
-
"malformed ads rows without a string name or one of the declaration's three kinds are
|
|
966
|
+
"malformed ads rows without a string name or one of the declaration's three kinds are dropped and counted",
|
|
814
967
|
],
|
|
815
968
|
provenance: [
|
|
816
969
|
{
|
|
@@ -827,9 +980,13 @@ const instagramAds = {
|
|
|
827
980
|
const advertisers = [];
|
|
828
981
|
const ad_topics = [];
|
|
829
982
|
const categories = [];
|
|
983
|
+
const dropped = new DroppedRows("instagram.ads");
|
|
830
984
|
for (const row of byStream(records, "ads")) {
|
|
831
|
-
if (typeof row.name !== "string"
|
|
985
|
+
if (typeof row.name !== "string" ||
|
|
986
|
+
!["advertiser", "ad_topic", "ad_category"].includes(row.kind)) {
|
|
987
|
+
dropped.add("ads", "Ad row lacks a string name or a declared kind");
|
|
832
988
|
continue;
|
|
989
|
+
}
|
|
833
990
|
if (row.kind === "advertiser")
|
|
834
991
|
advertisers.push({ name: row.name });
|
|
835
992
|
if (row.kind === "ad_topic")
|
|
@@ -843,7 +1000,11 @@ const instagramAds = {
|
|
|
843
1000
|
});
|
|
844
1001
|
}
|
|
845
1002
|
}
|
|
846
|
-
return {
|
|
1003
|
+
return {
|
|
1004
|
+
ok: true,
|
|
1005
|
+
payload: { advertisers, ad_topics, categories },
|
|
1006
|
+
...withDiagnostics(dropped.diagnostics()),
|
|
1007
|
+
};
|
|
847
1008
|
},
|
|
848
1009
|
};
|
|
849
1010
|
// ---------------------------------------------------------------------------
|
|
@@ -1014,7 +1175,7 @@ const githubHistory = {
|
|
|
1014
1175
|
"body is truncated to the legacy connector's 500-character limit",
|
|
1015
1176
|
"repoUrl is omitted because no repository URL field is declared on issue or pull request streams",
|
|
1016
1177
|
"fetchedAt is set at projection time because the legacy schema requires it and PDPP history records have no fetch timestamp",
|
|
1017
|
-
"
|
|
1178
|
+
"an authored item without an id or repository name, or an issue by the signed user without a boolean is_pull_request, fails the scope",
|
|
1018
1179
|
],
|
|
1019
1180
|
provenance: [
|
|
1020
1181
|
{
|
|
@@ -1085,6 +1246,16 @@ const githubHistory = {
|
|
|
1085
1246
|
: [];
|
|
1086
1247
|
return item;
|
|
1087
1248
|
};
|
|
1249
|
+
if (sourceIssues.some((r) => r.user_login === login && typeof r.is_pull_request !== "boolean")) {
|
|
1250
|
+
return {
|
|
1251
|
+
ok: false,
|
|
1252
|
+
error: {
|
|
1253
|
+
kind: "incomplete_scope",
|
|
1254
|
+
scope: "github.history",
|
|
1255
|
+
reason: "An issue by the signed user has no boolean is_pull_request, so it cannot be classified",
|
|
1256
|
+
},
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1088
1259
|
const pullRequests = byStream(records, "pull_requests").flatMap((r) => {
|
|
1089
1260
|
const item = projectItem(r, "pr");
|
|
1090
1261
|
return item ? [item] : [];
|
|
@@ -1183,6 +1354,7 @@ const githubProfile = {
|
|
|
1183
1354
|
error: { kind: "empty_singleton", scope: "github.profile" },
|
|
1184
1355
|
};
|
|
1185
1356
|
}
|
|
1357
|
+
const dropped = new DroppedRows("github.profile");
|
|
1186
1358
|
const payload = {
|
|
1187
1359
|
username: user.login,
|
|
1188
1360
|
profileUrl: `https://github.com/${user.login}`,
|
|
@@ -1199,7 +1371,12 @@ const githubProfile = {
|
|
|
1199
1371
|
payload[target] = user[source];
|
|
1200
1372
|
}
|
|
1201
1373
|
const stats = byStream(records, "user_stats")
|
|
1202
|
-
.filter((r) =>
|
|
1374
|
+
.filter((r) => {
|
|
1375
|
+
if (typeof r.observed_on === "string")
|
|
1376
|
+
return true;
|
|
1377
|
+
dropped.add("user_stats", "Stats record lacks observed_on");
|
|
1378
|
+
return false;
|
|
1379
|
+
})
|
|
1203
1380
|
.sort((a, b) => String(b.observed_on).localeCompare(String(a.observed_on)))[0];
|
|
1204
1381
|
if (stats) {
|
|
1205
1382
|
for (const [source, target] of [
|
|
@@ -1229,8 +1406,10 @@ const githubProfile = {
|
|
|
1229
1406
|
});
|
|
1230
1407
|
}
|
|
1231
1408
|
payload.pinnedRepositories = byStream(records, "pinned_repositories").flatMap((r) => {
|
|
1232
|
-
if (typeof r.full_name !== "string")
|
|
1409
|
+
if (typeof r.full_name !== "string") {
|
|
1410
|
+
dropped.add("pinned_repositories", "Pinned repository lacks full_name");
|
|
1233
1411
|
return [];
|
|
1412
|
+
}
|
|
1234
1413
|
const item = { fullName: r.full_name };
|
|
1235
1414
|
if (typeof r.html_url === "string" || r.html_url === null)
|
|
1236
1415
|
item.url = r.html_url;
|
|
@@ -1243,8 +1422,10 @@ const githubProfile = {
|
|
|
1243
1422
|
return [item];
|
|
1244
1423
|
});
|
|
1245
1424
|
payload.organizations = byStream(records, "organizations").flatMap((r) => {
|
|
1246
|
-
if (typeof r.login !== "string")
|
|
1425
|
+
if (typeof r.login !== "string") {
|
|
1426
|
+
dropped.add("organizations", "Organization lacks login");
|
|
1247
1427
|
return [];
|
|
1428
|
+
}
|
|
1248
1429
|
return [
|
|
1249
1430
|
{
|
|
1250
1431
|
login: r.login,
|
|
@@ -1254,7 +1435,7 @@ const githubProfile = {
|
|
|
1254
1435
|
},
|
|
1255
1436
|
];
|
|
1256
1437
|
});
|
|
1257
|
-
return { ok: true, payload };
|
|
1438
|
+
return { ok: true, payload, ...withDiagnostics(dropped.diagnostics()) };
|
|
1258
1439
|
},
|
|
1259
1440
|
};
|
|
1260
1441
|
const githubEvents = {
|
|
@@ -1270,7 +1451,7 @@ const githubEvents = {
|
|
|
1270
1451
|
"the events API retains only roughly 90 days; the connector emits each collected public event as a legacy event",
|
|
1271
1452
|
"legacy action, title, body, URL, branch, commit count, repository URL, and window description are omitted because events records do not declare those values",
|
|
1272
1453
|
"fetchedAt is set at projection time because the stream has no collection timestamp",
|
|
1273
|
-
"malformed records missing legacy-required fields are
|
|
1454
|
+
"malformed records missing legacy-required fields are dropped and counted",
|
|
1274
1455
|
],
|
|
1275
1456
|
provenance: [
|
|
1276
1457
|
{
|
|
@@ -1284,13 +1465,16 @@ const githubEvents = {
|
|
|
1284
1465
|
const guard = requireStreams("github.events", options, ["events"]);
|
|
1285
1466
|
if (guard)
|
|
1286
1467
|
return guard;
|
|
1468
|
+
const dropped = new DroppedRows("github.events");
|
|
1287
1469
|
const events = byStream(records, "events").flatMap((r) => {
|
|
1288
1470
|
if (typeof r.id !== "string" ||
|
|
1289
1471
|
typeof r.type !== "string" ||
|
|
1290
1472
|
typeof r.created_at !== "string" ||
|
|
1291
1473
|
typeof r.repository_full_name !== "string" ||
|
|
1292
|
-
typeof r.is_public !== "boolean")
|
|
1474
|
+
typeof r.is_public !== "boolean") {
|
|
1475
|
+
dropped.add("events", "Event lacks a legacy-required id, type, created_at, repository_full_name, or is_public");
|
|
1293
1476
|
return [];
|
|
1477
|
+
}
|
|
1294
1478
|
return [
|
|
1295
1479
|
{
|
|
1296
1480
|
id: r.id,
|
|
@@ -1304,6 +1488,7 @@ const githubEvents = {
|
|
|
1304
1488
|
return {
|
|
1305
1489
|
ok: true,
|
|
1306
1490
|
payload: { events, fetchedAt: projectionTime(options) },
|
|
1491
|
+
...withDiagnostics(dropped.diagnostics()),
|
|
1307
1492
|
};
|
|
1308
1493
|
},
|
|
1309
1494
|
};
|
|
@@ -1318,6 +1503,7 @@ const githubContributions = {
|
|
|
1318
1503
|
"daily records are projected to legacy days; level is absent from the source and omitted",
|
|
1319
1504
|
"totalContributionsLastYear, yearTotals, monthlyTotals, and topDay are omitted because collected contribution records may cover only an incremental or caller-limited interval",
|
|
1320
1505
|
"fetchedAt is set at projection time because contribution records have no collection timestamp",
|
|
1506
|
+
"days without a date or numeric count are dropped and counted",
|
|
1321
1507
|
],
|
|
1322
1508
|
provenance: [
|
|
1323
1509
|
{
|
|
@@ -1333,13 +1519,20 @@ const githubContributions = {
|
|
|
1333
1519
|
]);
|
|
1334
1520
|
if (guard)
|
|
1335
1521
|
return guard;
|
|
1522
|
+
const dropped = new DroppedRows("github.contributions");
|
|
1336
1523
|
const days = byStream(records, "contributions").flatMap((r) => {
|
|
1337
1524
|
if (typeof r.date !== "string" ||
|
|
1338
|
-
typeof r.contribution_count !== "number")
|
|
1525
|
+
typeof r.contribution_count !== "number") {
|
|
1526
|
+
dropped.add("contributions", "Contribution day lacks a date or a numeric contribution_count");
|
|
1339
1527
|
return [];
|
|
1528
|
+
}
|
|
1340
1529
|
return [{ date: r.date, count: r.contribution_count }];
|
|
1341
1530
|
});
|
|
1342
|
-
return {
|
|
1531
|
+
return {
|
|
1532
|
+
ok: true,
|
|
1533
|
+
payload: { days, fetchedAt: projectionTime(options) },
|
|
1534
|
+
...withDiagnostics(dropped.diagnostics()),
|
|
1535
|
+
};
|
|
1343
1536
|
},
|
|
1344
1537
|
};
|
|
1345
1538
|
// ---------------------------------------------------------------------------
|
|
@@ -1431,6 +1624,7 @@ const chatgptConversations = {
|
|
|
1431
1624
|
"A conversation without complete current-branch message evidence, a valid id, or valid timestamps is dropped and counted in a records_dropped diagnostic; nested full threads are not invented. Messages whose conversation is absent are dropped and counted the same way.",
|
|
1432
1625
|
"Only user/assistant text or multimodal_text messages with nonempty string content are retained, matching the legacy connector's walkMessages filter; message_count is the retained count.",
|
|
1433
1626
|
"A missing title becomes 'Untitled', matching the legacy connector's toConversationRecord fallback; model_slug is renamed to model.",
|
|
1627
|
+
"Legacy message ids are source node ids. ChatGPT 0.3.0 stores a message under '<conversation_id>:<node_id>' (and links parent_id the same way); the projection strips that prefix, reads a bare pre-0.3.0 id as it is, and counts a node stored under both forms once, preferring the 0.3.0 row.",
|
|
1434
1628
|
],
|
|
1435
1629
|
provenance: [
|
|
1436
1630
|
{
|
|
@@ -1456,14 +1650,17 @@ const chatgptConversations = {
|
|
|
1456
1650
|
const dropped = new Map();
|
|
1457
1651
|
const drop = (reason) => dropped.set(reason, (dropped.get(reason) ?? 0) + 1);
|
|
1458
1652
|
let malformedMessages = 0;
|
|
1653
|
+
let unlinkedMessages = 0;
|
|
1459
1654
|
const messagesByConversation = new Map();
|
|
1460
1655
|
for (const message of byStream(records, "messages")) {
|
|
1461
1656
|
if (!isObjectRow(message)) {
|
|
1462
1657
|
malformedMessages += 1;
|
|
1463
1658
|
continue;
|
|
1464
1659
|
}
|
|
1465
|
-
if (typeof message.conversation_id !== "string")
|
|
1660
|
+
if (typeof message.conversation_id !== "string") {
|
|
1661
|
+
unlinkedMessages += 1;
|
|
1466
1662
|
continue;
|
|
1663
|
+
}
|
|
1467
1664
|
const group = messagesByConversation.get(message.conversation_id) ?? [];
|
|
1468
1665
|
group.push(message);
|
|
1469
1666
|
messagesByConversation.set(message.conversation_id, group);
|
|
@@ -1513,20 +1710,29 @@ const chatgptConversations = {
|
|
|
1513
1710
|
drop("Conversation lacks a current-branch message count");
|
|
1514
1711
|
continue;
|
|
1515
1712
|
}
|
|
1516
|
-
|
|
1713
|
+
// Legacy names a message by its source node id. Messages are stored
|
|
1714
|
+
// under their record id: the bare node id before ChatGPT 0.3.0,
|
|
1715
|
+
// `<conversation_id>:<node_id>` since. An instance that holds both
|
|
1716
|
+
// forms of one node uses the current one.
|
|
1717
|
+
const { byNode, unkeyed } = chatgptMessagesByNode(id, messagesByConversation.get(id));
|
|
1718
|
+
const branch = [...byNode.values(), ...unkeyed].filter((m) => m.on_current_branch === true);
|
|
1517
1719
|
if (branch.length !== message_count_on_current_branch) {
|
|
1518
1720
|
drop("Current branch message count does not match conversation");
|
|
1519
1721
|
continue;
|
|
1520
1722
|
}
|
|
1521
|
-
const byId = new Map(branch.filter((m) => typeof m.id === "string").map((m) => [m.id, m]));
|
|
1522
1723
|
const ordered = [];
|
|
1523
1724
|
const seen = new Set();
|
|
1524
1725
|
let node = typeof current_node === "string" ? current_node : null;
|
|
1525
|
-
while (node &&
|
|
1726
|
+
while (node && !seen.has(node)) {
|
|
1727
|
+
const message = byNode.get(node);
|
|
1728
|
+
if (!message || message.on_current_branch !== true)
|
|
1729
|
+
break;
|
|
1526
1730
|
seen.add(node);
|
|
1527
|
-
const message = byId.get(node);
|
|
1528
1731
|
ordered.push(message);
|
|
1529
|
-
node =
|
|
1732
|
+
node =
|
|
1733
|
+
typeof message.parent_id === "string"
|
|
1734
|
+
? chatgptNodeId(id, message.parent_id)
|
|
1735
|
+
: null;
|
|
1530
1736
|
}
|
|
1531
1737
|
if (ordered.length !== branch.length) {
|
|
1532
1738
|
drop("Current branch message chain is incomplete");
|
|
@@ -1540,7 +1746,7 @@ const chatgptConversations = {
|
|
|
1540
1746
|
typeof m.content === "string" &&
|
|
1541
1747
|
m.content.length > 0)
|
|
1542
1748
|
.map((m) => ({
|
|
1543
|
-
id: m.id,
|
|
1749
|
+
id: chatgptNodeId(id, m.id),
|
|
1544
1750
|
role: m.role,
|
|
1545
1751
|
content: m.content,
|
|
1546
1752
|
content_type: m.content_type,
|
|
@@ -1567,17 +1773,18 @@ const chatgptConversations = {
|
|
|
1567
1773
|
reasons: [...dropped.keys()],
|
|
1568
1774
|
});
|
|
1569
1775
|
}
|
|
1570
|
-
if (orphanMessages + malformedMessages > 0) {
|
|
1776
|
+
if (orphanMessages + malformedMessages + unlinkedMessages > 0) {
|
|
1571
1777
|
diagnostics.push({
|
|
1572
1778
|
kind: "records_dropped",
|
|
1573
1779
|
scope,
|
|
1574
1780
|
stream: "messages",
|
|
1575
|
-
count: orphanMessages + malformedMessages,
|
|
1781
|
+
count: orphanMessages + malformedMessages + unlinkedMessages,
|
|
1576
1782
|
reasons: [
|
|
1577
1783
|
...(orphanMessages > 0
|
|
1578
1784
|
? ["Messages have no matching conversation"]
|
|
1579
1785
|
: []),
|
|
1580
1786
|
...(malformedMessages > 0 ? ["Message record is not an object"] : []),
|
|
1787
|
+
...(unlinkedMessages > 0 ? ["Message lacks a conversation id"] : []),
|
|
1581
1788
|
],
|
|
1582
1789
|
});
|
|
1583
1790
|
}
|
|
@@ -1595,6 +1802,41 @@ const chatgptConversations = {
|
|
|
1595
1802
|
};
|
|
1596
1803
|
},
|
|
1597
1804
|
};
|
|
1805
|
+
/**
|
|
1806
|
+
* The source node id of a ChatGPT message record id. ChatGPT 0.3.0 and later
|
|
1807
|
+
* name a message `<conversation_id>:<node_id>`, because a node id can recur in
|
|
1808
|
+
* a forked conversation; earlier versions stored the bare node id. A
|
|
1809
|
+
* conversation id is a UUID and never contains ":", so the prefix is
|
|
1810
|
+
* unambiguous. Applies to `parent_id` too, which names a record the same way.
|
|
1811
|
+
*/
|
|
1812
|
+
function chatgptNodeId(conversationId, recordId) {
|
|
1813
|
+
const prefix = `${conversationId}:`;
|
|
1814
|
+
return recordId.startsWith(prefix) ? recordId.slice(prefix.length) : recordId;
|
|
1815
|
+
}
|
|
1816
|
+
/**
|
|
1817
|
+
* One conversation's stored messages by source node id. When an instance holds
|
|
1818
|
+
* a node under both the pre-0.3.0 key and the 0.3.0 key, the 0.3.0 row wins:
|
|
1819
|
+
* it was written later, from the full source body.
|
|
1820
|
+
*/
|
|
1821
|
+
function chatgptMessagesByNode(conversationId, messages) {
|
|
1822
|
+
const byNode = new Map();
|
|
1823
|
+
const unkeyed = [];
|
|
1824
|
+
const current = new Set();
|
|
1825
|
+
for (const message of messages ?? []) {
|
|
1826
|
+
if (typeof message.id !== "string") {
|
|
1827
|
+
unkeyed.push(message);
|
|
1828
|
+
continue;
|
|
1829
|
+
}
|
|
1830
|
+
const node = chatgptNodeId(conversationId, message.id);
|
|
1831
|
+
const isCurrent = node !== message.id;
|
|
1832
|
+
if (byNode.has(node) && (current.has(node) || !isCurrent))
|
|
1833
|
+
continue;
|
|
1834
|
+
byNode.set(node, message);
|
|
1835
|
+
if (isCurrent)
|
|
1836
|
+
current.add(node);
|
|
1837
|
+
}
|
|
1838
|
+
return { byNode, unkeyed };
|
|
1839
|
+
}
|
|
1598
1840
|
/**
|
|
1599
1841
|
* Newest `update_time` first; a missing or unparseable time sorts last.
|
|
1600
1842
|
* Ties fall back to the id, so the order never depends on input order.
|
|
@@ -1611,7 +1853,9 @@ function newestUpdateFirst(a, b) {
|
|
|
1611
1853
|
}
|
|
1612
1854
|
// This is the merged 0.1.3 source declaration. Admission remains a separate
|
|
1613
1855
|
// decision; this local manifest hash is not the signed OCI artifact digest.
|
|
1614
|
-
const
|
|
1856
|
+
export const ANTHROPIC_CONNECTOR_ID = "https://registry.pdpp.dev/connectors/anthropic";
|
|
1857
|
+
// Connector/method identity differs from the public source in the admitted declaration.
|
|
1858
|
+
export const CLAUDE_SOURCE_ID = "https://registry.pdpp.dev/sources/claude";
|
|
1615
1859
|
const anthropicProjectionProvenance = [
|
|
1616
1860
|
{
|
|
1617
1861
|
kind: "collection-profile",
|
|
@@ -1664,7 +1908,7 @@ function claudeConversationsBinding() {
|
|
|
1664
1908
|
const scope = "claude.conversations";
|
|
1665
1909
|
return {
|
|
1666
1910
|
scope,
|
|
1667
|
-
pdppSource:
|
|
1911
|
+
pdppSource: ANTHROPIC_CONNECTOR_ID,
|
|
1668
1912
|
pdppStreams: ["account_profile", "conversations", "messages"],
|
|
1669
1913
|
legacySchemaPath: "claude.conversations.json",
|
|
1670
1914
|
fieldsRead: {
|
|
@@ -1824,7 +2068,7 @@ function claudeConversationsBinding() {
|
|
|
1824
2068
|
const claudeConversations = claudeConversationsBinding();
|
|
1825
2069
|
const claudeProjects = {
|
|
1826
2070
|
scope: "claude.projects",
|
|
1827
|
-
pdppSource:
|
|
2071
|
+
pdppSource: ANTHROPIC_CONNECTOR_ID,
|
|
1828
2072
|
pdppStreams: ["account_profile", "projects", "project_documents"],
|
|
1829
2073
|
legacySchemaPath: "claude.projects.json",
|
|
1830
2074
|
fieldsRead: {
|
|
@@ -2403,7 +2647,7 @@ const spotifySavedTracks = {
|
|
|
2403
2647
|
},
|
|
2404
2648
|
primaryKey: { saved_tracks: ["id"] },
|
|
2405
2649
|
lossy: [
|
|
2406
|
-
"Rows without the required legacy name and artist list are
|
|
2650
|
+
"Rows without the required legacy name and artist list are dropped and counted; nullable album and optional fields retain the connector's empty-string, zero, and false sentinels.",
|
|
2407
2651
|
"isrc, popularity, and id have no corresponding legacy fields and are omitted.",
|
|
2408
2652
|
],
|
|
2409
2653
|
provenance: [
|
|
@@ -2420,16 +2664,27 @@ const spotifySavedTracks = {
|
|
|
2420
2664
|
]);
|
|
2421
2665
|
if (guard)
|
|
2422
2666
|
return guard;
|
|
2667
|
+
const dropped = new Set();
|
|
2668
|
+
let droppedCount = 0;
|
|
2423
2669
|
const tracks = byStream(records, "saved_tracks").flatMap((r) => {
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2670
|
+
const { name, artist_names: artistNames } = r;
|
|
2671
|
+
const artists = Array.isArray(artistNames) &&
|
|
2672
|
+
artistNames.every((entry) => typeof entry === "string")
|
|
2673
|
+
? artistNames
|
|
2674
|
+
: null;
|
|
2675
|
+
if (typeof name !== "string" || !artists) {
|
|
2676
|
+
droppedCount += 1;
|
|
2677
|
+
if (typeof name !== "string")
|
|
2678
|
+
dropped.add("Saved track lacks a name");
|
|
2679
|
+
if (!artists)
|
|
2680
|
+
dropped.add("Saved track lacks a valid artist list");
|
|
2427
2681
|
return [];
|
|
2682
|
+
}
|
|
2428
2683
|
return [
|
|
2429
2684
|
{
|
|
2430
2685
|
added_at: orEmptyString(r.added_at),
|
|
2431
|
-
name
|
|
2432
|
-
artists:
|
|
2686
|
+
name,
|
|
2687
|
+
artists: artists.map((artist) => ({ name: artist })),
|
|
2433
2688
|
album: {
|
|
2434
2689
|
name: orEmptyString(r.album_name),
|
|
2435
2690
|
artists: Array.isArray(r.album_artist_names)
|
|
@@ -2444,7 +2699,23 @@ const spotifySavedTracks = {
|
|
|
2444
2699
|
},
|
|
2445
2700
|
];
|
|
2446
2701
|
});
|
|
2447
|
-
return {
|
|
2702
|
+
return {
|
|
2703
|
+
ok: true,
|
|
2704
|
+
payload: { savedTracks: tracks, total: tracks.length },
|
|
2705
|
+
...(droppedCount > 0
|
|
2706
|
+
? {
|
|
2707
|
+
diagnostics: [
|
|
2708
|
+
{
|
|
2709
|
+
kind: "records_dropped",
|
|
2710
|
+
scope: "spotify.savedTracks",
|
|
2711
|
+
stream: "saved_tracks",
|
|
2712
|
+
count: droppedCount,
|
|
2713
|
+
reasons: [...dropped],
|
|
2714
|
+
},
|
|
2715
|
+
],
|
|
2716
|
+
}
|
|
2717
|
+
: {}),
|
|
2718
|
+
};
|
|
2448
2719
|
},
|
|
2449
2720
|
};
|
|
2450
2721
|
const spotifyPlaylists = {
|
|
@@ -2503,8 +2774,16 @@ const spotifyPlaylists = {
|
|
|
2503
2774
|
const itemsByPlaylist = new Map();
|
|
2504
2775
|
for (const item of byStream(records, "playlist_items")) {
|
|
2505
2776
|
const playlistId = item.playlist_id;
|
|
2506
|
-
if (typeof playlistId !== "string")
|
|
2507
|
-
|
|
2777
|
+
if (typeof playlistId !== "string") {
|
|
2778
|
+
return {
|
|
2779
|
+
ok: false,
|
|
2780
|
+
error: {
|
|
2781
|
+
kind: "incomplete_scope",
|
|
2782
|
+
scope: "spotify.playlists",
|
|
2783
|
+
reason: "A playlist item has no playlist_id to join on",
|
|
2784
|
+
},
|
|
2785
|
+
};
|
|
2786
|
+
}
|
|
2508
2787
|
const group = itemsByPlaylist.get(playlistId);
|
|
2509
2788
|
if (group) {
|
|
2510
2789
|
group.push(item);
|
|
@@ -2695,6 +2974,7 @@ const ouraActivity = {
|
|
|
2695
2974
|
lossy: [
|
|
2696
2975
|
"timestamp: no source field; omitted (legacy schema makes it optional)",
|
|
2697
2976
|
"target_calories: has no legacy field and is not projected",
|
|
2977
|
+
"Legacy integer fields: fractional source numbers are rounded with Math.round at projection time",
|
|
2698
2978
|
],
|
|
2699
2979
|
provenance: [
|
|
2700
2980
|
{
|
|
@@ -2708,26 +2988,26 @@ const ouraActivity = {
|
|
|
2708
2988
|
const guard = requireStreams("oura.activity", options, ["activity"]);
|
|
2709
2989
|
if (guard)
|
|
2710
2990
|
return guard;
|
|
2991
|
+
const { rows, diagnostics } = partitionOuraRows("oura.activity", "activity", byStream(records, "activity"), () => "score");
|
|
2711
2992
|
return {
|
|
2712
2993
|
ok: true,
|
|
2994
|
+
...(diagnostics.length > 0 ? { diagnostics } : {}),
|
|
2713
2995
|
payload: {
|
|
2714
|
-
days:
|
|
2996
|
+
days: rows.map((r) => ({
|
|
2715
2997
|
id: r.id,
|
|
2716
2998
|
day: r.day,
|
|
2717
|
-
|
|
2718
|
-
...
|
|
2719
|
-
...
|
|
2720
|
-
...
|
|
2721
|
-
...
|
|
2722
|
-
...
|
|
2723
|
-
...
|
|
2724
|
-
...
|
|
2725
|
-
...
|
|
2726
|
-
...
|
|
2727
|
-
...
|
|
2728
|
-
...(r.contributors
|
|
2729
|
-
? { contributors: r.contributors }
|
|
2730
|
-
: {}),
|
|
2999
|
+
score: ouraInteger(r.score),
|
|
3000
|
+
...ifDefinedOuraInteger(r, "active_calories", "activeCalories"),
|
|
3001
|
+
...ifDefinedOuraInteger(r, "total_calories", "totalCalories"),
|
|
3002
|
+
...ifDefinedOuraInteger(r, "steps", "steps"),
|
|
3003
|
+
...ifDefinedOuraInteger(r, "equivalent_walking_distance", "equivalentWalkingDistance"),
|
|
3004
|
+
...ifDefinedOuraInteger(r, "high_activity_time", "highActivityTime"),
|
|
3005
|
+
...ifDefinedOuraInteger(r, "medium_activity_time", "mediumActivityTime"),
|
|
3006
|
+
...ifDefinedOuraInteger(r, "low_activity_time", "lowActivityTime"),
|
|
3007
|
+
...ifDefinedOuraInteger(r, "sedentary_time", "sedentaryTime"),
|
|
3008
|
+
...ifDefinedOuraInteger(r, "resting_time", "restingTime"),
|
|
3009
|
+
...ifDefinedOuraInteger(r, "inactivity_alerts", "inactivityAlerts"),
|
|
3010
|
+
...withOuraContributors(r.contributors, OURA_ACTIVITY_CONTRIBUTOR_INTEGERS),
|
|
2731
3011
|
})),
|
|
2732
3012
|
},
|
|
2733
3013
|
};
|
|
@@ -2751,6 +3031,7 @@ const ouraReadiness = {
|
|
|
2751
3031
|
primaryKey: { readiness: ["id"] },
|
|
2752
3032
|
lossy: [
|
|
2753
3033
|
"timestamp: no source field; omitted (legacy schema makes it optional)",
|
|
3034
|
+
"Legacy integer fields: fractional source numbers are rounded with Math.round at projection time",
|
|
2754
3035
|
],
|
|
2755
3036
|
provenance: [
|
|
2756
3037
|
{
|
|
@@ -2764,18 +3045,18 @@ const ouraReadiness = {
|
|
|
2764
3045
|
const guard = requireStreams("oura.readiness", options, ["readiness"]);
|
|
2765
3046
|
if (guard)
|
|
2766
3047
|
return guard;
|
|
3048
|
+
const { rows, diagnostics } = partitionOuraRows("oura.readiness", "readiness", byStream(records, "readiness"), () => "score");
|
|
2767
3049
|
return {
|
|
2768
3050
|
ok: true,
|
|
3051
|
+
...(diagnostics.length > 0 ? { diagnostics } : {}),
|
|
2769
3052
|
payload: {
|
|
2770
|
-
days:
|
|
3053
|
+
days: rows.map((r) => ({
|
|
2771
3054
|
id: r.id,
|
|
2772
3055
|
day: r.day,
|
|
2773
|
-
|
|
3056
|
+
score: ouraInteger(r.score),
|
|
2774
3057
|
...ifDefined(r, "temperature_deviation", "temperatureDeviation"),
|
|
2775
3058
|
...ifDefined(r, "temperature_trend_deviation", "temperatureTrendDeviation"),
|
|
2776
|
-
...(r.contributors
|
|
2777
|
-
? { contributors: r.contributors }
|
|
2778
|
-
: {}),
|
|
3059
|
+
...withOuraContributors(r.contributors, OURA_READINESS_CONTRIBUTOR_INTEGERS),
|
|
2779
3060
|
})),
|
|
2780
3061
|
},
|
|
2781
3062
|
};
|
|
@@ -2813,8 +3094,9 @@ const ouraSleep = {
|
|
|
2813
3094
|
lossy: [
|
|
2814
3095
|
"awakeTime: no declared PDPP field and is omitted",
|
|
2815
3096
|
"dailyScores.timestamp: no source field and is omitted",
|
|
2816
|
-
"sleepPeriods.averageHrv: PDPP
|
|
3097
|
+
"sleepPeriods.averageHrv: fractional PDPP numbers are rounded with Math.round to satisfy the legacy integer type",
|
|
2817
3098
|
"sleepPeriods.type: unrecognized or null values are omitted because the legacy enum accepts only long_sleep, short_sleep or rest",
|
|
3099
|
+
"Legacy integer fields: fractional source numbers are rounded with Math.round at projection time",
|
|
2818
3100
|
],
|
|
2819
3101
|
provenance: [
|
|
2820
3102
|
{
|
|
@@ -2828,50 +3110,85 @@ const ouraSleep = {
|
|
|
2828
3110
|
const guard = requireStreams("oura.sleep", options, ["sleep"]);
|
|
2829
3111
|
if (guard)
|
|
2830
3112
|
return guard;
|
|
2831
|
-
const
|
|
2832
|
-
const
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
...(r.type === "long_sleep" ||
|
|
2836
|
-
r.type === "short_sleep" ||
|
|
2837
|
-
r.type === "rest"
|
|
2838
|
-
? { type: r.type }
|
|
2839
|
-
: {}),
|
|
2840
|
-
...ifDefined(r, "bedtime_start", "bedtimeStart"),
|
|
2841
|
-
...ifDefined(r, "bedtime_end", "bedtimeEnd"),
|
|
2842
|
-
...ifDefined(r, "total_sleep_duration", "totalSleepDuration"),
|
|
2843
|
-
...ifDefined(r, "time_in_bed", "timeInBed"),
|
|
2844
|
-
...ifDefined(r, "deep_sleep_duration", "deepSleepDuration"),
|
|
2845
|
-
...ifDefined(r, "light_sleep_duration", "lightSleepDuration"),
|
|
2846
|
-
...ifDefined(r, "rem_sleep_duration", "remSleepDuration"),
|
|
2847
|
-
...ifDefined(r, "efficiency", "efficiency"),
|
|
2848
|
-
...ifDefined(r, "latency", "latency"),
|
|
2849
|
-
...ifDefined(r, "average_heart_rate", "averageHeartRate"),
|
|
2850
|
-
...(r.average_hrv === null
|
|
2851
|
-
? { averageHrv: null }
|
|
2852
|
-
: typeof r.average_hrv === "number" && Number.isInteger(r.average_hrv)
|
|
2853
|
-
? { averageHrv: r.average_hrv }
|
|
2854
|
-
: {}),
|
|
2855
|
-
...ifDefined(r, "lowest_heart_rate", "lowestHeartRate"),
|
|
2856
|
-
...ifDefined(r, "average_breath", "averageBreath"),
|
|
2857
|
-
...ifDefined(r, "restless_periods", "restlessPeriods"),
|
|
2858
|
-
}));
|
|
3113
|
+
const sourceRows = byStream(records, "sleep");
|
|
3114
|
+
const { rows, diagnostics } = partitionOuraRows("oura.sleep", "sleep", sourceRows, () => "sleep_score");
|
|
3115
|
+
const { rows: periodRows } = partitionOuraRows("oura.sleep", "sleep", sourceRows, () => undefined);
|
|
3116
|
+
const projectionOmissions = periodRows.flatMap(ouraSleepPeriodOmissions);
|
|
2859
3117
|
return {
|
|
2860
3118
|
ok: true,
|
|
3119
|
+
...withDiagnostics(diagnostics),
|
|
3120
|
+
...(projectionOmissions.length > 0 ? { projectionOmissions } : {}),
|
|
2861
3121
|
payload: {
|
|
2862
3122
|
dailyScores: rows.map((r) => ({
|
|
2863
3123
|
id: r.id,
|
|
2864
3124
|
day: r.day,
|
|
2865
|
-
|
|
2866
|
-
...(r.contributors
|
|
2867
|
-
? { contributors: r.contributors }
|
|
2868
|
-
: {}),
|
|
3125
|
+
score: ouraInteger(r.sleep_score),
|
|
3126
|
+
...withOuraContributors(r.contributors, OURA_SLEEP_CONTRIBUTOR_INTEGERS),
|
|
2869
3127
|
})),
|
|
2870
|
-
sleepPeriods:
|
|
3128
|
+
sleepPeriods: periodRows.map(ouraSleepPeriod),
|
|
2871
3129
|
},
|
|
2872
3130
|
};
|
|
2873
3131
|
},
|
|
2874
3132
|
};
|
|
3133
|
+
/** One legacy `sleepPeriods` entry from a source sleep session row with a valid day. */
|
|
3134
|
+
export function ouraSleepPeriod(r) {
|
|
3135
|
+
return {
|
|
3136
|
+
id: r.id,
|
|
3137
|
+
day: r.day,
|
|
3138
|
+
...(r.type === "long_sleep" || r.type === "short_sleep" || r.type === "rest"
|
|
3139
|
+
? { type: r.type }
|
|
3140
|
+
: {}),
|
|
3141
|
+
...(r.bedtime_start === null
|
|
3142
|
+
? {}
|
|
3143
|
+
: ifDefined(r, "bedtime_start", "bedtimeStart")),
|
|
3144
|
+
...(r.bedtime_end === null
|
|
3145
|
+
? {}
|
|
3146
|
+
: ifDefined(r, "bedtime_end", "bedtimeEnd")),
|
|
3147
|
+
...ifDefinedOuraInteger(r, "total_sleep_duration", "totalSleepDuration"),
|
|
3148
|
+
...ifDefinedOuraInteger(r, "time_in_bed", "timeInBed"),
|
|
3149
|
+
...ifDefinedOuraInteger(r, "deep_sleep_duration", "deepSleepDuration"),
|
|
3150
|
+
...ifDefinedOuraInteger(r, "light_sleep_duration", "lightSleepDuration"),
|
|
3151
|
+
...ifDefinedOuraInteger(r, "rem_sleep_duration", "remSleepDuration"),
|
|
3152
|
+
...ifDefinedOuraInteger(r, "efficiency", "efficiency"),
|
|
3153
|
+
...ifDefinedOuraInteger(r, "latency", "latency"),
|
|
3154
|
+
...ifDefined(r, "average_heart_rate", "averageHeartRate"),
|
|
3155
|
+
...ifDefinedOuraInteger(r, "average_hrv", "averageHrv"),
|
|
3156
|
+
...ifDefinedOuraInteger(r, "lowest_heart_rate", "lowestHeartRate"),
|
|
3157
|
+
...ifDefined(r, "average_breath", "averageBreath"),
|
|
3158
|
+
...ifDefinedOuraInteger(r, "restless_periods", "restlessPeriods"),
|
|
3159
|
+
};
|
|
3160
|
+
}
|
|
3161
|
+
export function ouraSleepPeriodOmissions(r) {
|
|
3162
|
+
const entityKey = r.id;
|
|
3163
|
+
if ((typeof entityKey !== "string" &&
|
|
3164
|
+
typeof entityKey !== "number" &&
|
|
3165
|
+
typeof entityKey !== "boolean") ||
|
|
3166
|
+
entityKey === "") {
|
|
3167
|
+
return [];
|
|
3168
|
+
}
|
|
3169
|
+
const omissions = [];
|
|
3170
|
+
if (r.type === null ||
|
|
3171
|
+
(r.type !== undefined &&
|
|
3172
|
+
!["long_sleep", "short_sleep", "rest"].includes(String(r.type)))) {
|
|
3173
|
+
omissions.push({
|
|
3174
|
+
entityPath: "$.sleepPeriods[]",
|
|
3175
|
+
entityKey,
|
|
3176
|
+
fieldPath: "type",
|
|
3177
|
+
reason: r.type === null ? "null_type" : "off_enum_type",
|
|
3178
|
+
});
|
|
3179
|
+
}
|
|
3180
|
+
for (const field of ["bedtime_start", "bedtime_end"]) {
|
|
3181
|
+
if (r[field] === null) {
|
|
3182
|
+
omissions.push({
|
|
3183
|
+
entityPath: "$.sleepPeriods[]",
|
|
3184
|
+
entityKey,
|
|
3185
|
+
fieldPath: field === "bedtime_start" ? "bedtimeStart" : "bedtimeEnd",
|
|
3186
|
+
reason: "null_timestamp",
|
|
3187
|
+
});
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
3190
|
+
return omissions;
|
|
3191
|
+
}
|
|
2875
3192
|
// ---------------------------------------------------------------------------
|
|
2876
3193
|
// amazon
|
|
2877
3194
|
// declarations/amazon.collection-profile.json, streams "orders" +
|
|
@@ -2910,6 +3227,10 @@ const amazonOrders = {
|
|
|
2910
3227
|
}
|
|
2911
3228
|
const orders = byStream(records, "orders");
|
|
2912
3229
|
const items = byStream(records, "order_items");
|
|
3230
|
+
const orderIds = new Set(orders.map((o) => o.id));
|
|
3231
|
+
const orphanItems = items.filter((i) => !orderIds.has(i.order_id)).length;
|
|
3232
|
+
const dropped = new DroppedRows("amazon.orders");
|
|
3233
|
+
dropped.add("order_items", "Order item has no matching order", orphanItems);
|
|
2913
3234
|
// orderDate: legacy scrapes "MonthName D, YYYY" out of card text via
|
|
2914
3235
|
// regex (amazon-playwright.js:184-187); toLegacyLongDate reproduces
|
|
2915
3236
|
// that exact text shape from PDPP's ISO order_date, not an ISO
|
|
@@ -2936,6 +3257,7 @@ const amazonOrders = {
|
|
|
2936
3257
|
})),
|
|
2937
3258
|
total: orders.length,
|
|
2938
3259
|
},
|
|
3260
|
+
...withDiagnostics(dropped.diagnostics()),
|
|
2939
3261
|
};
|
|
2940
3262
|
},
|
|
2941
3263
|
};
|
|
@@ -3227,8 +3549,31 @@ const hebNutrition = {
|
|
|
3227
3549
|
};
|
|
3228
3550
|
},
|
|
3229
3551
|
};
|
|
3230
|
-
//
|
|
3552
|
+
// Grocery order semantics follow the accepted Unity projection fold
|
|
3553
|
+
// (vana-unity 9256b5a8e): an order with no usable line is a detail gap unless
|
|
3554
|
+
// the source count is a known zero; a gap or an unkeyable order is left out and
|
|
3555
|
+
// counted; zero survivors from nonempty orders is rejected, never served empty.
|
|
3556
|
+
// Reason strings are that fold's diagnostic codes.
|
|
3231
3557
|
// ---------------------------------------------------------------------------
|
|
3558
|
+
/** Reason for an order dropped because its id is unusable; index.ts lets exactly these rows past its primary-key check. */
|
|
3559
|
+
export const GROCERY_INVALID_ORDER = "projection_invalid_order";
|
|
3560
|
+
const GROCERY_ORDER_ITEMS_GAP = "projection_order_items_gap";
|
|
3561
|
+
function partialRows(scope, stream, reason, count) {
|
|
3562
|
+
return count > 0
|
|
3563
|
+
? [{ kind: "records_partial", scope, stream, count, reasons: [reason] }]
|
|
3564
|
+
: [];
|
|
3565
|
+
}
|
|
3566
|
+
function groceryRejection(scope, diagnostics) {
|
|
3567
|
+
return {
|
|
3568
|
+
ok: false,
|
|
3569
|
+
error: {
|
|
3570
|
+
kind: "incomplete_scope",
|
|
3571
|
+
scope,
|
|
3572
|
+
reason: "No source order could be projected",
|
|
3573
|
+
diagnostics,
|
|
3574
|
+
},
|
|
3575
|
+
};
|
|
3576
|
+
}
|
|
3232
3577
|
const hebOrders = {
|
|
3233
3578
|
scope: "heb.orders",
|
|
3234
3579
|
pdppSource: "https://registry.pdpp.dev/connectors/heb",
|
|
@@ -3251,7 +3596,9 @@ const hebOrders = {
|
|
|
3251
3596
|
"orderUrl and address: the signed profile has no equivalent fields; fulfillment_location is not treated as an address.",
|
|
3252
3597
|
"status_code, store_name, timeslot_start/end, unfulfilled_count, fulfillment_method, and fetched_at are not represented by the legacy schema.",
|
|
3253
3598
|
"total and item price: derived from declared integer cents when present; omitted when absent. No currency or decimal text is guessed.",
|
|
3254
|
-
"the retained collector emitted only product-linked named lines; a source line without either field
|
|
3599
|
+
"the retained collector emitted only product-linked named lines; a source line without either field is dropped and counted, and its order is delivered without it.",
|
|
3600
|
+
"An order without a usable id, or with no usable line and an item_count that is not a known zero, is left out and counted; valid peers are still delivered.",
|
|
3601
|
+
"item_count counts quantity units; an order whose summed line quantities differ is delivered and reported partial.",
|
|
3255
3602
|
"quantity: a declared numeric quantity is converted to its decimal string form to match the legacy connector output type.",
|
|
3256
3603
|
],
|
|
3257
3604
|
provenance: [
|
|
@@ -3268,11 +3615,8 @@ const hebOrders = {
|
|
|
3268
3615
|
if (guard)
|
|
3269
3616
|
return guard;
|
|
3270
3617
|
const sourceItems = byStream(records, "order_items");
|
|
3271
|
-
const
|
|
3272
|
-
|
|
3273
|
-
let skippedOrders = 0;
|
|
3274
|
-
let orderItemsShortOfDeclaredCount = false;
|
|
3275
|
-
const missingFields = new Set();
|
|
3618
|
+
const sourceOrders = byStream(records, "orders");
|
|
3619
|
+
const sourceOrderIds = new Set(sourceOrders.map((order) => order.id));
|
|
3276
3620
|
if (sourceItems.some((item) => !sourceOrderIds.has(item.order_id))) {
|
|
3277
3621
|
return {
|
|
3278
3622
|
ok: false,
|
|
@@ -3283,23 +3627,24 @@ const hebOrders = {
|
|
|
3283
3627
|
},
|
|
3284
3628
|
};
|
|
3285
3629
|
}
|
|
3286
|
-
const
|
|
3630
|
+
const dropped = new DroppedRows("heb.orders");
|
|
3631
|
+
let unitCountMismatches = 0;
|
|
3632
|
+
const orders = sourceOrders.flatMap((order) => {
|
|
3633
|
+
// Without a usable id an order cannot be keyed: it is left out and
|
|
3634
|
+
// counted (see GROCERY_INVALID_ORDER), never failing its valid peers.
|
|
3287
3635
|
if (!isNonEmptyString(order.id)) {
|
|
3288
|
-
|
|
3289
|
-
missingFields.add("orders.id");
|
|
3636
|
+
dropped.add("orders", GROCERY_INVALID_ORDER);
|
|
3290
3637
|
return [];
|
|
3291
3638
|
}
|
|
3292
3639
|
const items = sourceItems
|
|
3293
3640
|
.filter((item) => item.order_id === order.id)
|
|
3294
3641
|
.flatMap((item) => {
|
|
3295
|
-
if (!isNonEmptyString(item.name)
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
missingFields.add("order_items.product_id");
|
|
3302
|
-
}
|
|
3642
|
+
if (!isNonEmptyString(item.name)) {
|
|
3643
|
+
dropped.add("order_items", "projection_order_item_missing_name");
|
|
3644
|
+
return [];
|
|
3645
|
+
}
|
|
3646
|
+
if (!isNonEmptyString(item.product_id)) {
|
|
3647
|
+
dropped.add("order_items", "projection_order_item_missing_product_id");
|
|
3303
3648
|
return [];
|
|
3304
3649
|
}
|
|
3305
3650
|
const quantity = typeof item.quantity === "number" && Number.isFinite(item.quantity)
|
|
@@ -3319,10 +3664,19 @@ const hebOrders = {
|
|
|
3319
3664
|
},
|
|
3320
3665
|
];
|
|
3321
3666
|
});
|
|
3667
|
+
// No usable line proves an empty order only when the source count is a
|
|
3668
|
+
// known zero. A positive, missing, or malformed count is a detail gap:
|
|
3669
|
+
// the order is left out until its lines arrive.
|
|
3670
|
+
if (items.length === 0 && order.item_count !== 0) {
|
|
3671
|
+
dropped.add("orders", GROCERY_ORDER_ITEMS_GAP);
|
|
3672
|
+
return [];
|
|
3673
|
+
}
|
|
3674
|
+
// H-E-B item_count counts quantity units, not lines.
|
|
3322
3675
|
if (typeof order.item_count === "number" &&
|
|
3323
|
-
Number.isInteger(order.item_count)
|
|
3324
|
-
|
|
3325
|
-
|
|
3676
|
+
Number.isInteger(order.item_count)) {
|
|
3677
|
+
const units = items.reduce((total, item) => total + (item.quantity === null ? 0 : Number(item.quantity)), 0);
|
|
3678
|
+
if (units !== order.item_count)
|
|
3679
|
+
unitCountMismatches += 1;
|
|
3326
3680
|
}
|
|
3327
3681
|
return [
|
|
3328
3682
|
{
|
|
@@ -3341,35 +3695,13 @@ const hebOrders = {
|
|
|
3341
3695
|
},
|
|
3342
3696
|
];
|
|
3343
3697
|
});
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
},
|
|
3352
|
-
]
|
|
3353
|
-
: undefined;
|
|
3354
|
-
if (diagnostics) {
|
|
3355
|
-
return {
|
|
3356
|
-
ok: false,
|
|
3357
|
-
error: {
|
|
3358
|
-
kind: "incomplete_scope",
|
|
3359
|
-
scope: "heb.orders",
|
|
3360
|
-
reason: `Cannot preserve every valid order line: ${[...missingFields].join(", ")}`,
|
|
3361
|
-
},
|
|
3362
|
-
};
|
|
3363
|
-
}
|
|
3364
|
-
if (orderItemsShortOfDeclaredCount) {
|
|
3365
|
-
return {
|
|
3366
|
-
ok: false,
|
|
3367
|
-
error: {
|
|
3368
|
-
kind: "incomplete_scope",
|
|
3369
|
-
scope: "heb.orders",
|
|
3370
|
-
reason: "order_items are short of the count H-E-B reported for an order",
|
|
3371
|
-
},
|
|
3372
|
-
};
|
|
3698
|
+
// Zero survivors from nonempty orders is not an empty history.
|
|
3699
|
+
const diagnostics = [
|
|
3700
|
+
...dropped.diagnostics(),
|
|
3701
|
+
...partialRows("heb.orders", "orders", "projection_order_unit_count_mismatch", unitCountMismatches),
|
|
3702
|
+
];
|
|
3703
|
+
if (sourceOrders.length > 0 && orders.length === 0) {
|
|
3704
|
+
return groceryRejection("heb.orders", diagnostics);
|
|
3373
3705
|
}
|
|
3374
3706
|
return {
|
|
3375
3707
|
ok: true,
|
|
@@ -3378,7 +3710,7 @@ const hebOrders = {
|
|
|
3378
3710
|
totalOrders: orders.length,
|
|
3379
3711
|
totalItems: orders.reduce((total, order) => total + order.items.length, 0),
|
|
3380
3712
|
},
|
|
3381
|
-
...(diagnostics
|
|
3713
|
+
...withDiagnostics(diagnostics),
|
|
3382
3714
|
};
|
|
3383
3715
|
},
|
|
3384
3716
|
};
|
|
@@ -3472,7 +3804,8 @@ const wholefoodsOrders = {
|
|
|
3472
3804
|
},
|
|
3473
3805
|
primaryKey: { orders: ["id"], order_items: ["id"] },
|
|
3474
3806
|
lossy: [
|
|
3475
|
-
"The retained collector emitted only product-linked named lines; a source line without either field
|
|
3807
|
+
"The retained collector emitted only product-linked named lines; a source line without either field is dropped and counted, and its order is delivered without it.",
|
|
3808
|
+
"An order with no usable line and an item_count that is not a known zero is left out and counted; an order whose line count differs from item_count, or whose item_count is unknown, is delivered and reported partial. A missing or duplicate order id still fails the scope.",
|
|
3476
3809
|
"Product identity is the source Amazon ASIN in product_id.",
|
|
3477
3810
|
],
|
|
3478
3811
|
provenance: [
|
|
@@ -3502,41 +3835,30 @@ const wholefoodsOrders = {
|
|
|
3502
3835
|
},
|
|
3503
3836
|
};
|
|
3504
3837
|
}
|
|
3505
|
-
|
|
3506
|
-
!isNonEmptyString(item.name) ||
|
|
3507
|
-
!isNonEmptyString(item.product_id));
|
|
3508
|
-
if (unrepresentableItem) {
|
|
3509
|
-
return {
|
|
3510
|
-
ok: false,
|
|
3511
|
-
error: {
|
|
3512
|
-
kind: "incomplete_scope",
|
|
3513
|
-
scope: "wholefoods.orders",
|
|
3514
|
-
reason: "Cannot preserve every valid order line: order_items.name or order_items.product_id is missing",
|
|
3515
|
-
},
|
|
3516
|
-
};
|
|
3517
|
-
}
|
|
3518
|
-
if (sourceOrders.some((order) => typeof order.item_count !== "number" ||
|
|
3519
|
-
!Number.isSafeInteger(order.item_count) ||
|
|
3520
|
-
order.item_count < 0 ||
|
|
3521
|
-
sourceItems.filter((item) => item.order_id === order.id).length !==
|
|
3522
|
-
order.item_count)) {
|
|
3838
|
+
if (sourceItems.some((item) => !sourceOrderIds.has(item.order_id))) {
|
|
3523
3839
|
return {
|
|
3524
3840
|
ok: false,
|
|
3525
3841
|
error: {
|
|
3526
3842
|
kind: "incomplete_scope",
|
|
3527
3843
|
scope: "wholefoods.orders",
|
|
3528
|
-
reason: "
|
|
3844
|
+
reason: "order_items.order_id has no matching order",
|
|
3529
3845
|
},
|
|
3530
3846
|
};
|
|
3531
3847
|
}
|
|
3532
|
-
const
|
|
3533
|
-
|
|
3534
|
-
|
|
3848
|
+
const dropped = new DroppedRows("wholefoods.orders");
|
|
3849
|
+
let countMismatches = 0;
|
|
3850
|
+
const orders = sourceOrders.flatMap((o) => {
|
|
3535
3851
|
const items = sourceItems
|
|
3536
3852
|
.filter((i) => i.order_id === o.id)
|
|
3537
3853
|
.flatMap((i) => {
|
|
3538
|
-
if (!isNonEmptyString(i.name)
|
|
3854
|
+
if (!isNonEmptyString(i.name)) {
|
|
3855
|
+
dropped.add("order_items", "projection_order_item_missing_name");
|
|
3539
3856
|
return [];
|
|
3857
|
+
}
|
|
3858
|
+
if (!isNonEmptyString(i.product_id)) {
|
|
3859
|
+
dropped.add("order_items", "projection_order_item_missing_product_id");
|
|
3860
|
+
return [];
|
|
3861
|
+
}
|
|
3540
3862
|
return [
|
|
3541
3863
|
{
|
|
3542
3864
|
name: i.name,
|
|
@@ -3554,6 +3876,18 @@ const wholefoodsOrders = {
|
|
|
3554
3876
|
},
|
|
3555
3877
|
];
|
|
3556
3878
|
});
|
|
3879
|
+
// Whole Foods item_count counts lines. Only a known zero proves an
|
|
3880
|
+
// order with no usable line is empty; otherwise it is a detail gap.
|
|
3881
|
+
const itemCount = o.item_count;
|
|
3882
|
+
const knownCount = typeof itemCount === "number" &&
|
|
3883
|
+
Number.isSafeInteger(itemCount) &&
|
|
3884
|
+
itemCount >= 0;
|
|
3885
|
+
if (items.length === 0 && !(knownCount && itemCount === 0)) {
|
|
3886
|
+
dropped.add("orders", GROCERY_ORDER_ITEMS_GAP);
|
|
3887
|
+
return [];
|
|
3888
|
+
}
|
|
3889
|
+
if (!knownCount || items.length !== itemCount)
|
|
3890
|
+
countMismatches += 1;
|
|
3557
3891
|
return [
|
|
3558
3892
|
{
|
|
3559
3893
|
orderId: o.id,
|
|
@@ -3578,6 +3912,13 @@ const wholefoodsOrders = {
|
|
|
3578
3912
|
},
|
|
3579
3913
|
];
|
|
3580
3914
|
});
|
|
3915
|
+
const diagnostics = [
|
|
3916
|
+
...dropped.diagnostics(),
|
|
3917
|
+
...partialRows("wholefoods.orders", "orders", "projection_order_item_count_mismatch", countMismatches),
|
|
3918
|
+
];
|
|
3919
|
+
if (sourceOrders.length > 0 && orders.length === 0) {
|
|
3920
|
+
return groceryRejection("wholefoods.orders", diagnostics);
|
|
3921
|
+
}
|
|
3581
3922
|
return {
|
|
3582
3923
|
ok: true,
|
|
3583
3924
|
payload: {
|
|
@@ -3585,6 +3926,7 @@ const wholefoodsOrders = {
|
|
|
3585
3926
|
totalOrders: orders.length,
|
|
3586
3927
|
totalItems: orders.reduce((sum, o) => sum + o.items.length, 0),
|
|
3587
3928
|
},
|
|
3929
|
+
...withDiagnostics(diagnostics),
|
|
3588
3930
|
};
|
|
3589
3931
|
},
|
|
3590
3932
|
};
|
|
@@ -3786,10 +4128,16 @@ const icloudNoteFolders = {
|
|
|
3786
4128
|
};
|
|
3787
4129
|
},
|
|
3788
4130
|
};
|
|
4131
|
+
const ICLOUD_NOTES_PROFILE_PROVENANCE = {
|
|
4132
|
+
kind: "collection-profile",
|
|
4133
|
+
ref: "https://github.com/PDP-Connect/data-connectors/tree/3ac4dd636938fbff9168eb662d2300eb85554801/connectors/icloud_notes",
|
|
4134
|
+
digest: "sha256:ad6d28dac69a84019be2c81ac5f1f2ae8fa9c6602eb179370f0bb9d9022e33b2",
|
|
4135
|
+
path: "declarations/icloud-notes-0.1.24.collection-profile.json#streams[name=profile,notes,folders]",
|
|
4136
|
+
};
|
|
3789
4137
|
const icloudNotes = {
|
|
3790
4138
|
scope: "icloud_notes.notes",
|
|
3791
4139
|
pdppSource: "https://registry.pdpp.dev/connectors/icloud-notes",
|
|
3792
|
-
pdppStreams: ["notes", "folders"],
|
|
4140
|
+
pdppStreams: ["notes", "folders", "profile"],
|
|
3793
4141
|
legacySchemaPath: "icloud_notes.notes.json",
|
|
3794
4142
|
fieldsRead: {
|
|
3795
4143
|
notes: [
|
|
@@ -3804,27 +4152,29 @@ const icloudNotes = {
|
|
|
3804
4152
|
"text_content",
|
|
3805
4153
|
],
|
|
3806
4154
|
folders: ["id", "name"],
|
|
4155
|
+
profile: ["id", "user_name"],
|
|
3807
4156
|
},
|
|
3808
|
-
primaryKey: { notes: ["id"], folders: ["id"] },
|
|
4157
|
+
primaryKey: { notes: ["id"], folders: ["id"], profile: ["id"] },
|
|
3809
4158
|
lossy: [
|
|
3810
|
-
"userName is
|
|
4159
|
+
"userName is null when the profile stream has no readable user_name (legacy's fullName default); it is never inferred from an Apple ID or the account id.",
|
|
3811
4160
|
"folder uses the legacy display-name shape; unresolved folder references fall back to folder_id.",
|
|
3812
4161
|
],
|
|
3813
|
-
provenance: [
|
|
3814
|
-
{
|
|
3815
|
-
kind: "oci",
|
|
3816
|
-
ref: "ghcr.io/pdp-connect/connector/icloud-notes:0.1.0",
|
|
3817
|
-
digest: "sha256:fe5497bb79b0d567c30790cc71b1e591b955fe474725679b996441bd70c09944",
|
|
3818
|
-
path: "declarations/icloud-notes.collection-profile.json#streams[name=notes,folders]",
|
|
3819
|
-
},
|
|
3820
|
-
],
|
|
4162
|
+
provenance: [ICLOUD_NOTES_PROFILE_PROVENANCE],
|
|
3821
4163
|
project(records, options) {
|
|
3822
4164
|
const guard = requireStreams("icloud_notes.notes", options, [
|
|
3823
4165
|
"notes",
|
|
3824
4166
|
"folders",
|
|
4167
|
+
"profile",
|
|
3825
4168
|
]);
|
|
3826
4169
|
if (guard)
|
|
3827
4170
|
return guard;
|
|
4171
|
+
// The profile stream is a singleton: legacy's fullName maps to exactly one
|
|
4172
|
+
// account. No row, several rows, or an unreadable name leave userName null.
|
|
4173
|
+
const profiles = byStream(records, "profile");
|
|
4174
|
+
const profileName = profiles.length === 1 ? profiles[0]?.user_name : undefined;
|
|
4175
|
+
const userName = typeof profileName === "string" && profileName.trim().length > 0
|
|
4176
|
+
? profileName
|
|
4177
|
+
: null;
|
|
3828
4178
|
const folders = new Map();
|
|
3829
4179
|
for (const folder of byStream(records, "folders")) {
|
|
3830
4180
|
if (typeof folder.id !== "string" ||
|
|
@@ -3890,10 +4240,91 @@ const icloudNotes = {
|
|
|
3890
4240
|
}
|
|
3891
4241
|
return {
|
|
3892
4242
|
ok: true,
|
|
3893
|
-
payload: { notes, total: notes.length, userName
|
|
4243
|
+
payload: { notes, total: notes.length, userName },
|
|
3894
4244
|
};
|
|
3895
4245
|
},
|
|
3896
4246
|
};
|
|
4247
|
+
// ---------------------------------------------------------------------------
|
|
4248
|
+
// strava (strava.activities, strava.coverage_diagnostics)
|
|
4249
|
+
// declarations/strava.collection-profile.json (1.0.8, digest-pinned)
|
|
4250
|
+
// connector_id: https://registry.pdpp.dev/connectors/strava
|
|
4251
|
+
//
|
|
4252
|
+
// Desktop's export served each scope as `{ <stream>: [record.data, ...] }`
|
|
4253
|
+
// (apps/desktop pdpp_export_projection.rs at e970213f). Both bindings are
|
|
4254
|
+
// that identity: every native field, null and unknown field is kept, and the
|
|
4255
|
+
// two streams never read each other.
|
|
4256
|
+
// ---------------------------------------------------------------------------
|
|
4257
|
+
const STRAVA_DIGEST = "sha256:b33086156d3003d0cbe48cb0de4adf48a7fde7241f28c2ea5e350cff1f17c82c";
|
|
4258
|
+
const STRAVA_FIELDS = {
|
|
4259
|
+
activities: [
|
|
4260
|
+
"id",
|
|
4261
|
+
"activity_type",
|
|
4262
|
+
"start_date",
|
|
4263
|
+
"start_time",
|
|
4264
|
+
"start_time_basis",
|
|
4265
|
+
"distance_m",
|
|
4266
|
+
"moving_time_s",
|
|
4267
|
+
"elapsed_time_s",
|
|
4268
|
+
"total_elevation_gain_m",
|
|
4269
|
+
"average_heartrate",
|
|
4270
|
+
"max_heartrate",
|
|
4271
|
+
"calories_kcal",
|
|
4272
|
+
"gear",
|
|
4273
|
+
"freshness",
|
|
4274
|
+
"exported_at",
|
|
4275
|
+
],
|
|
4276
|
+
coverage_diagnostics: [
|
|
4277
|
+
"id",
|
|
4278
|
+
"stream",
|
|
4279
|
+
"status",
|
|
4280
|
+
"reason",
|
|
4281
|
+
"record_count",
|
|
4282
|
+
"fields_unavailable",
|
|
4283
|
+
"window_requested_from",
|
|
4284
|
+
"window_requested_to",
|
|
4285
|
+
"window_covered_from",
|
|
4286
|
+
"window_covered_to",
|
|
4287
|
+
"freshness",
|
|
4288
|
+
"exported_at",
|
|
4289
|
+
],
|
|
4290
|
+
};
|
|
4291
|
+
function stravaIdentity(stream, lossy) {
|
|
4292
|
+
const scope = `strava.${stream}`;
|
|
4293
|
+
return {
|
|
4294
|
+
scope,
|
|
4295
|
+
pdppSource: "https://registry.pdpp.dev/connectors/strava",
|
|
4296
|
+
pdppStreams: [stream],
|
|
4297
|
+
legacySchemaPath: `${scope}.json`,
|
|
4298
|
+
fieldsRead: { [stream]: [...STRAVA_FIELDS[stream]] },
|
|
4299
|
+
primaryKey: { [stream]: ["id"] },
|
|
4300
|
+
lossy,
|
|
4301
|
+
rowOrder: "arrival",
|
|
4302
|
+
provenance: [
|
|
4303
|
+
{
|
|
4304
|
+
kind: "oci",
|
|
4305
|
+
ref: "ghcr.io/pdp-connect/connector/strava:1.0.8",
|
|
4306
|
+
digest: STRAVA_DIGEST,
|
|
4307
|
+
path: `declarations/strava.collection-profile.json#streams[name=${stream}]`,
|
|
4308
|
+
},
|
|
4309
|
+
],
|
|
4310
|
+
project(records, options) {
|
|
4311
|
+
const guard = requireStreams(scope, options, [stream]);
|
|
4312
|
+
if (guard)
|
|
4313
|
+
return guard;
|
|
4314
|
+
return { ok: true, payload: { [stream]: byStream(records, stream) } };
|
|
4315
|
+
},
|
|
4316
|
+
};
|
|
4317
|
+
}
|
|
4318
|
+
const stravaActivities = stravaIdentity("activities", [
|
|
4319
|
+
"No field is dropped, derived or defaulted: the body is the native records.",
|
|
4320
|
+
"Row order: Desktop's snapshot was in arrival order (a re-emitted id, changed or not, moved to the end). The binding keeps its input order; the read facade supplies arrival order and reports rows it cannot place.",
|
|
4321
|
+
"Coverage: the body carries rows only, as Desktop's did. Whether the export was read in full is producer data in strava.coverage_diagnostics; this scope neither reads nor judges it.",
|
|
4322
|
+
]);
|
|
4323
|
+
const stravaCoverageDiagnostics = stravaIdentity("coverage_diagnostics", [
|
|
4324
|
+
"No field is dropped, derived or defaulted: the body is the native records, so a partial or empty status, its reason and fields_unavailable reach the reader as written.",
|
|
4325
|
+
"Row order: as for strava.activities.",
|
|
4326
|
+
"Absence of a row is not evidence of a complete collection: the producer emits no diagnostic when only diagnostics are requested and the import directory exists. The read facade serves an empty list only with that stream's own completion receipt.",
|
|
4327
|
+
]);
|
|
3897
4328
|
export const LEGACY_SCOPE_BINDINGS = new Map(Object.entries({
|
|
3898
4329
|
"instagram.profile": instagramProfile,
|
|
3899
4330
|
"instagram.posts": instagramPosts,
|
|
@@ -3938,6 +4369,8 @@ export const LEGACY_SCOPE_BINDINGS = new Map(Object.entries({
|
|
|
3938
4369
|
"youtube.history": youtubeHistory,
|
|
3939
4370
|
"claude.conversations": claudeConversations,
|
|
3940
4371
|
"claude.projects": claudeProjects,
|
|
4372
|
+
"strava.activities": stravaActivities,
|
|
4373
|
+
"strava.coverage_diagnostics": stravaCoverageDiagnostics,
|
|
3941
4374
|
}));
|
|
3942
4375
|
/** Known legacy contracts checked against signed declarations but not
|
|
3943
4376
|
* projectable without fabricating fields or changing the retained contract. */
|