Verify that no data is lost on load and save #132

Closed
opened 2026-05-25 08:02:59 +00:00 by maxtkc · 0 comments
Owner

Summary

When a GTFS feed is imported and then re-exported by gtfs.zone, four data fidelity bugs corrupt the output: (1) stop and shape coordinates are silently rounded to 6 decimal places at import time; (2) all string field values are trimmed of leading/trailing whitespace at import time; (3) every exported CSV file is missing its final newline; and (4) any unrecognized .txt files (e.g. MBTA proprietary extensions) are silently discarded on import and absent from the export. The fix is: remove the artificial rounding and .trim() in both copies of parseFieldValue, append a trailing \n to every exported CSV, and implement a passthrough mechanism that preserves unrecognized files as opaque blobs through the import→export round-trip.

Relevant context

  • Rounding bugsrc/modules/gtfs-parser.ts:112-113, inside parseFieldValue(). Any field whose name contains _lat or _lon is run through num.toFixed(6) before being stored in IndexedDB. The rounding is permanent: the full-precision value is never stored and cannot be recovered on export.
  • Trailing newline bug (primary export path)src/modules/gtfs-parser.ts:456-459, generateCSVFromRows(). Returns Papa.unparse(…) directly; PapaParse does not add a trailing newline after the last row.
  • Trailing newline bug (legacy export path)src/modules/gtfs-database.ts:427, exportCurrentBlobsAsZip(). Same issue: Papa.unparse(rows) is used directly without appending \n.
  • Papa.unparse() accepts a newline option but it only controls the between-row separator, not a trailing one. The simplest fix is to append '\n' to the return value of both call sites.

Costs / trade-offs

  • Removing the rounding means coordinates imported going forward will have full IEEE 754 double precision. Existing IndexedDB data already has rounded values — there is no way to recover the lost digits without re-importing the original feed. Users who need full precision must reset their database (Settings → Reset) and re-import.
  • The rounding was intentional (the comment says "preserve 6 decimal places"). 6 decimal places is ~11 cm precision, which is more than enough for transit stops. However, the invariant of the app is round-trip fidelity, so silently losing data is worse than storing extra digits. Remove the rounding entirely — parseFloat() already gives full double precision.
  • No backwards-compatibility burden: dropping the rounding is a non-breaking change for all new imports.

Phase 1 — Remove coordinate rounding and whitespace stripping on import

Goal: Stop parseFieldValue() from truncating coordinate precision and trimming trailing/leading spaces from string fields when reading GTFS files into IndexedDB.

Coordinate rounding (confirmed scope from before/after analysis):

  • stops.txt: 32 stops had lat/lon truncated (e.g. 42.369118642.369119). Includes parent stations, child platforms, boarding nodes, and bus berths — every stop with a 7th decimal place.
  • shapes.txt: all ~393,217 shape points had lat/lon truncated. No points were added or removed; only precision was lost.

Whitespace stripping (confirmed scope from before/after analysis):

  • stops.txt: 2 stop names had trailing spaces stripped ("Ground level of Elevator 806 ", one door node name).
  • pathways.txt: 11 pathway rows had trailing spaces stripped from pathway_name and signposted_as fields (e.g. "Public Garden ", "Green Line ", "Track 8 ").
  • Root cause: const stringValue = String(value).trim() on line 72 of gtfs-parser.ts (and identically on line 58 of gtfs-parser.worker.ts). The .trim() applies to every field value before any type detection — so all string fields are silently stripped.
  • The parseFieldValue function is duplicated in both files; both copies must be fixed.

Steps:

  • In src/workers/gtfs-parser.worker.ts:58, change const stringValue = String(value).trim()const stringValue = String(value). This is the hot path used during import.
  • In src/modules/gtfs-parser.ts:72, make the same change. This copy is used during patch replay and other main-thread parsing.
  • In src/workers/gtfs-parser.worker.ts:93-94, delete the _lat/_lon rounding block (return parseFloat(num.toFixed(6))). The parseFloat(stringValue) on line 91 already returns full-precision.
  • In src/modules/gtfs-parser.ts:112-113, delete the same _lat/_lon rounding block.
  • Commit: fix(gtfs-parser): preserve full coordinate precision and field whitespace on import

