colregs 0.1.1 → 0.2.2

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.
Files changed (37) hide show
  1. package/LICENSE +194 -26
  2. package/PROVENANCE.md +45 -12
  3. package/README.md +222 -23
  4. package/data/applicability.json +1252 -190
  5. package/data/deprecated-identifiers.json +7 -0
  6. package/data/facts.json +556 -113
  7. package/data/geometry.json +33 -33
  8. package/data/images.json +51 -25
  9. package/data/lights.json +35 -35
  10. package/data/rules.json +787 -3
  11. package/docs/adr/0001-name-and-jurisdiction-model.md +119 -1
  12. package/docs/adr/0003-language-as-a-dimension.md +220 -0
  13. package/docs/adr/0004-licence-layering.md +78 -0
  14. package/docs/adr/0005-rule-categories-and-the-situation-record.md +323 -0
  15. package/docs/adr/0006-json-schema-and-identifier-diff.md +95 -0
  16. package/docs/adr/0007-rule26-overrides-and-aground.md +68 -0
  17. package/docs/adr/0008-mooring-buoy-modifier.md +87 -0
  18. package/docs/budgets.json +162 -0
  19. package/docs/conventions.md +27 -0
  20. package/docs/gates.json +84 -0
  21. package/docs/identifiers.md +435 -0
  22. package/docs/part-b-invariants.md +922 -0
  23. package/docs/requirements.md +1251 -23
  24. package/docs/verification/2026-08-30-q6-q8.md +278 -0
  25. package/docs/verification/2026-09-05-rule27f-rule28-anchor-scenarios.md +330 -0
  26. package/fixtures/applicability-fixtures.json +361 -227
  27. package/fixtures/situation-fixtures.json +3673 -0
  28. package/package.json +16 -4
  29. package/schema/applicability-fixtures.schema.json +49 -0
  30. package/schema/applicability.schema.json +401 -0
  31. package/schema/deprecated-identifiers.schema.json +29 -0
  32. package/schema/facts.schema.json +793 -0
  33. package/schema/geometry.schema.json +239 -0
  34. package/schema/images.schema.json +53 -0
  35. package/schema/lights.schema.json +131 -0
  36. package/schema/rules.schema.json +50 -0
  37. package/schema/situation-fixtures.schema.json +325 -0
