Repository navigation
Merged
Conversation
added 30 commits
July 30, 2026 14:26
PR #75 published thelmabiotel-tblive@2.0.0; PR #76 (merge ef4480b) published the other five. dev == main == ef4480b. Records the production check of the version policy: installing the three wrappers from npm in an empty directory resolved each one's library to the matching major, so the correlation holds outside the monorepo too. Next: septentrio-sbf and sbg-ecom, the two binary parsers — BinaryParser rather than StringParser, so Base64 raw and length-prefixed framing instead of text-delimited. sbg-ecom has zero specs today.
Untracks TBLIVE-NOTES-FOR-TRACKER.md and gitignores it. cru is moving it into the Tracker repo, and a copy left here would go stale the moment Tracker's version changed. It stays on disk; STATUS.md now says it is expected to disappear and must not be recreated. Rewrites the next-session prompt for the two BINARY parsers. It deliberately carries no research on septentrio — cru asked for none, so the next session reads the package fresh, the same discipline that worked for tblive. What it does carry is why the last three devices' habits will NOT transfer: BinaryParser rather than StringParser (Uint8Array buffer, MAX_BYTES), CMA `raw` is Base64 at both levels, framing is length-prefixed with a CRC rather than text-delimited, and Septentrio has a real per-frame time (TOW + WNc) so unlike norsub and tblive it SHOULD populate metadata.timestamp.sentence. Also flags that its knowledge base is already per-firmware, which is closer to tblive's approach than to nmea's YAML — a decision to take before writing anything. Plus the conventions now settled across three devices (major correlation, nothing dropped silently, Result never null, the API vocabulary, structural-not-plausibility validation), the patterns to reuse, the CI checklist a new package needs, and the `files`-exclusion trap that has now bitten this repo three times. AGENTS.md updated: three of five devices done, next is binary.
cru refreshed the Node-RED flow-library listings for all three components after their major bumps. That was the last manual step, so nmea-parser, norsub-emru and thelmabiotel-tblive are done end to end: library and wrapper published at matching majors, and both listed. Removes it from the next session's ALSO OPEN list so only genuinely open items remain there.
Three additive modules the binary parsers need, in core because more than one package needs the same answers. No existing behaviour changes. - `bytes.ts` — base64 encode/decode over `Uint8Array`, plain arithmetic only: no `Buffer`, no `btoa`, no `TextEncoder`, so the libraries still run on node, deno, bun and the browser. Base64 is the CMA representation of `raw` for binary protocols, at both sentence and field level, so it is a core concern. - `gps.ts` — GPS epoch + continuous-week-count → Unix epoch ms, with the GPS-UTC offset SUBTRACTED. Two silent traps are handled: GPS time has no leap seconds while a CMA timestamp is UTC, and getting it wrong still yields a plausible-looking date. `leapSeconds` is a parameter because the DEVICE is authoritative when it reports its own offset (SBF `ReceiverTime.DeltaLS`); the built-in table is only the fallback for streams that never carry it. This replaces the `gpstime` dependency, which was the source of a real timestamp bug (it wants seconds, SBF sends milliseconds) and needed a hand-written `.d.ts`. - `pseudorandom.ts` — FNV-1a seed hash + mulberry32, so a fake sentence is IDEMPOTENT by default. A fake exists to be committed — into a spec, an example flow, a bug report — and a fixture that changes every call cannot be any of those. Shared because nmea-parser and septentrio-sbf must derive the same values from the same seed. 43/43 core specs green.
…rospection is part of the contract, and errors come back as arrays
BREAKING CHANGE: every `Result` error side is now `ParserError[]` instead of a
single `{ kind, message }`, and `getFakeSentence` is idempotent unless asked
for randomness.
`DeviceParser` gains the introspection surface — `sentenceIds`,
`getSentenceDefinition(id, protocol?)`, `getFakeSentence(id, protocol?,
options?)` — and `Parser` declares them ABSTRACT, so "every parser has the same
API" is enforced by the compiler instead of by convention. A parser that cannot
describe or fabricate a sentence returns a failed Result explaining why; it does
not omit the method. The members are declared with method syntax deliberately:
TypeScript checks method parameters bivariantly, so tblive can narrow `id` to
its own literal union and make `protocol` mandatory, and septentrio can widen it
to accept a block number, while all of them still satisfy the contract.
The error side is an ARRAY because one call can fail for several reasons at
once (three malformed options) and each reason should keep its own `kind`
rather than being flattened into prose — the same plurality a CMA's `errors`
already has. `ParserError`, `FieldSpec` and `SentenceDefinition` move into core
so the shape is shared rather than re-declared per parser; nmea-parser
re-exports them because protocol-core is private and unpublished.
Per package:
- nmea-parser: `lookup` takes an optional `protocol`, matching a definition by
protocol NAME or version, with a new `unknown-protocol` error kind that names
what the id IS defined by. Omitting it keeps the existing "all of them,
newest last" behaviour. Fake values now come from the shared seeded
generator, so `getFakeSentence('GGA')` returns the same string forever;
`{ random: true }` restores varied values. This also removes the
`crypto.getRandomValues` calls, which were doing nothing security-relevant.
- norsub-emru: the facade DELEGATES the three members to the active protocol
parser — the exception to its deliberate non-delegation, because they are the
shared contract. A failure carries a second `inactive-protocol` error, since
`getSentenceDefinition('X')` failing does not mean the device cannot speak X,
only that the currently selected protocol does not.
- thelmabiotel-tblive: its `string[]` errors are mapped to `ParserError[]` with
a `kind`, so the reasons survive individually.
Fixes the three Node-RED wrappers, which read the old single-error shape and
would have shown users `undefined` (nmea, norsub) or `[object Object]`
(tblive) instead of an actual message — e.g. `sentences: undefined` in place of
`bad indentation of a mapping entry (1:11)`. Fixed at all 9 call sites with a
per-wrapper `messages(errors)` helper. Their own suites caught it as soon as
they were run against this tree; the PUBLISHED packages were never affected,
since published nmea-parser@5.0.0 still returns a single error and its
published wrapper matches. Worth knowing why it stayed hidden: the wrappers run
their tests with `tsx`, which strips types without checking them, and
`<pkg>:nodered:lint` does not typecheck either — `tsc --noEmit` inside a
wrapper is the check that finds this class of break.
core 43 · nmea 120 · norsub 48 · tblive 260 · wrappers 28/37/45 — all green.
… block — 2.0.0
BREAKING CHANGE: the legacy `SBFResponse` output and the `SBFParser(firmware,
memory)` constructor are gone. The parser now extends `BinaryParser` from
protocol-core and emits `CMA[]` like every other device in this repo:
`new SBFParser(options)` → `addData(bytes)` / `parseData(bytes)`.
ALL 108 BLOCKS of Appendix B of the 4.10.1 reference guide are modelled, up
from 11, across every §4.2 category — each in its category folder, each opening
with its datasheet table transcribed as a comment. Every block number AND name
is script-verified against the appendix, with a coverage guard in
`tests/blocks.test.ts`. Driven over cru's three captures: unmodelled frames went
1080 → 0, with zero errors and zero garbage.
Structure: a table-driven `engine.ts` reads a block's field table instead of
each block hand-rolling offset arithmetic; `addresses.ts` maps number+revision
to a definition (never by payload length — sub-block counts make payload length
variable); `introspect.ts` and `fake.ts` implement the shared introspection
contract; `protocol-sbf.ts` plus a device facade leave room for the NMEA
protocol these receivers also emit, composed the way norsub does it.
Output shape, per the decisions locked with cru:
- payload = the SBF BODY only; header and time block go to `metadata`.
- `id` = the block number as a string, `metadata.name` = the block name,
`metadata.revision` = `{ raw, value }`, `protocol.version` = the firmware.
An unmodelled block is NOT an error, exactly like nmea's unknown sentence.
- `value` and `units` follow the DATASHEET verbatim — no scaling into `value`.
The converted value lives in field metadata as `{ value, units }`.
- `$root.timestamp` is overwritten with the receiver's own clock when it can be
trusted, driven by Appendix B's Time stamp column: R/E promote, S (satellite
transmission time) does not. Leap seconds are learned in-band from
`ReceiverTime.DeltaLS`, with the core table as fallback.
- Do-Not-Use ⇒ `value: null` plus a marker saying why, never a plausible zero.
- Sub-block fields are flattened into `payload` in wire order, with a positional
`metadata.subBlocks` mirror so a consumer can read occurrence i directly.
- padding goes to `metadata.padding`, since §4.1.5 says its value is undefined
and must not be looked at.
Two faults found and fixed on the way, neither in the brief:
- `ExtEventBaseVectGeod` was numbered 4216; it is 4217. A fake round trip
cannot catch this — it builds the frame from the same wrong number and agrees
with itself — so a real 4217 frame fell silently into the
identified-but-not-modelled tier. Only an external authority catches it,
which is why all 108 numbers and names are now checked against the appendix.
- `bufferLimit` defaulted to 1024 bytes, inherited from the generic binary
default. SBF framing is length-prefixed, so a block only decodes once its
LAST byte arrives — and cru's own receiver emits `Commands` blocks of 1052
and 1060 bytes. Measured on the real 1052-byte block at one byte per chunk:
28 garbage sentences. Chunk-size dependent, so it passes a file replay and
eats blocks on a serial line. Now `MAXIMAL_BLOCK_LENGTH` (65535).
`gpstime` is dropped — it wanted seconds while SBF sends milliseconds (a real
timestamp bug) and needed a hand-written `.d.ts`; its job is now core's `gps.ts`.
`crc` stays, imported through its pure `crc/calculators/*` subpaths, which are
index arithmetic over a bare `Uint8Array` — so no `buffer` polyfill and no Node
API. tsup builds `platform: 'neutral'` and inlines the private core's JS *and*
types, so the published package references nothing unpublished and runs on node,
deno, bun and the browser.
The per-block unit tests are replaced by suites that test the engine, the
address table, the timestamp rules, the facade and every block against real
frames from cru's captures: 190 specs, lint + tsc + build clean.
BREAKING CHANGE: the node now emits `CMA[]` on `msg.payload`, because the library it wraps does. Aligned at major 2.0.0 with `@coremarine/septentrio-sbf`, per the repo's version policy. This closes a live release hazard. The old wrapper called `parser.getFrames()`, which 2.0.0 removed, while its `workspace:^` dep had quietly begun resolving to `^2.0.0` — so a published 1.0.1 wrapper would have pulled the very library it cannot drive and thrown on the first message. Nothing caught it: the version-correlation guard existed only in the three refactored wrappers, and this package's CI test job was disabled. Same trap nmea-parser-nodered fell into during its own refactor. The guard now exists here too. It is the nmea/tblive TEMPLATE, not a new design — same package.json / tsup / tsconfig / copy-assets / dev-server shape, same pure `src/lib.ts` with zero node-red imports plus a thin `src/parser.ts` adapter, same three test files. Its msg channels are exactly the union of the other three, nothing invented: `memory` and `protocol` (norsub's), `firmware` and `ids` (tblive's), plus `definition` and `fake`. No `sentences` channel, because SBF definitions are compiled in — the same call tblive makes. The node type stays `cma-septentrio-parser`: it already matches the `cma-<device>-parser` shape, and renaming it would make the node vanish from every deployed flow. The one genuine difference is that this is the first BINARY wrapper: - `payload` takes a Buffer — what the serial, TCP and file nodes hand over, no conversion needed. - A base64 string is accepted too, deliberately: every `raw` in the CMA output is base64, so it is this package's own vocabulary for bytes, and it closes the diagnostic loop — copy a `raw` out of a debug node, inject it back, re-parse the exact frame that misbehaved. Validated strictly, so an ASCII string is refused with a message instead of parsed into a flood of garbage sentences. A byte array works as well, for a JSON-only path. - `fake` returns a Buffer, so it can be wired straight into another node's `payload`. - The memory report says `bytes` where the string wrappers say `characters`. Not cosmetic: a whole block has to fit in the buffer. The example flow was verified by DRIVING it, not by loading it: eight groups, 21 injects, every frame a real one from cru's captures (AttEuler, PVTGeodetic rev 2, ReceiverTime, a CRC-corrupted copy, a frame split in two). A real headless node-red was booted against the shipped flow file and all 21 fired — every one behaves as its label claims, none produced no output. Only built-in node types, so it imports with no contrib nodes. Two things only driving could show: firing ReceiverTime and then `firmware: get` reports `leapSeconds: 18`, i.e. the parser learned the GPS-UTC offset in-band from the device through a real flow, and the split-frame pair genuinely buffers. The packing leak that has bitten this repo three times is checked both ways: `files` carries both exclusions (`!**/*.backup`, `!**/*_cred.json`), `.gitignore` has the matching rules, and it was verified by CREATING the two artefacts node-red writes and re-packing — 8 files, neither artefact present. `tests/version.unit.test.ts` asserts both exclusions are declared, so a future edit cannot quietly drop them. Removed with the rebuild: `src/parser.js`, `tests/parser.test.js`, the `tests/nodered/` docker mirror, `Dockerfile`, `docker-compose.yml` and `manual_tests.sh`. Root scripts now match the template — `:lint` / `:build` / `:test` / `:dev` / `:examples`, with `:docker` gone. 61/61 tests (unit plus a real headless node-red integration), lint + tsc + build clean.
…en triggers Regenerated from the tblive workflow — byte-identical apart from the package name, verified with a normalising diff — so all five wrappers now share one CI shape. - The test job is BACK, and `needs: test` is restored on publish. It was commented out because the old wrapper called a removed library API; the rebuild fixed that, and leaving it disabled is what let a broken wrapper sit in the tree unnoticed. - Triggers widened to `packages/septentrio-sbf/**` and `packages/core/**`. The wrapper's tests run against the real library, which bundles the private protocol-core, so a change to either could break this package with no job running at all. The version gate keeps the extra triggers harmless: tests run, publish no-ops unless the version changed. - protocol-core and the library are built before the tests, and the wrapper itself is built before both the tests and the pack — node-red auto-loads the node from its published entry (`dist/parser.js`), and `files` ships `dist/`.
STATUS.md no longer says "uncommitted" about work that is now on `dev`: the banner, the session-summary header and the seven section titles from this session are updated, and a new §"THE SESSION IS COMMITTED" records the six commits, why each one stands alone, and how the lockfile was split so that `pnpm install --frozen-lockfile` holds at every commit rather than only at the end. PACKAGES.md gains the one drift no test in this repo can catch: `nmea-parser`, `norsub-emru` and `thelmabiotel-tblive` still carry their PUBLISHED versions while `dev` has already changed their error shape and their fake-sentence determinism. The `version.unit.test.ts` guards compare a wrapper to its sibling library, not to npm, so nothing flags it — and publishing at those versions would ship a breaking change as if it were a rebuild. The bumps belong to the release that closes QUEUED item 1.
…3.0.0, wrappers aligned
Every parser touched by this refactor goes up a MAJOR, because every one of them
broke its public API — not merely because protocol-core changed.
| pair | npm | now |
| ------------------------------- | ----- | --------- |
| nmea-parser + wrapper | 5.0.0 | 6.0.0 |
| norsub-emru + wrapper | 5.0.0 | 6.0.0 |
| thelmabiotel-tblive + wrapper | 2.0.0 | 3.0.0 |
| septentrio-sbf + wrapper | 1.0.1 | 2.0.0 (already set) |
| sbg-ecom + wrapper | — | unchanged |
The breaks were MEASURED against the published packages, not assumed: each
tarball was pulled from npm, its `dist/index.d.ts` diffed against a fresh build,
and both run side by side.
- `getSentenceDefinition('NOPE').error` was `{ kind, message }` and is now
`[{ kind, message }]`, so a consumer reading `.error.message` gets
`undefined`. Same change reaches `addSentences` and `getFakeSentence`.
- tblive's error side went `string[]` → `ParserError[]`, so `error.join('; ')`
yields `[object Object]`.
- `getFakeSentence('GGA')` returned a different string every call and now
returns the same one.
Each of those compiles and runs at the call site and quietly does the wrong
thing, which is exactly what a major is for. nmea-parser and tblive changed
their own source; norsub-emru changed too (the three delegated introspection
members) and its packed dep moves to `^6.0.0`.
`sbg-ecom` is deliberately left alone: untouched since `ef4480b`, and it does
not depend on `protocol-core` at all — only `crc`.
Note for the release notes: protocol-core is NOT the reason for the majors. It
is `private: true`, version `0.0.0`, never published, and bundled into each
library by tsup `noExternal` — a core-only change would justify a release, not a
major. The libraries' own APIs are what changed. (`norsub-emru` does not even
bundle core; it keeps nmea-parser external and inherits through it.)
Verified after bumping: the lockfile is unchanged by the bumps and
`--frozen-lockfile` still installs; all four `version.unit.test.ts` guards pass,
and they do fail on a half-bump (checked on purpose — `wrapper 5.0.0 and library
6.0.0 must share a major`); the packed tarballs carry `^6.0.0` / `^6.0.0` /
`^3.0.0` / `^2.0.0` on their sibling deps; libs 43/120/48/190/260 and wrappers
28/37/45/61 green; repo-wide lint clean.
docs/PACKAGES.md also corrects a wrong attribution found while checking this:
the introspection contract was credited to 5.0.0, but the published 5.0.0
`.d.ts` has no `sentenceIds`, no `protocol` argument and a single-object error
side. Those are 6.0.0. And the QUEUED item that planned this republish is marked
absorbed, with its premise ("behaviour unchanged") corrected.
… were wrong
Audited `docs/PACKAGES.md` claim by claim instead of only patching what the
version bump touched. Nine were inaccurate or stale; each fix below was measured.
Wrong facts:
- **`Math.random` in the septentrio bundle.** The note claimed the shipped bundle
has none. It has exactly one — core's `generator` returning it for the
`{ random: true }` opt-in of `getFakeSentence`. The parse path is clean, which
is the property that matters, so the claim is now stated that way rather than
as an absolute. (`node:` imports and `Buffer.from`/`alloc`/`isBuffer` really
are zero — re-checked on `dist/index.js`.)
- **"one folder per category" for the 108 blocks.** 16 categories, but **11
folders**: the six decoded-message categories share `DecodedMessage/`, and
`LBandTrackerStatus` sits in `LBand/` while the other 14 status blocks are in
`Status/`. Block count confirmed at runtime — `sentenceIds.length` is 108.
- **`engines.node >=22` "everywhere except sbg-ecom".** `protocol-core` is also
`>= 18`. Harmless (private, never published) but it was stated as universal.
- **nmea's "remaining cruft".** The `legacy/` folder and `morenmea.tss` are no
longer in the package — they live under `misc/parsers/nmea/`, untracked.
- **tblive's coverage.** 100% statements/lines/functions and 96.19% branches are
the ACHIEVED numbers; the thresholds enforced in `vitest.config.ts` are 95/90.
The note read as if the high numbers were the enforced floor.
- **"Node id is `cma-<device>` in all of them".** They are not uniform: three
carry a `-parser` suffix and two do not. Replaced with a measured table
(package.json / NODE_TYPE / parser.html agree per package) and a note that the
inconsistency is permanent — a node type is what deployed flows reference.
Stale after this session:
- The `Result`-array section still called the change "part of the UNCOMMITTED
septentrio work"; it is `da8c0db`.
- The QUEUED re-release of nmea/norsub/tblive is absorbed into this release, and
its premise is corrected: majors because their own APIs broke, not because
protocol-core gained code.
- norsub's note said 4.0.0 inherited nmea's changes "with no source change of its
own" — true then, but 6.0.0 does change its source (the three delegated
introspection members + `inactive-protocol`). tblive is no longer simply
"published 2.0.0" either.
Tightened rather than corrected:
- The capture metric is re-measured over all **five** captures (492 KB, 4092
sentences): 0 garbage, 0 errors, 0 unmodelled. Also records how to read it —
570 of those sentences have an empty `payload` and are NOT gaps, they are the
`opaque` no-published-layout blocks and the end-of-epoch markers, all carrying
`metadata.name`. A first pass at this measurement mistook them for gaps.
Two follow-ups recorded, neither acted on: the `misc/parsers/septentrio/samples/`
baselines are still 1.x-shaped (`{ header, time, body }`) and unread by anything;
and the per-category tally comment in `firmware/4-10-1/index.ts` calls
`LBandTrackerStatus` Status's "14th" when `Status/` already holds 14 — the totals
sum to 108 correctly, but the parenthetical needs someone with the guide open.
Verified right, left alone: the 7 `opaque` blocks (5 `Meas3*` + 2 `PVTSupport*` —
a grep for `opaque: true` finds 3 because a helper generates the five Meas3
definitions), the 108 total, sbg-ecom's 22 LOG parsers, its missing `index.js`
`main`, and that it is the only wrapper left with `tests/nodered/` and no
version guard.
…e groups The example flow now follows the convention the nmea and norsub flows use: a single vertical column of groups at x=34. The reason it did not was structural, not cosmetic — all 21 injects funnelled into ONE parser node parked at x=1000, so the groups had to spread sideways. Each group now owns its own parser and debug, exactly as nmea (6 parsers) and norsub (7) do. | | before | after | | --- | --- | --- | | group x | 20, 500, 1000 | 34 | | parser nodes | 1 shared | 7, one per group | | longest wire span | 1010 px | 260 px | That last number is the point: with per-group parsers no wire crosses the canvas, so no link node is needed to keep it readable. A parser per group is also a correctness fix. Each node instance owns its own buffer, so the split-frame demo can no longer be corrupted by an unrelated inject firing into a shared parser — which the old layout allowed. Two injects are cloned, and one is load-bearing: an order-dependent demo only works when both halves sit in the same group. - `ReceiverTime 5914` is cloned into the firmware group, because it teaches the parser the GPS-UTC offset in-band and `firmware: get` then reports `leapSeconds: 18`. Split across groups that demo quietly shows the fallback table instead. Its label now reads "fire this FIRST". - `AttEuler 5938` is cloned into "a missing measurement is null, never a zero", a group that previously held a comment and nothing else. That frame's `Roll` and `RollDot` really are at Do-Not-Use, so the group now demonstrates its own claim rather than pointing at another group's inject. Verified by DRIVING the shipped file, not by loading it: a real headless node-red booted against `examples/septentrio-sbf-examples.json` with every debug node swapped for a capture sink, and all 23 injects fired one at a time in flow order. All 26 messages behave as their labels claim — `leapSeconds: 18` after ReceiverTime, 1/2 → `[]` then 2/2 → AttEuler, `Roll: null` with `metadata.doNotUse`, a bad CRC decoding WITH errors and junk coalescing into one garbage sentence, `ids` 108, `definition: 4007` returning three entries (one per revision) while `1234` is refused with a message, and `fake` returning Buffers. 61/61 wrapper tests, lint and repo-wide lint clean. Not done here, because it cannot be: node-RED's `x` is a node's CENTRE and the flow JSON carries no `w` for ordinary nodes (the editor derives width from the label at load), so right-edge alignment of injects is not computable offline — which is why norsub's sit at 200/210/220/240/260 rather than one value. Ours are uniformly at x=200, tidy but left-aligned; the final nudge is a minute in the editor.
…emits, plus Trimble and Leica
Appendix C of the AsteRx SB3 Pro+ 4.10.1 reference guide lists 30 NMEA formatters
the receiver can emit. Nine were already known; this adds ten more, so the
built-in count goes 16 -> 26. They are STANDARD sentences, so they belong here
rather than in the Septentrio package — every device on this parser gains them.
Standard NMEA: `GBS` `GLL` `GNS` `GRS` `RMC` `ROT` `TXT`. `RMC` in particular was
missing entirely, which is hard to justify for a GNSS parser.
Third-party proprietary, because Septentrio emits them for compatibility with
other vendors' equipment: Trimble `PTNLAVR` / `PTNLGGK` and Leica `LLQ`.
FIVE of them exist in more than one LENGTH, and a definition is matched by EXACT
field count, so each length is its own definition: `RMC` 11/12/13, `GLL` 6/7,
`GNS` 12/13, `GBS` 8/10, `GRS` 14/16. The longer forms live in a new `NMEA 4.11`
protocol block, which turns the version into information — a 13-field RMC means
the device speaks NMEA 4.1+, and `getSentenceDefinition('RMC')` returns all three.
`$PTNL,AVR` and `$PTNL,GGK` are the `$PSXN` trap again: same id, same field count,
and the real type sits in field 0. They are resolved by a new `PTNL:12` entry in
`BUILTIN_SENTENCE_RESOLVERS` — the mechanism proved reusable, which is the point
of it existing.
Types chosen so a value cannot lie:
- `latitude`/`longitude` stay STRINGS (`ddmm.mmmm` mixes degrees and minutes, so a
float would be a wrong coordinate), matching the existing GGA definition.
- Dates stay strings: `RMC.date` is `ddmmyy` while `PTNLGGK.utc_date` is `mmddyy`,
MONTH FIRST — a numeric type would hide that.
- `PTNLGGK.ellipsoidal_height` is a string because Trimble puts an `EHT` prefix
inside the value (`EHT150.790`).
- `GNS.mode_indicator` is one character PER CONSTELLATION, so its length is data.
Provenance, since these definitions are only as good as their source: the standard
sentences come from gpsd's "NMEA Revealed", the Trimble pair from Trimble's own
Alloy receiver documentation cross-checked against its FieldSystems help, and LLQ
from Trimble's documentation of the Leica format. The three third-party sentences
are tested with their VENDOR EXAMPLES VERBATIM, which is what makes the field
order trustworthy — a wrong order still parses and would pass a hand-made fixture.
Two things deliberately NOT done. `PTNLAVR` fields 7-8 are named `reserved_1` and
`reserved_2`: both Trimble sources skip them and both leave them empty in their
own example, so they pass through rather than being guessed at as roll. And
Appendix C's `GMP`, `GFA`, `GGQ` and `LLK` are absent — no field table with a
verified example could be found for them, and inventing one from prose would be
worse than letting them decode generically. `ALM` is skipped too: it is a raw hex
almanac dump, so naming its fields adds nothing.
`version: '1'` on the Trimble and Leica blocks is THIS knowledge base's revision,
not a vendor version — neither publishes one for these sentences, and CMA requires
a version string.
The README's built-in list said `AAM`, `GGA`, `HDT`, `ZDA` + PSXN, which was
already wrong before this change (it omitted DTM, GSA, GST, GSV, MWV, THS, VTG and
the three Miros sentences). Replaced with a full table per protocol, why some ids
appear twice, and the type choices above.
133 tests (13 new), lint + tsc + build clean.
STATUS.md records what landed in `c5d04e8` (16 -> 26 built-ins, why five ids appear twice, what was deliberately left out and why) and adds §"THE $PSSN QUESTION" with the analysis cru asked for. The headline of that analysis: five of the six Septentrio proprietary sentences need NO new machinery — they are ordinary comma-delimited NMEA, so plain YAML definitions plus resolver entries in a `SeptentrioNMEAParser extends NMEAParser`, the norsub pattern unchanged. Field counts measured from Appendix B/C: TFM 6, RBD 11, RBP 12, RBV 12 (same length as RBP, told apart by field 0), HRP 13. `SNC` is the exception and it is a real one. Its payload is bracket-nested with a run of sub-messages that repeats per NTRIP connection, so its field count is variable and comma-splitting glues brackets onto the values. Definitions match by exact field count, so no YAML can express it. Recommendation recorded: do the five, let SNC decode generically (nothing is dropped — `raw` and every field are still emitted, just unnamed), and note that the same data is already modelled on the SBF side as NTRIPClientStatus 4053. If it is ever needed over NMEA specifically, the fix is a decoder in the septentrio package that flattens the sub-messages the way the SBF sub-blocks already are — not bending CMA. Also recorded: Appendix C gives examples only for SNC and TFM, so HRP/RBD/RBP/RBV rest on the datasheet table alone. Authoritative, but with no vendor example to catch a transcription slip, they are worth checking against the first real capture. PACKAGES.md: nmea-parser test count 120 -> 133 and the full built-in inventory per protocol block.
… sentences A Septentrio box can be configured to emit NMEA 0183 instead of SBF, so the facade now fronts two protocols: `protocol: 'sbf' | 'nmea'`. The norsub semantics cru asked for — one protocol active at a time, composed not extended, and switching discards the buffer because the bytes were being framed under different rules. Both protocol parsers take BYTES, so the facade stays uniform: the NMEA one wraps nmea-parser (a StringParser) and converts internally. The conversion is byte-per-character because NMEA 0183 is ASCII — no TextEncoder/TextDecoder, so the package stays runtime-agnostic, and no multi-byte sequence can be split across chunks. FIVE of the six $PSSN sentences are ordinary comma-delimited NMEA and are plain YAML definitions in `protocols/septentrio.yml`, transcribed from Appendix C.1 of the 4.10.1 reference guide: HRP (13 fields), RBD (11), RBP (12), RBV (12), TFM (6). All arrive as `$PSSN,<SUBTYPE>,...` with the subtype in the FIRST field — the $PSXN/$PTNL trap a third time — so they are resolved by `PSSN:<length>` entries. RBP and RBV share a length and are told apart by the subtype, which is precisely what a resolver is for. SNC IS THE INTERESTING ONE, and it is shaped the way cru designed it. Its payload is a bracket group holding three scalars plus ONE SUB-GROUP PER NTRIP CONNECTION, so the comma-split field count changes per message (4, 8, 12, 16 …) while definitions are matched by exact field count. No YAML can describe it. So: - **The payload is ALWAYS TWO FIELDS** — `submessage_id`, and the whole bracket group as one field. The variable part moves into metadata, where nothing is keyed by field count. That is the property that solves the problem; an earlier draft of mine kept the count varying in the payload and merely relocated it. - `payload[1].raw` is sliced from the sentence's own `raw`, so it stays byte-faithful and the checksum still verifies against it. - The decoded values live in `metadata.fields` (the outer scalars) and `metadata.submessages` (`Field[][]`, one entry per connection) — the same idiom the SBF side already uses for repeated groups in `metadata.subBlocks`, and nestable if a future firmware adds a level. Inside metadata the types are honest (`uint8`, `uint32` + `units`) while `payload[1].type` stays the string it is. - **CMA is untouched**, and `tests/nmea.test.ts` asserts that rather than assuming it: every sentence this layer emits is validated against `CMASchema`. The structure is parsed from `raw` by bracket DEPTH, not from the comma split, and that dissolves the one thing the datasheet never says: whether consecutive sub-groups are comma-separated. `],[`, `][` and even a mix produce identical results, which is a test. An unbalanced group is REFUSED — the sentence stays a generic `PSSN` with its fields unnamed, so a corrupt message degrades to "complete but unnamed" instead of "confidently wrong". Two bugs the tests caught in my own first draft, both from the same mistake — SNC was in the resolver map, so its id was renamed BEFORE the decoder could recognise it, and a 2-connection SNC (12 fields) collided with the RBP/RBV resolver key. SNC is now deliberately absent from that map, and the decoder matches on the subtype field rather than the id. Also here: - `getSentenceDefinition`/`getFakeSentence` on the FACADE now return the shared `SentenceDefinition`/`ParserError` types, because a facade can only promise what every protocol it fronts can deliver. SBF's richer shape (`name`, `revision`, `timestamp`, `opaque`) comes from `.parser` — the rule this facade already applied to every other protocol-specific extra. `SeptentrioNMEAParser`, `SentenceDefinition` and `ParserError` are exported for it. - The "switching protocol discards the buffer" branch has a test for the FIRST TIME: with one protocol there was nothing to switch to. - The wrapper: `msg.protocol` now reports `['sbf', 'nmea']` and accepts `nmea`; `firmwareReport` narrows with `instanceof SBFParser` because `reportedFirmware` and `leapSeconds` are learned from SBF blocks and do not exist under NMEA; and `msg.definition` is still asked of the SBF parser directly when SBF is active, so a flow debugging a receiver keeps seeing block names. THREE of those were found by `tsc --noEmit`, not by the 62 passing tests — the wrapper runs on tsx, which strips types without checking them. - The example flow is node-red's own normalisation of the layout committed in `17af24c` (one column at x=54, exact per-label group bounds). Re-driven through a real headless node-red afterwards: all 23 injects still behave as labelled. - README: the NMEA protocol, the $PSSN table, the two traps (HRP modes 1/2/5 carry NO roll, so an empty roll is null and never level; TFM's values ARE RTCM message numbers and null means "none of that group"), and a full worked SNC example saying plainly to read `metadata`, not `value`.⚠️ Appendix C gives worked examples only for SNC and TFM, so HRP/RBD/RBP/RBV rest on the datasheet tables alone — authoritative, but a wrong field order still parses cleanly, so they are worth checking against the first real capture with NMEA output enabled. Recorded in the YAML header too. septentrio 211 tests (18 new) · wrapper 62 · repo-wide lint, tsc and build clean.
… up shaped
Records `a859621`: the facade speaks `sbf | nmea`, five $PSSN sentences are plain
YAML and SNC is decoded in code with a payload that is ALWAYS two fields, the
decoded tree living in `metadata.fields` / `metadata.submessages`.
Credits the design where it belongs: cru's two-field shape is what actually solved
the problem, because it moves the variable part out of the payload entirely — my
first proposal kept the field count varying and merely relocated the mess. The one
deviation from his sketch is `metadata: { fields, submessages }` instead of a bare
array, because the runtime schema accepts an array but TypeScript will not assign
`Field[]` to `Record<string, unknown>`.
Also recorded: the undocumented sub-group separator stopped mattering once the
decoder parsed bracket depth instead of the comma split; the facade's introspection
narrowed to the shared contract (SBF's richer shape moves to `.parser`); the
"switching protocol discards the buffer" branch is finally testable; three wrapper
breaks were caught by `tsc --noEmit` rather than by its 62 passing tests; and
HRP/RBD/RBP/RBV remain unverified against hardware because Appendix C only gives
worked examples for SNC and TFM.
PACKAGES.md: septentrio test count 190 -> 211 and the per-library note rewritten
for the second protocol.
STATUS.md gets a §"NEXT SESSION — START HERE" directly under the banner, because a new agent reads this file top-first and the previous banner described only the SBF half of the work. It carries what a fresh session actually needs: the state (`dev` @ `7557769`, tree clean, 13 commits ahead of the published `main`), the version table with what breaks for a consumer and why each major exists, the measured gate to RE-RUN rather than trust, the release-PR steps in order (push, PR body, verify against npm not the workspace, then cru's manual flow-library step), and what comes after in cru's stated order — `sbg-ecom` last device, with its known state spelled out. Also recorded, so they are not rediscovered: the four `$PSSN` sentences that rest on datasheet tables alone and need a real capture to confirm; why the example flow's injects cannot be right-edge aligned offline; that nothing in this repo compares a version to npm; the stale 1.x baselines under `misc/parsers/septentrio/samples/`; and the confused `LBandTrackerStatus` parenthetical in the 4.10.1 tally comment. Plus a §"Traps this session paid for" — the wrappers' `tsx` blindness to types (three real breaks hid behind 62 passing tests), why a fake round trip cannot catch a wrong block number, why vendor examples are the only defence against a wrong field order, that a datasheet example can itself be wrong (Appendix C.1.5's SNC checksum), the lockfile-split rule, and that `cd` persists between tool calls. The session ledger is now all 13 commits rather than the first six, and ends with a paste-ready prompt for the next chat. Final gate, re-run for this commit: core 43 · nmea 133 · norsub 48 · septentrio 211 · tblive 260 · wrappers 28/37/62/45 · `tsc --noEmit` clean in all nine packages · repo-wide lint clean · `--frozen-lockfile` clean.
Layout only — group ordering, link nodes, group label colours — across the norsub-emru, septentrio-sbf and thelmabiotel-tblive example flows. No node behaviour, no payloads and no wiring semantics change. Committed on its own so the protocol-naming work that follows does not bury it.
….10 GSA/GSV forms
Three things, all inside the unpublished 6.0.0 — which is why they happen now
rather than costing a 7.0.0 later.
THERE HAS NEVER BEEN AN NMEA 0183 "3.1". The published revisions are 2.00, 2.01,
2.10, 2.20, 2.30, 3.00, 3.01, 4.00, 4.10 and 4.11; `3.1` was picked years ago from
PDFs of unknown provenance and every standard sentence claimed it. `version` now
means: the NEWEST published revision whose table for that sentence matches EXACTLY
those fields. An unchanged sentence reads 4.11; a superseded form carries the last
revision where it WAS current. cru asked for "just the latest everywhere", but that
would make an 8-field GBS claim to be 4.11 — a form 4.11 does not define — and throw
away the only signal saying the device speaks an older generation.
4.11 AAM DTM GBS(10) GGA GLL(7) GNS(13) GRS(16) GSA(18) GST GSV(20) HDT MWV
RMC(13) ROT THS TXT VTG ZDA
4.00 GBS(8) GNS(12) GRS(14) GSA(17) GSV(19) RMC(12) superseded by 4.10
2.20 GLL(6) RMC(11) superseded by 2.30
The two deltas, from gpsd's NMEA Revealed, a v4 vendor field dictionary and the
trade coverage of the 4.10/4.11 releases: 2.30 added the FAA mode indicator to
GLL/RMC/VTG; 4.10 added System ID to GSA, Signal ID to GSV, both to GBS and GRS,
and navigational status to RMC and GNS. The standard itself is paid and non-public,
so these are SECONDARY sources — GST is the softest row, listed as "updated" in 4.10
with no field change found anywhere.
MIROS IS A VENDOR, NOT A PROTOCOL. PMIRWM/PMIRCV/PMIRLD are Miros SM-050 wave-radar
sentences and were labelled `protocol: NMEA`, `standard: false` — the output claimed
a vendor sentence was standard NMEA. They now sit in their own MIROS block at
version 1, the convention Kongsberg/Trimble/Leica already use for "vendor publishes
no revision". That they are NMEA-framed is still in the output: metadata.standard is
false and the talker/checksum metadata is untouched.
GSA WITH 18 FIELDS AND GSV WITH 20 ARE NEW. They were sitting in nmea.yml COMMENTED
OUT, so any multi-constellation receiver emitting System ID or Signal ID matched no
definition and fell through as a generic sentence with unnamed fields. Septentrio
boxes emit exactly those. Both old forms are KEPT — a definition is matched by id +
exact field count, so each length is its own definition and getSentenceDefinition
returns the array (GSA and GSV now have two, RMC three).
Ordering, as cru asked: NMEA blocks first, newest revision first, then the
proprietary ones, with the convention stated in a banner at the top of the file.
protocols/nmea.yml is the source of truth and src/nmea.ts is GENERATED from it, so
the YAML was restructured by line-span surgery and checked by loading it before and
after: every sentence's serialised payload is byte-identical bar the two new ones.
BREAKING: protocol.version changes for every standard sentence, and protocol.name
changes from NMEA to MIROS for the three $PMIR* ones. Both compile and run at the
call site — anything filtering on the old strings silently stops matching.
…ire format
This is the only package that parses two wire formats, so a bare vendor name in
protocol.name was ambiguous: `SEPTENTRIO` on the $PSSN sentences read as if it
covered the binary blocks too, and a bare `SBF` does not read as Septentrio unless
you already know the acronym.
SEPTENTRIO SBF every binary SBF block (was SBF)
SEPTENTRIO NMEA the six proprietary $PSSN ones (was SEPTENTRIO)
NMEA / TRIMBLE … standard sentences, unchanged — straight from nmea-parser
So `protocol.name.startsWith('SEPTENTRIO')` now means "proprietary to this device,
either wire format", and standard sentences stay labelled exactly as nmea-parser
labels them everywhere else.
The name is ALSO the lookup key for getSentenceDefinition/getFakeSentence, so
protocols/septentrio.yml and PSSN_PROTOCOL have to move together — there is a
comment on both saying so. septentrio-nmea.ts is generated from the YAML.
The wrapper's integration assertion moves with it. It only surfaced after
rebuilding septentrio-sbf's dist: the Node-RED suites run against the built
library, not the source, so 62 tests passed against a stale copy first.
Done now because 2.0.0 is bumped but unpublished; after the release PR it would
cost a 3.0.0.
BREAKING: protocol.name changes on every sentence this package emits.
… mid-flow
"Two protocols on one node — switch, then feed", cru's ask: four injects —
`protocol: set sbf`, an SBF AttEuler frame, `protocol: set nmea`, and one NMEA
payload carrying $PSSN,TFM followed by $GPGGA. Firing them in order shows all
three protocol names in one run: SEPTENTRIO SBF, SEPTENTRIO NMEA, NMEA.
Both payloads were run through the wrapper's OWN applyProtocol/parsePayload before
being written into the flow, so the group is verified rather than plausible.
Two things the comment node spells out, because both catch people:
* The payload is BYTES in both protocols. toBytes takes a Buffer, a byte array
or a base64 string; a plain `$PSSN,…` ASCII string is refused with "payload
string must be base64". The NMEA inject therefore carries base64 — which is
what a serial-in node delivers anyway. Whether the wrapper SHOULD accept a
plain string in nmea mode is cru's call, and free only while 2.0.0 is
unpublished; it is logged in docs/STATUS.md.
* The parser node is shared with the other groups on the tab and switching
discards the buffer, so the group says to leave it on `sbf`.
The flow file is exactly JSON.stringify(flow, null, 4) with no trailing newline,
so this was appended programmatically: the diff against cru's layout is the group
plus one id added to `link in 1`, and not one of his nodes moved. The new injects
are LEFT-ALIGNED at one x — Node-RED derives node width from the label at load
time, so the right-edge nudge is cru's, in the editor, as with the other groups.
A structural validator (unique ids, group membership both ways, wire and
link-out/link-in symmetry, members inside the group box, no group overlap) was run
over all four wrapper example flows: all clean.
…new group
docs/STATUS.md gains §"PROTOCOL NAMES AND NMEA VERSIONS" covering the five commits
of this session: the septentrio rename, MIROS, the NMEA revision convention with
its sources and confidence, the ordering cru asked for, and the ninth example group.
Two things recorded there that are worth more than the changes themselves:
* THE NODE-RED WRAPPER SUITES RUN AGAINST THE LIBRARY'S BUILT dist, NOT ITS
SOURCE. All four wrappers passed while septentrio-sbf's dist was stale;
rebuilding it turned up a real assertion break. Build the libraries before
believing a wrapper suite — this is the second time a variant of this has
cost a session (the first was tsx stripping types without checking them).
* protocols/*.yml is the SOURCE OF TRUTH; src/nmea.ts and src/septentrio-nmea.ts
are generated by scripts/yaml-to-ts.mjs. Do not hand-edit the .ts.
Open items now: the four unverified $PSSN sentences still need a hardware capture,
whether the wrapper should accept a plain NMEA string as payload, and the release PR.
Gate re-measured over the whole repo: core 43 · nmea 135 (was 133, +2 for the new
GSA/GSV forms) · norsub 48 · septentrio 211 · tblive 260 · wrappers 28/37/62/45 ·
repo-wide eslint clean · tsc --noEmit clean in every package.
…group Right-edge alignment for the four injects and the link out, plus the group box resized to fit them — the pass that cannot be done offline, because Node-RED derives node width from the label at load time and the flow JSON carries no `w` for ordinary nodes. Geometry only: x/y on six nodes, y/w on the group. No payload, wire, prop or name changed. Structure re-validated: 62 nodes, 10 groups, no dangling ids, link in/out symmetric, every member inside its group box, no overlaps.
Replaces the "UNCOMMITTED" wording with the actual commit list (c3d281d -> 196d7fa) and corrects the counts: dev is 20 commits ahead of the published main (ef4480b) and still unpushed, origin/dev being 2f32d57. No version bumps were needed: everything landed inside the already-bumped, still-unpublished nmea-parser@6.0.0 and septentrio-sbf@2.0.0.
The previous entry said "20 commits ahead of main", which conflated the two remote refs. Measured: dev is 21 ahead of origin/dev (2f32d57) and 24 ahead of the published main (ef4480b). The older "13 commits ahead of main" further down this file has the same fault, and is now annotated rather than rewritten. main is the ref the release PR targets, so this is the number that matters.
…not the datasheet cru has no capture to hand, so the evidence came from GitHub code search: four unrelated repositories carry real Septentrio NMEA logs. semuconsulting/pynmeagps tests/septentriox5_nmea.log a Septentrio X5 dup06087/autonomous_ship_controller GNSS_processing/* a vessel, mode 2 Jailander/localisation-1 mel_amcl/gps_logs/norway/* no attitude fix Team-Abhiyaan/mosaic_gnss_driver test/data/nmea/* a mosaic All five fixtures parse with NO errors, so their checksums verify against our own computation — four receivers, one field order, and this parser agreeing with all of them. They are copied VERBATIM rather than rebuilt with the `sentence()` helper, because a retyped fixture would no longer prove anything. The mode-2 capture is the one that proves ORDER rather than field count: $PSSN,HRP,060851.00,110324,189.972,,0.135,0.495,,0.561,23,2,8.835,W*14 fields 5 and 8 — roll and its standard deviation — are the empty pair, and nothing else is, exactly as the datasheet's "modes 1, 2 and 5 carry no roll" note predicts. Had roll and pitch been transposed in the table, the gap would sit on the wrong pair here. TFM and SNC are confirmed too, and the real SNC ends *4C — so the checksum 68 printed in Appendix C.1.5 is a DATASHEET TYPO, which this repo had suspected from computation alone since the septentrio session. RBD/RBP/RBV are still datasheet-only: no public capture exists (they need a rover-base setup, rarer than attitude). dtc-pronto/dgps-ros transcribed the same Appendix C.1.2/3/4 independently and agrees field for field, which rules out a transcription slip here but not an error in the guide — and its test strings carry placeholder *00 checksums, so they cannot serve as fixtures. The README now states the evidence level per sentence instead of leaving it implicit.
…, as a string SeptentrioNMEAParser composes nmea-parser — a StringParser — and NMEA 0183 is ASCII, but the facade demanded Uint8Array on both protocols and converted to text internally. A plain '$PSSN,HRP,...' was refused at the wrapper and parsed as garbage at the library. That is backwards for a text protocol, as cru put it. BOTH FORMS ARE ACCEPTED; TEXT IS THE DOCUMENTED ONE. The READMEs, the node's help panel and the example flow all use a string. Bytes are stated as the serial-path escape hatch rather than advertised, which is cru's call verbatim: "document it accepts just ascii string, or the examples only with ascii strings, because it is not the purpose to have bytes as input". Bytes stay accepted because a serial, TCP or file node emits them whichever protocol the receiver is configured for, and a Septentrio box can emit SBF and NMEA on the same port. Had the accepted type flipped with the setting, a working flow would break the moment the protocol is switched — the exact thing the uniform interface existed to prevent. protocol | a string means | bytes ---------|---------------------------------------------|-------------------- nmea | THE SENTENCE | converted at the door sbf | base64 (wrapper) / byte-per-char (library) | the normal form Mechanics. addData/parseData widen to `string | Uint8Array` on the NMEA parser and on the facade, which STILL SATISFIES DeviceParser<Uint8Array> — a function taking more is usable where one taking less is expected — so `buffer` stays Uint8Array and the protocol-core contract did not move. The ASCII conversions move from private helpers in protocol-nmea.ts to src/utils.ts (toText/toBytes/asText/ asBytes) because the facade needs them too. In the wrapper, parsePayload branches on parser.protocol before toBytes, and a bad payload on nmea now gets a message asking for a sentence instead of for base64. The example flow's NMEA inject carries the two plain sentences instead of base64; its label goes from "NMEA bytes" to "NMEA text", one character shorter, so cru's alignment survives without another editor pass. septentrio 216 -> 221, its wrapper 62 -> 66. BREAKING only in the widening direction: everything that worked before still works.
…ever have passed Found while checking cru's pre-PR list, and it is a real break rather than a tidy-up: septentrio-sbf.yml ran `septentrio-sbf:test` with NO dependency build. The workflow was written when the package had no @coremarine dependency and was never updated when the NMEA protocol added one. dist is gitignored, so a fresh checkout has nothing to resolve. Reproduced by moving the two dists aside: Test Files 6 failed (6) Tests no tests Every test file failed to resolve before a single test ran. Nothing has been pushed since the NMEA work landed, so CI has never seen it. The wrapper had the same hole one level up. It built protocol-core + septentrio-sbf, but septentrio-sbf:build itself now needs nmea-parser for its DTS step — `DTS Build error` on a clean tree. Both now mirror norsub-emru.yml, which has the identical dependency shape and got it right, in the test AND the publish job. Replayed from a clean state after the fix: deps build -> 221 tests -> build -> wrapper build -> 66 tests. Triggers, same reasoning as the comments already in those files: septentrio gains packages/nmea-parser/**, packages/core/** and scripts/**; the septentrio and norsub WRAPPERS gain packages/nmea-parser/** — a change there reaches their output and both jobs already build it. The version gate keeps extra triggers harmless: tests run, publish no-ops unless the version changed. Third variant of one lesson this session: a package is not tested against what CI will actually give it. tsx stripping types without checking them, wrapper suites running against a stale dist, and now a workflow with no dep build at all.
…ndled into Checking cru's "two latest LTS" question across the repo: the septentrio pair was already aligned with the other three — engines >=22, node-red >=4.0.0, CI matrix [22.x, 24.x], identical package.json shape and files exclusions — but protocol-core still said ">= 18". It is private and bundled into each parser's dist (tsup noExternal), so the field never reaches npm and nothing was broken by it. It was simply stale: every package that bundles it requires >=22, so the declaration claimed support the code around it does not offer. sbg-ecom keeps >= 18 / node-red >=3.0.0 deliberately — it is the last device and has not been touched yet. Also drops a claim that stopped being true one commit ago: the septentrio README said "input is a Uint8Array", which is now "a Uint8Array, or on the nmea protocol the sentence as a string".
…gainst the .d.ts
cru pointed out that his checklist had an explicit READMEs item and I had ticked
it. The sweep behind that tick was a grep for OLD STRINGS ('3.1', 'SBF'), which by
construction cannot catch prose that became wrong because the API moved underneath
it. Redone properly: every documented signature compared against the built
dist/index.d.ts, member by member.
Nine wrong claims, and the worst of them is the headline breaking change of this
very release — THE ERROR SIDE OF `Result` BECAME AN ARRAY, and three API tables
still showed the singular:
nmea-parser addSentences Result<void, NMEAError> -> NMEAError[]
getSentenceDefinition Result<Sentence[], NMEAError> -> [], + the
`protocol` parameter was missing entirely
getFakeSentence Result<string, NMEAError> -> Result<NMEALike,
NMEAError[]>, + `protocol` and `options`
2 inline examples and 2 upgrade-table rows, same fault
norsub-emru getSentenceDefinition Result<Sentence[], NMEAError> -> ParserError[]
thelmabiotel-tblive getFakeSentence Result<string, string[]> -> ParserError[]
getSentenceDefinition Result<SentenceDefinition[], string[]> -> same
an example printing result.error as if it were string[]
tblive's is the same class of miss: `string[]` -> `ParserError[]` IS what its 3.0.0
breaks, and its own README still described the old type.
septentrio also had two rows left from before this session's input change
(`addData(data: Uint8Array)`, `parseData(data?: Uint8Array)`) and named the second
argument `firmware?` where the declared name is `protocol?`.
The audit script is throwaway, but the lesson is not: a README claim is only
verified when it is diffed against the emitted types. Greping for the old value
only finds what you already knew changed.
…n is gone Three contiguous slices, every frame CRC-checked: stream-mixed (71 frames + 3 interleaved GGA, no loss), stream-lossy (an orphan GGA tail the capture dropped) and stream-logs (12 log types, ending in a truncated sync that must stay PENDING). Thirteen log types between them; the other eleven implemented logs have no capture anywhere and stay datasheet-only. tests/sbg.bin is deleted: 320 well-framed frames, 0 passing CRC. It was untracked, and it is the reason the legacy parser looked broken.
…pm publish found in the wrapper CI
…, all 33 class-0 logs SBGParser extends BinaryParser. ONE buffer holds both framings: the eCom side is a pure decodeFrame, the NMEA side is nmea-parser at memory:false fed runs delimited here, so neither can keep a tail this buffer also keeps. Four tiers, as septentrio: decoded / identified / failed / garbage, and nothing is dropped silently. All 33 logs of SBG_ECOM_CLASS_LOG_ECOM_0 are modelled as field TABLES, up from the 24 hand-written decoders — the nine event/DIAG/RTCM logs are new. id is '<class>:<message>' per D4, large frames emit one CMA per page per D7, and SBG_ECOM_LOG_UTC_TIME teaches the parser the uptime->UTC correspondence per D6. Real bugs the datasheet tables exposed in the 0.0.x decoders: - GPS_POS read UNDULATION as a float64 at offset 36 where it is a 4-byte float, overlapping POS_ACC_LAT. Invisible, because every later field had its own hardcoded offset and stayed right. - EKF_EULER called ROLL_ACC/PITCH_ACC/YAW_ACC 'rollAcceleration' etc. They are 1 sigma ACCURACIES, so a consumer read a standard deviation as a rate of change. - IMU_SHORT applied NONE of its three scale factors, reporting raw counts as m/s2 and rad/s -- off by ~10^6. - STATUS masked the 3-bit CAN bus enum with a 4-bit window, so bit 31 leaked in. - isLargeFrame tested the payload LENGTH (>4096) instead of CLASS bit 7, and 4096 is not even the right ceiling: the standard maximum is 4086. - SBG_ECOM_CLOCK_STEERINGA is a transcription typo for SBG_ECOM_CLOCK_STEERING. And two datasheet errors, both settled by the frame length: GPS_POS's offset column prints 54/56 for its last two fields, which contradicts its own 'Total size 57' -- the fields are packed at 53/55, and every GPS1_POS frame in the corpus is LEN 57. DIAG's table is self-contradictory and is reconstructed from its stated total. Corpus tests, not just fakes: stream-mixed parses to exactly 71 eCom + 3 NMEA CMAs with zero garbage and zero errors, stream-lossy to 13 frames + 1 garbage for the orphan sentence tail, stream-logs to 249 frames with the trailing 0xFF still PENDING -- and feeding it one byte at a time gives byte-identical output.
… never have passed The suites: corpus (the three real captures, against the counts in the fixture README), framing (the four tiers, split-at-every-offset, buffer limit, string vs bytes), logs (field tables against PHYSICS -- 1 g on a stationary IMU, a normalised quaternion, a position in Spain, undulation matching the capture's own GGA geoid field, UTC hour matching its GGA timestamps), timestamp (D6: uptime is never a clock; UTC_TIME teaches one; an INVALID status is refused; HP dates backwards) and introspect (34 ids, refusals as Results, fake round trips, large-frame pages). Two bugs the tests found in code I had just written: frameAt read LEN at offset 2 instead of 4, and PENDING shared the value 0 with textRunAt's 'nothing here' -- so one byte of binary junk stopped the scan dead and an 11,776-byte capture came out as a single buffer-overflow report. Also corrected: the class-0 log total is 34, not the 33 I first wrote. The old docs said 22, 24 and 25 in three places for the same thing; sentenceIds is now asserted against 34 so it cannot drift again. CI: sbg-ecom.yml ran the tests with NO dependency build, and after this refactor the package needs protocol-core + nmea-parser, whose exports point only at gitignored dist/. Verified the way the last one was: moved both dists aside, reproduced '5 test files failed, no tests', then replayed the fixed steps from the same clean state -- deps build, 78 pass, build. Publish gets the same chain, or it would ship a broken tarball.
…s, docs, examples A rewrite, not a port, as cru asked. What the 0.0.x node was: 153 lines of plain JS calling the removed getFrames(), wrapping 'parser.firmware =' in try/catch because the old library THREW, exposing only memory/firmware/firmwares/payload, accepting ONLY a Buffer, logging to console.error, with 'main' pointing at an index.js that never existed, a duplicated copy of the whole source under tests/nodered/components/, and mocha with zero test files. Its README was a copy-paste of the NMEA one -- titled 'NMEA-Parser-NodeRED', documenting protocols/sentence channels this node never had. Now: src/lib.ts (pure, no node-red import) + src/parser.ts (thin RED adapter) -> tsup -> dist/parser.js, on node --test. New channels ids/definition/fake -- the old node had no introspection at all. memory reports BYTES, not characters. payload takes a Buffer, base64, a byte array, OR an NMEA sentence: base64's alphabet has no '$' and every sentence starts with one, so the mixed stream needs no protocol switch. 64 tests: the pure logic against a real parser, a real node-red runtime booted headless (four injects through one node -- binary, UTC_TIME teaching the clock, junk + every diagnostic channel, then a plain ), the version-correlation guard this pair never had, and the SHIPPED EXAMPLE FLOW -- structurally validated and every inject run through the wrapper's own handlers, so a published example cannot rot. Eight example groups, every payload a real frame from the committed corpus. 🔴 CI: the test job was commented out AND so was 'needs: test', so publish ran with NO gate -- that is how 0.0.2 reached npm. Both re-enabled, plus the full dependency chain and an explicit typecheck step, because tsx strips types without checking them and that hid a real break here too. Verified from a clean state: all four dists moved aside, then deps build -> wrapper build -> typecheck -> 64 tests.
…ay so
STATUS.md: the plan section becomes a done-record for phases 0-4, with the six real
0.0.x bugs, the two datasheet errors, the two bugs my own tests caught the same day,
and what is deliberately NOT in 1.0.0. Phase 5 is the release checklist, updated to
TEN packages. The scoping analysis and the old 'next session' section are marked
HISTORICAL rather than deleted — the scoping call ('no core change found') was right
and worth keeping as a checkable prediction.
PACKAGES.md: sbg pair at 1.0.0 with real notes; the four stale 'last un-refactored
wrapper' items resolved; the release hazard recorded as a class of mistake, not just
an incident. CMA.md: conformance table complete, all five rows CMA. SBG-REPORT.md
gets a HISTORICAL banner — it described the pre-1.0.0 output and gave a third
different log count. AGENTS.md: refactor complete, still 80 lines.
Also fixed a self-inflicted wound: an over-broad slice deleted three historical
sections (the protocol-naming overhaul, the scoping analysis, the 07-31 handoff).
Restored from HEAD and verified by diffing the section-header lists — the only
headings gone now are the six planned-phase ones I meant to replace.
….mjs replaced it The TS rewrite made it broken rather than merely dated: manual_tests.sh copied src/ into tests/nodered/components, which is now TypeScript that node-red cannot load, and docker-compose mounted tests/nodered/tests, deleted with the old fixtures. The other four wrappers dropped docker for dev-server.mjs when they were refactored; this is the fifth. Shipping a broken env would have been worse than either option.
…in norsub's tables Found by cross-checking the SBG and Septentrio datasheets against nmea-parser's knowledge base, which cru asked for before adding SBG's proprietary sentences. VBW and DPT were the only STANDARD sentences either device emits that nmea-parser did not know. Both belong here, not in a device package: the next device that emits DPT would otherwise duplicate it. Versions follow the convention cru locked on 2026-08-01 — the newest published revision whose table matches EXACTLY those fields, sourced from gpsd's NMEA Revealed as the rest of the file is: - DPT 3 fields -> 4.11 (field 3, maximum range scale, arrived in 3.00) - VBW 10 fields -> 4.11, and VBW 6 fields -> a NEW 2.30 block, because 3.00 appended the four stern-speed fields. An SBG ELLIPSE emits exactly six (§3.2.11), which is what made the second definition necessary. Verified against the SBG manual's own printed examples: both parse with no errors, VBW as 2.30 and DPT as 4.11. norsub-emru's tables had TWO copy-paste bugs, both on a ROLL field, both invisible until another vendor's rendering of the same sentence was read beside them: - GYROCOMPAS1 PHTRO roll_direction said 'M bow up, P bow down' — the pitch text. The roll letters are B and T (SBG §3.3.9, and its own example ends ',T'). - RDI ADCP PRDID roll said 'bow up / bow down' — also the pitch text. Roll is about the port side (SBG §3.3.3: 'positive port up'). Descriptions only, so no field, type or order moved and nothing breaks; both are wrong in the PUBLISHED 5.0.0 and fixed in the unreleased 6.0.0.
… norsub bugs found
…t the shipped file `18305c3` corrected both descriptions in `packages/nmea-parser/protocols/norsub.yml`, which only generates `packages/nmea-parser/tests/norsub.ts` — a fixture. The knowledge base norsub-emru actually ships is `packages/norsub-emru/protocols/norsub.yml`, compiled into `src/norsub.ts` and bundled, and both wrong descriptions were still sitting in it. So `norsub-emru@6.0.0` would have published carrying the exact bugs docs/STATUS.md and docs/PACKAGES.md both recorded as fixed: - GYROCOMPAS1 PHTRO roll_direction said 'M bow up, P bow down' — the PITCH letters. Roll uses B and T (SBG SBGFWM.2.3 §3.3.9, and its own example ends ',T'). - RDI ADCP PRDID roll said 'bow up / bow down' — also the pitch text. Roll is about the port side (SBG §3.3.3: 'positive port up'). Both are wrong in the PUBLISHED 5.0.0 and now really fixed in the unreleased 6.0.0. Descriptions only, so no field, type or order moved and nothing breaks. Regenerated with `pnpm --filter @coremarine/norsub-emru run protocols`; 48/48 green. Found by reading SBG's rendering of the same two vendor sentences while adding §3.3 to sbg-ecom — the same cross-check that found the bugs in the first place, which is what makes the method worth keeping. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… PHINF status decoder cru asked for these in 1.0.0, which is unpublished, so they are free to add. They go here and not in nmea-parser because each is THIS DEVICE's rendering of another vendor's sentence, and two renderings can legitimately differ — reading SBG's §3.3.9 PHTRO beside norsub's is what found norsub's wrong roll description, so the difference is not hypothetical. `src/sbg-nmea.ts` holds the definitions and `src/nmea-metadata.ts` the one decoder, both registered on the composed SBGNMEAParser exactly the way septentrio-sbf registers its $PSSN family. `protocol.name` is the VENDOR, following the MIROS precedent: PRDID 3 fields TELEDYNE RDI PASHR 11 ASHTECH <- messages 02 AND 12, see below PSBGI 8 SBG NMEA PSBGB 23 SBG NMEA PHINF 1 IXBLUE <- 27 named flags decoded into metadata PHTRO 4 IXBLUE PHLIN 3 IXBLUE PHOCT 19 IXBLUE INDYN 10 IXBLUE Every field count was verified by computing the checksum of the manual's own printed example and counting its commas. All thirteen §3.3 examples verify — the "SBG's examples are wrong" warning applies to §3.2, not §3.3. §3.3's own defect is that §3.3.10 prints the PHTRO example under PHLIN, so PHLIN has no valid example and the one in its spec is constructed and marked as such.⚠️ PSBGI IS 8 FIELDS AND PSBGB IS 23 — one more each than their tables list. SBG's own formatter emits a trailing comma before the checksum, and the printed checksum verifies only WITH it, independently in both sentences; none of the nine sentences SBG renders for other vendors has one. This is the difference between parsing and not: definitions are matched by EXACT field count. The shorter table forms are deliberately NOT defined — no capture contains either sentence, so the examples are the only witness, and a device emitting the short form falls through as a generic sentence with raw and values intact. PASHR is ONE definition for TWO messages, cru's decision (option A of two): §3.3.6 WASSP (msg 12) uses the same wire id and the same 11 fields as §3.3.5 PASHR (msg 02), differing ONLY in heave sign. The sbgECom message id never reaches the wire because the NMEA half of the stream is not wrapped in eCom frames (§2.1.4), and the manual's null example is byte-identical under both sections, checksum included — so no resolver could separate them. The ambiguity is STATED in the heave field's description rather than resolved. The rejected option B was a `new SBGParser({ heaveSign })` option, still purely additive. GGK (§3.3.13) needed nothing: it is `$PTNL,GGK,...` with 12 fields, already modelled as PTNLGGK and already resolved by nmea-parser's `PTNL:12` resolver, and SBG's §3.2.1 quality enum agrees with Trimble's numbering. A spec pins that it still parses. PHINF is the only sentence needing a decoder — one field, 8 hex characters, 28 named bits in §3.3.8 — decoded at field AND payload level per the locked CMA placement rules and norsub's identical case. Bits 4, 26 and 28-30 are NOT decoded: 4 is explicitly reserved and the rest have no row, and a name invented for an undocumented bit would be indistinguishable from a real one. A spec asserts the flag count is exactly 27, and another that a non-hex word is refused rather than becoming NaN and then a word of confidently-false 'all clear' flags. Also pinned: `$INDYN` has no `$P` prefix, and this device really does use talker IN ($INGGA/$INHDT/$INZDA are in the corpus), so two specs assert it is not read as IN + DYN — one that $INDYN keeps its full id, one that $INHDT still becomes HDT with talker IN, so the first cannot pass by breaking talker handling. One known artefact is pinned rather than hidden: nmea-parser labels $INDYN with talker IN regardless, which is generic behaviour and not this package's call to change. tests/nmea.test.ts is new. sbg-ecom 78 -> 112 specs; lint and tsc --noEmit clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… shipped STATUS.md gains §"✅ §3.3 IS IN", which records the three things the reading changed rather than what the plan predicted: PSBGI/PSBGB carry one more field than their tables list (trailing comma, checksum-verified in both), GGK needed nothing because it is already nmea-parser's PTNLGGK enum included, and $INDYN has no $P prefix on a device that really uses talker IN. The old "⛔ THE ONE OPEN DECISION" section is now the recorded decision: cru chose option A for PASHR, with the rejected option B kept as an additive future path and a warning not to infer the sign from heave's printed precision. The norsub near-miss is written up where the claim it falsifies lives, in §"🐛 Two real bugs in norsub-emru's tables" — the fix had been applied to a test-fixture copy of norsub.yml, so the docs asserted a fix the shipped package did not have. Two new entries in the traps list, both paid for this session: - TWO PACKAGES CAN HOLD FILES WITH THE SAME NAME AND ONLY ONE SHIPS. A grep that finds the corrected text proves nothing about which file it found — check the path, and check the generated artefact, because that is what the bundle contains. - Transcribe datasheet examples by copy, never from memory. Two strings 'quoted' from the SBG manual in a checksum script were not in the manual, and one produced a confident report of a datasheet error that did not exist. Gate re-run from scratch (builds first, since the wrapper suites run against dist): 45 steps, exit 0 — core 43, nmea 135, norsub 48, septentrio 221, tblive 260, sbg 112, wrappers 28/37/66/45/64, eslint clean, tsc --noEmit clean in all eleven packages. PACKAGES.md had the septentrio wrapper at 61/61; measured, it is 66. Also recorded: cru's standing rule that NOTHING is published — no push, no PR, no merge — until he says the word. JOB 2, the release PR that publishes ten packages, is now the only thing left. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
His edit, committed as made. The eight groups move from one 972-wide column to a two-column layout (x 54 and 834) and each is tightened to fit its own nodes, so the whole flow is visible without horizontal scrolling. Group styles gain an explicit black stroke and label colour rather than relying on the theme default. Two content changes beyond the layout: - the catch node and its debug are both named ERRORS, so the error path reads as one thing - dbg-err switches to targetType 'full', printing the whole msg instead of msg.payload — which is what you want from an error catch, since the useful part is not in the payload The rest of the diff is Node-RED's own key ordering on re-export (`g` moved ahead of `wires`), not a change. Verified rather than assumed: 55 nodes, no duplicate ids, no dangling wires, no bad group references, and group membership agrees in both directions. The wrapper suite is 64/64 — and it is the suite that matters here, because tests/examples.unit.test.ts validates the SHIPPED example flow and runs every inject through the wrapper's own handlers. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ssing upgrade sections
Found by checking every symbol and method call in each README against the built .d.ts,
then proving the failures at runtime rather than by reading types.
THREE REAL BUGS, all in the two packages whose Result shape changed:
- nmea-parser README: `result.error.kind, result.error.message` in the addSentences
example, in a snippet that types the same value as `Result<void, NMEAError[]>` two lines
above. Self-contradictory, and verified against dist: both read `undefined`. The error
side is an ARRAY, so a consumer copying that line gets nothing.
- nmea-parser README: the same mistake in the 5.0.0 getSentenceDefinition example.
- norsub-emru README: `getFakeSentenceByID`, renamed in 5.0.0 and absent from every src/
and .d.ts in the repo. It documented a method that cannot be called.
The getFrames / availableFirmwares / SBFResponse / getSentence hits elsewhere are all
inside legitimate "Upgrading from" tables and were left alone.
MISSING UPGRADE SECTIONS. Four of the five READMEs documented an older major than the one
they are about to publish, which for a release PR is the section a consumer actually needs:
- nmea-parser: new "Upgrading from 5.x" — Result errors became arrays, getFakeSentence is
idempotent with `{ random: true }` as the opt-out, and `protocol` selects WHICH
definition of an id is used. All three verified at runtime against dist, including that
the 5.x idiom now yields undefined and that an unknown protocol reports
'unknown-protocol' naming what the id IS defined by.
- norsub-emru: new "Upgrading from 5.x" — what it inherits from nmea-parser 6.0.0 plus its
own change, the three introspection members now delegating to the active protocol
parser. Verified: facade sentenceIds/getSentenceDefinition/getFakeSentence all answer,
and a failed lookup does add `inactive-protocol`.
- thelmabiotel-tblive: new "Upgrading from 2.x" — `string[]` became `ParserError[]`.
Verified that the 2.x `error.join('; ')` really does yield "[object Object]".
- sbg-ecom: new "Upgrading from 0.0.x" — everything changed, so it is a table rather than a
migration, plus the two behaviours a 0.0.x user would not expect (uptime is never
presented as a clock; nothing is dropped silently) and a warning that six decoder bugs
were fixed, so values may legitimately differ.
septentrio-sbf already had a complete one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ver followed The rename happened in the code and in the upgrade notes, but the parts a user actually reads still described the old key. Verified against the source and the test suites: `msg.sentence` appears NOWHERE in any wrapper's src or tests — the handled key is `definition`, and it answers with an ARRAY. nmea-parser-nodered: - README: the "how to interact" prose and the optionals list said `sentence`. Its input and output TABLES were already correct, which is how this survived. - parser.html — the Node-RED editor help panel, so this is what a user sees in the sidebar: the Input and Output lists said `sentence`, and `definition` did not appear anywhere in the file. The `#### Sentence` section is now `#### Definition` and documents the real return types: an ARRAY of definitions (one per revision of the standard), or an error STRING — not `object | null`. The Fake section had the same `| null` claim; it now says error string, and mentions that the same id always gives the same sentence. - also fixed a typo: "Each input proerty would be responded in the same output property". norsub-emru-nodered: - README: the input and output tables listed `sentence`, and the output types were wrong in a way that mattered — `object | null` and `string | null` for handlers that return an array or a readable error string and NEVER null. The code's own comment says so: "an unknown id now arrives as an error STRING the user can read rather than a bare null". - the "Sentence & Fake" section is now "Definition & Fake" and says the same thing. septentrio, tblive and sbg were already correct — their msg.command / msg.protocols mentions are confined to upgrade sections, which is where they belong. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… a dead msg key cru asked whether the shipped example flows still match the API their libraries expose. Checked all five by extracting every msg key each flow touches and comparing it against the keys each wrapper actually handles, then replaying the injects through the real handlers. Two needed changes; norsub, septentrio and tblive were already current. nmea-parser-nodered: the "Sentences" debug node evaluated a JSONata expression reading `msg.sentence`, which the wrapper stopped setting when the key became `msg.definition`. It would have shown an undefined field to anyone who opened the flow. Now reads `msg.definition`. sbg-ecom-nodered: a new group, "Proprietary NMEA — the §3.3 sentences", because the library learned nine sentences this session and the flow demonstrated none of them. Four injects, each the manual's own printed example verbatim (all checksum-verified by computation): $PSBGB — 23 fields, one MORE than the datasheet table lists $PHINF — the 32-bit OCTANS status word, 27 flags decoded into field metadata $PASHR — one definition covering sbgECom messages 02 AND 12 $INDYN — the id with no $P prefix Its comment node explains what to look for in each, including why PSBGB has 23 fields and why PHINF is the interesting one. Placed in free space below the existing bottom row so no existing group moves: the diff is purely additive apart from the closing bracket, and cru's two-column layout is untouched. The wrapper's examples.unit.test.ts validates the shipped flow and replays every inject through the handlers, so these four are now covered by it — 64/64 green, including the structural checks (unique ids, group membership both ways, members inside their box, no group overlap, no dangling wires, every group has a comment).⚠️ Only sbg-ecom-nodered has such a test. The other four flows were checked by hand here; nothing stops them drifting again. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… 80% is now enforced Measured coverage for all six libraries and, with node:test's --experimental-test-coverage, all five wrappers. One package was genuinely below cru's 80% bar: norsub-emru branches 79.54% -> 84.09% The gap was the status aggregator's guards, and all three are things a real device sends: an EMPTY status field (which decodes to null), a value too large for a uint32, and PNORSUB7b with both uint16 halves empty. Each must produce NO status metadata rather than a confidently wrong word of all-clear flags — the same class of bug as tblive reading an empty `data` field as a real 0.0° inclination. Three specs now pin them. Three branches stay uncovered on purpose and are documented as such: the `protocol` setter's switch body and protocol-nmea.ts's `if (builtin.success)` false branch cannot be reached while only one protocol is registered. ENFORCED FLOORS. Only thelmabiotel-tblive had `thresholds`; the other five measured coverage and could never fail on it. All six now enforce 80/80/80/80 — the bar, not the achieved numbers, so a legitimate refactor does not trip the build but a real loss of cover does. tblive keeps its stricter 95/90. Verified by running each: all six pass their own floor. WRAPPER COVERAGE IS NOW MEASURABLE. None of the five had a `test:coverage` script; all five do now, plus root `<pkg>:nodered:test:coverage` aliases.⚠️ Scoped to `src/**`, and that scoping is the whole point. The bundle in dist/ contains a SECOND copy of src/lib.ts that only the real-node-red integration test reaches, so including it double-counts and reports ~55-62% branches. Measured that way, septentrio-sbf-nodered looked like a 76% failure; measured on its source it is 87.60%. All five are 87-98% on branches: nmea 96.06/90.91 · norsub 99.41/94.25 · septentrio 98.73/87.60 tblive 98.77/97.73 · sbg 98.37/91.23 (lines/branches) ROOT SCRIPTS NORMALISED, since the checklist exposed that they were not uniform despite 4941437 saying so: two wrappers had no `:nodered:lint` alias (both packages had the script, only the alias was missing) and septentrio/sbg used `<pkg>:coverage` where the other four used `<pkg>:test:coverage`. Now every library has :test:coverage and every wrapper has :nodered:lint and :nodered:test:coverage. Each new script was run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Audited every workflow against live sources rather than memory. TYPECHECK — the real gap. Only sbg-ecom-nodered ran `tsc --noEmit`. All five wrappers build with `dts: false`, so tsup transpiles WITHOUT typechecking; their tests run under tsx, which strips types without checking them; and eslint does not typecheck either. So four wrapper workflows had NO type checking anywhere — the exact trap this repo has already paid for, and which its own docs warn about. Added to nmea-parser, norsub-emru, septentrio-sbf and thelmabiotel-tblive, placed after the build and before the tests, matching sbg's. All four pass today, so this is a guard rather than a fix. The six libraries were never exposed: they all emit declarations, so their build IS the typecheck. actions/setup-node v6 -> v7 in all eleven workflows. v7.0.0 is the current major (checked against the GitHub API, not assumed). checkout@v7 and pnpm/action-setup@v6 are already current. Node matrix left alone at [22.x, 24.x] — confirmed correct against nodejs.org's release index: 24 (Krypton) and 22 (Jod) are the two current LTS lines, and 26.5.1 exists but is not LTS, so excluding it is right rather than an oversight. Verified by replaying every workflow's test job from a genuinely clean state — all dist/ wiped first, like a fresh runner — with the commands read OUT OF the workflow files so the replay cannot drift from what CI runs. All 11 green, including the four new typecheck steps, plus `pnpm install --frozen-lockfile`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ption Compared all five wrapper package.json files field by field. They were already uniform on everything that matters — license, main, author, engines.node >=22, node-red.version >=4.0.0, node-red.nodes -> dist/parser.js, files (including the !**/*.backup and !**/*_cred.json packing exclusions), repository + directory, bugs, homepage — and all five carry `node-red` as their FIRST keyword, which is what the Node-RED flow library indexes on. Two things were not: - nmea-parser-nodered's description read "Node-Red component to read NMEA 0183 sentences": wrong casing (Node-Red vs Node-RED in the other four) and the only one of the five not mentioning CMA, which is the entire point of this release. - the `cma` keyword was missing from six of the ten published packages. Now on all ten, kept after `node-red` in the wrappers so the flow-library keyword stays first. No duplicates. packageManager pnpm@11.15.1 -> 11.18.0 (latest on npm). This is also the CI bump: the workflows pin no pnpm version, `pnpm/action-setup` reads packageManager, so the two cannot drift. Verified locally — corepack picked up 11.18.0 and `pnpm install --frozen-lockfile` is unchanged. Also verified while in here, since it is what actually reaches a consumer: every one of the ten tarballs packs README + LICENSE + package.json + 4 dist files (wrappers also ship examples/), and none contains a _cred.json, a .backup, a .env or a source map. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The checklist's docs point turned up more than staleness in one place. Every claim below was checked against the code, not remembered. TOOLING.md had five false rows: - packageManager pnpm@11.10.0 (actual 11.18.0) - "Test (Node-RED): Mocha + node-red-node-test-helper" — mocha is gone from every package.json; all five wrappers use node:test through tsx - "Node >= 18, CI tests 18.x + 20.x, publishes on 20" — engines.node is >=22 in all eleven packages, CI tests 22.x + 24.x and publishes on 24 - "pnpm/action-setup@v4 -> actions/setup-node@v4" — v6 and v7 - 🔴 "All 5 nodered workflows have the whole `test` job commented out — they publish untested." The opposite is now true: all five have enabled test jobs with `needs: test`. A doc that describes a FIXED safety problem as current is worse than no doc — it invites someone to re-fix it, or to distrust the gate that exists. Its CI section is rewritten to what the workflows actually do: upstream-aware path triggers, the mandatory wrapper typecheck step and why it exists, the publish gate (needs: test AND refs/heads/main, then a version check against npm so a re-run cannot republish), OIDC trusted publishing with provenance, and protocol-core having no publish job because it is private. AGENTS.md said wrapper tests are "mocha" — the file every agent reads first. ARCHITECTURE.md described the Node-RED component layout as parser.js + Mocha .test.js specs + a Docker-based tests/nodered/ mirror + docker-compose.yml/Dockerfile/manual_tests.sh. All of those are ABSENT from the repo — verified with find. Replaced with the real shape all five wrappers share, plus the tsx/dist warnings. Two more stale claims in the same file: the parser API convention is no longer "not yet uniform" (DeviceParser<B> is compiler-enforced), and CMA is no longer a goal with only tblive conforming — all five devices emit it. COMMANDS.md: `protocols` is not "nmea-parser only" (norsub-emru has one too, and septentrio has a package-level one), and `<package>:nodered:lint` is now true for all five rather than three, since the two missing root aliases were added. PACKAGES.md/STATUS.md: real test counts (norsub 48 -> 55), the measured coverage table with the enforced floors, nmea-parser's knowledge base is 28 ids / 37 definitions after VBW and DPT (was recorded as 26/34), and a new §"THE RELEASE CHECKLIST" recording all ten points — including the one finding that cannot be closed from this repo: four packages have never published through OIDC, and trusted publishing is configured per package on npmjs.com. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…PUBLISHED runtime dep cru asked whether chai/mocha/sinon and friends are still needed. Audited by walking every source and config file in the repo — never node_modules, dist or the lockfile — and checking each declared dependency for a real reference, then confirming every candidate individually rather than trusting the grep. REMOVED, zero references anywhere: chai root devDep left from when wrappers ran Mocha + chai mocha root devDep same deep-equal-in-any-order root devDep same (a chai plugin) @valibot/to-json-schema nmea-parser RUNTIME dep That is 46 packages out of the install. The last one matters most and is not a dev-only tidy: it was in the PUBLISHED dependency list of @coremarine/nmea-parser, so every consumer has been installing it for nothing. Added in 3d656bf during a 2.2.0 schemas refactor, never imported, and absent from the built dist. Removing it now is free — nmea-parser is already going out at a new major. serialize-javascript dropped from `overrides`: `pnpm why` showed mocha was its ONLY route into the tree, so the override had nothing left to override. `diff` KEPT — mocha was one source, but node-red-node-test-helper still reaches it through sinon. sinon is NOT ours: not declared anywhere, it arrives transitively under node-red-node-test-helper, which the wrapper integration tests genuinely need.⚠️ Four things look unused and are REQUIRED, recorded in TOOLING.md so a second pass does not remove them. None appears in an import statement, which is exactly why a text search cannot justify a removal on its own: @types/node tsconfig.json sets types: ["node"] @types/node-red node-red ships no types; all five wrappers import types from it @types/js-yaml js-yaml ships no types either @vitest/coverage-v8 the provider --coverage needs Verified after removal: full gate exit 0, all 11 workflows replayed from a wiped dist/, and every remaining override (diff, js-yaml, jsonata, form-data, esbuild) still has a live route into the tree. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s hiding cru flagged js-yaml and asked for an upgrade pass. The js-yaml problem was worse than a stale version: it was a published vulnerability that this repo could not see. 🔴 nmea-parser declared "js-yaml": "4.2.0" — an EXACT pin — while pnpm-workspace.yaml carried `overrides: js-yaml: '>=4.1.1'`. An override applies only INSIDE this repo, so the local tree resolved to 4.3.0 and the tests ran against a patched version, while every consumer of @coremarine/nmea-parser installed 4.2.0 — vulnerable to GHSA-52cp-r559-cp3m (HIGH: YAML merge-key chains force quadratic CPU; >=4.0.0 <4.3.0, fixed in 4.3.0). `pnpm audit` was silent throughout, because it audits the resolved local tree and not the range we publish. The lesson, written into pnpm-workspace.yaml where the mistake was: an override CANNOT protect a consumer. Only the dependency range in the published package.json can. The override is deleted; js-yaml is `^5.2.3` in nmea-parser itself. js-yaml 4 -> 5 is a major, and two things affect us: - No default export. `import yaml from 'js-yaml'` -> `import { load } from 'js-yaml'` in packages/nmea-parser/src/protocols.ts and scripts/yaml-to-ts.mjs. - `load` now defaults to the YAML 1.2 CORE schema — no YAML 1.1 types, no `!!merge`. This was the real risk, since the protocol YAML IS the knowledge base and output shapes are contracts for Tracker. Verified two ways rather than reasoned about: no 1.1-sensitive construct exists in any protocols/*.yml (no unquoted yes/no/on/off, no merge keys, no octal, no sexagesimal, no timestamps — only plain strings and `false`), and all four generated knowledge bases regenerate BYTE-IDENTICAL under v5. That diff is the proof. @types/js-yaml removed: v5 ships its own types and DefinitelyTyped is frozen at 4.0.9, so keeping it would shadow the real types with a v4 shape — including the default export that no longer exists. Also bumped: eslint 10.7.0 -> 10.8.0, typescript-eslint 8.63.0 -> 8.65.0, eslint-plugin-sonarjs 4.1.0 -> 4.2.0, @types/node 26.1.1 -> 26.1.2, node-red 5.0.1 -> 5.0.4. The node-red bump alone cleared TEN axios advisories and one in tar — try a direct bump before reaching for an override. The four that survived (fast-uri, postcss, brace-expansion HIGH; body-parser LOW) are dev-only transitives no published package can reach, and are now closed with overrides. `pnpm audit`: no known vulnerabilities. sonarjs 4.2.0's new rules found one real weakness, not a style nit: sbg-ecom-nodered's "each control channel answers" test kept its assertions in a helper, so as written it would have passed even if it checked ZERO channels. The helper now reports whether it handled the channel and the test asserts a non-zero count — fixed rather than silenced. TypeScript 7 was attempted and is still blocked; see docs/TOOLING.md §"TypeScript 7". Verified: full gate exit 0, all 11 workflows replayed from a wiped dist/, audit clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s to change cru gave the word: push, PR, merge. Recorded before doing it, so the doc describes the state at the moment of the release rather than after the fact. docs/STATUS.md gains §"THE RELEASE": the ten packages with their from -> to versions and why each major exists, what breaks SILENTLY for a consumer (the ones that compile and run — .error.message reading undefined, tblive's error.join giving [object Object], idempotent fakes, and sbg-ecom changing completely), the verification measured immediately before the push, and the two things that come after the merge — verify against npm rather than the workspace, then the flow-library entries, which are cru's manual step. Also records that cru confirmed trusted publishing is configured on npmjs.com for every package except protocol-core. That was the one open risk from the checklist: four packages had never published through OIDC — no provenance attestations, last released May 2024 — so their publish jobs would have failed. Closed. The standing no-publish rule is kept in both files, scoped correctly: cru authorised THIS release explicitly and in order, and that consent does not carry forward to the next change. AGENTS.md: the version line said "NOTHING IS LEFT BUT THE RELEASE PR", which stops being true the moment it merges. Now states what shipped, tells a reader to verify with `npm view` rather than trust it, and keeps the file under its own 80-line limit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Merging this publishes ten packages to npm.
protocol-coreisprivateand stays unpublished.What ships
nmea-parser+ wrapperResulterrors became arrays; fakes idempotent;protocolselects a definitionnorsub-emru+ wrapperthelmabiotel-tblive+ wrapperstring[]→ParserError[]septentrio-sbf+ wrappersbg-ecom+ wrapperA library and its Node-RED wrapper share a major, enforced by a
version.unit.test.tsin each wrapper.That is why these are majors: nothing throws, so nothing tells you except the version.
.error.messageon a failedResultnow readsundefined. The error side is an array — one call can be wrong for more than one reason. Useresult.error.map((e) => e.message).thelmabiotel-tblive'serror.join('; ')now yields[object Object]— errors went fromstring[]toParserError[], so every reason carries its ownkind.getFakeSentencereturns the same string every call. It used to callMath.random()per field, so its output could not be committed as a fixture.{ random: true }restores the old behaviour.sbg-ecomchanged completely — CMA output,parseData()instead ofgetFrames(), ids like'0:6'. Treat it as a new library.msg.sentenceismsg.definition, and it answers with an array (one entry per revision of the standard) or an error string — nevernull.Each package's README has an Upgrading from … section written for this release.
The refactor this completes
All five devices now sit on the shared
@coremarine/protocol-coreand emit the unified CMA format.sbg-ecomwas the last, rebuilt from scratch along with its wrapper.protocol-coreitself was not modified to take the fifth device —BinaryParserabsorbed it unchanged, so no second round of majors is owed.Also in this release:
sbg-ecommodels the nine proprietary NMEA sentences of the manual's §3.3, andnmea-parserlearnedVBWandDPT(28 sentence ids / 37 definitions).Verification
devat85b2acb, includingsbg-ecomandsbg-ecom-nodered, which ran with a real test job for the first time ever — both workflows were previously broken, and the wrapper's publish job had itsneeds: testgate commented out, which is how an untested0.0.2reached npm.eslintclean ·tsc --noEmitclean in all eleven packages.dist/, with the commands read out of the workflow files.pnpm audit: no known vulnerabilities. This also fixes a HIGH advisory that was reaching consumers —nmea-parserpinnedjs-yamlat an exact4.2.0(vulnerable to GHSA-52cp-r559-cp3m) while a repo-localoverridesentry masked it locally. Now^5.2.3.dist/and nothing else.After the merge
npm ieach wrapper and check its library resolved to the matching major.sbg-ecom-nodered@1.0.0andseptentrio-sbf-nodered@2.0.0are new listings; the other three need refreshing for their new majors.🤖 Generated with Claude Code