fit-file-parser 5.2.0 → 6.0.2

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/CHANGELOG.md CHANGED
@@ -1,195 +1,26 @@
1
1
  # Change Log
2
2
 
3
- ## Unreleased
4
-
5
- ### Added
6
-
7
- - Add opt-in, message-filterable `raw_messages` output with exact native and
8
- developer-field bytes, wire base types, endianness, message occurrence, and
9
- reconstructed compressed timestamps. Existing decoded output remains
10
- unchanged when the option is disabled.
11
- - Add `rawMessagesOnly` for bounded metadata consumers that do not need decoded
12
- activity collections alongside retained messages.
13
-
14
- ### Fixed
15
-
16
- - Decode compressed timestamp records without consuming bytes for the omitted
17
- timestamp field.
18
-
19
- ## 5.1.0
20
-
21
- ### Added
22
-
23
- - Add opt-in, message-filterable `raw_developer_fields` output with exact
24
- developer-field bytes, global-message occurrence, developer-data index, and
25
- field-definition number. Existing decoded output remains unchanged when the
26
- option is disabled.
27
-
28
- ## 5.0.2
29
-
30
- ### Fixed
31
-
32
- - Clarify the 5.0 migration for standard session field 196: use canonical
33
- `metabolic_calories` rather than the old duplicate `resting_calories` label.
34
- - Clarify that the removed guessed `recovery_advisor` label was standard
35
- session field 140, which the SDK defines as `avg_depth` in meters.
36
-
37
- ## 5.0.1
38
-
39
- ### Fixed
40
-
41
- - Run CI and version-driven npm publishing from the repository's `main`
42
- branch, with a guarded manual recovery path for a missed release.
43
-
44
- ## 5.0.0
3
+ ## 6.0.2 - 2026-09-21
45
4
 
46
5
  ### Changed
47
6
 
48
- - Generate the complete standard message, field, and type surface from the
49
- pinned Garmin FIT SDK without handwritten standard-profile overrides.
50
- - Preserve SDK alphanumeric tokens when generating public `snake_case` names,
51
- including `n2`, `po2`, `time128`, and Garmin product identifiers.
52
- - Restrict non-SDK support to a collision-checked allowlist of Garmin fields
53
- and private messages observed in the external FIT corpus.
54
- - Emit only parsed FIT values: Garmin product names are no longer inferred,
55
- and record elapsed/timer values are generated only when
56
- `elapsedRecordField: true` is requested.
57
-
58
- ### Breaking
59
-
60
- - Remove compatibility aliases such as `resting_calories`,
61
- `recovery_advisor`, `repeat_dive_time`, and deprecated time-in-zone names.
62
- - Remove guessed stress fields, handwritten enum/type additions, placeholder
63
- zero-mask names, and the misspelled `hip_swing_excercise_name` type.
64
- - Use the pinned SDK's `cadence_zone_high_bondary` spelling and generated
65
- alphanumeric field/type spellings instead of manual corrections.
66
- - Correct generated declarations to expose runtime `Date` values, numeric FIT
67
- `bool` fields and unknown enum IDs, decoded mask objects, nullable array
68
- entries, and optional profile fields.
69
- - Keep undated cascade rows in file order and avoid duplicating them when a lap
70
- or session boundary timestamp is absent.
71
-
72
- ## 4.1.1
73
-
74
- ### Fixed
75
-
76
- - Apply Garmin FIT profile scale and offset to primitive numeric fields, while
77
- retaining semicircle conversion only for fields declared in semicircles.
78
- - Apply FIT developer-field offsets with the parser's equivalent signed form,
79
- including scaled signed developer values.
80
- - Decode native diving ascent-rate fields as meters per second instead of
81
- geographic coordinates.
82
- - Decode native dive depth and bottom-time fields with their Garmin FIT SDK
83
- scale instead of exposing their encoded integer representation.
84
- - Keep the pinned Garmin SDK authoritative for standard field types, arrays,
85
- scales, offsets, and units while retaining compatible public field names.
86
-
87
- ## 4.1.0
7
+ - Consolidate the complete message, field, wire-metadata, type, enum, and
8
+ product contract into one static maintained profile.
9
+ - Maintain the FIT interoperability profile directly as static project source.
10
+ - Preserve decoded output and the public TypeScript surface.
88
11
 
89
12
  ### Added
90
13
 
