fit-file-parser 5.2.1 → 6.1.1

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,205 +1,49 @@
1
1
  # Change Log
2
2
 
3
- ## Unreleased
4
-
5
- ## 5.2.1
6
-
7
- ### Fixed
8
-
9
- - Preserve unusual vendor field definitions in lossless raw-message output so
10
- domain consumers can validate only the messages they understand.
11
- - Add corpus checks for raw-message-only and combined raw/decoded parsing.
12
-
13
- ## 5.2.0
14
-
15
- ### Added
16
-
17
- - Add opt-in, message-filterable `raw_messages` output with exact native and
18
- developer-field bytes, wire base types, endianness, message occurrence, and
19
- reconstructed compressed timestamps. Existing decoded output remains
20
- unchanged when the option is disabled.
21
- - Add `rawMessagesOnly` for bounded metadata consumers that do not need decoded
22
- activity collections alongside retained messages.
23
-
24
- ### Fixed
25
-
26
- - Decode compressed timestamp records without consuming bytes for the omitted
27
- timestamp field.
28
-
29
- ## 5.1.0
3
+ ## 6.1.1 - 2026-09-21
30
4
 
31
5
  ### Added
32
6
 
33
- - Add opt-in, message-filterable `raw_developer_fields` output with exact
34
- developer-field bytes, global-message occurrence, developer-data index, and
35
- field-definition number. Existing decoded output remains unchanged when the
36
- option is disabled.
37
-
38
- ## 5.0.2
39
-
40
- ### Fixed
7
+ - Export lookup helpers for FIT manufacturer, Garmin product, sport, and
8
+ sub-sport identifiers from the package root.
9
+ - Export the same helpers from the lightweight `fit-file-parser/profile`
10
+ entry point for lookup-only consumers.
11
+ - Add reverse sport, sub-sport, and course-point identifier lookups.
12
+ - Add lightweight `fit-file-parser/raw` and `fit-file-parser/encoder` entry
13
+ points for profile-independent message scanning and encoding.
14
+ - Add a strict raw-message reader with CRC, definition, developer-field, and
15
+ compressed-timestamp validation.
16
+ - Export an opt-in Garmin product display-name helper without changing parsed
17
+ `product` or `product_name` fields.
18
+ - Decode mountain enduro and mountain downhill sub-sport identifiers.
41
19
 
42
- - Clarify the 5.0 migration for standard session field 196: use canonical
43
- `metabolic_calories` rather than the old duplicate `resting_calories` label.
44
- - Clarify that the removed guessed `recovery_advisor` label was standard
45
- session field 140, which the SDK defines as `avg_depth` in meters.
20
+ ### Compatibility
46
21
 
47
- ## 5.0.1
22
+ - Existing parsed output remains unchanged except that sub-sport IDs 153 and
23
+ 154 now resolve to `mountain_enduro` and `mountain_downhill`.
24
+ - Existing Garmin product display names ported from SportsLib are preserved.
48
25
 
49
- ### Fixed
50
-
51
- - Run CI and version-driven npm publishing from the repository's `main`
52
- branch, with a guarded manual recovery path for a missed release.
53
-
54
- ## 5.0.0
26
+ ## 6.0.2 - 2026-09-21
55
27
 
56
28
  ### Changed
57
29
 