package/data/facts.json CHANGED
@@ -1,74 +1,80 @@
1
1
  {
2
2
  "note": "The fact record a consumer evaluates applicability predicates against. Three orthogonal axes plus scalars; there is no separate COLREGS vessel-class field, because under COLREGS what a vessel is follows from what it is doing.",
3
3
  "axes": {
4
- "propulsion": {
4
+ "fact:propulsion": {
5
5
  "values": [
6
- "power",
7
- "sail",
8
- "oars"
6
+ "propulsion:power",
7
+ "propulsion:sail",
8
+ "propulsion:oars"
9
9
  ],
10
10
  "cites": {
11
- "power": "3(b)",
12
- "sail": "3(c)",
13
- "oars": "25(d)(ii)"
11
+ "propulsion:power": "3(b)",
12
+ "propulsion:sail": "3(c)",
13
+ "propulsion:oars": "25(d)(ii)"
14
14
  },
15
15
  "note": "A vessel under sail with propelling machinery in use is 'power' (Rule 3(c)); the 25(e) day shape is the only trace motorsailing leaves at night."
16
16
  },
17
- "activity": {
17
+ "fact:activity": {
18
18
  "values": [
19
- "none",
20
- "fishing",
21
- "trawling",
22
- "towing",
23
- "pushing",
24
- "being_towed",
25
- "nuc",
26
- "ram",
27
- "ram_underwater",
28
- "cbd",
29
- "mine",
30
- "pilot",
31
- "diving"
19
+ "activity:none",
20
+ "activity:fishing",
21
+ "activity:trawling",
22
+ "activity:towing",
23
+ "activity:pushing",
24
+ "activity:being_towed",
25
+ "activity:nuc",
26
+ "activity:ram",
27
+ "activity:ram_underwater",
28
+ "activity:cbd",
29
+ "activity:mine",
30
+ "activity:pilot",
31
+ "activity:diving"
32
32
  ],
33
33
  "cites": {
34
- "fishing": "3(d)",
35
- "trawling": "26(b)",
36
- "nuc": "3(f)",
37
- "ram": "3(g)",
38
- "ram_underwater": "27(d)",
39
- "cbd": "3(h)",
40
- "mine": "27(f)",
41
- "pilot": "29(a)",
42
- "diving": "27(e)",
43
- "towing": "24(a)",
44
- "pushing": "24(b)",
45
- "being_towed": "24(e)"
34
+ "activity:fishing": "3(d)",
35
+ "activity:trawling": "26(b)",
36
+ "activity:nuc": "3(f)",
37
+ "activity:ram": "3(g)",
38
+ "activity:ram_underwater": "27(d)",
39
+ "activity:cbd": "3(h)",
40
+ "activity:mine": "27(f)",
41
+ "activity:pilot": "29(a)",
42
+ "activity:diving": "27(e)",
43
+ "activity:towing": "24(a)",
44
+ "activity:pushing": "24(b)",
45
+ "activity:being_towed": "24(e)"
46
46
  },
47
47
  "note": "'ram_underwater' is a vessel restricted in her ability to maneuver that is dredging or engaged in underwater operations (27(d)); it is a refinement of 'ram', not a peer of it, and predicates written for 'ram' also read it."
48
48
  },
49
- "position": {
49
+ "fact:position": {
50
50
  "values": [
51
- "underway",
52
- "anchored",
53
- "aground",
54
- "moored"
51
+ "position:underway",
52
+ "position:anchored",
53
+ "position:aground",
54
+ "position:moored"
55
55
  ],
56
56
  "cites": {
57
- "underway": "3(i)"
57
+ "position:underway": "3(i)"
58
58
  },
59
59
  "note": "Rule 3(i): underway means not at anchor, not made fast to the shore, not aground."
60
60
  }
61
61
  },
62
62
  "modifiers": {
63
- "making_way": {
63
+ "fact:making_way": {
64
64
  "type": "boolean",
65
- "refines": "position=underway",
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": {
71
- "length_m": {
77
+ "fact:length_m": {
72
78
  "type": "number",
73
79
  "unit": "m",
74
80
  "cite": "3(j)",
@@ -76,7 +82,7 @@
76
82
  "actuable": true,
77
83
  "signalk": "design.length.value.overall"
78
84
  },
79
- "tow_length_m": {
85
+ "fact:tow_length_m": {
80
86
  "type": "number",
81
87
  "unit": "m",
82
88
  "cite": "24(a)(i)",
@@ -84,7 +90,7 @@
84
90
  "actuable": true,
85
91
  "signalk": null
86
92
  },
87
- "max_speed_kn": {
93
+ "fact:max_speed_kn": {
88
94
  "type": "number",
89
95
  "unit": "kn",
90
96
  "cite": "23(d)(ii)",
@@ -92,14 +98,14 @@
92
98
  "actuable": true,
93
99
  "signalk": null
94
100
  },
95
- "gear_extent_m": {
101
+ "fact:gear_extent_m": {
96
102
  "type": "number",
97
103
  "unit": "m",
98
104
  "cite": "26(c)(ii)",
99
105
  "actuable": false,
100
106
  "signalk": null
101
107
  },
102
- "beam_m": {
108
+ "fact:beam_m": {
103
109
  "type": "number",
104
110
  "unit": "m",
105
111
  "cite": "24(g)",
@@ -108,61 +114,197 @@
108
114
  }
109
115
  },
110
116
  "booleans": {
111
- "composite_unit": {
117
+ "fact:composite_unit": {
112
118
  "cite": "24(b)",
113
119
  "actuable": false
114
120
  },
115
- "non_displacement": {
121
+ "fact:non_displacement": {
116
122
  "cite": "23(b)",
117
123
  "actuable": false,
118
124
  "note": "Air-cushion vessel operating in non-displacement mode."
119
125
  },
120
- "wig": {
126
+ "fact:wig": {
121
127
  "cite": "23(c)",
122
128
  "actuable": false,
123
129
  "note": "The vessel is a wing-in-ground craft. Doesn't by itself mean 23(c) applies -- see wig_near_surface."
124
130
  },
125
- "wig_near_surface": {
131
+ "fact:wig_near_surface": {
126
132
  "cite": "23(c)",
127
133
  "actuable": false,
128
134
  "note": "Taking off, landing, or in flight near the surface. 23(c) is scoped to this phase, not to being a WIG craft generally."
129
135
  },
130
- "near_channel": {
136
+ "fact:near_channel": {
131
137
  "cite": "30(e)",
132
138
  "actuable": false,
133
139
  "note": "At anchor in or near a narrow channel, fairway, or where other vessels normally navigate."
134
140
  },
135
- "inconspicuous_partly_submerged_tow": {
141
+ "fact:inconspicuous_partly_submerged_tow": {
136
142
  "cite": "24(g)",
137
143
  "actuable": false
138
144
  },
139
- "towed_alongside": {
145
+ "fact:towed_alongside": {
140
146
  "cite": "24(c)",
141
147
  "actuable": false
142
148
  },
143
- "obstruction_exists": {
149
+ "fact:obstruction_exists": {
144
150
  "cite": "27(d)",
145
151
  "actuable": false
152
+ },
153
+ "fact:tow_restricts_deviation": {
154
+ "cite": "27(c)",
155
+ "actuable": false,
156
+ "note": "The towing operation severely restricts the towing vessel and her tow in their ability to deviate from their course. Rule 27(c) makes such a pair restricted in her ability to manoeuvre; `activity:towing` alone does not, because most tows are not. Read only by the fact:rule18_class decode table, which is where Q-32's missing 27(c) case lives."
157
+ },
158
+ "fact:confined_to_channel": {
159
+ "cite": "9(b)",
160
+ "actuable": false,
161
+ "note": "A vessel which can safely navigate only within the narrow channel or fairway -- 9(b)'s and 9(d)'s protected vessel, read of whichever vessel it is asked of rather than assumed from `pair:env:narrow_channel`. Closes Q-34's 9(b)/9(d) half: a consumer sets this from the vessel's draft and the channel, the same judgement `pair:env:narrow_channel` already asks of a chart or an ENC."
162
+ },
163
+ "fact:following_traffic_lane": {
164
+ "cite": "10(i)",
165
+ "actuable": false,
166
+ "note": "Following a traffic lane of a scheme adopted by the Organization -- 10(i)'s and 10(j)'s protected vessel, read of whichever vessel it is asked of rather than assumed from `pair:env:traffic_lane`. Closes Q-34's 10(i)/10(j) half."
146
167
  }
147
168
  },
148
169
  "enums": {
149
- "obstruction_side": {
170
+ "fact:obstruction_side": {
150
171
  "values": [
151
- "port",
152
- "starboard"
172
+ "obstruction_side:port",
173
+ "obstruction_side:starboard"
153
174
  ],
154
175
  "cite": "27(d)(i)",
155
176
  "actuable": false
156
177
  }
157
178
  },
179
+ "derived": {
180
+ "note": "Facts this package computes from the fact record rather than asking a consumer for. A derived fact carries `derived: true` and a decode table that *is* its definition, in the same style as `signalk_navigation_state` below: an ordered list of {when, value} rows read top to bottom, first match wins, and no row a match means the fact is absent. `when` is the ordinary predicate language of README.md 'Predicate semantics', so the decode is checkable by the same evaluator the entries use rather than by a second one. A consumer never supplies a derived fact and is never asked to; an evaluator computes it before matching.",
181
+ "fact:rule18_class": {
182
+ "type": "enum",
183
+ "derived": true,
184
+ "cite": "18",
185
+ "values": [
186
+ "rule18_class:nuc",
187
+ "rule18_class:ram",
188
+ "rule18_class:fishing",
189
+ "rule18_class:wig",
190
+ "rule18_class:cbd",
191
+ "rule18_class:sail",
192
+ "rule18_class:power"
193
+ ],
194
+ "cites": {
195
+ "rule18_class:nuc": "3(f)",
196
+ "rule18_class:ram": "3(g)",
197
+ "rule18_class:fishing": "3(d)",
198
+ "rule18_class:wig": "3(m)",
199
+ "rule18_class:cbd": "3(h)",
200
+ "rule18_class:sail": "3(c)",
201
+ "rule18_class:power": "3(b)"
202
+ },
203
+ "actuable": false,
204
+ "signalk": null,
205
+ "note": "A vessel's rank under Rule 18, which is not the same thing as the lights she shows. `fact:activity` is a *display* axis -- it answers what a vessel exhibits -- and Rule 18 ranks vessels by the Rule 3 terms of art instead, which cut across it: mine clearance (27(f)) and diving (27(e)) are restricted in their ability to manoeuvre by 3(g), trawling is fishing by 3(d), and a towing operation is RAM by 27(c) only when it severely restricts the pair. Before this fact each precedence entry re-listed those correspondences by hand and had to be edited every time the activity axis gained a value, with no test behind the omission. This is Q-32's answer: the rank is a derived fact, decoded once here, and a precedence entry reads the rank. `fact:activity` is unchanged and no entry that reads it was touched.",
206
+ "decode": [
207
+ {
208
+ "when": {
209
+ "fact:activity": "activity:nuc"
210
+ },
211
+ "value": "rule18_class:nuc",
212
+ "cite": "3(f)"
213
+ },
214
+ {
215
+ "when": {
216
+ "fact:activity": [
217
+ "activity:ram",
218
+ "activity:mine",
219
+ "activity:diving"
220
+ ]
221
+ },
222
+ "value": "rule18_class:ram",
223
+ "cite": "3(g)",
224
+ "note": "A bare list, which it could not be until the refinement bug was fixed: the ram -> ram_underwater refinement used to fire on a scalar constraint only, so a list would have silently missed `activity:ram_underwater` and `any_of` was written here to route each disjunct through the scalar path. The refinement is now a property of the fact's value and applies to equality, list membership and `any_of` alike, so the list says what it means. Mine clearance and diving are here because 3(g) enumerates them as restricted in their ability to manoeuvre while 27(e)/(f) give them their own lights -- the exact display/rank divergence this fact exists for."
225
+ },
226
+ {
227
+ "when": {
228
+ "fact:activity": {
229
+ "any_of": [
230
+ "activity:towing",
231
+ "activity:being_towed"
232
+ ]
233
+ },
234
+ "fact:tow_restricts_deviation": true
235
+ },
236
+ "value": "rule18_class:ram",
237
+ "cite": "27(c)",
238
+ "note": "The case Q-32 recorded as uncaught. 3(g)(vi) names \"the towing vessel and her tow\" together, so both sides of a severely-restricted tow rank as RAM here -- each from her own fact record, since a consumer supplies `fact:tow_restricts_deviation` per vessel and this decode never looks at the other subject. `any_of` for the same reason as the row above: a bare list would still work today, but a scalar disjunct keeps this open to a future refinement the way 3(g)'s row is. A tow that does not severely restrict the pair falls through to her propulsion, which is right: most tows are ordinary power-driven vessels under Rule 18."
239
+ },
240
+ {
241
+ "when": {
242
+ "fact:activity": [
243
+ "activity:fishing",
244
+ "activity:trawling"
245
+ ]
246
+ },
247
+ "value": "rule18_class:fishing",
248
+ "cite": "3(d)",
249
+ "note": "Ranked by activity and not by propulsion: 18(c) does not distinguish, so a fishing vessel under sail is fishing here and not sail."
250
+ },
251
+ {
252
+ "when": {
253
+ "fact:wig": true,
254
+ "fact:wig_near_surface": true
255
+ },
256
+ "value": "rule18_class:wig",
257
+ "cite": "3(m)",
258
+ "note": "18(f)(i) is scoped to the phase, not to being a WIG craft. A WIG on the water surface falls through to `rule18_class:power`, which is 18(f)(ii) -- recorded in known_omissions as a decode-table statement rather than a precedence norm, and this is that table."
259
+ },
260
+ {
261
+ "when": {
262
+ "fact:activity": "activity:cbd"
263
+ },
264
+ "value": "rule18_class:cbd",
265
+ "cite": "3(h)",
266
+ "note": "3(h) makes a vessel constrained by her draught a power-driven vessel, so `rule18_class:cbd` is a refinement of `rule18_class:power` and not a peer of it. Rule 18 does not rank her above anyone: 18(d) protects her, and 18(a) still obliges her, which is why the 18(a) entries gate on `fact:propulsion` rather than on this class."
267
+ },
268
+ {
269
+ "when": {
270
+ "fact:propulsion": "propulsion:sail"
271
+ },
272
+ "value": "rule18_class:sail",
273
+ "cite": "3(c)"
274
+ },
275
+ {
276
+ "when": {
277
+ "fact:propulsion": "propulsion:power"
278
+ },
279
+ "value": "rule18_class:power",
280
+ "cite": "3(b)"
281
+ }
282
+ ],
283
+ "undecodable": [
284
+ {
285
+ "case": "fact:propulsion = propulsion:oars, with no ranking activity",
286
+ "why": "Rule 3 has no term for a vessel under oars and Rule 18 does not rank her. 25(d)(ii) lets her show a sailing vessel's lights, which is a permission about lights and not a statement about rank, so decoding her to `rule18_class:sail` would put a display fact back into the rank this fact exists to separate from display. She decodes to nothing, and a predicate reading the class never matches her -- conservative, and visible here rather than silent."
287
+ },
288
+ {
289
+ "case": "a seaplane (3(e))",
290
+ "why": "The eighth term Rule 18 distinguishes, and the one value this fact does not carry: 18(e) needs a fact for being a seaplane and none exists, which known_omissions already records. A value with no row and no fact behind it would be a promise the data cannot keep, so it is absent rather than unreachable."
291
+ },
292
+ {
293
+ "case": "a fact record with no fact:propulsion",
294
+ "why": "Absent is absent. No row matches, the derived fact is absent, and every constraint over it is unsatisfied -- including a negated one."
295
+ }
296
+ ]
297
+ }
298
+ },
158
299
  "actuable_subset": {
159
- "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.",
160
301
  "fields": [
161
- "propulsion",
162
- "activity",
163
- "position",
164
- "making_way",
165
- "length_m"
302
+ "fact:propulsion",
303
+ "fact:activity",
304
+ "fact:position",
305
+ "fact:making_way",
306
+ "fact:on_mooring_buoy",
307
+ "fact:length_m"
166
308
  ]
167
309
  },
168
310
  "signalk_navigation_state": {
@@ -170,108 +312,108 @@
170
312
  "verified": "2026-08-29",
171
313
  "decode": {
172
314
  "not under command": {
173
- "activity": "nuc",
174
- "position": "underway"
315
+ "fact:activity": "activity:nuc",
316
+ "fact:position": "position:underway"
175
317
  },
176
318
  "anchored": {
177
- "activity": "none",
178
- "position": "anchored"
319
+ "fact:activity": "activity:none",
320
+ "fact:position": "position:anchored"
179
321
  },
180
322
  "moored": {
181
- "activity": "none",
182
- "position": "moored"
323
+ "fact:activity": "activity:none",
324
+ "fact:position": "position:moored"
183
325
  },
184
326
  "sailing": {
185
- "propulsion": "sail",
186
- "activity": "none",
187
- "position": "underway"
327
+ "fact:propulsion": "propulsion:sail",
328
+ "fact:activity": "activity:none",
329
+ "fact:position": "position:underway"
188
330
  },
189
331
  "motoring": {
190
- "propulsion": "power",
191
- "activity": "none",
192
- "position": "underway"
332
+ "fact:propulsion": "propulsion:power",
333
+ "fact:activity": "activity:none",
334
+ "fact:position": "position:underway"
193
335
  },
194
336
  "towing < 200m": {
195
- "propulsion": "power",
196
- "activity": "towing",
197
- "position": "underway",
198
- "tow_length_m": {
337
+ "fact:propulsion": "propulsion:power",
338
+ "fact:activity": "activity:towing",
339
+ "fact:position": "position:underway",
340
+ "fact:tow_length_m": {
199
341
  "lte": 200
200
342
  }
201
343
  },
202
344
  "towing > 200m": {
203
- "propulsion": "power",
204
- "activity": "towing",
205
- "position": "underway",
206
- "tow_length_m": {
345
+ "fact:propulsion": "propulsion:power",
346
+ "fact:activity": "activity:towing",
347
+ "fact:position": "position:underway",
348
+ "fact:tow_length_m": {
207
349
  "gt": 200
208
350
  }
209
351
  },
210
352
  "pushing": {
211
- "propulsion": "power",
212
- "activity": "pushing",
213
- "position": "underway"
353
+ "fact:propulsion": "propulsion:power",
354
+ "fact:activity": "activity:pushing",
355
+ "fact:position": "position:underway"
214
356
  },
215
357
  "fishing": {
216
- "activity": "fishing",
217
- "position": "underway"
358
+ "fact:activity": "activity:fishing",
359
+ "fact:position": "position:underway"
218
360
  },
219
361
  "fishing-hampered": {
220
- "activity": "fishing",
221
- "position": "underway",
362
+ "fact:activity": "activity:fishing",
363
+ "fact:position": "position:underway",
222
364
  "annex_ii_signal": "purse_seine_hampered"
223
365
  },
224
366
  "trawling": {
225
- "activity": "trawling",
226
- "position": "underway"
367
+ "fact:activity": "activity:trawling",
368
+ "fact:position": "position:underway"
227
369
  },
228
370
  "trawling-shooting": {
229
- "activity": "trawling",
230
- "position": "underway",
371
+ "fact:activity": "activity:trawling",
372
+ "fact:position": "position:underway",
231
373
  "annex_ii_signal": "shooting"
232
374
  },
233
375
  "trawling-hauling": {
234
- "activity": "trawling",
235
- "position": "underway",
376
+ "fact:activity": "activity:trawling",
377
+ "fact:position": "position:underway",
236
378
  "annex_ii_signal": "hauling"
237
379
  },
238
380
  "pilotage": {
239
- "activity": "pilot"
381
+ "fact:activity": "activity:pilot"
240
382
  },
241
383
  "not-under-way": {},
242
384
  "aground": {
243
- "activity": "none",
244
- "position": "aground"
385
+ "fact:activity": "activity:none",
386
+ "fact:position": "position:aground"
245
387
  },
246
388
  "restricted manouverability": {
247
- "activity": "ram"
389
+ "fact:activity": "activity:ram"
248
390
  },
249
391
  "restricted manouverability towing < 200m": {
250
- "propulsion": "power",
251
- "activity": "ram",
252
- "also_activity": "towing",
253
- "tow_length_m": {
392
+ "fact:propulsion": "propulsion:power",
393
+ "fact:activity": "activity:ram",
394
+ "also_activity": "activity:towing",
395
+ "fact:tow_length_m": {
254
396
  "lte": 200
255
397
  }
256
398
  },
257
399
  "restricted manouverability towing > 200m": {
258
- "propulsion": "power",
259
- "activity": "ram",
260
- "also_activity": "towing",
261
- "tow_length_m": {
400
+ "fact:propulsion": "propulsion:power",
401
+ "fact:activity": "activity:ram",
402
+ "also_activity": "activity:towing",
403
+ "fact:tow_length_m": {
262
404
  "gt": 200
263
405
  }
264
406
  },
265
407
  "restricted manouverability underwater operations": {
266
- "activity": "ram_underwater"
408
+ "fact:activity": "activity:ram_underwater"
267
409
  },
268
410
  "constrained by draft": {
269
- "propulsion": "power",
270
- "activity": "cbd",
271
- "position": "underway"
411
+ "fact:propulsion": "propulsion:power",
412
+ "fact:activity": "activity:cbd",
413
+ "fact:position": "position:underway"
272
414
  },
273
415
  "mine clearance": {
274
- "activity": "mine"
416
+ "fact:activity": "activity:mine"
275
417
  }
276
418
  },
277
419
  "undecodable": [
@@ -284,6 +426,11 @@
284
426
  "not defined (example)"
285
427
  ],
286
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
+ },
287
434
  {
288
435
  "what": "fishing or trawling at anchor",
289
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.",
@@ -310,5 +457,301 @@
310
457
  "cite": "24(e), 27(e)"
311
458
  }
312
459
  ]
460
+ },
461
+ "situation": {
462
+ "note": "The input a two-subject rule reads (ADR 0005, REQ-CAT-4). A situation wraps two per-vessel fact records without changing either: `own.fact` and `other.fact` are exactly the record above, key for key. Beside them sit three new fact classes -- kinematic state (`kin:`), relative geometry (`geo:`) and history (`hist:`) -- which no `display` entry reads, so a consumer that only shows lights never constructs a situation and never supplies one of these.",
463
+ "status": "pencil",
464
+ "adr": "0005",
465
+ "requirements": [
466
+ "REQ-CAT-4",
467
+ "REQ-CAT-6"
468
+ ],
469
+ "pencil": "Everything in this section is pencil (docs/conventions.md): ADR 0005 puts the whole shape there and v0.x allows the break. Settled by the first two-subject entry -- Rule 18 -- actually being written against it.",
470
+ "namespace": {
471
+ "form": "<subject>:<class>:<key>",
472
+ "doc": "docs/identifiers.md, section 'Two subjects'",
473
+ "subjects": {
474
+ "own": "the vessel the rule addresses -- the subject of every one-subject entry in applicability.json today",
475
+ "other": "the vessel own is in an encounter with",
476
+ "pair": "the encounter itself. Only facts that are symmetric between the two vessels live here."
477
+ },
478
+ "classes": {
479
+ "fact": "the per-vessel fact record above, unchanged and unprefixed inside the record. `own` and `other` only.",
480
+ "kin": "kinematic state: where a vessel is, where it is pointing, how fast it is going and turning, and what it handles like. `own` and `other` only.",
481
+ "geo": "relative geometry. Under `own`/`other` it is directional, measured in that subject's own frame; under `pair` it is symmetric between them.",
482
+ "hist": "what has already been true of this encounter and latches. `own` and `other` only; directional.",
483
+ "env": "environmental scope: a property of the water the encounter is in, not of either vessel. `pair` only -- a narrow channel is not one vessel's channel. Added by the Rule 18 PR, which is the first data to need it; fixtures/situation-fixtures.json flagged the gap before the class existed."
484
+ },
485
+ "bare_key": "A key with no subject segment means `own:`. Every predicate in data/applicability.json today is therefore already a valid situation predicate, unedited, and every fixture in fixtures/applicability-fixtures.json is already a valid one-subject situation (REQ-CAT-4).",
486
+ "directional_note": "Swapping `own` and `other` reverses the encounter, which is what a `precedence` rule needs. It also earns the directional geometry class its keep: `own:geo:rel_bearing_deg` is the bearing of the other vessel off own's bow, and `other:geo:rel_bearing_deg` is the same fact from the other side -- which is aspect. Aspect is not a separate fact; it is the other subject's relative bearing."
487
+ },
488
+ "kinematics": {
489
+ "note": "A new fact class, not an extension of the fact record (ADR 0005 sec. 2). Absolute quantities, in the world frame, one set per vessel. `kin:dynamics` is the one exception left uncited -- it is not a COLREGS concept and no paragraph will ever justify it.",
490
+ "kin:position": {
491
+ "type": "position",
492
+ "unit": "deg",
493
+ "cite": "7(b)",
494
+ "cite_pending": null,
495
+ "note": "WGS84 latitude/longitude as {\"latitude\": <deg>, \"longitude\": <deg>}. Range and bearing are derived from the pair of positions; a predicate never reads a position directly. Rule 7(b)'s \"radar plotting or equivalent systematic observation of detected objects\" is the tracking of positions this fact supplies.",
496
+ "actuable": true,
497
+ "signalk": "navigation.position.value"
498
+ },
499
+ "kin:heading_deg": {
500
+ "type": "number",
501
+ "unit": "deg",
502
+ "cite": "13(b)",
503
+ "cite_pending": null,
504
+ "note": "True heading, 0-360, clockwise from true north. Heading, not course over ground: Rule 13(b)'s overtaking sector and Rule 14's 'reciprocal or nearly reciprocal courses' are both about where the vessel is pointing. SignalK publishes radians.",
505
+ "actuable": true,
506
+ "signalk": "navigation.headingTrue.value"
507
+ },
508
+ "kin:sog_kn": {
509
+ "type": "number",
510
+ "unit": "kn",
511
+ "cite": "6",
512
+ "cite_pending": null,
513
+ "note": "Speed over ground. Cited to Rule 6 ('every vessel shall at all times proceed at a safe speed'), not the originally-pending 13(a): 13(a) only states the overtaking vessel's duty and never mentions speed, and 13(b)'s overtaking test turns on relative bearing alone ('more than 22.5 degrees abaft her beam'), not on a speed comparison. Rule 6 is the paragraph that actually reads speed.",
514
+ "actuable": true,
515
+ "signalk": "navigation.speedOverGround.value"
516
+ },
517
+ "kin:rot_deg_min": {
518
+ "type": "number",
519
+ "unit": "deg/min",
520
+ "cite": "8(b)",
521
+ "cite_pending": null,
522
+ "note": "Rate of turn, positive to starboard. Read by a `conduct` monitor rather than by a predicate at a point: Rule 8(b)'s 'readily apparent' alteration is a property of a trace.",
523
+ "actuable": true,
524
+ "signalk": "navigation.rateOfTurn.value"
525
+ },
526
+ "kin:dynamics": {
527
+ "type": "enum",
528
+ "values": [
529
+ "dynamics:tanker",
530
+ "dynamics:cargo",
531
+ "dynamics:ferry",
532
+ "dynamics:fishing",
533
+ "dynamics:yacht",
534
+ "dynamics:rib",
535
+ "dynamics:unknown"
536
+ ],
537
+ "cite": null,
538
+ "cite_pending": null,
539
+ "note": "What the vessel handles like -- the manoeuvring envelope a solver bounds own's actions by. Not a COLREGS concept and deliberately not derivable from the three axes: two power-driven vessels underway have identical facts and unrelated turning circles. The value list is a first cut and is Q-18; `dynamics:unknown` is the honest default and must stay in the set.",
540
+ "actuable": false,
541
+ "signalk": null
542
+ },
543
+ "kin:wind_side": {
544
+ "type": "enum",
545
+ "values": [
546
+ "wind_side:port",
547
+ "wind_side:starboard",
548
+ "wind_side:unknown"
549
+ ],
550
+ "cite": "12(b)",
551
+ "cite_pending": null,
552
+ "note": "Which side this vessel has the wind on, which 12(b) defines as the side opposite that on which the mainsail is carried, or for a square-rigged vessel the side opposite the largest fore-and-aft sail. `wind_side:unknown` is not a missing value: it is 12(a)(iii)'s case, a vessel with the wind on the port side who cannot determine with certainty which side the other has it on, and entry 12a3 reads it. An absent fact would be silence, and silence satisfies nothing. Declared in `kin` rather than in the fact record because a display consumer never supplies it and the fact record does not change for Part B (REQ-CAT-4) -- but it is a sailing state and not kinematics, which is recorded as strain in Q-44 rather than fixed by widening the class.",
553
+ "actuable": false,
554
+ "signalk": null
555
+ }
556
+ },
557
+ "geometry": {
558
+ "note": "Relative geometry. Directional facts are stated once per subject and mean 'measured from this subject'; symmetric facts are stated once, under `pair`. All of these are computable from the two kinematic states, and are declared as facts anyway because a consumer that has them from radar or ARPA should not be forced to round-trip through positions it does not trust.",
559
+ "directional": {
560
+ "geo:rel_bearing_deg": {
561
+ "type": "number",
562
+ "unit": "deg",
563
+ "cite": "13(b)",
564
+ "cite_pending": null,
565
+ "note": "Bearing of the *other* subject, 0-360 clockwise from this subject's own heading. Under `own` this is relative bearing; under `other` it is aspect. Rule 13(b)'s overtaking sector is `other:geo:rel_bearing_deg` in (112.5, 247.5) -- own is more than 22.5 degrees abaft the other vessel's beam. The two sector edges are declared once in `constants` below (`overtaking_sector_from_deg`, `overtaking_sector_to_deg`) and entries 13b-overtaking, 13b-overtaken and 15a-crossing all read them, so the sector and its complement cannot drift apart.",
566
+ "actuable": false,
567
+ "signalk": null
568
+ },
569
+ "geo:windward": {
570
+ "type": "boolean",
571
+ "cite": "12(a)(ii)",
572
+ "cite_pending": null,
573
+ "note": "This subject is to windward of the other. Directional and antisymmetric: where it is true for one subject it is false for the other. Computable from the wind direction and the two positions; declared as a fact for the same reason `geo:rel_bearing_deg` is, because a consumer who has it should not be made to round-trip through a wind vector this package does not carry.",
574
+ "actuable": false,
575
+ "signalk": null
576
+ }
577
+ },
578
+ "symmetric": {
579
+ "geo:range_m": {
580
+ "type": "number",
581
+ "unit": "m",
582
+ "cite": "7(a)",
583
+ "cite_pending": null,
584
+ "note": "Distance between the two vessels. Symmetric by construction. Rule 7(a)'s 'all available means... to determine if risk of collision exists' is the duty this and the other derived-geometry facts (`geo:cpa_m`, `geo:tcpa_s`) exist to serve.",
585
+ "actuable": false,
586
+ "signalk": null
587
+ },
588
+ "geo:bearing_change_deg_min": {
589
+ "type": "number",
590
+ "unit": "deg/min",
591
+ "cite": "7(d)(i)",
592
+ "cite_pending": null,
593
+ "note": "Rate of change of the compass bearing between the two vessels, signed. Rule 7(d)(i): a bearing that does not appreciably change means risk of collision exists. The Rules do not say what appreciably means; `constants.appreciable_bearing_change_deg_min` fixes it in pencil and entry 7d1 is the only reader.",
594
+ "actuable": false,
595
+ "signalk": null
596
+ },
597
+ "geo:cpa_m": {
598
+ "type": "number",
599
+ "unit": "m",
600
+ "cite": "7(a)",
601
+ "cite_pending": null,
602
+ "note": "Closest point of approach on present courses and speeds. Derived, and declared so a consumer with an ARPA solution can supply it directly. See `geo:range_m` for why Rule 7(a) is the cite.",
603
+ "actuable": false,
604
+ "signalk": null
605
+ },
606
+ "geo:tcpa_s": {
607
+ "type": "number",
608
+ "unit": "s",
609
+ "cite": "7(a)",
610
+ "cite_pending": null,
611
+ "note": "Time to closest point of approach on present courses and speeds. Negative once the CPA is past. See `geo:range_m` for why Rule 7(a) is the cite.",
612
+ "actuable": false,
613
+ "signalk": null
614
+ },
615
+ "geo:in_sight": {
616
+ "type": "boolean",
617
+ "cite": "3(k)",
618
+ "cite_pending": null,
619
+ "note": "Rule 3(k): vessels are deemed to be in sight of one another only when one can be observed visually from the other. Symmetric because the rule defines it that way, not because visibility is. This is the `scope` gate that selects Section II over Section III, and it is a fact of the pair, never of one vessel.",
620
+ "actuable": false,
621
+ "signalk": null
622
+ },
623
+ "geo:risk_of_collision": {
624
+ "type": "boolean",
625
+ "cite": "7(a)",
626
+ "cite_pending": null,
627
+ "note": "Risk of collision exists between the two vessels. Rule 7 is the whole apparatus for determining it and Rule 7(a) makes it a judgement on all available means, so this is supplied by the consumer rather than derived here from a bearing-change threshold this package declines to fix. Read by 8(f)(iii), and by every Section II conduct rule when those land.",
628
+ "actuable": false,
629
+ "signalk": null
630
+ }
631
+ },
632
+ "consistency": {
633
+ "status": "pencil",
634
+ "note": "The relative geometry is redundant with the kinematic state, and a record may state a set that no two vessels can occupy (Q-48). A situation is geometrically consistent when every quantity it states agrees with every other under the equations below; a quantity the record does not state constrains nothing, so a record with bearings and no headings is not inconsistent, only unchecked. The suite enforces this on every situation fixture and on every situation its sweeps construct (REQ-VERIFY-8). This block is the definition, so that an engine applying the same check before it classifies applies this one and not a private one.",
635
+ "equations": {
636
+ "headings": "own:geo:rel_bearing_deg + own:kin:heading_deg + 180 == other:geo:rel_bearing_deg + other:kin:heading_deg (mod 360). Both bearings are of the same line of sight, read from its two ends, so this needs no positions.",
637
+ "positions": "The great-circle range and the initial bearings between the two kin:position values reproduce pair:geo:range_m and, less each subject's heading, both geo:rel_bearing_deg.",
638
+ "motion": "With each vessel moving along her heading at her kin:sog_kn, the relative velocity in own's frame reproduces pair:geo:cpa_m, pair:geo:tcpa_s and pair:geo:bearing_change_deg_min, the last positive when the compass bearing of the other from own is increasing. The range and own's relative bearing the motion starts from are the stated pair:geo:range_m and own:geo:rel_bearing_deg, or, where the record leaves either out, the values the two kin:position fix. Course over ground is taken as heading: the record carries no set or drift.",
639
+ "steady_bearing": "A consequence of `motion` worth stating because Rule 15 rests on it: on a steady bearing with both vessels making way, own's speed times the sine of her relative bearing equals minus the other's speed times the sine of the aspect, so the two bearings lie on opposite sides. Two vessels each with the other on her starboard side are never on a collision course, which is why 15(a) never lays give-way on both -- and the suite asserts that over a sweep of steady-bearing geometries rather than over free bearings."
640
+ },
641
+ "tolerances": {
642
+ "bearing_deg": 1.0,
643
+ "range_m": 15,
644
+ "range_fraction": 0.01,
645
+ "cpa_m": 10,
646
+ "cpa_fraction": 0.05,
647
+ "tcpa_s": 5,
648
+ "tcpa_fraction": 0.05,
649
+ "bearing_change_deg_min": 0.1
650
+ },
651
+ "tolerance_note": "A position stated to five decimal places is about a metre; to four, about ten. The absolute floors are for close ranges, where a fraction of the range is smaller than the position is stated to; the fractions are for long ranges, where the floor is tighter than a bearing rounded to a tenth of a degree. A stated quantity is compared to the computed one within the larger of the two.",
652
+ "settled_by": "Whether the relative quantities become derived facts computed from the two kinematic states -- in which case these equations become the decode and the check goes with them -- or stay supplied and checked, for the consumer with an ARPA solution and no positions it trusts. Q-48 names both; this block is the second, done."
653
+ }
654
+ },
655
+ "history": {
656
+ "note": "The situation is not memoryless: Rule 13(d) latches a classification that the instantaneous geometry would later contradict. History is directional -- it is own that was or was not the overtaking vessel -- so it takes a subject segment like the fact record does.",
657
+ "hist:was_overtaking": {
658
+ "type": "boolean",
659
+ "cite": "13(d)",
660
+ "cite_pending": null,
661
+ "note": "This subject was, at some earlier point in this encounter, an overtaking vessel with respect to the other. Rule 13(d): any subsequent alteration of the bearing shall not make the overtaking vessel a crossing vessel, nor relieve her of the duty of keeping clear until finally past and clear. Without this fact the geometry alone reclassifies an overtaking as a crossing and hands the duty to the wrong vessel -- which is the exact error the paragraph exists to forbid.",
662
+ "actuable": false,
663
+ "signalk": null
664
+ },
665
+ "hist:latched_at_s": {
666
+ "type": "number",
667
+ "unit": "s",
668
+ "cite": "13(d)",
669
+ "cite_pending": null,
670
+ "note": "Seconds since the latched classification first held, or null if nothing has latched. Carried so a `conduct` monitor can say when the duty attached; a predicate at a point does not read it.",
671
+ "actuable": false,
672
+ "signalk": null
673
+ }
674
+ },
675
+ "record": {
676
+ "note": "The shape, as an illustration rather than a schema. `own.fact` and `other.fact` hold the fact record defined above, verbatim. `own` and `other` carry directional geometry; `pair` carries only the symmetric geometry. Absent is absent: a missing key never satisfies a constraint, exactly as in the one-subject evaluator.",
677
+ "shape": {
678
+ "own": {
679
+ "fact": "<the fact record above>",
680
+ "kin": "<kinematics>",
681
+ "geo": "<directional geometry>",
682
+ "hist": "<history>"
683
+ },
684
+ "other": {
685
+ "fact": "<the fact record above>",
686
+ "kin": "<kinematics>",
687
+ "geo": "<directional geometry>",
688
+ "hist": "<history>"
689
+ },
690
+ "pair": {
691
+ "geo": "<symmetric geometry>",
692
+ "env": "<environment>"
693
+ }
694
+ }
695
+ },
696
+ "environment": {
697
+ "note": "Where the encounter is happening. `pair` only, and symmetric by construction: both vessels are in the same channel or the same scheme. Declared because Rules 9 and 10 are scoped to places, and a place is neither a vessel's fact nor the pair's geometry. These are booleans a consumer sets from a chart or an ENC; nothing here derives them from a position.",
698
+ "env:narrow_channel": {
699
+ "type": "boolean",
700
+ "cite": "9(a)",
701
+ "cite_pending": null,
702
+ "note": "The encounter is in or near a narrow channel or fairway. Does not say which vessel can safely navigate only within it -- that further fact is `fact:confined_to_channel`, read per vessel (Q-34).",
703
+ "actuable": false,
704
+ "signalk": null
705
+ },
706
+ "env:traffic_lane": {
707
+ "type": "boolean",
708
+ "cite": "10(a)",
709
+ "cite_pending": null,
710
+ "note": "The encounter is within a traffic separation scheme adopted by the Organization. Does not say which vessel is following a lane -- that further fact is `fact:following_traffic_lane`, read per vessel (Q-34).",
711
+ "actuable": false,
712
+ "signalk": null
713
+ }
714
+ },
715
+ "constants": {
716
+ "note": "Numeric constants a Part B predicate needs and the Rules do not always state. Each names the fact it thresholds, its status under docs/conventions.md, and -- where it is pencil -- what would settle it. They are declared here rather than written inline in an entry so that the number appears once and a test can assert every entry uses it; they are not identifiers (docs/identifiers.md, 'What is not an identifier'), they are values like a modality or a jurisdiction.",
717
+ "appreciable_bearing_change_deg_min": {
718
+ "value": 1.0,
719
+ "unit": "deg/min",
720
+ "fact": "pair:geo:bearing_change_deg_min",
721
+ "status": "pencil",
722
+ "cite": null,
723
+ "cite_pending": "7(d)(i)",
724
+ "note": "Rule 7(d)(i) deems risk of collision to exist if the compass bearing of an approaching vessel 'does not appreciably change', and nowhere says what appreciably means. Read as a magnitude: entry 7d1 gates on the open interval (-1.0, 1.0) so a bearing drawing out either way is treated alike, because the paragraph asks whether the bearing changes and not which way.",
725
+ "settled_by": "A source that fixes the figure -- Cockcroft & Lameijer, or an administration's guidance -- or a sensitivity sweep in colregs-engine showing which values change a region finding. Until then it is one session's number and any session may change it."
726
+ },
727
+ "head_on_half_angle_deg": {
728
+ "value": 11.25,
729
+ "unit": "deg",
730
+ "fact": "own:geo:rel_bearing_deg and other:geo:rel_bearing_deg",
731
+ "status": "pencil",
732
+ "cite": null,
733
+ "cite_pending": "14(b)",
734
+ "note": "Half the head-on cone: 14(a)'s 'reciprocal or nearly reciprocal courses' and 14(b)'s 'ahead or nearly ahead'. 11.25 degrees is one point of the compass, which is the angular unit the Rules themselves use -- 13(b)'s 22.5 degrees is two points -- so the sub-unit is drawn from the same vocabulary rather than invented. Both vessels must be inside it: 14(b) is a statement about aspect as well as bearing.",
735
+ "settled_by": "The same sweep as above. The usual published figures are 5 or 6 degrees, so this constant is deliberately the wider of the candidates: 14(c) says a vessel in doubt shall assume a head-on situation exists, and a wider cone is the reading that errs the way the paragraph errs."
736
+ },
737
+ "overtaking_sector_from_deg": {
738
+ "value": 112.5,
739
+ "unit": "deg",
740
+ "fact": "geo:rel_bearing_deg",
741
+ "status": "ink",
742
+ "cite": "13(b)",
743
+ "cite_pending": null,
744
+ "note": "22.5 degrees abaft the beam, stated in 13(b) itself, and the forward edge of the sternlight arc of Rule 21(c). Ink because the number is in the rule text, not because anyone is confident about it."
745
+ },
746
+ "overtaking_sector_to_deg": {
747
+ "value": 247.5,
748
+ "unit": "deg",
749
+ "fact": "geo:rel_bearing_deg",
750
+ "status": "ink",
751
+ "cite": "13(b)",
752
+ "cite_pending": null,
753
+ "note": "The mirror of the above: 360 - 112.5. The sector is open at both ends -- 13(b) says 'more than 22.5 degrees abaft her beam', so a vessel exactly on the edge is not overtaking."
754
+ }
755
+ }
313
756
  }
314
757
  }