colregs 0.2.0 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -24,10 +24,16 @@ fixtures/ fact records and the entries that apply to them
24
24
 
25
25
  ## Coverage
26
26
 
27
- Part C lights (Rules 20-31) only, `intl` jurisdiction only, night only. Day
28
- shapes, Part D signals, and every other jurisdiction (US Inland, Canada,
29
- CEVNI) are on the roadmap in [`docs/requirements.md`](docs/requirements.md)
30
- but not present here.
27
+ Part C lights (Rules 20-31), `intl` jurisdiction, night only. Day shapes,
28
+ Part D signals, and every other jurisdiction (US Inland, Canada, CEVNI) are
29
+ on the roadmap in [`docs/requirements.md`](docs/requirements.md) but not
30
+ present here.
31
+
32
+ One exception, not a claim to model the Inland Rules: `us/inland` carries
33
+ two entries, `30a-buoy` and `30b-buoy`, for a vessel made fast to a mooring
34
+ buoy — 33 CFR 90.5 deems her at anchor and the Convention does not, so the
35
+ case cannot be stated under `intl` at all
36
+ ([ADR 0008](docs/adr/0008-mooring-buoy-modifier.md)).
31
37
 
32
38
  ## The four layers
33
39
 
@@ -151,9 +157,9 @@ add to Rule 23 rather than replacing it. Relations between them:
151
157
  | `rel:includes` | import the referenced entry's **lights only**, never its predicate |
152
158
  | `rel:conditional_includes` | import lights when the stated `when` holds; `one_of` is a set of legal alternatives |
153
159
  | `rel:in_lieu_of` | this entry's lights replace the referenced entries' lights |
154
- | `rel:excludes` | must not be shown together (25(c) and the tricolor) |
160
+ | `rel:excludes` | must not be shown together: a pick-one between alternatives (25(c) and the tricolor), never one obligation vetoing another |
155
161
  | `rel:exempts` | the referenced requirement does not apply (30(e)) |
156
- | `rel:overrides` | the superiority relation: this paragraph's requirement prevails over the referenced one's when both apply (Rule 18's "except where Rules 9, 10 and 13 otherwise require") |
162
+ | `rel:overrides` | the superiority relation: this paragraph's requirement prevails over the referenced one's when both apply (Rule 18's "except where Rules 9, 10 and 13 otherwise require"); for lights, Rule 26(a)'s "only the lights prescribed in this Rule" displacing Rule 30's anchor lights |
157
163
 
158
164
  A condition on whether a paragraph applies to the vessel at all goes in the
159
165
  predicate; a condition on which of two applicable paragraphs prevails is a
@@ -341,7 +347,7 @@ came out right.
341
347
  From 0.2.0 on, every identifier in the package is immutable once published
342
348
  (REQ-MODEL-10). [`data/deprecated-identifiers.json`](data/deprecated-identifiers.json)
343
349
  is the REQ-MODEL-11 registry a retired identifier goes into, and `npm test`
344
- refuses a removal that is not recorded there.
350
+ refuses a removal that is not recorded there. [`data/version.json`](data/version.json) mirrors `package.json`; release-please keeps it in sync (see [ADR 0009](docs/adr/0009-data-version-stamp.md)).
345
351
 
346
352
  ## Provenance and licence
347
353
 
@@ -410,8 +410,15 @@
410
410
  "count": 1
411
411
  }
412
412
  ],