58
- - Generate the complete standard message, field, and type surface from the
59
- pinned Garmin FIT SDK without handwritten standard-profile overrides.
60
- - Preserve SDK alphanumeric tokens when generating public `snake_case` names,
61
- including `n2`, `po2`, `time128`, and Garmin product identifiers.
62
- - Restrict non-SDK support to a collision-checked allowlist of Garmin fields
63
- and private messages observed in the external FIT corpus.
64
- - Emit only parsed FIT values: Garmin product names are no longer inferred,
65
- and record elapsed/timer values are generated only when
66
- `elapsedRecordField: true` is requested.
67
-
68
- ### Breaking
69
-
70
- - Remove compatibility aliases such as `resting_calories`,
71
- `recovery_advisor`, `repeat_dive_time`, and deprecated time-in-zone names.
72
- - Remove guessed stress fields, handwritten enum/type additions, placeholder
73
- zero-mask names, and the misspelled `hip_swing_excercise_name` type.
74
- - Use the pinned SDK's `cadence_zone_high_bondary` spelling and generated
75
- alphanumeric field/type spellings instead of manual corrections.
76
- - Correct generated declarations to expose runtime `Date` values, numeric FIT
77
- `bool` fields and unknown enum IDs, decoded mask objects, nullable array
78
- entries, and optional profile fields.
79
- - Keep undated cascade rows in file order and avoid duplicating them when a lap
80
- or session boundary timestamp is absent.
81
-
82
- ## 4.1.1
83
-
84
- ### Fixed
85
-
86
- - Apply Garmin FIT profile scale and offset to primitive numeric fields, while
87
- retaining semicircle conversion only for fields declared in semicircles.
88
- - Apply FIT developer-field offsets with the parser's equivalent signed form,
89
- including scaled signed developer values.
90
- - Decode native diving ascent-rate fields as meters per second instead of
91
- geographic coordinates.
92
- - Decode native dive depth and bottom-time fields with their Garmin FIT SDK
93
- scale instead of exposing their encoded integer representation.
94
- - Keep the pinned Garmin SDK authoritative for standard field types, arrays,
95
- scales, offsets, and units while retaining compatible public field names.
96
-
97
- ## 4.1.0
30
+ - Consolidate the complete message, field, wire-metadata, type, enum, and
31
+ product contract into one static maintained profile.
32
+ - Maintain the FIT interoperability profile directly as static project source.
33
+ - Preserve decoded output and the public TypeScript surface.
98
34
 
99
35
  ### Added
100
36
 
