colregs 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,76 @@
1
+ # ADR 0001 — Package name, and jurisdiction as a dimension
2
+
3
+ Date: 2026-08-29
4
+ Status: accepted
5
+
6
+ ## Context
7
+
8
+ The data package needed a name, and the naming had to fit an existing family:
9
+ `ampacity` and `coastlines` (plain or term-of-art nouns for ready-made data),
10
+ `portolani` (an archaic term of art naming a format), `wire-wright` and
11
+ `coast-wright` (code that derives or draws).
12
+
13
+ Three candidates were considered.
14
+
15
+ **`fanali`** — Italian for ships' lanterns. Matched the `portolani` register
16
+ exactly. Rejected: a *fanale* is a lantern, and day shapes are black balls,
17
+ cones and diamonds. Part C's own title is "Lights and Shapes" precisely because
18
+ no single word covers the pair, so the name would have been wrong as soon as
19
+ day shapes landed.
20
+
21
+ **`navrules`** — the USCG's term. Rejected on reflection: "Navigation Rules" is
22
+ the name of *one country's amalgamation*. It denotes COLREGS plus the US Inland
23
+ Navigational Rules plus 33 CFR 26 and the VTS regs. Naming a multi-jurisdiction
24
+ package after one jurisdiction's compilation is backwards.
25
+
26
+ **`colregs`** — accepted. It was unclaimed on npm, which for a canonical domain
27
+ term is unusual enough to be worth taking on sight.
28
+
29
+ ## Decision
30
+
31
+ The package is **`colregs`**, and jurisdiction is a first-class dimension
32
+ (REQ-SCOPE-2) rather than a fork or a later bolt-on.
33
+
34
+ The name and the data model agree, which is the actual argument:
35
+
36
+ The national amalgamations are *deltas on COLREGS*, not peer bodies of rules.
37
+ The US Inland Navigational Rules were harmonised with COLREGS in 1980 and share
38
+ its rule numbering — Rule 25 is Rule 25 in both. That is why keying on paragraph
39
+ paths works across jurisdictions at all. COLREGS is the stem every national
40
+ version grows from, so it is the correct name for the trunk, and each
41
+ jurisdiction is a set of overrides hanging off it.
42
+
43
+ `navrules` would have been actively wrong under this model. `fanali` would have
44
+ been too narrow. Neither failure was about taste.
45
+
46
+ ## Consequences
47
+
48
+ - The README must state covered jurisdictions explicitly (REQ-SCOPE-6). A name
49
+ this broad promises more than v1 ships, and the honest fix is disclosure, not
50
+ a narrower name that would have to change later.
51
+ - `navrules`, `inland-rules`, `33-cfr-83` and `cevni` go in package keywords
52
+ (REQ-PKG-5) so jurisdiction-specific searches land.
53
+ - Jurisdictions become a work queue rather than a scope question. Ranked by
54
+ delta-worth-modelling against licence cleanliness, and all licence terms
55
+ below are **unverified** (Q-3):
56
+
57
+ | Jurisdiction | Instrument | Delta | Licence, unverified |
58
+ |---|---|---|---|
59
+ | `us/inland` | 33 CFR 83–90 | large | public domain |
60
+ | `eu/cevni` | CEVNI (UNECE) / RPNR | largest of any | UN copyright — the risk |
61
+ | `ca/inland` | Collision Regs, C.R.C. c.1416 | moderate | Reproduction of Federal Law Order |
62
+ | `de/binnen` | SeeSchStrO / BinSchStrO | large | §5 UrhG, *amtliche Werke* |
63
+ | `uk` | SI 1996/75 | near-zero | OGL v3.0 |
64
+ | `au` | Marine Order 30 | near-zero | CC BY 4.0 |
65
+
66
+ UK and Australia are worth taking *because* their delta is near-zero: they
67
+ cost almost nothing and let the README name six jurisdictions honestly.
68
+ CEVNI is the interesting one — genuinely different inland light
69
+ configurations, no prior art as structured data, and the only licence on the
70
+ list that might block outright.
71
+
72
+ ## Not decided here
73
+
74
+ The switching plugin's name and the renderer's name. The plugin needs a
75
+ `signalk-` prefix for app-store discovery regardless; `lamp-wright` and
76
+ `fanali` are both available and both fit the family for the renderer.
@@ -0,0 +1,77 @@
1
+ # ADR 0002 — WIG operating-condition gate, and dropping the file-level `jurisdiction` field
2
+
3
+ Date: 2026-08-29
4
+ Status: accepted
5
+
6
+ ## Context
7
+
8
+ Two findings came out of PR #2 review (CodeRabbit and a human/agent pass),
9
+ both about predicates not saying what they mean.
10
+
11
+ ### WIG gate
12
+
13
+ The `23c` entry gated on `wig: true` alone. Rule 23(c) only applies "when
14
+ taking off, landing and in flight near the surface" — a WIG craft cruising
15
+ above the surface is still `wig: true` but not subject to 23(c). The
16
+ condition lived in the entry's prose `notes`, not in `when`, so the predicate
17
+ accepted a case the rule doesn't cover. This is the same failure mode as the
18
+ 30(d) fixture bug: the 30(d) entry hung the unconditional 30(a)/(b)
19
+ anchor-light obligation off a `length_m >= 12` gate that only 30(f)'s
20
+ red-light exemption actually carries. In both cases a real-world condition
21
+ was demoted to a `notes` comment instead of encoded in the predicate,
22
+ contradicting the README's own design principle: "a predicate cannot omit a
23
+ case it was never asked about."
24
+
25
+ ### `jurisdiction` field duplication
26
+
27
+ `data/applicability.json` and `data/rules.json` each carry a *file-level*
28
+ `jurisdiction` field and, separately, every one of their ~140 records repeats
29
+ `"jurisdiction": "intl"` on itself. Nothing tested that the two agreed.
30
+ `data/lights.json` and `data/geometry.json` only have the file-level field —
31
+ no per-record duplication there, because those files aren't record-per-rule
32
+ in the same way.
33
+
34
+ REQ-MODEL-1 already requires the per-record field: "Where a jurisdiction's
35
+ text differs, both MUST be stored, keyed by jurisdiction" — i.e. two
36
+ paragraph records can share a path and differ only in jurisdiction, which
37
+ only works if the field lives on the record. REQ-MODEL-4 lists `jurisdiction`
38
+ as part of the applicability entry's own tuple, same reason. Nothing in
39
+ `docs/requirements.md` specifies a role for the file-level field; it was
40
+ added as a summary/header, and today it's true only because every record
41
+ still says `intl`.
42
+
43
+ ## Decision
44
+
45
+ **WIG:** split the single `wig` boolean into two facts in `data/facts.json`:
46
+ `wig` (the vessel is a WIG craft — a permanent characteristic) and
47
+ `wig_near_surface` (the vessel is taking off, landing, or in flight near the
48
+ surface — an operating phase). `23c`'s `when` now requires both. Same
49
+ pattern the data already used for `non_displacement` on 23(b) — a phase
50
+ condition gets its own boolean, not a note.
51
+
52
+ **`jurisdiction` duplication:** the per-record field is the one REQ-MODEL-1/4
53
+ actually specify and the one that has to hold once `us/inland` entries start
54
+ landing in the same files as `intl` ones (a delta "hangs off" the base per
55
+ ADR 0001, which means mixed jurisdictions in one file is the expected shape,
56
+ not a one-file-per-jurisdiction split). The file-level field can't stay true
57
+ once that happens, so it was dropped from `data/rules.json` and
58
+ `data/applicability.json` rather than kept as a second thing to keep in sync.
59
+ `data/lights.json` and `data/geometry.json` keep their file-level field —
60
+ they have no per-record duplication to drift against, and splitting them
61
+ per-jurisdiction is a decision for whenever a jurisdiction actually changes
62
+ light geometry, not now.
63
+
64
+ ## Consequences
65
+
66
+ - `fixtures/applicability-fixtures.json` gained a negative WIG fixture
67
+ (cruising, not near the surface — no 23c) alongside the existing positive
68
+ one.
69
+ - `data/applicability.json` and `data/rules.json` no longer have a top-level
70
+ `jurisdiction` key. A reader wanting "what jurisdictions does this file
71
+ cover" reads the per-record values (currently: `intl`, uniformly) or the
72
+ README's Coverage table, not a header field that would go stale the moment
73
+ a delta lands.
74
+ - Same disease, not yet swept: `docs/adr/0002` doesn't do a full audit of
75
+ every other entry for a note-instead-of-predicate condition. That's a
76
+ reasonable follow-up if there's evidence of more, not something to
77
+ speculatively fix here.
@@ -0,0 +1,221 @@
1
+ # colregs — design requirements
2
+
3
+ Status: **draft**, seeded 2026-08-29. This is the source of truth for what the
4
+ package must do. Coding sessions work against these IDs; tests cite them.
5
+
6
+ Requirement IDs are stable and never reused. If a requirement is dropped it is
7
+ struck through and kept, not deleted — a spec whose IDs shift silently cannot
8
+ be cited by a test.
9
+
10
+ Language: **MUST** / **SHOULD** / **MAY** in the RFC 2119 sense.
11
+
12
+ ---
13
+
14
+ ## 1. Purpose
15
+
16
+ Publish the international collision regulations, and the national amalgamations
17
+ derived from them, as language-neutral data that more than one implementation
18
+ can consume and verify against.
19
+
20
+ Two named consumers shape the design:
21
+
22
+ - **an educational app** — wants every rule, including those with no switchable
23
+ output, plus imagery and prose;
24
+ - **a switching plugin** — wants only the subset a boat can actually act on,
25
+ evaluated against live vessel state.
26
+
27
+ Neither consumer lives in this repo.
28
+
29
+ ### Non-goals
30
+
31
+ - **No inference.** The package does not decide what a vessel *is doing*.
32
+ Deriving `making_way`, propulsion or activity from sensor data belongs to a
33
+ separate consumer. This package is a pure function of a fact record.
34
+ - **No runtime.** Data and fixtures only; no evaluator ships here.
35
+ - **No advice.** The package states what the rules require. It does not tell a
36
+ mariner what to do, and carries no claim of fitness for navigation.
37
+
38
+ ---
39
+
40
+ ## 2. Definitions
41
+
42
+ | Term | Meaning |
43
+ |---|---|
44
+ | **paragraph path** | The citation unit: `27(a)(i)`, `25(d)(ii)`. Not the rule number. |
45
+ | **fact record** | A set of facts about one vessel at one moment; the input. |
46
+ | **entry** | One applicability record: predicate → lights/refs → modality → citation. |
47
+ | **modality** | `shall` / `may` / `shall-if-practicable`. |
48
+ | **jurisdiction** | A body of rules: `intl`, `us/inland`, `ca/inland`, … |
49
+ | **delta** | A jurisdiction's departures from the international text. |
50
+
51
+ ---
52
+
53
+ ## 3. Scope and jurisdictions
54
+
55
+ - **REQ-SCOPE-1** — The package MUST model the international regulations
56
+ (COLREGS 72) as its base body of rules.
57
+ - **REQ-SCOPE-2** — Jurisdiction MUST be a first-class dimension on every
58
+ applicability entry and every rule-text record, expressed as
59
+ `<country-or-body>/<waters>` with `intl` as the reserved base value.
60
+ Examples: `intl`, `us/inland`, `us/great-lakes`, `us/western-rivers`,
61
+ `ca/inland`, `de/binnen`, `eu/cevni`.
62
+ - **REQ-SCOPE-3** — A jurisdiction MUST be expressible as a *delta*: entries
63
+ absent from a jurisdiction's data inherit from `intl`. A jurisdiction MUST
64
+ NOT require restating the whole body of rules.
65
+ - **REQ-SCOPE-4** — Adding a jurisdiction MUST be additive. It MUST NOT require
66
+ a schema change or edits to existing `intl` entries.
67
+ - **REQ-SCOPE-5** — Geography that gates a rule (Great Lakes, Western Rivers,
68
+ a designated special anchorage area) MUST be an ordinary fact read by a
69
+ predicate, NOT a jurisdiction value of its own where the rule is a
70
+ conditional inside a wider jurisdiction.
71
+ - **REQ-SCOPE-6** — Every release MUST state, in the README, exactly which
72
+ jurisdictions and which rule parts it contains. Silence MUST NOT imply
73
+ coverage.
74
+
75
+ ### 3.1 Rule parts
76
+
77
+ Part C (Rules 20–31, lights and shapes) is v1. The structure MUST accommodate
78
+ the rest without redesign.
79
+
80
+ - **REQ-PART-1** — Part C lights MUST be complete for `intl` before any other
81
+ part or jurisdiction is added.
82
+ - **REQ-PART-2** — Day shapes MUST use the same entry model as lights, differing
83
+ only in the fixture vocabulary they emit.
84
+ - **REQ-PART-3** — Sound and light signals (Part D, Rules 32–37) SHOULD be
85
+ representable by the same entry model. Where they are not — signals are
86
+ event-triggered rather than state-derived — the divergence MUST be recorded
87
+ as an ADR before any Part D data is written.
88
+ - **REQ-PART-4** — Steering and sailing rules (Part B) are OUT of v1 scope and
89
+ MAY never be modelled; they govern conduct between two vessels, not the
90
+ appearance of one, and the fact record is single-vessel by construction.
91
+
92
+ ---
93
+
94
+ ## 4. Data model
95
+
96
+ Four layers, each independently addressable.
97
+
98
+ - **REQ-MODEL-1** — **Rule text**, verbatim, keyed by paragraph path. Text MUST
99
+ NOT be paraphrased, summarised or reflowed. Where a jurisdiction's text
100
+ differs, both MUST be stored, keyed by jurisdiction.
101
+ - **REQ-MODEL-2** — **Light definitions** (Rule 21) MUST carry colour, arc of
102
+ visibility in degrees, and range by length band (Rule 22). Jurisdictions MAY
103
+ add definitions (e.g. the US special flashing light, Inland 21(g)).
104
+ - **REQ-MODEL-3** — **Facts**: the input vocabulary. Three orthogonal axes MUST
105
+ be used, never a single flattened status enum:
106
+ - `propulsion` ∈ power / sail / oars
107
+ - `activity` ∈ none / fishing / trawling / towing / pushing / being-towed /
108
+ nuc / ram / cbd / mine / pilot / diving
109
+ - `position` ∈ underway / anchored / aground / moored
110
+ plus `making_way` as a boolean refining `position=underway`, and numeric and
111
+ boolean facts (`length_m`, `tow_length_m`, `max_speed_kn`, `composite_unit`,
112
+ and the education-only facts).
113
+ - **REQ-MODEL-4** — **Applicability entries**: `when` (predicate over facts) →
114
+ lights or refs → modality → citation → jurisdiction. Every entry MUST have a
115
+ stable id derived from its paragraph path (`25b`, `25d1`).
116
+ - **REQ-MODEL-5** — Gates MUST be expressed as predicates over facts
117
+ (`length_m < 7`), never as pre-enumerated tuples or configuration counts. Any
118
+ count of "configurations" is an output of evaluation, never an input to the
119
+ data.
120
+ - **REQ-MODEL-6** — Entries MUST compose. Multiple entries applying to one fact
121
+ record is the normal case, not an error (Rule 28 is "in addition to" Rule 23).
122
+ - **REQ-MODEL-7** — Three relations MUST be supported:
123
+ - `includes` — import another entry's **lights only**, never its predicate;
124
+ - `in_lieu_of` — legal alternatives for the same fact record;
125
+ - `excludes` — mutual exclusion, including across rules.
126
+ - **REQ-MODEL-8** — Alternatives MUST be first-class. Where the rules permit a
127
+ choice, the data MUST express all lawful options with their differing
128
+ modalities and gates, and MUST NOT pick one.
129
+ - **REQ-MODEL-9** — A decode table from SignalK `navigation.state` to the three
130
+ axes MUST ship with the package. Its lossy cases MUST be enumerated in data,
131
+ not prose — at minimum, the flat enum cannot express fishing-at-anchor.
132
+
133
+ ---
134
+
135
+ ## 5. Provenance and licensing
136
+
137
+ - **REQ-PROV-1** — Every text and image asset MUST record its source, the date
138
+ retrieved, and its licence or public-domain basis.
139
+ - **REQ-PROV-2** — A jurisdiction MUST NOT be added until its reproduction terms
140
+ have been checked against the primary source and recorded. Recalled or assumed
141
+ terms are not sufficient.
142
+ - **REQ-PROV-3** — Where a licence requires attribution (e.g. OGL, CC BY), the
143
+ attribution text MUST ship in the package, not only in the repo.
144
+ - **REQ-PROV-4** — Code licence and data licence MUST be stated separately. The
145
+ code licence does not cover third-party scans.
146
+ - **REQ-PROV-5** — Images MUST be addressable as data: an image record per file,
147
+ naming what it illustrates by entry id or paragraph path. Unexplained filename
148
+ prefixes are a provenance defect.
149
+
150
+ ---
151
+
152
+ ## 6. Verification
153
+
154
+ - **REQ-VERIFY-1** — Fixtures MUST pair fact records with the entries that apply,
155
+ and MUST be consumable by an implementation in any language.
156
+ - **REQ-VERIFY-2** — A **drift test** MUST cross-check the forward direction
157
+ (fact record → lights) against a reverse direction (observed lights →
158
+ candidate fact records), so the two cannot silently disagree.
159
+ - **REQ-VERIFY-3** — Every applicability entry MUST be covered by at least one
160
+ fixture that exercises it, and at least one that excludes it.
161
+ - **REQ-VERIFY-4** — Every `in_lieu_of` and `excludes` relation MUST have a
162
+ fixture demonstrating it.
163
+ - **REQ-VERIFY-5** — Predicates MUST be tested at their boundaries. Every numeric
164
+ gate MUST have fixtures immediately either side of the threshold.
165
+ - **REQ-VERIFY-6** — CI MUST fail on a fixture that references an entry id, a
166
+ light definition or a paragraph path that does not exist.
167
+
168
+ ---
169
+
170
+ ## 7. Packaging
171
+
172
+ - **REQ-PKG-1** — Zero runtime dependencies.
173
+ - **REQ-PKG-2** — Data MUST be consumable without a JavaScript runtime: plain
174
+ JSON, no code-carrying formats.
175
+ - **REQ-PKG-3** — The published package MUST contain data, images, fixtures and
176
+ provenance, and MUST NOT contain build tooling or source scans.
177
+ - **REQ-PKG-4** — Breaking changes to entry ids, fact vocabulary or relation
178
+ semantics MUST be a major version.
179
+ - **REQ-PKG-5** — Package keywords MUST include the terms a searcher would
180
+ actually use for each covered jurisdiction (`colregs`, `navrules`,
181
+ `inland-rules`, `33-cfr-83`, `cevni`).
182
+
183
+ ---
184
+
185
+ ## 8. Consumer contracts
186
+
187
+ - **REQ-CONS-1** — The switching subset MUST be derivable from the data by
188
+ filtering, not by a separate hand-maintained list. A consumer MUST be able to
189
+ select actuable entries mechanically.
190
+ - **REQ-CONS-2** — Education-only facts MUST be marked as such in the fact
191
+ vocabulary, so a switching consumer can assert it never reads them.
192
+ - **REQ-CONS-3** — Final selection among lawful alternatives belongs to the
193
+ consumer (for switching, the vessel's fixture map). The package MUST NOT
194
+ encode a preference.
195
+ - **REQ-CONS-4** — The package MUST NOT assume a SignalK consumer beyond the
196
+ optional decode table of REQ-MODEL-9.
197
+
198
+ ---
199
+
200
+ ## 9. Open questions
201
+
202
+ Tracked here until resolved; each becomes an ADR.
203
+
204
+ - **Q-1** — Do Part D sound signals fit the entry model, or do they need an
205
+ event dimension? Blocks REQ-PART-3.
206
+ - **Q-2** — Are the USCG scans the educational payload, or a stopgap until
207
+ light geometry is rendered from data? Affects how hard REQ-PROV-5 is pushed.
208
+ - **Q-3** — Jurisdiction licence terms are **unverified**: US public domain,
209
+ UK OGL v3.0, AU CC BY 4.0, DE §5 UrhG *amtliche Werke*, CA Reproduction of
210
+ Federal Law Order, EU/UNECE CEVNI unclear. REQ-PROV-2 blocks each until
211
+ checked against the primary source. CEVNI is the one most likely to fail.
212
+ - **Q-4** — Two upstream SignalK spec asks are outstanding and independent of
213
+ this package: a making-way indicator, and `design.maxSpeed`.
214
+ - **Q-5** — REQ-VERIFY-5 asks for boundary fixtures on every numeric gate.
215
+ Three gates (`23a2`, `26b-mast`, `30c`'s `length_m` thresholds) live only in
216
+ `modality_by`, not in the entry's `when` — they flip `shall` to `may`, not
217
+ which entries apply. The fixture format only asserts applying entry ids, not
218
+ expected modality, so there is no way to fixture these three without
219
+ extending the schema to carry expected modality per entry. Not done
220
+ speculatively; blocks a clean REQ-VERIFY-5 pass on these three gates until
221
+ decided.