Gotchas:

  • The worker's parseFieldValue is a standalone function copy (not imported from the module) — both files must be changed independently.
  • Existing IndexedDB rows already have rounded coordinates and trimmed strings. Only future imports benefit; a database reset is required to recover the original values.
  • shape_pt_lat/shape_pt_lon are stored in the file_blobs store as JSON, not per-row IDB. The rounding fix still applies because the worker processes those fields before blobs are assembled.

Phase 2 — Add trailing newline to all exported CSV files

Goal: Every .txt file written into the export zip ends with a \n, satisfying RFC 4180 and GTFS validators.

  • In src/modules/gtfs-parser.ts:459, change the return of generateCSVFromRows() to append '\n':
    return Papa.unparse({ fields: Array.from(fields), data: rows }) + '\n';
    
  • In src/modules/gtfs-parser.ts:448, makeHeaderOnlyCSV() is returned for empty tables — makeHeaderOnlyCSV was defined in src/modules/gtfs-file-registry.ts without a trailing newline. Fixed it there too.
  • In src/modules/gtfs-database.ts:427, the legacy export path also calls Papa.unparse(rows) directly. Appended '\n' there.
  • Commit: fix(gtfs-parser): add trailing newline to all exported CSV files

Gotchas:

  • There is also a csv branch at gtfs-database.ts:422 that writes raw stored CSV strings directly to the zip. If those blobs were stored without a trailing newline, they will export without one. Check whether that path is still reachable (it handles the old v9 schema); if so, append '\n' there too or note it as out-of-scope legacy.

Phase 3 — Passthrough for unrecognized files

Goal: Any .txt file in the imported ZIP that the app doesn't recognize (e.g. MBTA extensions: calendar_attributes.txt, checkpoints.txt, directions.txt, facilities.txt, facilities_properties.txt, facilities_properties_definitions.txt, lines.txt, linked_datasets.txt, multi_route_trips.txt, route_patterns.txt, trips_properties.txt, trips_properties_definitions.txt) is stored as an opaque raw-text blob in IndexedDB and re-emitted verbatim on export. Users cannot view or edit these files in the app; they are silently preserved.

Relevant context:

  • src/workers/gtfs-parser.worker.ts:165-174: unknownFiles[] is already identified by isSupportedFile(). The worker currently reads nothing and just pushes the filename. We need it to also read and return the raw content.
  • WorkerDoneMessage (defined in src/workers/gtfs-parser.worker.ts:27): needs a new passthroughFiles: { [fileName: string]: string } field.
  • src/modules/gtfs-parser.ts:672-736: The main thread consumes the worker result. After processing known files, it must save passthrough files to IDB and hold them in memory.
  • src/modules/gtfs-database.ts:46-69: GTFSStoreName union — add | 'passthrough_files'. Schema version is currently 10; bump to 11.
  • src/modules/gtfs-database.ts:295-310: The onupgradeneeded handler creates per-table stores. The passthrough_files store needs a simple keyPath: 'fileName' entry here, guarded by if (!db.objectStoreNames.contains('passthrough_files')).
  • src/modules/gtfs-parser.ts:952-1019 (exportAsZip): iterates this.gtfsData (known files only). Passthrough files must be appended to the zip after this loop.
  • src/modules/gtfs-database.ts:390-435 (exportCurrentBlobsAsZip): the pre-upgrade export path; also needs to include passthrough files so they survive a schema migration.
  • src/modules/ui.ts:275-277 and 404-407: currently shows "Ignoring unknown files: …" notification — update to "Preserving N unknown file(s) for export".

Steps:

Database layer (src/modules/gtfs-database.ts):

  • Add | 'passthrough_files' to the GTFSStoreName type (line 69).
  • Add passthrough_files: { key: string; value: { fileName: string; rawContent: string } } to GTFSDBSchema.
  • Bump dbVersion from 10 to 11. In the onupgradeneeded handler, added db.createObjectStore('passthrough_files', { keyPath: 'fileName' }) in the clean-slate creation block (the handler always wipes and recreates all stores).
  • Add async savePassthroughFiles(files: Record<string, string>): Promise<void> — opens a readwrite transaction on passthrough_files and puts each { fileName, rawContent } entry.
  • Add async getAllPassthroughFiles(): Promise<Record<string, string>> — reads all entries and returns a plain { [fileName]: rawContent } map.
  • clearPassthroughFiles() not needed as a separate method — clearDatabase() already clears all stores including passthrough_files. initializeEmpty() calls clearDatabase() and clears the in-memory map.
  • In exportCurrentBlobsAsZip(): after the existing loop over file_blobs, reads passthrough_files via raw IDB API (guarded by objectStoreNames.contains('passthrough_files') for pre-v11 safety) and adds each entry to the zip verbatim.

Worker (src/workers/gtfs-parser.worker.ts):

  • Add passthroughFiles: { [fileName: string]: string } to the WorkerDoneMessage interface.
  • In the parse loop, when !isSupportedFile(fileName), read the raw content with zipContent.files[fileName].async('text') and store it in a local passthroughFiles map. Keep unknownFiles populated too.
  • Pass passthroughFiles in the post({ type: 'done', ..., passthroughFiles }) call.

Parser (src/modules/gtfs-parser.ts):

  • Add private passthroughFiles: Map<string, string> = new Map() to the class.
  • In parseFile(), after the main worker result loop, call this.gtfsDatabase.savePassthroughFiles(passthroughFiles) and populate this.passthroughFiles from the result.
  • In restoreDataFromDatabase(), call this.gtfsDatabase.getAllPassthroughFiles() and repopulate this.passthroughFiles so the in-memory map is ready after a page reload.
  • In initializeEmpty(), this.passthroughFiles.clear() (DB is cleared by clearDatabase()).
  • In exportAsZip(), after the existing file loop, iterate this.passthroughFiles and add each entry to the zip verbatim.

UI (src/modules/ui.ts):

  • Changed both "Ignoring unknown files" notification strings to Preserving ${unknownFiles.length} unrecognized file(s) for export: ${unknownFiles.join(', ')}.

Gotchas:

  • The DB version bump from 10 → 11 triggers the existing pre-upgrade modal that prompts the user to export before the schema changes. That modal calls exportCurrentBlobsAsZip() — which won't have the passthrough_files store yet (it doesn't exist until after the upgrade). The getAllPassthroughFiles() call in that export path must handle the store not existing gracefully (return {} if the store is absent).
  • isSupportedFile is already defined in the worker; the passthrough read just needs to happen in the else branch where unknownFiles.push is currently the only statement.
  • Do not apply trailing newline handling (Phase 2) to passthrough file content — they must be emitted byte-for-byte as imported.

Phase 4 — Switch stop_sequence to 0-based indexing

Goal: Change stop_sequence generation from 1-based to 0-based indexing so that newly created stop_times sequences start at 0. The GTFS spec requires values to increase along the trip but imposes no minimum, so 0-based is valid. This only affects stop_times rows created or renumbered by the app; imported feeds with 1-based sequences are left as-is.

All generation sites are in src/modules/timetable-database.ts:

  • Line 262: stop_sequence: index + 1 — new stop inserted into an existing trip (arrival/departure path)
  • Line 377: stop_sequence: index + 1 — new stop inserted into an existing trip (linked time path)
  • Line 587: stop_sequence: index + 1rebuildStopTimesFromTable(), the full renumber that runs after every timetable save

Temporary placeholder values (stop_sequence: 1 / '1' at lines 242, 358, 556) are immediately overwritten by the renumber — they don't need to change, but update them to 0 for consistency.