101
- - Generate all 124 standard messages, 1,406 fields, and 200 profile types from
102
- the exactly pinned Garmin FIT SDK 21.208.0 profile.
103
- - Retain every recognized message in file order under the typed
104
- `ParsedFit.messages` index without changing existing list, cascade, or
105
- singleton outputs.
106
- - Decode Garmin strength-training `set` messages in list and cascade modes.
107
- - Add reproducible generated-profile and privacy-safe external corpus audits,
108
- and enforce profile freshness and coverage in CI.
109
-
110
- ### Fixed
111
-
112
- - Decode fields from their wire base types, including compatible developer
113
- enum/uint8/byte definitions and correctly sized numeric arrays.
114
- - Reconstruct compressed timestamps and keep timestamp state isolated between
115
- parser instances.
116
- - Accept omitted header CRCs, validate file CRCs across the complete FIT header
117
- and data section, and report strict header and file CRC failures explicitly.
118
- - Reject structurally unsafe FIT inputs consistently in callback and Promise
119
- APIs while retaining force-mode recovery for CRC corruption.
120
- - Correct Garmin profile mappings for OHR settings, monitoring HR, sleep,
121
- time-in-zone, altitude offsets, and lap/segment flow and grit summaries.
122
- - Correct Celsius-to-Kelvin conversion, add `celsius` as the canonical
123
- temperature unit, and retain `°C` as a supported alias.
124
-
125
- ### Compatibility and documentation
126
-
127
- - Preserve compatible legacy field names, scales, value shapes, parser
128
- signatures, output modes, and date behavior while adding canonical profile
129
- names.
130
- - Add regression coverage for temperature, pressure, validation, compressed
131
- timestamps, generated profile messages, repeated messages, and MTB
132
- flow/grit data.
133
- - Refresh the README with current runtime, API, units, output modes, developer
134
- fields, encoder behavior, and repository commands.
135
-
136
- ## 4.0.2
137
-
138
- - Preserve record alignment when developer-field descriptions are missing or
139
- appear after their message definitions.
140
- - Decode subsequent developer-field values once their descriptions become
141
- available, in both strict and force modes.
142
-
143
- ## 4.0.0
144
-
145
- ### FIT decoder performance
146
-
147
- - Parse `ArrayBuffer` inputs and exact Node.js `Buffer` views directly, avoiding a full copy of the source file.
148
- - Reuse one parse-local `DataView` instead of allocating temporary views for each supported endian field.
149
- - Cache standard field metadata and enum/mask lookups on reusable message definitions.
150
- - Reuse raw field storage for records that share a local message definition.
151
- - Generate elapsed and timer fields once per record instead of once per decoded field.
152
- - Skip header and file CRC scans in `force: true` mode, where CRC mismatches are intentionally ignored; strict mode continues to validate both CRCs.
153
- - Preserve legacy malformed-field zero-padding, field-boundary, developer-field, invalid-value, and offset-buffer behavior.
154
-
155
- ### Measured benefit
156
-
157
- - Suunto 93-hour / 7.5 MB FIT decoding: 1.057 s to 0.375 s, a 64.5% reduction.
158
- - Garmin 110-hour / 28.6 MB FIT decoding: 3.643 s to 1.241 s, a 65.9% reduction.
159
- - Input-related array-buffer memory is approximately halved by removing the full source copy:
160
- - Suunto: 15.0 MB to 7.5 MB.
161
- - Garmin: 57.2 MB to 28.6 MB.
162
-
163
- 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.
164
-
165
- ## 3.1.0
166
-
167
- - Add the public `FitEncoder` API for writing FIT headers, definitions, data messages, and CRCs.
168
- - Preserve the `course.sub_sport` field while parsing FIT course files.
169
-
170
- <a name="1.5.4"></a>
171
-
172
- ## [1.5.4](https://github.com/jimmykane/fit-parser/compare/v1.0.0...v1.5.3) (2019-03-01)
173
-
174
- - **Features**: HRV, Developer fields, devices and much more
175
- - **Miscellaneous**: Fix most of the issues parsing fit files
176
-
177
- <a name="1.0.1"></a>
178
-
179
- ## [1.0.1](https://github.com/pierremtb/easy-fit/compare/v1.0.0...v1.0.1) (2018-09-18)
180
-
181
- ### 😭 Unclassified (not [following convention](https://github.com/sportheroes/bk-conventional-changelog#types-of-commits))
182
-
183
- - **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))
184
-
185
- <a name="1.0.0"></a>
186
-
187
- # [1.0.0](https://github.com/pierremtb/easy-fit/compare/0.0.7...1.0.0) (2018-09-17)
188
-
189
- ### 😭 Unclassified (not [following convention](https://github.com/sportheroes/bk-conventional-changelog#types-of-commits))
190
-
191
- - **Miscellaneous**: Fix readme typo ([2282c06](https://github.com/pierremtb/easy-fit/commit/2282c06)))
192
- - **Miscellaneous**: chore: preparing to release fork internally ([3eef5f7](https://github.com/pierremtb/easy-fit/commit/3eef5f7) - [JoeTheFkingFrypan](https://github.com/JoeTheFkingFrypan))
193
- - **Miscellaneous**: fix: Typo leading to uint16 to never be invalidated ([9ed802c](https://github.com/pierremtb/easy-fit/commit/9ed802c) - [JoeTheFkingFrypan](https://github.com/JoeTheFkingFrypan))
194
- - **Miscellaneous**: Merge remote-tracking branch 'jenglert/master' ([819d78e](https://github.com/pierremtb/easy-fit/commit/819d78e)))
195
- - **Miscellaneous**: fix missing buffer dependency and compile dst ([a4b237a](https://github.com/pierremtb/easy-fit/commit/a4b237a)))
196
- - **Miscellaneous**: Merge remote-tracking branch 'FrostDigital/master' ([6fa9258](https://github.com/pierremtb/easy-fit/commit/6fa9258)))
197
- - **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)
198
- - **Miscellaneous**: fix: Adjusted offset for all altitude-related fields ([1ff0fa4](https://github.com/pierremtb/easy-fit/commit/1ff0fa4) - [JoeTheFkingFrypan](https://github.com/JoeTheFkingFrypan))
199
- - **Miscellaneous**: chore(devices): add forerunner 735 xt ([aacaa04](https://github.com/pierremtb/easy-fit/commit/aacaa04)))
200
- - **Miscellaneous**: Compile src ([ee4aa00](https://github.com/pierremtb/easy-fit/commit/ee4aa00)))
201
- - **Miscellaneous**: Add support for other lap types ([dcf42a1](https://github.com/pierremtb/easy-fit/commit/dcf42a1)))
202
- - **Miscellaneous**: Compile src ([50b9b21](https://github.com/pierremtb/easy-fit/commit/50b9b21)))
203
- - **Miscellaneous**: Merge remote-tracking branch 'pierremtb/master' ([f7f5ff0](https://github.com/pierremtb/easy-fit/commit/f7f5ff0)))
204
- - **Miscellaneous**: Add support for more manufacturers ([2d122d2](https://github.com/pierremtb/easy-fit/commit/2d122d2)))
205
- - **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)))
37
+ - Add opt-in `unmapped_messages` preservation, including exact bytes and
38
+ definition metadata, without changing default parsed output.
39
+ - Add regression coverage for unknown messages, unknown fields inside known
40
+ messages, the immutable compatibility profile, and default-output stability.
41
+ - Add an optional privacy-safe corpus summary for unmapped message/field IDs,
42
+ wire base types, sizes, occurrences, and file counts.
43
+ - Document the manual update requirements and corpus verification workflow in
44
+ `PROFILE.md`.
45
+
46
+ There are no intentional parsed-output or public API changes in this release.
47
+ Output parity was verified across 162 checked-in fixture/mode combinations,
48
+ 8,320 generated malformed endian-definition cases, and both private
49
+ 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
@@ -276,6 +257,57 @@ unknown enum IDs remain numbers, and invalid entries retained inside FIT arrays
276
257
  are `null`. All profile fields are optional because each FIT message definition
277
258
  chooses which fields are present.
278
259
 
260
+ Applications that need profile lookups independently of parsing can use the
261
+ exported manufacturer, Garmin product, sport, sub-sport, and course-point helpers:
262
+
263
+ ```javascript
264
+ import {
265
+ getFitCoursePointId,
266
+ getFitGarminProductDisplayName,
267
+ getFitManufacturerName,
268
+ getFitSportId,
269
+ getFitSportName,
270
+ getFitSubSportId,
271
+ getFitSubSportName,
272
+ } from 'fit-file-parser'
273
+
274
+ getFitManufacturerName(1) // "garmin"
275
+ getFitGarminProductDisplayName(4655) // "Edge MTB"
276
+ getFitSportName(2) // "cycling"
277
+ getFitSubSportName(153) // "mountain_enduro"
278
+ getFitSportId('cycling') // 2
279
+ getFitSubSportId('indoor_cycling') // 6
280
+ getFitCoursePointId('rest_area') // 29
281
+ ```
282
+
283
+ These helpers read the same maintained profile used by the decoder. The product
284
+ display helper is opt-in and does not synthesize or overwrite parsed
285
+ `product_name` values.
286
+
287
+ Lookup-only consumers can import the same helpers from
288
+ `fit-file-parser/profile` without loading the parser entry point.
289
+
290
+ ### Lightweight raw messages
291
+
292
+ Consumers that need strict FIT framing, CRC validation, compressed timestamps,
293
+ and exact fields without loading the semantic profile can use the lightweight
294
+ raw entry point:
295
+
296
+ ```javascript
297
+ import { readFitMessages } from 'fit-file-parser/raw'
298
+
299
+ const { messages } = readFitMessages(content, {
300
+ messageNumbers: [18, 26, 72],
301
+ maxInputBytes: 64 * 1024 * 1024,
302
+ })
303
+ ```
304
+
305
+ The result retains native and developer fields as defensive `Uint8Array`
306
+ copies. It does not apply names, enum formatting, scales, units, or
307
+ provider-specific interpretation. Invalid input throws `FitMessageReaderError`
308
+ with a stable `code` such as `invalid_header`, `invalid_crc`, or
309
+ `invalid_structure`.
310
+
279
311
  ## Inputs
280
312
 
281
313
  Both parser methods accept:
@@ -302,7 +334,7 @@ base types, sizes, and values in their raw FIT representation. Applying FIT
302
334
  scales and offsets is the caller's responsibility.
303
335
 
304
336
  ```javascript
305
- import { FitBaseType, FitEncoder } from 'fit-file-parser'
337
+ import { FitBaseType, FitEncoder } from 'fit-file-parser/encoder'
306
338
 
307
339
  const encoder = new FitEncoder()
308
340
  encoder.writeMessage(0, [
@@ -338,6 +370,9 @@ Scalar 64-bit values use `bigint`. Strings, numeric arrays, and other
338
370
  variable-length values use exact-size `Uint8Array` values. Invalid field
339
371
  definitions or numeric ranges throw before a partial message is written.
340
372
 
373
+ The encoder remains available from the package root; the `/encoder` entry
374
+ point avoids loading the decoder and semantic profile.
375
+
341
376
  ## TypeScript and module formats
342
377
 
343
378
  The package includes TypeScript declarations and exports
@@ -363,21 +398,22 @@ const {
363
398
 
364
399
  Run commands from the repository root.
365
400
 
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. |
401
+ | Command | Purpose |
402
+ | ---------------------------------- | --------------------------------------------------- |
403
+ | `npm ci` | Install locked dependencies. |
404
+ | `npm run build` | Build ESM and CommonJS output. |
405
+ | `npm run clean` | Remove the generated build output. |
406
+ | `npm test -- --run` | Run the complete test suite once. |
407
+ | `npm test -- --run test/<file>.ts` | Run a focused test file. |
408
+ | `npm run codegen` | Regenerate public types from the static profile. |
409
+ | `npm run codegen:check` | Verify generated public types are current. |
410
+ | `npm run profile:check` | Audit profile counts, structure, and fingerprints. |
411
+ | `npm run corpus:check -- <path>` | Validate an external FIT corpus without file data. |
412
+ | `npm run lint` | Check lint and formatting rules. |
413
+ | `npm run fmt` | Apply the configured formatting rules. |
414
+ | `npm run type-check` | Run TypeScript without emitting files. |
415
+ | `npm run examples` | Build and regenerate checked-in example outputs. |
416
+ | `npm run check` | Run profile checks, lint, types, tests, and builds. |
381
417
 
382
418
  ### External FIT corpus (optional)
383
419
 
@@ -390,18 +426,26 @@ git clone https://github.com/ThomasKuehne/FIT-test-files.git ../FIT-test-files
390
426
  npm run corpus:check -- ../FIT-test-files --allow-force-recovery
391
427
  npm run corpus:check -- ../FIT-test-files --allow-force-recovery --raw-messages
392
428
  npm run corpus:check -- ../FIT-test-files --allow-force-recovery --raw-messages-with-decoded-output
429
+ npm run compatibility:check -- /path/to/reference-package ../FIT-test-files
393
430
  ```
394
431
 
395
432
  The command accepts any corpus path; the sibling location is only a convenient
396
433
  convention. Add `--raw-messages` to exercise lossless raw-message-only parsing
397
434
  for every message in each file, or `--raw-messages-with-decoded-output` to also
398
- verify the ordinary decoded output path. The corpus contains a known header-CRC
399
- failure that is expected to recover only in force mode. It reports aggregate
400
- counts and never prints file names or parsed activity data.
401
-
402
- Do not edit `src/garmin_profile.generated.ts` or `src/fit_types.ts` manually.
403
- Update the pinned SDK, audited vendor extensions, or a generator, then run
404
- `npm run codegen`.
435
+ verify the ordinary decoded output path. Add `--unmapped-summary` for a
436
+ privacy-safe aggregate of unknown message/field identifiers, base types, sizes,
437
+ occurrences, and file counts. The corpus contains a known header-CRC failure
438
+ that is expected to recover only in force mode. Reports never print file names
439
+ or parsed activity values.
440
+
441
+ `compatibility:check` parses every corpus file with both the current build and
442
+ a reference package, compares their complete default outputs, and reports only
443
+ aggregate counts. Files rejected by both strict parsers are retried in force
444
+ mode.
445
+
446
+ Edit `src/profile.ts` only through reviewed profile changes with focused
447
+ regression coverage and a documented source. Do not edit `src/fit_types.ts`
448
+ manually; run `npm run codegen` after changing the maintained profile.
405
449
 
406
450
  Repository-specific automation guidance is tracked in
407
451
  [`.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
@@ -522,6 +522,8 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
522
522
  && options.includeRawDeveloperFields.includes(messageType.globalMessageNumber));
523
523
  const rawFields = includeRawMessage ? [] : undefined;
524
524
  const rawDeveloperFields = includeRawDeveloperFields || includeRawMessage ? [] : undefined;
525
+ const unmappedFields = options.includeUnmappedMessages ? [] : undefined;
526
+ const unmappedDeveloperFields = options.includeUnmappedMessages ? [] : undefined;
525
527
  if (retainsRawMessages(options)) {
526
528
  const nativeSize = messageType.fieldDefs.reduce((total, field, index) => (total + (isCompressedTimestamp && index === 0 && field.fDefNo === 253 ? 0 : field.size)), 0);
527
529
  const developerSize = developerFieldDefs.reduce((total, field) => total + field.size, 0);
@@ -537,12 +539,17 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
537
539
  rawData[i] = InvalidFieldData;
538
540
  continue;
539
541
  }
540
- if (rawFields && readDataFromIndex + fDef.size <= dataEnd) {
541
- rawFields.push({
542
+ if ((rawFields || (unmappedFields && !isOutputFieldName(fDef.name)))
543
+ && readDataFromIndex + fDef.size <= dataEnd) {
544
+ const rawField = {
542
545
  fieldDefinitionNumber: fDef.fDefNo,
543
546
  baseType: fDef.baseTypeNo,
544
547
  rawValue: Array.from(blob.subarray(readDataFromIndex, readDataFromIndex + fDef.size)),
545
- });
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
+ }
546
553
  }
547
554
  const data = readData(blob, dataView, fDef, readDataFromIndex);
548
555
  if (data !== InvalidFieldData
@@ -566,15 +573,21 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
566
573
  for (let i = 0; i < developerFieldDefs.length; i++) {
567
574
  const developerFieldDef = developerFieldDefs[i];
568
575
  const rawDataIndex = messageType.fieldDefs.length + i;
569
- if (rawDeveloperFields
576
+ let rawDeveloperField;
577
+ if ((rawDeveloperFields
578
+ || (unmappedDeveloperFields && developerFieldDef.resolvedFieldDef === undefined))
570
579
  && readDataFromIndex + developerFieldDef.size <= dataEnd) {
571
- rawDeveloperFields.push({
580
+ rawDeveloperField = {
572
581
  developerDataIndex: developerFieldDef.developerDataIndex,
573
582
  fieldDefinitionNumber: developerFieldDef.fieldDefinitionNumber,
574
583
  rawValue: Array.from(blob.subarray(readDataFromIndex, readDataFromIndex + developerFieldDef.size)),
575
- });
584
+ };
585
+ rawDeveloperFields === null || rawDeveloperFields === void 0 ? void 0 : rawDeveloperFields.push(rawDeveloperField);
576
586
  }
577
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
+ }
578
591
  if (fDef) {
579
592
  const data = readData(blob, dataView, fDef, readDataFromIndex);
580
593
  if (data !== InvalidFieldData
@@ -692,6 +705,10 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
692
705
  message: fields,
693
706
  rawFields,
694
707
  rawDeveloperFields,
708
+ unmappedFields: unmappedFields && unmappedFields.length > 0 ? unmappedFields : undefined,
709
+ unmappedDeveloperFields: unmappedDeveloperFields && unmappedDeveloperFields.length > 0
710
+ ? unmappedDeveloperFields
711
+ : undefined,
695
712
  };
696
713
  }
697
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;