413
- "rel:includes": [
414
- "23a34"
413
+ "rel:conditional_includes": [
414
+ {
415
+ "when": {
416
+ "fact:position": "position:underway"
417
+ },
418
+ "rel:includes": [
419
+ "23a34"
420
+ ]
421
+ }
415
422
  ],
416
423
  "modality": "shall",
417
424
  "images": [
@@ -427,10 +434,17 @@
427
434
  "fact:composite_unit": true
428
435
  },
429
436
  "lights": [],
430
- "rel:includes": [
431
- "23a1",
432
- "23a2",
433
- "23a34"
437
+ "rel:conditional_includes": [
438
+ {
439
+ "when": {
440
+ "fact:position": "position:underway"
441
+ },
442
+ "rel:includes": [
443
+ "23a1",
444
+ "23a2",
445
+ "23a34"
446
+ ]
447
+ }
434
448
  ],
435
449
  "modality": "shall",
436
450
  "images": [
@@ -453,8 +467,15 @@
453
467
  "arrangement": "vertical"
454
468
  }
455
469
  ],
456
- "rel:includes": [
457
- "23a34"
470
+ "rel:conditional_includes": [
471
+ {
472
+ "when": {
473
+ "fact:position": "position:underway"
474
+ },
475
+ "rel:includes": [
476
+ "23a34"
477
+ ]
478
+ }
458
479
  ],
459
480
  "rel:in_lieu_of": [
460
481
  "23a1",
@@ -640,7 +661,10 @@
640
661
  "jurisdiction": "intl",
641
662
  "cite": "26(b)(i)",
642
663
  "when": {
643
- "fact:activity": "activity:trawling"
664
+ "fact:activity": "activity:trawling",
665
+ "fact:position": {
666
+ "not": "position:aground"
667
+ }
644
668
  },
645
669
  "lights": [
646
670
  {
@@ -656,7 +680,7 @@
656
680
  "position": "lower"
657
681
  }
658
682
  ],
659
- "rel:excludes": [
683
+ "rel:overrides": [
660
684
  "30a",
661
685
  "30b"
662
686
  ],
@@ -664,14 +688,17 @@
664
688
  "images": [
665
689
  "NRHB_26_b.png"
666
690
  ],
667
- "notes": "Rule 26(a): a vessel engaged in fishing shall exhibit only the lights of Rule 26, whether underway or at anchor hence the exclusion of Rule 30."
691
+ "notes": "Rule 26(a) 'only the lights prescribed in this Rule' prevails over Rule 30's anchor lights underway or at anchor (rel:overrides, ADR 0007). Aground is neither (Rule 3(i)); she shows 30(d) alone."
668
692
  },
669
693
  {
670
694
  "id": "26b-mast",
671
695
  "jurisdiction": "intl",
672
696
  "cite": "26(b)(ii)",
673
697
  "when": {
674
- "fact:activity": "activity:trawling"
698
+ "fact:activity": "activity:trawling",
699
+ "fact:position": {
700
+ "not": "position:aground"
701
+ }
675
702
  },
676
703
  "lights": [
677
704
  {
@@ -722,7 +749,10 @@
722
749
  "jurisdiction": "intl",
723
750
  "cite": "26(c)(i)",
724
751
  "when": {
725
- "fact:activity": "activity:fishing"
752
+ "fact:activity": "activity:fishing",
753
+ "fact:position": {
754
+ "not": "position:aground"
755
+ }
726
756
  },
727
757
  "lights": [
728
758
  {
@@ -738,7 +768,7 @@
738
768
  "position": "lower"
739
769
  }
740
770
  ],
741
- "rel:excludes": [
771
+ "rel:overrides": [
742
772
  "30a",
743
773
  "30b"
744
774
  ],
@@ -750,6 +780,9 @@
750
780
  "cite": "26(c)(ii)",
751
781
  "when": {
752
782
  "fact:activity": "activity:fishing",
783
+ "fact:position": {
784
+ "not": "position:aground"
785
+ },
753
786
  "fact:gear_extent_m": {
754
787
  "gt": 150
755
788
  }
@@ -1940,7 +1973,7 @@
1940
1973
  "effect": {
1941
1974
  "encounter": "overtaking"
1942
1975
  },
1943
- "note": "The latch, and the reason `hist` is a fact class rather than a note. Once either vessel has been the overtaking vessel the encounter stays an overtaking whatever the bearing does next, so this entry reads history alone and no geometry at all. It is the counterpart of the `hist:was_overtaking: false` gate on 14b and 15a-crossing: without both halves the drawn-out bearing would produce a head-on or a crossing beside the overtaking, which is exactly the reclassification 13(d) forbids.",
1976
+ "note": "The latch, and the reason `hist` is a fact class rather than a note. Once either vessel has been the overtaking vessel the encounter stays an overtaking whatever the bearing does next, so this entry reads history alone and no geometry at all. It is the counterpart of the `hist:was_overtaking: false` gate on 14b and 15a-crossing: without both halves the drawn-out bearing would produce a head-on or a crossing beside the overtaking, which is exactly the reclassification 13(d) forbids. `pair:geo:in_sight` stays in the gate; a Rule 19(d)(i) entry reads `hist:was_overtaking` directly (Q-49).",
1944
1977
  "gap": "'Until she is finally past and clear' is the paragraph's own end condition and nothing in the fact vocabulary carries it, so in this model the latch never clears. A consumer sets and clears `hist:was_overtaking` itself. Recorded rather than approximated with a range or a bearing: past and clear is a seamanship judgement, and inventing a threshold for it would be the kind of number Q-40's neighbours are about. Q-47."
1945
1978
  },
1946
1979
  {
@@ -2114,6 +2147,56 @@
2114
2147
  },
2115
2148
  "note": "The role half of 15(a): the vessel which has the other on her own starboard side keeps out of the way. Own's bearing constraint is the starboard half of the non-overtaking sector, and its upper edge is the same declared constant 13b reads, so it is not a hand-listed sector either; the partition test asserts that this entry selects exactly the crossings in which the other bears between 0 and 180 relative, which is what makes the shorthand checkable rather than trusted. 'Two power-driven vessels' is 3(b) and nothing more, so both subjects are gated on `fact:propulsion: propulsion:power` and on no Rule 18 rank (Q-40): a vessel engaged in fishing, not under command, restricted in her ability to manoeuvre or constrained by her draught is a power-driven vessel whenever her machinery is in use, and is inside Rule 15. Where Rule 18 also ranks the pair, that entry carries `rel:overrides` against this one -- 18(a)(i)-(iii) from the unranked vessel's side, 18(c)(i)-(ii) from the fishing vessel's, 18(f)(i) from the WIG craft's -- because Rule 18's opening words except Rules 9, 10 and 13 and no others; 13(a)'s 'notwithstanding' would override it the same way, and does not need to, because this entry's bearing gates and its `hist:was_overtaking: false` already exclude every overtaking. Where Rule 18 leaves the pair unordered -- two vessels engaged in fishing, or one not under command and one restricted in her ability to manoeuvre -- this entry is the only norm in force, and it lays a helm duty on a vessel that may be unable to discharge it; that is Rule 2's region and is recorded in Q-40 rather than gated away.",
2116
2149
  "gap": "15(a)'s second duty -- 'shall, if the circumstances of the case admit, avoid crossing ahead of the other vessel' -- is `conduct` with a practicability caveat, and neither half fits a precedence entry. It is dropped here, which is Q-31's shape again: the caveat has no field to move into unless the duty happens to be a role, and this one is an action."
2150
+ },
2151
+ {
2152
+ "id": "30a-buoy",
2153
+ "jurisdiction": "us/inland",
2154
+ "cite": "30(a)",
2155
+ "when": {
2156
+ "fact:position": "position:moored",
2157
+ "fact:on_mooring_buoy": true
2158
+ },
2159
+ "lights": [
2160
+ {
2161
+ "light": "light:all_round",
2162
+ "color": "white",
2163
+ "count": 1,
2164
+ "position": "in the fore part"
2165
+ },
2166
+ {
2167
+ "light": "light:all_round",
2168
+ "color": "white",
2169
+ "count": 1,
2170
+ "position": "at or near the stern, at a lower level than the fore one"
2171
+ }
2172
+ ],
2173
+ "modality": "shall",
2174
+ "note": "33 CFR 90.5 deems a vessel made fast to a mooring buoy to be a vessel at anchor, so Inland Rule 30(a) reaches her. The Convention has no such deeming provision: under intl she is 'moored' by Rule 3(i) and no paragraph prescribes lights for her."
2175
+ },
2176
+ {
2177
+ "id": "30b-buoy",
2178
+ "jurisdiction": "us/inland",
2179
+ "cite": "30(b)",
2180
+ "when": {
2181
+ "fact:position": "position:moored",
2182
+ "fact:on_mooring_buoy": true,
2183
+ "fact:length_m": {
2184
+ "lt": 50
2185
+ }
2186
+ },
2187
+ "lights": [
2188
+ {
2189
+ "light": "light:all_round",
2190
+ "color": "white",
2191
+ "count": 1,
2192
+ "position": "where it can best be seen"
2193
+ }
2194
+ ],
2195
+ "rel:in_lieu_of": [
2196
+ "30a-buoy"
2197
+ ],
2198
+ "modality": "may",
2199
+ "note": "The under-50 m alternative, on the same 33 CFR 90.5 deeming that brings 30a-buoy in. It is a separate entry because widening 30b's predicate would edit an intl entry to carry a national delta, which REQ-SCOPE-4 forbids."
2117
2200
  }
2118
2201
  ]
2119
2202
  }
package/data/facts.json CHANGED
@@ -65,6 +65,12 @@
65
65
  "refines": "fact:position=position:underway",
66
66
  "note": "Undefined in the Rules; it means moving through the water. Underway but not making way is free drifting. Only the 26/27-family predicates read it (26(b)(iii), 26(c)(iii), 27(a)(iii), 27(b)(iii)); Rules 23 and 25 gate on 'underway' alone, so a power-driven vessel drifting in neutral still shows full running lights.",
67
67
  "actuable": true
68
+ },
69
+ "fact:on_mooring_buoy": {
70
+ "type": "boolean",
71
+ "refines": "fact:position=position:moored",
72
+ "note": "Made fast to a mooring buoy rather than to the shore. Rule 3(i) makes both 'moored', and the Convention prescribes no lights for either. Only a jurisdiction that deems a buoy-moored vessel to be at anchor reads it -- US Inland, 33 CFR 90.5 -- so under intl it selects nothing.",
73
+ "actuable": true
68
74
  }
69
75
  },
70
76
  "numerics": {
@@ -291,12 +297,13 @@
291
297
  }
292
298
  },
293
299
  "actuable_subset": {
294
- "note": "What a switching consumer needs and can get: the three axes (decodable from navigation.state), making_way, and length_m. Everything else is education-side.",
300
+ "note": "What a switching consumer needs: the three axes (decodable from navigation.state), making_way, on_mooring_buoy, and length_m. Decodability is not the test -- making_way and on_mooring_buoy change which lights must burn and the helm knows both, so a consumer must ask. Everything else is education-side.",
295
301
  "fields": [
296
302
  "fact:propulsion",
297
303
  "fact:activity",
298
304
  "fact:position",
299
305
  "fact:making_way",
306
+ "fact:on_mooring_buoy",
300
307
  "fact:length_m"
301
308
  ]
302
309
  },
@@ -419,6 +426,11 @@
419
426
  "not defined (example)"
420
427
  ],