Validator gotchasrc/modules/gtfs-validator.ts:546:

if (!stopTime.stop_sequence) { // falsy check — 0 is falsy!

With 0-based indexing the first stop has stop_sequence: 0, which fails this check silently marking valid rows as missing the field. Change the check to an explicit null/undefined test: if (stopTime.stop_sequence == null).

Steps:

  • In src/modules/timetable-database.ts, change all three index + 1 renumber expressions (lines 262, 377, 587) to index.
  • In the same file, update the three temporary placeholder values (lines 242, 358, 556) from 1 / '1' to 0 / '0' for consistency.
  • In src/modules/gtfs-validator.ts:546, change if (!stopTime.stop_sequence) to if (stopTime.stop_sequence === null || stopTime.stop_sequence === undefined) so that 0 is accepted as a valid sequence number (ESLint required === over ==).
  • Commit: fix(timetable-database): use 0-based stop_sequence indexing

Gotchas:

  • Existing IndexedDB data created by the app has 1-based sequences. Only rows renumbered after this change will be 0-based; imported feeds are untouched either way.
  • The validator fix is critical — without it, every trip's first stop would be flagged as missing stop_sequence.
  • All sorting and comparison logic elsewhere (sort((a, b) => a.stop_sequence - b.stop_sequence)) is unaffected since 0-based values still sort correctly.

Original

I'm a bit nervous that an upload and download of a file doesn't preserve the data. This is critical. Hopefully I'm wrong. If I'm not, we gotta fix it.

## Summary When a GTFS feed is imported and then re-exported by gtfs.zone, four data fidelity bugs corrupt the output: (1) stop and shape coordinates are silently rounded to 6 decimal places at import time; (2) all string field values are trimmed of leading/trailing whitespace at import time; (3) every exported CSV file is missing its final newline; and (4) any unrecognized `.txt` files (e.g. MBTA proprietary extensions) are silently discarded on import and absent from the export. The fix is: remove the artificial rounding and `.trim()` in both copies of `parseFieldValue`, append a trailing `\n` to every exported CSV, and implement a passthrough mechanism that preserves unrecognized files as opaque blobs through the import→export round-trip. ## Relevant context - **Rounding bug** — `src/modules/gtfs-parser.ts:112-113`, inside `parseFieldValue()`. Any field whose name contains `_lat` or `_lon` is run through `num.toFixed(6)` before being stored in IndexedDB. The rounding is permanent: the full-precision value is never stored and cannot be recovered on export. - **Trailing newline bug (primary export path)** — `src/modules/gtfs-parser.ts:456-459`, `generateCSVFromRows()`. Returns `Papa.unparse(…)` directly; PapaParse does not add a trailing newline after the last row. - **Trailing newline bug (legacy export path)** — `src/modules/gtfs-database.ts:427`, `exportCurrentBlobsAsZip()`. Same issue: `Papa.unparse(rows)` is used directly without appending `\n`. - `Papa.unparse()` accepts a `newline` option but it only controls the *between-row* separator, not a trailing one. The simplest fix is to append `'\n'` to the return value of both call sites. ## Costs / trade-offs - **Removing the rounding** means coordinates imported going forward will have full IEEE 754 double precision. Existing IndexedDB data already has rounded values — there is no way to recover the lost digits without re-importing the original feed. Users who need full precision must reset their database (Settings → Reset) and re-import. - **The rounding was intentional** (the comment says "preserve 6 decimal places"). 6 decimal places is ~11 cm precision, which is more than enough for transit stops. However, the invariant of the app is round-trip fidelity, so silently losing data is worse than storing extra digits. Remove the rounding entirely — `parseFloat()` already gives full double precision. - **No backwards-compatibility burden**: dropping the rounding is a non-breaking change for all new imports. --- ## Phase 1 — Remove coordinate rounding and whitespace stripping on import **Goal:** Stop `parseFieldValue()` from truncating coordinate precision and trimming trailing/leading spaces from string fields when reading GTFS files into IndexedDB. **Coordinate rounding (confirmed scope from before/after analysis):** - `stops.txt`: 32 stops had lat/lon truncated (e.g. `42.3691186` → `42.369119`). Includes parent stations, child platforms, boarding nodes, and bus berths — every stop with a 7th decimal place. - `shapes.txt`: all ~393,217 shape points had lat/lon truncated. No points were added or removed; only precision was lost. **Whitespace stripping (confirmed scope from before/after analysis):** - `stops.txt`: 2 stop names had trailing spaces stripped (`"Ground level of Elevator 806 "`, one door node name). - `pathways.txt`: 11 pathway rows had trailing spaces stripped from `pathway_name` and `signposted_as` fields (e.g. `"Public Garden "`, `"Green Line "`, `"Track 8 "`). - Root cause: `const stringValue = String(value).trim()` on line 72 of `gtfs-parser.ts` (and identically on line 58 of `gtfs-parser.worker.ts`). The `.trim()` applies to every field value before any type detection — so all string fields are silently stripped. - The `parseFieldValue` function is duplicated in both files; **both copies must be fixed**. **Steps:** - [x] In `src/workers/gtfs-parser.worker.ts:58`, change `const stringValue = String(value).trim()` → `const stringValue = String(value)`. This is the hot path used during import. - [x] In `src/modules/gtfs-parser.ts:72`, make the same change. This copy is used during patch replay and other main-thread parsing. - [x] In `src/workers/gtfs-parser.worker.ts:93-94`, delete the `_lat`/`_lon` rounding block (`return parseFloat(num.toFixed(6))`). The `parseFloat(stringValue)` on line 91 already returns full-precision. - [x] In `src/modules/gtfs-parser.ts:112-113`, delete the same `_lat`/`_lon` rounding block. - [x] Commit: `fix(gtfs-parser): preserve full coordinate precision and field whitespace on import` **Gotchas:** - The worker's `parseFieldValue` is a standalone function copy (not imported from the module) — both files must be changed independently. - Existing IndexedDB rows already have rounded coordinates and trimmed strings. Only future imports benefit; a database reset is required to recover the original values. - `shape_pt_lat`/`shape_pt_lon` are stored in the `file_blobs` store as JSON, not per-row IDB. The rounding fix still applies because the worker processes those fields before blobs are assembled. --- ## Phase 2 — Add trailing newline to all exported CSV files **Goal:** Every `.txt` file written into the export zip ends with a `\n`, satisfying RFC 4180 and GTFS validators. - [x] In `src/modules/gtfs-parser.ts:459`, change the return of `generateCSVFromRows()` to append `'\n'`: ```ts return Papa.unparse({ fields: Array.from(fields), data: rows }) + '\n'; ``` - [x] In `src/modules/gtfs-parser.ts:448`, `makeHeaderOnlyCSV()` is returned for empty tables — `makeHeaderOnlyCSV` was defined in `src/modules/gtfs-file-registry.ts` without a trailing newline. Fixed it there too. - [x] In `src/modules/gtfs-database.ts:427`, the legacy export path also calls `Papa.unparse(rows)` directly. Appended `'\n'` there. - [x] Commit: `fix(gtfs-parser): add trailing newline to all exported CSV files` **Gotchas:** - There is also a `csv` branch at `gtfs-database.ts:422` that writes raw stored CSV strings directly to the zip. If those blobs were stored without a trailing newline, they will export without one. Check whether that path is still reachable (it handles the old v9 schema); if so, append `'\n'` there too or note it as out-of-scope legacy. --- ## Phase 3 — Passthrough for unrecognized files **Goal:** Any `.txt` file in the imported ZIP that the app doesn't recognize (e.g. MBTA extensions: `calendar_attributes.txt`, `checkpoints.txt`, `directions.txt`, `facilities.txt`, `facilities_properties.txt`, `facilities_properties_definitions.txt`, `lines.txt`, `linked_datasets.txt`, `multi_route_trips.txt`, `route_patterns.txt`, `trips_properties.txt`, `trips_properties_definitions.txt`) is stored as an opaque raw-text blob in IndexedDB and re-emitted verbatim on export. Users cannot view or edit these files in the app; they are silently preserved. **Relevant context:** - `src/workers/gtfs-parser.worker.ts:165-174`: `unknownFiles[]` is already identified by `isSupportedFile()`. The worker currently reads nothing and just pushes the filename. We need it to also read and return the raw content. - `WorkerDoneMessage` (defined in `src/workers/gtfs-parser.worker.ts:27`): needs a new `passthroughFiles: { [fileName: string]: string }` field. - `src/modules/gtfs-parser.ts:672-736`: The main thread consumes the worker result. After processing known files, it must save passthrough files to IDB and hold them in memory. - `src/modules/gtfs-database.ts:46-69`: `GTFSStoreName` union — add `| 'passthrough_files'`. Schema version is currently `10`; bump to `11`. - `src/modules/gtfs-database.ts:295-310`: The `onupgradeneeded` handler creates per-table stores. The `passthrough_files` store needs a simple `keyPath: 'fileName'` entry here, guarded by `if (!db.objectStoreNames.contains('passthrough_files'))`. - `src/modules/gtfs-parser.ts:952-1019` (`exportAsZip`): iterates `this.gtfsData` (known files only). Passthrough files must be appended to the zip after this loop. - `src/modules/gtfs-database.ts:390-435` (`exportCurrentBlobsAsZip`): the pre-upgrade export path; also needs to include passthrough files so they survive a schema migration. - `src/modules/ui.ts:275-277` and `404-407`: currently shows "Ignoring unknown files: …" notification — update to "Preserving N unknown file(s) for export". **Steps:** *Database layer (`src/modules/gtfs-database.ts`):* - [x] Add `| 'passthrough_files'` to the `GTFSStoreName` type (line 69). - [x] Add `passthrough_files: { key: string; value: { fileName: string; rawContent: string } }` to `GTFSDBSchema`. - [x] Bump `dbVersion` from `10` to `11`. In the `onupgradeneeded` handler, added `db.createObjectStore('passthrough_files', { keyPath: 'fileName' })` in the clean-slate creation block (the handler always wipes and recreates all stores). - [x] Add `async savePassthroughFiles(files: Record<string, string>): Promise<void>` — opens a `readwrite` transaction on `passthrough_files` and puts each `{ fileName, rawContent }` entry. - [x] Add `async getAllPassthroughFiles(): Promise<Record<string, string>>` — reads all entries and returns a plain `{ [fileName]: rawContent }` map. - [x] `clearPassthroughFiles()` not needed as a separate method — `clearDatabase()` already clears all stores including `passthrough_files`. `initializeEmpty()` calls `clearDatabase()` and clears the in-memory map. - [x] In `exportCurrentBlobsAsZip()`: after the existing loop over `file_blobs`, reads `passthrough_files` via raw IDB API (guarded by `objectStoreNames.contains('passthrough_files')` for pre-v11 safety) and adds each entry to the zip verbatim. *Worker (`src/workers/gtfs-parser.worker.ts`):* - [x] Add `passthroughFiles: { [fileName: string]: string }` to the `WorkerDoneMessage` interface. - [x] In the parse loop, when `!isSupportedFile(fileName)`, read the raw content with `zipContent.files[fileName].async('text')` and store it in a local `passthroughFiles` map. Keep `unknownFiles` populated too. - [x] Pass `passthroughFiles` in the `post({ type: 'done', ..., passthroughFiles })` call. *Parser (`src/modules/gtfs-parser.ts`):* - [x] Add `private passthroughFiles: Map<string, string> = new Map()` to the class. - [x] In `parseFile()`, after the main worker result loop, call `this.gtfsDatabase.savePassthroughFiles(passthroughFiles)` and populate `this.passthroughFiles` from the result. - [x] In `restoreDataFromDatabase()`, call `this.gtfsDatabase.getAllPassthroughFiles()` and repopulate `this.passthroughFiles` so the in-memory map is ready after a page reload. - [x] In `initializeEmpty()`, `this.passthroughFiles.clear()` (DB is cleared by `clearDatabase()`). - [x] In `exportAsZip()`, after the existing file loop, iterate `this.passthroughFiles` and add each entry to the zip verbatim. *UI (`src/modules/ui.ts`):* - [x] Changed both "Ignoring unknown files" notification strings to `Preserving ${unknownFiles.length} unrecognized file(s) for export: ${unknownFiles.join(', ')}`. **Gotchas:** - The DB version bump from 10 → 11 triggers the existing pre-upgrade modal that prompts the user to export before the schema changes. That modal calls `exportCurrentBlobsAsZip()` — which won't have the `passthrough_files` store yet (it doesn't exist until after the upgrade). The `getAllPassthroughFiles()` call in that export path must handle the store not existing gracefully (return `{}` if the store is absent). - `isSupportedFile` is already defined in the worker; the passthrough read just needs to happen in the `else` branch where `unknownFiles.push` is currently the only statement. - Do not apply trailing newline handling (Phase 2) to passthrough file content — they must be emitted byte-for-byte as imported. --- ## Phase 4 — Switch stop_sequence to 0-based indexing **Goal:** Change `stop_sequence` generation from 1-based to 0-based indexing so that newly created stop_times sequences start at 0. The GTFS spec requires values to increase along the trip but imposes no minimum, so 0-based is valid. This only affects stop_times rows created or renumbered by the app; imported feeds with 1-based sequences are left as-is. **All generation sites are in `src/modules/timetable-database.ts`:** - Line 262: `stop_sequence: index + 1` — new stop inserted into an existing trip (arrival/departure path) - Line 377: `stop_sequence: index + 1` — new stop inserted into an existing trip (linked time path) - Line 587: `stop_sequence: index + 1` — `rebuildStopTimesFromTable()`, the full renumber that runs after every timetable save Temporary placeholder values (`stop_sequence: 1` / `'1'` at lines 242, 358, 556) are immediately overwritten by the renumber — they don't need to change, but update them to `0` for consistency. **Validator gotcha** — `src/modules/gtfs-validator.ts:546`: ```ts if (!stopTime.stop_sequence) { // falsy check — 0 is falsy! ``` With 0-based indexing the first stop has `stop_sequence: 0`, which fails this check silently marking valid rows as missing the field. Change the check to an explicit null/undefined test: `if (stopTime.stop_sequence == null)`. **Steps:** - [x] In `src/modules/timetable-database.ts`, change all three `index + 1` renumber expressions (lines 262, 377, 587) to `index`. - [x] In the same file, update the three temporary placeholder values (lines 242, 358, 556) from `1` / `'1'` to `0` / `'0'` for consistency. - [x] In `src/modules/gtfs-validator.ts:546`, change `if (!stopTime.stop_sequence)` to `if (stopTime.stop_sequence === null || stopTime.stop_sequence === undefined)` so that `0` is accepted as a valid sequence number (ESLint required `===` over `==`). - [x] Commit: `fix(timetable-database): use 0-based stop_sequence indexing` **Gotchas:** - Existing IndexedDB data created by the app has 1-based sequences. Only rows renumbered after this change will be 0-based; imported feeds are untouched either way. - The validator fix is critical — without it, every trip's first stop would be flagged as missing `stop_sequence`. - All sorting and comparison logic elsewhere (`sort((a, b) => a.stop_sequence - b.stop_sequence)`) is unaffected since 0-based values still sort correctly. ## Original I'm a bit nervous that an upload and download of a file doesn't preserve the data. This is critical. Hopefully I'm wrong. If I'm not, we gotta fix it.
maxtkc self-assigned this 2026-05-25 08:02:59 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
gtfs.zone/coloring-book#132
No description provided.