91
- - Generate all 124 standard messages, 1,406 fields, and 200 profile types from
92
- the exactly pinned Garmin FIT SDK 21.208.0 profile.
93
- - Retain every recognized message in file order under the typed
94
- `ParsedFit.messages` index without changing existing list, cascade, or
95
- singleton outputs.
96
- - Decode Garmin strength-training `set` messages in list and cascade modes.
97
- - Add reproducible generated-profile and privacy-safe external corpus audits,
98
- and enforce profile freshness and coverage in CI.
99
-
100
- ### Fixed
101
-
102
- - Decode fields from their wire base types, including compatible developer
103
- enum/uint8/byte definitions and correctly sized numeric arrays.
104
- - Reconstruct compressed timestamps and keep timestamp state isolated between
105
- parser instances.
106
- - Accept omitted header CRCs, validate file CRCs across the complete FIT header
107
- and data section, and report strict header and file CRC failures explicitly.
108
- - Reject structurally unsafe FIT inputs consistently in callback and Promise
109
- APIs while retaining force-mode recovery for CRC corruption.
110
- - Correct Garmin profile mappings for OHR settings, monitoring HR, sleep,
111
- time-in-zone, altitude offsets, and lap/segment flow and grit summaries.
112
- - Correct Celsius-to-Kelvin conversion, add `celsius` as the canonical
113
- temperature unit, and retain `°C` as a supported alias.
114
-
115
- ### Compatibility and documentation
116
-
117
- - Preserve compatible legacy field names, scales, value shapes, parser
118
- signatures, output modes, and date behavior while adding canonical profile
119
- names.
120
- - Add regression coverage for temperature, pressure, validation, compressed
121
- timestamps, generated profile messages, repeated messages, and MTB
122
- flow/grit data.
123
- - Refresh the README with current runtime, API, units, output modes, developer
124
- fields, encoder behavior, and repository commands.
125
-
126
- ## 4.0.2
127
-
128
- - Preserve record alignment when developer-field descriptions are missing or
129
- appear after their message definitions.
130
- - Decode subsequent developer-field values once their descriptions become
131
- available, in both strict and force modes.
132
-
133
- ## 4.0.0
134
-
135
- ### FIT decoder performance
136
-
137
- - Parse `ArrayBuffer` inputs and exact Node.js `Buffer` views directly, avoiding a full copy of the source file.
138
- - Reuse one parse-local `DataView` instead of allocating temporary views for each supported endian field.
139
- - Cache standard field metadata and enum/mask lookups on reusable message definitions.
140
- - Reuse raw field storage for records that share a local message definition.
141
- - Generate elapsed and timer fields once per record instead of once per decoded field.
142
- - Skip header and file CRC scans in `force: true` mode, where CRC mismatches are intentionally ignored; strict mode continues to validate both CRCs.
143
- - Preserve legacy malformed-field zero-padding, field-boundary, developer-field, invalid-value, and offset-buffer behavior.
144
-
145
- ### Measured benefit
146
-
147
- - Suunto 93-hour / 7.5 MB FIT decoding: 1.057 s to 0.375 s, a 64.5% reduction.
148
- - Garmin 110-hour / 28.6 MB FIT decoding: 3.643 s to 1.241 s, a 65.9% reduction.
149
- - Input-related array-buffer memory is approximately halved by removing the full source copy:
150
- - Suunto: 15.0 MB to 7.5 MB.
151
- - Garmin: 57.2 MB to 28.6 MB.
152
-
153
- There are no intentional parsed-output or public API changes in this release. Output parity was verified across 162 checked-in fixture/mode combinations, 8,320 generated malformed endian-definition cases, and both private long-duration benchmark files.
154
-
155
- ## 3.1.0
156
-
157
- - Add the public `FitEncoder` API for writing FIT headers, definitions, data messages, and CRCs.
158
- - Preserve the `course.sub_sport` field while parsing FIT course files.
159
-
160
- <a name="1.5.4"></a>
161
-
162
- ## [1.5.4](https://github.com/jimmykane/fit-parser/compare/v1.0.0...v1.5.3) (2019-03-01)
163
-
164
- - **Features**: HRV, Developer fields, devices and much more
165
- - **Miscellaneous**: Fix most of the issues parsing fit files
166
-
167
- <a name="1.0.1"></a>
168
-
169
- ## [1.0.1](https://github.com/pierremtb/easy-fit/compare/v1.0.0...v1.0.1) (2018-09-18)
170
-
171
- ### 😭 Unclassified (not [following convention](https://github.com/sportheroes/bk-conventional-changelog#types-of-commits))
172
-
173
- - **Miscellaneous**: fix: Applied to src/fit offset adjustments from commit 9ed802 ([70b3eb6](https://github.com/pierremtb/easy-fit/commit/70b3eb6) - [JoeTheFkingFrypan](https://github.com/JoeTheFkingFrypan))
174
-
175
- <a name="1.0.0"></a>
176
-
177
- # [1.0.0](https://github.com/pierremtb/easy-fit/compare/0.0.7...1.0.0) (2018-09-17)
178
-
179
- ### 😭 Unclassified (not [following convention](https://github.com/sportheroes/bk-conventional-changelog#types-of-commits))
180
-
181
- - **Miscellaneous**: Fix readme typo ([2282c06](https://github.com/pierremtb/easy-fit/commit/2282c06)))
182
- - **Miscellaneous**: chore: preparing to release fork internally ([3eef5f7](https://github.com/pierremtb/easy-fit/commit/3eef5f7) - [JoeTheFkingFrypan](https://github.com/JoeTheFkingFrypan))
183
- - **Miscellaneous**: fix: Typo leading to uint16 to never be invalidated ([9ed802c](https://github.com/pierremtb/easy-fit/commit/9ed802c) - [JoeTheFkingFrypan](https://github.com/JoeTheFkingFrypan))
184
- - **Miscellaneous**: Merge remote-tracking branch 'jenglert/master' ([819d78e](https://github.com/pierremtb/easy-fit/commit/819d78e)))
185
- - **Miscellaneous**: fix missing buffer dependency and compile dst ([a4b237a](https://github.com/pierremtb/easy-fit/commit/a4b237a)))
186
- - **Miscellaneous**: Merge remote-tracking branch 'FrostDigital/master' ([6fa9258](https://github.com/pierremtb/easy-fit/commit/6fa9258)))
187
- - **Miscellaneous**: Makes parsing of fit files with developer defined fields possible (#1) ([9aea666](https://github.com/pierremtb/easy-fit/commit/9aea666))), closes [#1](https://github.com/pierremtb/easy-fit/issues/1)
188
- - **Miscellaneous**: fix: Adjusted offset for all altitude-related fields ([1ff0fa4](https://github.com/pierremtb/easy-fit/commit/1ff0fa4) - [JoeTheFkingFrypan](https://github.com/JoeTheFkingFrypan))
189
- - **Miscellaneous**: chore(devices): add forerunner 735 xt ([aacaa04](https://github.com/pierremtb/easy-fit/commit/aacaa04)))
190
- - **Miscellaneous**: Compile src ([ee4aa00](https://github.com/pierremtb/easy-fit/commit/ee4aa00)))
191
- - **Miscellaneous**: Add support for other lap types ([dcf42a1](https://github.com/pierremtb/easy-fit/commit/dcf42a1)))
192
- - **Miscellaneous**: Compile src ([50b9b21](https://github.com/pierremtb/easy-fit/commit/50b9b21)))
193
- - **Miscellaneous**: Merge remote-tracking branch 'pierremtb/master' ([f7f5ff0](https://github.com/pierremtb/easy-fit/commit/f7f5ff0)))
194
- - **Miscellaneous**: Add support for more manufacturers ([2d122d2](https://github.com/pierremtb/easy-fit/commit/2d122d2)))
195
- - **Miscellaneous**: Really compiled last changes on binary.js this time, small details on package.json file -> bumped to 0.0.8 ([e5dc98f](https://github.com/pierremtb/easy-fit/commit/e5dc98f)))
14
+ - Add opt-in `unmapped_messages` preservation, including exact bytes and
15
+ definition metadata, without changing default parsed output.
16
+ - Add regression coverage for unknown messages, unknown fields inside known
17
+ messages, the immutable compatibility profile, and default-output stability.
18
+ - Add an optional privacy-safe corpus summary for unmapped message/field IDs,
19
+ wire base types, sizes, occurrences, and file counts.
20
+ - Document the manual update requirements and corpus verification workflow in
21
+ `PROFILE.md`.
22
+
23
+ There are no intentional parsed-output or public API changes in this release.
24
+ Output parity was verified across 162 checked-in fixture/mode combinations,
25
+ 8,320 generated malformed endian-definition cases, and both private
26
+ long-duration benchmark files.
package/PROFILE.md ADDED
@@ -0,0 +1,43 @@
1
+ # FIT Profile Maintenance
2
+
3
+ The parser uses one static interoperability table in `src/profile.ts` for
4
+ normal installs, builds, tests, and releases. It contains message and field
5
+ identifiers, names, wire types, array behavior, scales, offsets, units, enum
6
+ labels, and product identifiers. `npm run profile:check` pins full-table
7
+ fingerprints and the resulting message, field, type, enum-value, and product
8
+ counts so accidental drift fails CI.
9
+
10
+ ## Unknown and newer fields
11
+
12
+ A FIT definition contains enough wire metadata to retain a field even when the
13
+ maintained table cannot assign it a semantic name. Pass
14
+ `includeUnmappedMessages: true` to expose unmapped native or unresolved
15
+ developer fields in `unmapped_messages`, including:
16
+
17
+ - global message number and occurrence index;
18
+ - endianness and reconstructed compressed timestamp, when applicable;
19
+ - field-definition number and FIT base-type byte;
20
+ - an exact copy of the field's wire bytes.
21
+
22
+ The opt-in keeps default decoded results unchanged. Consumers that need a
23
+ complete raw representation of selected messages can continue to use
24
+ `includeRawMessages`; `unmapped_messages` stores only the fields the maintained
25
+ table cannot decode.
26
+
27
+ ## Updating the table
28
+
29
+ Every mapping change requires focused regression coverage. Do not guess
30
+ adjacent identifiers or add a mapping solely to make `unmapped_messages`
31
+ disappear. Update `src/profile.ts`, the relevant tests, and the public types,
32
+ then verify the complete compatibility contract:
33
+
34
+ ```sh
35
+ npm run codegen
36
+ npm run profile:check
37
+ npm run check
38
+ npm run compatibility:check -- /path/to/reference-package /path/to/fit-corpus
39
+ npm run corpus:check -- ../FIT-test-files --allow-force-recovery
40
+ ```
41
+
42
+ `src/fit_types.ts` is generated from the maintained table and must not be
43
+ edited manually.
package/README.md CHANGED
@@ -15,6 +15,8 @@ including developer-defined data.
15
15
  - Convert speed, length, temperature, and pressure fields to preferred units.
16
16
  - Decode developer fields while preserving record alignment when descriptions
17
17
  arrive after their definitions.
18
+ - Optionally preserve every unmapped native or unresolved developer field in
19
+ `unmapped_messages` with its exact wire bytes.
18
20
  - Encode profile-agnostic FIT messages with validated field definitions and
19
21
  CRCs.
20
22
  - Use ESM or CommonJS with bundled TypeScript declarations.
@@ -29,56 +31,12 @@ including developer-defined data.
29
31
  npm install fit-file-parser
30
32
  ```
31
33
 
32
- ## Migrating from 4.x to 5.0
33
-
34
- Version 5.0 is a breaking release because parsed output now follows the pinned
35
- Garmin FIT SDK profile without compatibility aliases or guessed fields. Parser
36
- construction, module imports, output modes, and parser options are unchanged.
37
-
38
- Update field access, destructuring, persisted schemas, and snapshots to use the
39
- SDK-backed names. Common migrations include:
40
-
41
- | 4.x name | 5.0 name |
42
- | ------------------------------ | ---------------------------- |
43
- | `speed_1s` | `speed1s` |
44
- | `start_n_2`, `end_n_2` | `start_n2`, `end_n2` |
45
- | `o_2_toxicity` | `o2_toxicity` |
46
- | `avg_spo_2`, `reading_spo_2` | `avg_spo2`, `reading_spo2` |
47
- | `po_2` | `po2` |
48
- | `cycle_length_16` | `cycle_length16` |
49
- | `map_3_sample_mean` | `map3_sample_mean` |
50
- | `time_256` | `time256` |
51
- | `last_night_5_min_high` | `last_night5_min_high` |
52
- | `average_7_day_deviation` | `average7_day_deviation` |
53
- | `spo_2_data`, `hsa_spo_2_data` | `spo2_data`, `hsa_spo2_data` |
54
- | `repeat_dive_time` | `repeat_dive_interval` |
55
- | `cadence_zone_high_boundary` | `cadence_zone_high_bondary` |
56
- | `HipSwingExcerciseName` | `HipSwingExerciseName` |
57
- | `resting_calories` | `metabolic_calories` |
58
-
59
- The same alphanumeric-token rule applies to enum strings, for example
60
- `camera_orientation_90` becomes `camera_orientation90`, `po_2_warn` becomes
61
- `po2_warn`, and `power_3s` becomes `power3s`. The generated TypeScript
62
- declarations are the exhaustive name and value reference for the pinned SDK.
63
-
64
- Parser 4 exposed standard session field 196 (`metabolic_calories`) a second
65
- time as `resting_calories`; use the canonical `metabolic_calories` name in 5.0.
66
- `recovery_advisor` was a guessed label for standard session field 140, whose
67
- canonical SDK field is `avg_depth` in meters. Other behavior to account for
68
- during migration:
69
-
70
- - `product_name` is emitted only when it exists in the FIT input. It is no
71
- longer inferred from `manufacturer` and `product`.
72
- - Record `elapsed_time` and `timer_time` require
73
- `elapsedRecordField: true`.
74
- - FIT timestamps are typed as `Date`, FIT `bool` values remain numeric, mask
75
- fields are `{ value, ...flags }` objects, unknown enum values remain numbers,
76
- and array entries may be `null` when the FIT invalid sentinel is retained.
77
- - Every profile field is optional because individual FIT message definitions
78
- determine which fields are present.
79
- - When upgrading from 4.1.0 or earlier, remove application-side scale or offset
80
- corrections for parsed numeric values, including dive depth, bottom time,
81
- ascent rate, and developer fields. The parser now applies the SDK metadata.
34
+ ## Profile coverage
35
+
36
+ The static profile contains 126 messages, 1,444 fields, 200 types, and 4,403
37
+ mapped values. Pass `includeUnmappedMessages: true` to retain fields outside
38
+ that maintained surface by number and exact bytes; applications that need all
39
+ bytes for recognized messages can also opt into `raw_messages`.
82
40
 
83
41
  ## Quick start
84
42
 
@@ -186,6 +144,31 @@ provider-specific semantics. Pass `true` to retain developer fields from every
186
144
  global message, or an array such as `[18]` to bound collection to specific FIT
187
145
  message numbers.
188
146
 
147
+ ### Opt-in preservation of unmapped fields
148
+
149
+ Set `includeUnmappedMessages: true` to return fields that are absent from the
150
+ maintained profile or belong to an unknown global message in
151
+ `unmapped_messages`. Only unmapped fields are retained, so normal known fields
152
+ are not duplicated:
153
+
154
+ ```javascript
155
+ const data = await new FitParser({ includeUnmappedMessages: true })
156
+ .parseAsync(content)
157
+
158
+ for (const message of data.unmapped_messages ?? []) {
159
+ console.log({
160
+ globalMessageNumber: message.global_message_number,
161
+ messageIndex: message.message_index,
162
+ fields: message.fields,
163
+ developerFields: message.developer_fields,
164
+ })
165
+ }
166
+ ```
167
+
168
+ Each entry uses the same wire-level field representation as `raw_messages`.
169
+ This opt-in ensures a newer FIT field cannot disappear merely because it
170
+ does not yet have a reviewed semantic mapping.
171
+
189
172
  ### Lossless selected messages
190
173
 
191
174
  Set `includeRawMessages` when a consumer needs native FIT numeric codes rather
@@ -256,16 +239,14 @@ unchanged.
256
239
 
257
240
  ## Profile-backed output
258
241
 
259
- Standard message names, field names, enum values, wire types, scales, offsets,
260
- arrays, and units come from the exactly pinned Garmin FIT SDK profile. Public
261
- names use the parser's generated `snake_case` form while preserving SDK
262
- alphanumeric tokens such as `n2`, `po2`, and `time128`. The parser does not add
263
- compatibility aliases for alternate field spellings.
242
+ Recognized message names, field names, enum values, wire types, scales,
243
+ offsets, arrays, and units come from the community-maintained table in
244
+ `src/profile.ts`. Its maintenance and verification contract is documented in
245
+ [`PROFILE.md`](./PROFILE.md).
264
246
 
265
- The small vendor extension table contains only Garmin fields and private
266
- messages observed in the external FIT corpus. Extensions cannot replace a
267
- standard SDK field or type value; the profile audit rejects collisions and
268
- unregistered additions.
247
+ Public names use `snake_case` while preserving established alphanumeric tokens
248
+ such as `n2`, `po2`, and `time128`. Unmapped fields remain available by number
249
+ and exact bytes in `unmapped_messages` rather than receiving guessed names.
269
250
 
270
251
  Only values present in the FIT input are emitted. In particular,
271
252
  `product_name` is not inferred from `manufacturer` and `product`, and record
@@ -363,21 +344,22 @@ const {
363
344
 
364
345
  Run commands from the repository root.
365
346
 
366
- | Command | Purpose |
367
- | ---------------------------------- | -------------------------------------------------- |
368
- | `npm ci` | Install locked dependencies. |
369
- | `npm run build` | Build ESM and CommonJS output. |
370
- | `npm test -- --run` | Run the complete test suite once. |
371
- | `npm test -- --run test/<file>.ts` | Run a focused test file. |
372
- | `npm run codegen` | Regenerate the Garmin profile and public types. |
373
- | `npm run codegen:check` | Verify both generated files are current. |
374
- | `npm run profile:audit` | Audit SDK profile coverage and private overlays. |
375
- | `npm run corpus:check -- <path>` | Validate an external FIT corpus without file data. |
376
- | `npm run lint` | Check lint and formatting rules. |
377
- | `npm run fmt` | Apply the configured formatting rules. |
378
- | `npm run type-check` | Run TypeScript without emitting files. |
379
- | `npm run examples` | Build and regenerate checked-in example outputs. |
380
- | `npm run check` | Run profile audit, lint, types, tests, and builds. |
347
+ | Command | Purpose |
348
+ | ---------------------------------- | --------------------------------------------------- |
349
+ | `npm ci` | Install locked dependencies. |
350
+ | `npm run build` | Build ESM and CommonJS output. |
351
+ | `npm run clean` | Remove the generated build output. |
352
+ | `npm test -- --run` | Run the complete test suite once. |
353
+ | `npm test -- --run test/<file>.ts` | Run a focused test file. |
354
+ | `npm run codegen` | Regenerate public types from the static profile. |
355
+ | `npm run codegen:check` | Verify generated public types are current. |
356
+ | `npm run profile:check` | Audit profile counts, structure, and fingerprints. |
357
+ | `npm run corpus:check -- <path>` | Validate an external FIT corpus without file data. |
358
+ | `npm run lint` | Check lint and formatting rules. |
359
+ | `npm run fmt` | Apply the configured formatting rules. |
360
+ | `npm run type-check` | Run TypeScript without emitting files. |
361
+ | `npm run examples` | Build and regenerate checked-in example outputs. |
362
+ | `npm run check` | Run profile checks, lint, types, tests, and builds. |
381
363
 
382
364
  ### External FIT corpus (optional)
383
365
 
@@ -388,16 +370,28 @@ aggregate-only validation command:
388
370
  ```sh
389
371
  git clone https://github.com/ThomasKuehne/FIT-test-files.git ../FIT-test-files
390
372
  npm run corpus:check -- ../FIT-test-files --allow-force-recovery
373
+ npm run corpus:check -- ../FIT-test-files --allow-force-recovery --raw-messages
374
+ npm run corpus:check -- ../FIT-test-files --allow-force-recovery --raw-messages-with-decoded-output
375
+ npm run compatibility:check -- /path/to/reference-package ../FIT-test-files
391
376
  ```
392
377
 
393
378
  The command accepts any corpus path; the sibling location is only a convenient
394
- convention. The corpus contains a known header-CRC failure that is expected to
395
- recover only in force mode. It reports aggregate counts and never prints file
396
- names or parsed activity data.
397
-
398
- Do not edit `src/garmin_profile.generated.ts` or `src/fit_types.ts` manually.
399
- Update the pinned SDK, audited vendor extensions, or a generator, then run
400
- `npm run codegen`.
379
+ convention. Add `--raw-messages` to exercise lossless raw-message-only parsing
380
+ for every message in each file, or `--raw-messages-with-decoded-output` to also
381
+ verify the ordinary decoded output path. Add `--unmapped-summary` for a
382
+ privacy-safe aggregate of unknown message/field identifiers, base types, sizes,
383
+ occurrences, and file counts. The corpus contains a known header-CRC failure
384
+ that is expected to recover only in force mode. Reports never print file names
385
+ or parsed activity values.
386
+
387
+ `compatibility:check` parses every corpus file with both the current build and
388
+ a reference package, compares their complete default outputs, and reports only
389
+ aggregate counts. Files rejected by both strict parsers are retried in force
390
+ mode.
391
+
392
+ Edit `src/profile.ts` only through reviewed profile changes with focused
393
+ regression coverage and a documented source. Do not edit `src/fit_types.ts`
394
+ manually; run `npm run codegen` after changing the maintained profile.
401
395
 
402
396
  Repository-specific automation guidance is tracked in
403
397
  [`.agent/README.md`](./.agent/README.md). More examples are available in the
package/dist/binary.d.ts CHANGED
@@ -40,6 +40,8 @@ export declare function readRecord(blob: Uint8Array, messageTypes: MessageTypeDe
40
40
  compressedTimestamp?: number;
41
41
  rawFields?: RawFieldValue[];
42
42
  rawDeveloperFields?: RawDeveloperFieldValue[];
43
+ unmappedFields?: RawFieldValue[];
44
+ unmappedDeveloperFields?: RawDeveloperFieldValue[];
43
45
  };
44
46
  export declare function getArrayBuffer(buffer: ArrayBuffer | Buffer): ArrayBuffer;
45
47
  export declare function calculateCRC(blob: Uint8Array, start: number, end: number): number;
package/dist/binary.js CHANGED
@@ -8,36 +8,14 @@ const GarminTimeOffset = 631065600000;
8
8
  const InvalidFieldData = Symbol('invalid FIT field data');
9
9
  const formatTypeMetadata = new Map();
10
10
  const uint8CompatibleTypes = new Set(['enum', 'uint8', 'byte']);
11
- const fitBaseTypeWidths = new Map([
12
- [0, 1],
13
- [1, 1],
14
- [2, 1],
15
- [3, 2],
16
- [4, 2],
17
- [5, 4],
18
- [6, 4],
19
- [7, 1],
20
- [8, 4],
21
- [9, 8],
22
- [10, 1],
23
- [11, 2],
24
- [12, 4],
25
- [13, 1],
26
- [14, 8],
27
- [15, 8],
28
- [16, 8],
29
- ]);
30
11
  function retainsRawMessages(options) {
31
12
  return options.includeRawMessages === true
32
13
  || Array.isArray(options.includeRawMessages);
33
14
  }
34
- function isValidRawFieldDefinition(size, baseType) {
35
- if (size <= 0 || (baseType & 0x60) !== 0) {
36
- return false;
37
- }
38
- const typeId = baseType & 0x1F;
39
- const width = fitBaseTypeWidths.get(typeId);
40
- return width !== undefined && (typeId === 7 || size % width === 0);
15
+ function retainsRawMessage(options, globalMessageNumber) {
16
+ return options.includeRawMessages === true
17
+ || (Array.isArray(options.includeRawMessages)
18
+ && options.includeRawMessages.includes(globalMessageNumber));
41
19
  }
42
20
  function baseTypeSize(type) {
43
21
  switch (type) {
@@ -480,18 +458,11 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
480
458
  rawData: [],
481
459
  };
482
460
  const message = getFitMessage(mTypeDef.globalMessageNumber);
483
- const nativeFieldNumbers = new Set();
484
461
  for (let i = 0; i < numberOfFields; i++) {
485
462
  const fDefIndex = startIndex + 6 + i * 3;
486
463
  const baseType = blob[fDefIndex + 2];
487
464
  const fieldNumber = blob[fDefIndex];
488
465
  const fieldSize = blob[fDefIndex + 1];
489
- if (retainsRawMessages(options)
490
- && (nativeFieldNumbers.has(fieldNumber)
491
- || !isValidRawFieldDefinition(fieldSize, baseType))) {
492
- throw new Error('Invalid FIT native field definition');
493
- }
494
- nativeFieldNumbers.add(fieldNumber);
495
466
  const wireType = FIT.types.fit_base_type[baseType];
496
467
  const { field, type, baseType: profileBaseType, array, scale, offset, units, } = message.getAttributes(blob[fDefIndex]);
497
468
  const profileCompatible = areProfileBaseTypesCompatible(profileBaseType, wireType);
@@ -515,15 +486,8 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
515
486
  };
516
487
  mTypeDef.fieldDefs.push(fDef);
517
488
  }
518
- const developerFieldNumbers = new Set();
519
489
  for (let i = 0; i < numberOfDeveloperDataFields; i++) {
520
490
  const fDefIndex = startIndex + 6 + numberOfFields * 3 + 1 + i * 3;
521
- const developerFieldKey = `${blob[fDefIndex + 2]}:${blob[fDefIndex]}`;
522
- if (retainsRawMessages(options)
523
- && (blob[fDefIndex + 1] === 0 || developerFieldNumbers.has(developerFieldKey))) {
524
- throw new Error('Invalid FIT developer field definition');
525
- }
526
- developerFieldNumbers.add(developerFieldKey);
527
491
  (_a = mTypeDef.developerFieldDefs) === null || _a === void 0 ? void 0 : _a.push({
528
492
  fieldDefinitionNumber: blob[fDefIndex],
529
493
  size: blob[fDefIndex + 1],
@@ -546,29 +510,20 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
546
510
  if (!messageType) {
547
511
  throw new Error('FIT data record has no local definition');
548
512
  }
549
- if (isCompressedTimestamp && retainsRawMessages(options)) {
550
- const timestampField = messageType.fieldDefs[0];
551
- if (!timestampField
552
- || timestampField.fDefNo !== 253
553
- || timestampField.size !== 4
554
- || (timestampField.baseTypeNo & 0x1F) !== 6) {
555
- throw new Error('Invalid FIT compressed timestamp definition');
556
- }
557
- }
558
513
  let messageSize = 0;
559
514
  let readDataFromIndex = startIndex + 1;
560
515
  const fields = {};
561
516
  const message = getFitMessage(messageType.globalMessageNumber);
562
517
  const developerFieldDefs = (_b = messageType.developerFieldDefs) !== null && _b !== void 0 ? _b : [];
563
518
  const totalFieldCount = messageType.fieldDefs.length + developerFieldDefs.length;
564
- const includeRawMessage = options.includeRawMessages === true
565
- || (Array.isArray(options.includeRawMessages)
566
- && options.includeRawMessages.includes(messageType.globalMessageNumber));
519
+ const includeRawMessage = retainsRawMessage(options, messageType.globalMessageNumber);
567
520
  const includeRawDeveloperFields = options.includeRawDeveloperFields === true
568
521
  || (Array.isArray(options.includeRawDeveloperFields)
569
522
  && options.includeRawDeveloperFields.includes(messageType.globalMessageNumber));
570
523
  const rawFields = includeRawMessage ? [] : undefined;
571
524
  const rawDeveloperFields = includeRawDeveloperFields || includeRawMessage ? [] : undefined;
525
+ const unmappedFields = options.includeUnmappedMessages ? [] : undefined;
526
+ const unmappedDeveloperFields = options.includeUnmappedMessages ? [] : undefined;
572
527
  if (retainsRawMessages(options)) {
573
528
  const nativeSize = messageType.fieldDefs.reduce((total, field, index) => (total + (isCompressedTimestamp && index === 0 && field.fDefNo === 253 ? 0 : field.size)), 0);
574
529
  const developerSize = developerFieldDefs.reduce((total, field) => total + field.size, 0);
@@ -584,12 +539,17 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
584
539
  rawData[i] = InvalidFieldData;
585
540
  continue;
586
541
  }
587
- if (rawFields && readDataFromIndex + fDef.size <= dataEnd) {
588
- rawFields.push({
542
+ if ((rawFields || (unmappedFields && !isOutputFieldName(fDef.name)))
543
+ && readDataFromIndex + fDef.size <= dataEnd) {
544
+ const rawField = {
589
545
  fieldDefinitionNumber: fDef.fDefNo,
590
546
  baseType: fDef.baseTypeNo,
591
547
  rawValue: Array.from(blob.subarray(readDataFromIndex, readDataFromIndex + fDef.size)),
592
- });
548
+ };
549
+ rawFields === null || rawFields === void 0 ? void 0 : rawFields.push(rawField);
550
+ if (!isOutputFieldName(fDef.name)) {
551
+ unmappedFields === null || unmappedFields === void 0 ? void 0 : unmappedFields.push(rawField);
552
+ }
593
553
  }
594
554
  const data = readData(blob, dataView, fDef, readDataFromIndex);
595
555
  if (data !== InvalidFieldData
@@ -613,15 +573,21 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
613
573
  for (let i = 0; i < developerFieldDefs.length; i++) {
614
574
  const developerFieldDef = developerFieldDefs[i];
615
575
  const rawDataIndex = messageType.fieldDefs.length + i;
616
- if (rawDeveloperFields
576
+ let rawDeveloperField;
577
+ if ((rawDeveloperFields
578
+ || (unmappedDeveloperFields && developerFieldDef.resolvedFieldDef === undefined))
617
579
  && readDataFromIndex + developerFieldDef.size <= dataEnd) {
618
- rawDeveloperFields.push({
580
+ rawDeveloperField = {
619
581
  developerDataIndex: developerFieldDef.developerDataIndex,
620
582
  fieldDefinitionNumber: developerFieldDef.fieldDefinitionNumber,
621
583
  rawValue: Array.from(blob.subarray(readDataFromIndex, readDataFromIndex + developerFieldDef.size)),
622
- });
584
+ };
585
+ rawDeveloperFields === null || rawDeveloperFields === void 0 ? void 0 : rawDeveloperFields.push(rawDeveloperField);
623
586
  }
624
587
  const fDef = resolveDeveloperFieldDefinition(developerFieldDef, messageType.littleEndian, developerFields, options);
588
+ if (!fDef && rawDeveloperField) {
589
+ unmappedDeveloperFields === null || unmappedDeveloperFields === void 0 ? void 0 : unmappedDeveloperFields.push(rawDeveloperField);
590
+ }
625
591
  if (fDef) {
626
592
  const data = readData(blob, dataView, fDef, readDataFromIndex);
627
593
  if (data !== InvalidFieldData
@@ -739,6 +705,10 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
739
705
  message: fields,
740
706
  rawFields,
741
707
  rawDeveloperFields,
708
+ unmappedFields: unmappedFields && unmappedFields.length > 0 ? unmappedFields : undefined,
709
+ unmappedDeveloperFields: unmappedDeveloperFields && unmappedDeveloperFields.length > 0
710
+ ? unmappedDeveloperFields
711
+ : undefined,
742
712
  };
743
713
  }
744
714
  export function getArrayBuffer(buffer) {
@@ -40,6 +40,8 @@ export declare function readRecord(blob: Uint8Array, messageTypes: MessageTypeDe
40
40
  compressedTimestamp?: number;
41
41
  rawFields?: RawFieldValue[];
42
42
  rawDeveloperFields?: RawDeveloperFieldValue[];
43
+ unmappedFields?: RawFieldValue[];
44
+ unmappedDeveloperFields?: RawDeveloperFieldValue[];
43
45
  };
44
46
  export declare function getArrayBuffer(buffer: ArrayBuffer | Buffer): ArrayBuffer;
45
47
  export declare function calculateCRC(blob: Uint8Array, start: number, end: number): number;