421
428
  "lossy": [
429
+ {
430
+ "what": "made fast to a mooring buoy vs. made fast to the shore",
431
+ "why": "'moored' covers both and no SignalK path distinguishes them, so fact:on_mooring_buoy cannot be derived from navigation.state and must be asked of the consumer. Under us/inland it decides whether the 30(a)/(b) anchor lights are required.",
432
+ "cite": "3(i)"
433
+ },
422
434
  {
423
435
  "what": "fishing or trawling at anchor",
424
436
  "why": "Rule 26(a) applies 'whether underway or at anchor', but 'fishing' and 'anchored' are mutually exclusive values of one flat enum, so the pair cannot be stated.",
@@ -0,0 +1,3 @@
1
+ {
2
+ "version": "0.2.3"
3
+ }
@@ -0,0 +1,68 @@
1
+ # ADR 0007 — Rule 26 overrides Rule 30, and neither reaches a vessel aground
2
+
3
+ Date: 2026-09-08
4
+ Status: accepted
5
+
6
+ ## Context
7
+
8
+ colregs-engine's conformance register recorded two findings against this
9
+ package's data. FIND-01/02: `26b-id` and `26c-id` are `shall`, carry
10
+ `rel:excludes: ["30a","30b"]`, and `30a` is `shall` too — two obligations
11
+ excluding each other. `rel:excludes` (REQ-MODEL-7) is symmetric and
12
+ untimed: it says two things must not be shown together, the way 25(c) and
13
+ the tricolor are alternatives under 25(b), never that one obligation
14
+ displaces another. Rule 26(a) is directional — "a vessel engaged in fishing
15
+ ... shall exhibit only the lights prescribed in this Rule" — one paragraph
16
+ stating what prevails, not a mutual veto. REQ-MODEL-13 and ADR 0005 §4
17
+ already drew this line for Rule 18 over Rules 12 and 15; Rule 26 over Rule
18
+ 30 is the same shape and had not yet been moved.
19
+
20
+ colregs-engine#32 found the consequence: a fishing vessel aground evaluated
21
+ to `26c-id`'s red-over-white plus `30d-red`'s two reds and no anchor light
22
+ at all, because `26c-id` carried no position gate — it applied whether the
23
+ vessel was underway, anchored or aground — and its exclusion of `30a`/`30b`
24
+ stripped the anchor lights `30d-anchor` imports from them, leaving nothing
25
+ in their place. Rule 26(a)'s own words are "whether underway or at anchor";
26
+ aground is neither state (Rule 3(i) defines "underway" as not made fast to
27
+ the shore, at anchor, or aground). Rule 26 has no jurisdiction over a vessel
28
+ aground; Rule 30(d) alone does.
29
+
30
+ ## Decision
31
+
32
+ 1. **`rel:excludes` is pick-one between alternatives; two obligations never
33
+ exclude each other.** Where two `shall` entries were related by
34
+ `rel:excludes`, that was always a superiority relation misfiled — REQ-
35
+ MODEL-7's mutual-exclusion semantics never fit two unconditional duties.
36
+
37
+ 2. **`rel:overrides` carries Rule 26(a).** `26b-id` and `26c-id` now carry
38
+ `rel:overrides: ["30a","30b"]` in place of `rel:excludes`. On a `display`
39
+ entry, "prevails" means: while the overriding entry applies, the
40
+ overridden entry's lights are not shown, even though its own predicate
41
+ is satisfied. A consumer resolving `rel:overrides` (as colregs-engine
42
+ does) drops `30a`/`30b`'s lights whenever `26b-id`/`26c-id` fires; a
43
+ consumer that does not resolve it sees both, which is `rel:excludes`'
44
+ old, wrong behaviour restored — the reason to resolve it.
45
+
46
+ 3. **Aground is neither underway nor at anchor, so a vessel aground shows
47
+ Rule 30(d) alone.** `26b-id`, `26b-mast`, `26c-id` and `26c-gear` — the
48
+ four Rule 26 lights entries with no position gate — now read
49
+ `"fact:position": {"not": "position:aground"}`. `26b-mw`/`26c-mw` needed
50
+ no change: they already read `fact:making_way`, which refines underway
51
+ and is never true aground. Gating all four, not only the two FIND-01/02
52
+ named, is what "Rule 30(d) alone, full stop" requires: leaving the mast
53
+ or gear light ungated would still show a Rule 26 light beside 30(d)'s.
54
+
55
+ ## Consequences
56
+
57
+ - A consumer must resolve `rel:overrides` on `display` entries to get the
58
+ right light set at anchor; colregs-engine already does, in a PR landing
59
+ alongside this one, and will bump its `colregs` pin once this releases.
60
+ - `fact:position` is now a required read for Rule 26's lights entries. A
61
+ predicate never fires on an absent fact (`satisfies`, `test/data.test.mjs`),
62
+ so a fact record that omits `fact:position` no longer shows any Rule 26
63
+ light — same rule as `not` elsewhere in this package, applied for the
64
+ first time to a Rule 26 entry.
65
+ - Trawling while anchored remains a pair the data cannot rule out at the
66
+ facts level — `fact:activity` and `fact:position` are orthogonal axes —
67
+ pending its own declaration; this ADR does not add one.
68
+ - colregs-engine's FIND-01 and FIND-02 close on this ADR.
@@ -0,0 +1,87 @@
1
+ # ADR 0008 — `position:moored` stays one value; a mooring buoy is a modifier
2
+
3
+ Date: 2026-09-08
4
+ Status: accepted
5
+
6
+ ## Context
7
+
8
+ Rule 3(i) defines "underway" as "not at anchor, or made fast to the shore,
9
+ or aground". A vessel made fast to a mooring buoy is none of those three:
10
+ she is not at anchor, and a buoy is not the shore. Read strictly she is
11
+ therefore *underway*, and would owe Rule 23 or Rule 25 running lights while
12
+ lying still on a buoy all night — which nobody does and no authority
13
+ expects. Read the way every pilot book reads it, she is moored, and the
14
+ Convention prescribes her no lights at all: no paragraph of Part C reaches
15
+ `position:moored`.
16
+
17
+ The United States closed the gap by legislation, not by interpretation.
18
+ 33 CFR 90.5 provides that a vessel made fast to a mooring buoy is deemed to
19
+ be a vessel at anchor, which brings Inland Rule 30(a) — and for a vessel
20
+ under 50 m, 30(b) — to bear on her. That is a national deeming provision.
21
+ It has no counterpart in the Convention.
22
+
23
+ The modelling question was whether "moored to a buoy" is a fourth value of
24
+ `fact:position`, beside `position:moored`. It is not. Splitting the axis
25
+ would make every existing predicate that reads `position:moored` silently
26
+ wrong for the buoy case, and would encode a US rule in the shape of the
27
+ international vocabulary — every consumer, in every jurisdiction, paying
28
+ for a distinction only one jurisdiction draws.
29
+
30
+ ## Decision
31
+
32
+ 1. **`position:moored` stays one value.** The axis is not split, and no
33
+ existing identifier is renamed.
34
+
35
+ 2. **`fact:on_mooring_buoy` is a boolean modifier refining
36
+ `fact:position=position:moored`,** shaped exactly like `fact:making_way`
37
+ refines `position:underway`. A modifier is the established way this
38
+ package says "one state, two legally distinct sub-cases": the axis value
39
+ stays the coarse fact everyone agrees on, and the modifier carries the
40
+ refinement only the rules that need it read.
41
+
42
+ 3. **Alongside prescribes no lights, and that is the Convention's answer,
43
+ not a gap.** Rule 3(i)'s "made fast to the shore" is a moored vessel; no
44
+ Part C paragraph gives her lights. A fixture asserts the empty set, so
45
+ the silence is tested rather than assumed.
46
+
47
+ 4. **The buoy case gets the 30(a)/30(b) anchor lights only under a
48
+ jurisdiction that says so.** Two entries, `30a-buoy` and `30b-buoy`,
49
+ carry `jurisdiction: us/inland` and cite 30(a) and 30(b); `30b-buoy` is
50
+ `may`, `rel:in_lieu_of` `30a-buoy`, on the same under-50 m gate 30(b)
51
+ uses. Under `intl` the same fact record selects nothing. This is the
52
+ jurisdiction dimension of ADR 0001 doing the job it was built for: a
53
+ national departure is a delta on the base, never an edit to it.
54
+
55
+ 5. **The fixture corpus gains a per-case `jurisdiction`.** The corpus-level
56
+ value is now the default; a case may name its own and is evaluated
57
+ against `intl` plus that jurisdiction's deltas (REQ-SCOPE-3). Without it
58
+ the evaluator has no way to say which body of rules a fact record is
59
+ being read under, and a national entry would leak into every
60
+ international answer.
61
+
62
+ 6. **`fact:on_mooring_buoy` is actuable and is not derivable from SignalK.**
63
+ `navigation.state: "moored"` covers alongside and buoy alike and no
64
+ SignalK path separates them, so the fact is recorded in the decode
65
+ table's `lossy` list and must be asked of the consumer. SignalK is an
66
+ integration point, not a constraint on the model: a fact the rules turn
67
+ on stays in the vocabulary whether or not a sensor can report it.
68
+
69
+ ## Consequences
70
+
71
+ - This is the first non-`intl` applicability entry in the package, and it
72
+ arrives before Part C is complete for `intl` (REQ-PART-1) and before the
73
+ explicit suppression mechanism REQ-SCOPE-3 requires. Both are amended
74
+ with a narrow carve-out: a delta that only *adds* entries suppresses
75
+ nothing, so silence-means-inherit — the hazard Q-11 records, where a
76
+ jurisdiction deliberately has no rule and inherits one anyway — cannot
77
+ arise from it. A delta that suppresses or replaces an `intl` entry still
78
+ waits on that mechanism.
79
+ - The evaluator in `test/data.test.mjs` now filters by jurisdiction in both
80
+ directions, forward and drift. An entry from another jurisdiction is not
81
+ a drift candidate: it was never in force for that record.
82
+ - A consumer that ignores jurisdiction will show anchor lights to a
83
+ buoy-moored vessel in the English Channel. The filter is three lines and
84
+ the fixtures pin both answers.
85
+ - `us/inland` is now a jurisdiction with exactly two entries in it. It is
86
+ not a claim to model the Inland Rules; README's coverage statement
87
+ (REQ-SCOPE-6) says so explicitly.
@@ -0,0 +1,47 @@
1
+ # ADR 0009 — `data/version.json` is the single version stamp for `data/`
2
+
3
+ Date: 2026-09-08
4
+ Status: accepted
5
+
6
+ ## Context
7
+
8
+ Nothing in this package's schema or data files carries a version anywhere a
9
+ consumer can check it against. colregs-engine's `Evaluation.colregs.version`
10
+ names the engine's own resolved npm dependency, not the `data` object it was
11
+ actually handed — so a caller evaluating against a stale cached fixture, a
12
+ monorepo with mismatched `colregs` versions, or applicability data
13
+ reconstructed by hand from an old export gets a result silently attributed
14
+ to whatever release happens to be resolved locally: not a crash, not a
15
+ warning, just a wrong-but-plausible `Evaluation`.
16
+
17
+ Two alternatives were considered and rejected:
18
+
19
+ - **A `version`/`dataVersion` field on each of the seven `data/*.json`
20
+ files.** The files already release as one unit — one npm version, one
21
+ `package.json` — so seven independent copies can only drift, never add
22
+ information.
23
+ - **Leaning on the npm package version alone (status quo).** That is
24
+ exactly what colregs-engine already does, and exactly what it cannot
25
+ verify against, because the installed package version and the data
26
+ actually passed into `evaluate()` are two different things once the data
27
+ crosses a process/cache/file boundary.
28
+
29
+ ## Decision
30
+
31
+ A single manifest, `data/version.json`, is the one source of truth:
32
+
33
+ - `{"version": "<semver>"}`, matching `package.json` at release time.
34
+ - `schema/version.schema.json` validates it, same pattern as every other
35
+ `data/*.json` file.
36
+ - `release-please-config.json`'s `extra-files` generic JSON updater keeps
37
+ it in sync on every release, the same way `.release-please-manifest.json`
38
+ already is — the version cannot go stale without the release itself
39
+ failing.
40
+
41
+ ## Consequences
42
+
43
+ - colregs-engine (or any consumer) can compare its resolved package version
44
+ against `data/version.json` at the point `evaluate()` receives `data`,
45
+ and throw or warn on a mismatch instead of silently reporting the wrong
46
+ version. That comparison is a colregs-engine change, tracked separately.
47
+ - `data/version.json` is never hand-edited; release-please owns it.
@@ -0,0 +1,163 @@
1
+ {
2
+ "$comment": "Prose budgets enforced by prose-budget (the engine in dotfiles .local/bin), run locally by npm test and in CI by the shared workflow. Raising a number or adding an exception is a deliberate, reviewable diff.",
3
+ "lines": {
4
+ "README.md": 360,
5
+ "AGENTS.md": 120,
6
+ "CLAUDE.md": 10,
7
+ "docs/requirements.md": 1500,
8
+ "docs/identifiers.md": 450,
9
+ "docs/part-b-invariants.md": 950,
10
+ "docs/adr/0007-rule26-overrides-and-aground.md": 90,
11
+ "docs/adr/0008-mooring-buoy-modifier.md": 90,
12
+ "docs/adr/0009-data-version-stamp.md": 90,
13
+ "required": true,
14
+ "pending": []
15
+ },
16
+ "json_prose": {
17
+ "targets": [
18
+ "data/*.json",
19
+ "fixtures/*.json"
20
+ ],
21
+ "keys": [
22
+ "note",
23
+ "gap",
24
+ "narrative",
25
+ "settled_by",
26
+ "rationale",
27
+ "why"
28
+ ],
29
+ "max_chars": 450,
30
+ "grandfathered": [
31
+ "data/applicability.json#/effects/note",
32
+ "data/applicability.json#/known_omissions/9/why",
33
+ "data/applicability.json#/known_omissions/10/why",
34
+ "data/applicability.json#/known_omissions/11/why",
35
+ "data/applicability.json#/entries/43/note",
36
+ "data/applicability.json#/entries/44/note",
37
+ "data/applicability.json#/entries/45/note",
38
+ "data/applicability.json#/entries/47/note",
39
+ "data/applicability.json#/entries/50/note",
40
+ "data/applicability.json#/entries/51/note",
41
+ "data/applicability.json#/entries/52/note",
42
+ "data/applicability.json#/entries/53/note",
43
+ "data/applicability.json#/entries/55/note",
44
+ "data/applicability.json#/entries/56/note",
45
+ "data/applicability.json#/entries/60/note",
46
+ "data/applicability.json#/entries/61/note",
47
+ "data/applicability.json#/entries/62/note",
48
+ "data/applicability.json#/entries/63/note",
49
+ "data/applicability.json#/entries/64/note",
50
+ "data/applicability.json#/entries/64/gap",
51
+ "data/applicability.json#/entries/65/note",
52
+ "data/applicability.json#/entries/65/gap",
53
+ "data/applicability.json#/entries/66/note",
54
+ "data/applicability.json#/entries/67/note",
55
+ "data/applicability.json#/entries/68/note",
56
+ "data/applicability.json#/entries/68/gap",
57
+ "data/applicability.json#/entries/69/note",
58
+ "data/facts.json#/derived/note",
59
+ "data/facts.json#/derived/fact:rule18_class/note",
60
+ "data/facts.json#/derived/fact:rule18_class/decode/1/note",
61
+ "data/facts.json#/derived/fact:rule18_class/decode/2/note",
62
+ "data/facts.json#/situation/kinematics/kin:wind_side/note",
63
+ "data/facts.json#/situation/geometry/directional/geo:rel_bearing_deg/note",
64
+ "data/facts.json#/situation/geometry/consistency/note",
65
+ "data/facts.json#/situation/history/hist:was_overtaking/note",
66
+ "data/facts.json#/situation/constants/note",
67
+ "fixtures/situation-fixtures.json#/case_status/note",
68
+ "fixtures/situation-fixtures.json#/cases/0/narrative",
69
+ "fixtures/situation-fixtures.json#/cases/1/narrative",
70
+ "fixtures/situation-fixtures.json#/cases/2/narrative",
71
+ "fixtures/situation-fixtures.json#/cases/46/narrative",
72
+ "fixtures/situation-fixtures.json#/cases/63/narrative",
73
+ "fixtures/situation-fixtures.json#/cases/76/narrative",
74
+ "fixtures/situation-fixtures.json#/cases/77/narrative",
75
+ "fixtures/situation-fixtures.json#/cases/78/narrative",
76
+ "fixtures/situation-fixtures.json#/cases/79/narrative"
77
+ ]
78
+ },
79
+ "narration": {
80
+ "targets": [
81
+ "README.md",
82
+ "AGENTS.md",
83
+ "CLAUDE.md",
84
+ "data/*.json",
85
+ "fixtures/*.json",
86
+ "test/*.mjs",
87
+ "docs/**/*.md"
88
+ ],
89
+ "scope": {
90
+ "pr": [
91
+ "README.md",
92
+ "AGENTS.md",
93
+ "CLAUDE.md",
94
+ "data/*.json",
95
+ "fixtures/*.json",
96
+ "test/*.mjs"
97
+ ],
98
+ "issue": [
99
+ "README.md",
100
+ "AGENTS.md",
101
+ "CLAUDE.md",
102
+ "data/*.json",
103
+ "fixtures/*.json",
104
+ "test/*.mjs"
105
+ ],
106
+ "phase": [
107
+ "README.md",
108
+ "AGENTS.md",
109
+ "CLAUDE.md",
110
+ "data/*.json",
111
+ "fixtures/*.json",
112
+ "test/*.mjs"
113
+ ],
114
+ "split": [
115
+ "README.md",
116
+ "AGENTS.md",
117
+ "CLAUDE.md",
118
+ "data/*.json",
119
+ "fixtures/*.json",
120
+ "test/*.mjs"
121
+ ]
122
+ },
123
+ "grandfathered": [
124
+ {
125
+ "file": "data/applicability.json",
126
+ "hash": "447b3984d57b",
127
+ "match": "PR #24"
128
+ },
129
+ {
130
+ "file": "data/applicability.json",
131
+ "hash": "5ff9d39319b8",
132
+ "match": "PR #24"
133
+ },
134
+ {
135
+ "file": "docs/conventions.md",
136
+ "hash": "d1f1ed997431",
137
+ "match": "the session"
138
+ },
139
+ {
140
+ "file": "docs/requirements.md",
141
+ "hash": "cdbdaf145c0a",
142
+ "match": "seeded 2026-"
143
+ }
144
+ ]
145
+ },
146
+ "voice": {
147
+ "scope": "tree",
148
+ "targets": [
149
+ "README.md",
150
+ "AGENTS.md",
151
+ "CLAUDE.md",
152
+ "docs/**/*.md",
153
+ "data/*.json",
154
+ "fixtures/*.json"
155
+ ]
156
+ },
157
+ "unique_ids": [
158
+ {
159
+ "file": "docs/requirements.md",
160
+ "pattern": "\\*\\*(Q-\\d+|REQ-[A-Z]+-\\d+)\\*\\*"
161
+ }
162
+ ]
163
+ }
@@ -36,7 +36,7 @@ resolves that collision by construction rather than by convention.
36
36
  | form | class | examples |
37
37
  |---|---|---|
38
38
  | `light:<id>` | light definitions (`data/lights.json`) | `light:masthead`, `light:sidelight_starboard`, `light:all_round` |
39
- | `fact:<key>` | fact keys — the input vocabulary (`data/facts.json`) | `fact:activity`, `fact:length_m`, `fact:making_way` |
39
+ | `fact:<key>` | fact keys — the input vocabulary (`data/facts.json`) | `fact:activity`, `fact:length_m`, `fact:making_way`, `fact:on_mooring_buoy` |
40
40
  | `<fact>:<value>` | values of an enumerated fact | `activity:nuc`, `position:anchored`, `propulsion:sail`, `obstruction_side:port` |
41
41
  | `rel:<name>` | the five relation verbs (`data/applicability.json`) | `rel:includes`, `rel:in_lieu_of`, `rel:exempts` |
42
42