colregs 0.3.1 → 0.3.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 +22 -14
- package/data/applicability.json +295 -276
- package/data/facts.json +19 -19
- package/data/i18n/en.json +73 -7
- package/data/i18n/fi.json +5 -5
- package/data/lights.json +10 -20
- package/data/version.json +1 -1
- package/docs/adr/0010-text-withheld-jurisdictions.md +2 -0
- package/docs/adr/0011-api-shape.md +7 -7
- package/docs/adr/0012-trace-and-rule2-departure-api.md +2 -2
- package/docs/adr/0016-encounter-roles-are-pooled-across-frames.md +82 -0
- package/docs/adr/0017-closed-vocabularies-are-prefixed-identifiers.md +105 -0
- package/docs/adr/0018-jurisdiction-delta-is-a-merge-patch.md +102 -0
- package/docs/adr/0019-relation-reach-and-import-reads.md +106 -0
- package/docs/budgets.json +6 -1
- package/docs/decisions.md +8 -0
- package/docs/identifiers.md +77 -60
- package/docs/part-b-invariants.md +41 -38
- package/docs/requirements.md +137 -137
- package/fixtures/applicability-fixtures.json +42 -0
- package/fixtures/situation-fixtures.json +600 -329
- package/package.json +4 -1
- package/schema/applicability.schema.json +74 -59
- package/schema/conduct-evaluation.schema.json +1 -1
- package/schema/encounter-evaluation.schema.json +6 -6
- package/schema/evaluation.schema.json +2 -2
- package/schema/facts.schema.json +4 -4
- package/schema/i18n-catalog.schema.json +26 -5
- package/schema/lights.schema.json +2 -3
- package/schema/situation-fixtures.schema.json +39 -0
- package/schema/situation.schema.json +3 -3
package/data/facts.json
CHANGED
|
@@ -459,7 +459,7 @@
|
|
|
459
459
|
]
|
|
460
460
|
},
|
|
461
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: `
|
|
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: `self.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 showing only lights never builds a situation or supplies one of these.",
|
|
463
463
|
"status": "pencil",
|
|
464
464
|
"adr": "0005",
|
|
465
465
|
"requirements": [
|
|
@@ -471,19 +471,19 @@
|
|
|
471
471
|
"form": "<subject>:<class>:<key>",
|
|
472
472
|
"doc": "docs/identifiers.md, section 'Two subjects'",
|
|
473
473
|
"subjects": {
|
|
474
|
-
"
|
|
475
|
-
"other": "the vessel
|
|
474
|
+
"self": "the vessel the rule addresses -- the subject of every one-subject entry in applicability.json today",
|
|
475
|
+
"other": "the vessel self is in an encounter with",
|
|
476
476
|
"pair": "the encounter itself. Only facts that are symmetric between the two vessels live here."
|
|
477
477
|
},
|
|
478
478
|
"classes": {
|
|
479
|
-
"fact": "the per-vessel fact record above, unchanged and unprefixed inside the record. `
|
|
480
|
-
"kin": "kinematic state: where a vessel is, where it is pointing, how fast it is going and turning, and what it handles like. `
|
|
481
|
-
"geo": "relative geometry. Under `
|
|
482
|
-
"hist": "what has already been true of this encounter and
|
|
479
|
+
"fact": "the per-vessel fact record above, unchanged and unprefixed inside the record. `self` 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. `self` and `other` only.",
|
|
481
|
+
"geo": "relative geometry. Under `self`/`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 persists. `self` and `other` only; directional.",
|
|
483
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
484
|
},
|
|
485
|
-
"bare_key": "A key with no subject segment means `
|
|
486
|
-
"directional_note": "Swapping `
|
|
485
|
+
"bare_key": "A key with no subject segment means `self:`. 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 `self` and `other` reverses the encounter, which is what a `precedence` rule needs. It also earns the directional geometry class its keep: `self:geo:rel_bearing_deg` is the bearing of the other vessel off self'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
487
|
},
|
|
488
488
|
"kinematics": {
|
|
489
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.",
|
|
@@ -536,7 +536,7 @@
|
|
|
536
536
|
],
|
|
537
537
|
"cite": null,
|
|
538
538
|
"cite_pending": null,
|
|
539
|
-
"note": "What the vessel handles like -- the manoeuvring envelope a solver bounds
|
|
539
|
+
"note": "What the vessel handles like -- the manoeuvring envelope a solver bounds self'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
540
|
"actuable": false,
|
|
541
541
|
"signalk": null
|
|
542
542
|
},
|
|
@@ -562,7 +562,7 @@
|
|
|
562
562
|
"unit": "deg",
|
|
563
563
|
"cite": "13(b)",
|
|
564
564
|
"cite_pending": null,
|
|
565
|
-
"note": "Bearing of the *other* subject, 0-360 clockwise from this subject's own heading. Under `
|
|
565
|
+
"note": "Bearing of the *other* subject, 0-360 clockwise from this subject's own (self) heading. Under `self` 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) -- self 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 rule:13b and rule:15a:crossing both read them, so the sector and its complement cannot drift apart.",
|
|
566
566
|
"actuable": false,
|
|
567
567
|
"signalk": null
|
|
568
568
|
},
|
|
@@ -633,10 +633,10 @@
|
|
|
633
633
|
"status": "pencil",
|
|
634
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
635
|
"equations": {
|
|
636
|
-
"headings": "
|
|
636
|
+
"headings": "self:geo:rel_bearing_deg + self: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
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
|
|
639
|
-
"steady_bearing": "A consequence of `motion` worth stating because Rule 15 rests on it: on a steady bearing with both vessels making way,
|
|
638
|
+
"motion": "With each vessel moving along her heading at her kin:sog_kn, the relative velocity in self'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 self is increasing. The range and self's relative bearing the motion starts from are the stated pair:geo:range_m and self: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, self'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
640
|
},
|
|
641
641
|
"tolerances": {
|
|
642
642
|
"bearing_deg": 1.0,
|
|
@@ -653,7 +653,7 @@
|
|
|
653
653
|
}
|
|
654
654
|
},
|
|
655
655
|
"history": {
|
|
656
|
-
"note": "The situation is not memoryless: Rule 13(d)
|
|
656
|
+
"note": "The situation is not memoryless: Rule 13(d) carries forward a classification that the instantaneous geometry would later contradict. History is directional -- it is self that was or was not the overtaking vessel -- so it takes a subject segment like the fact record does.",
|
|
657
657
|
"hist:was_overtaking": {
|
|
658
658
|
"type": "boolean",
|
|
659
659
|
"cite": "13(d)",
|
|
@@ -667,15 +667,15 @@
|
|
|
667
667
|
"unit": "s",
|
|
668
668
|
"cite": "13(d)",
|
|
669
669
|
"cite_pending": null,
|
|
670
|
-
"note": "Seconds since the
|
|
670
|
+
"note": "Seconds since the carried-forward classification first held, or null if none has held yet. Carried so a `conduct` monitor can say when the duty attached; a predicate at a point does not read it.",
|
|
671
671
|
"actuable": false,
|
|
672
672
|
"signalk": null
|
|
673
673
|
}
|
|
674
674
|
},
|
|
675
675
|
"record": {
|
|
676
|
-
"note": "The shape, as an illustration rather than a schema. `
|
|
676
|
+
"note": "The shape, as an illustration rather than a schema. `self.fact` and `other.fact` hold the fact record defined above, verbatim. `self` 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
677
|
"shape": {
|
|
678
|
-
"
|
|
678
|
+
"self": {
|
|
679
679
|
"fact": "<the fact record above>",
|
|
680
680
|
"kin": "<kinematics>",
|
|
681
681
|
"geo": "<directional geometry>",
|
|
@@ -727,7 +727,7 @@
|
|
|
727
727
|
"head_on_half_angle_deg": {
|
|
728
728
|
"value": 11.25,
|
|
729
729
|
"unit": "deg",
|
|
730
|
-
"fact": "
|
|
730
|
+
"fact": "self:geo:rel_bearing_deg and other:geo:rel_bearing_deg",
|
|
731
731
|
"status": "pencil",
|
|
732
732
|
"cite": null,
|
|
733
733
|
"cite_pending": "14(b)",
|
package/data/i18n/en.json
CHANGED
|
@@ -7,21 +7,87 @@
|
|
|
7
7
|
"reviewed_by": [
|
|
8
8
|
"mark-brannan"
|
|
9
9
|
],
|
|
10
|
-
"review_date": "2026-09-
|
|
10
|
+
"review_date": "2026-09-16"
|
|
11
11
|
},
|
|
12
12
|
"strings": {
|
|
13
|
-
"
|
|
13
|
+
"lights": {
|
|
14
14
|
"light:masthead": "Masthead light",
|
|
15
|
+
"light:sidelights": "Sidelights",
|
|
15
16
|
"light:sidelight_starboard": "Starboard sidelight",
|
|
16
17
|
"light:sidelight_port": "Port sidelight",
|
|
17
18
|
"light:sternlight": "Sternlight",
|
|
18
19
|
"light:towing": "Towing light",
|
|
19
|
-
"light:all_round": "All-round light"
|
|
20
|
+
"light:all_round": "All-round light",
|
|
21
|
+
"light:flashing": "Flashing light",
|
|
22
|
+
"light:torch": "Electric torch or lighted lantern",
|
|
23
|
+
"light:deck_lights": "Working or equivalent lights"
|
|
20
24
|
},
|
|
21
|
-
"
|
|
22
|
-
"shall": "Required",
|
|
23
|
-
"may": "Permitted",
|
|
24
|
-
"shall-if-practicable": "Required if practicable"
|
|
25
|
+
"modalities": {
|
|
26
|
+
"modality:shall": "Required",
|
|
27
|
+
"modality:may": "Permitted",
|
|
28
|
+
"modality:shall-if-practicable": "Required if practicable",
|
|
29
|
+
"modality:conditional": "Conditional",
|
|
30
|
+
"modality:exempt": "Exempt",
|
|
31
|
+
"modality:shall-not": "Prohibited",
|
|
32
|
+
"modality:shall-not-impede": "Must not impede"
|
|
33
|
+
},
|
|
34
|
+
"roles": {
|
|
35
|
+
"role:give-way": "Give way",
|
|
36
|
+
"role:stand-on": "Stand on",
|
|
37
|
+
"role:shall-not-impede": "Must not impede",
|
|
38
|
+
"role:keep-clear": "Keep clear",
|
|
39
|
+
"role:none": "No role"
|
|
40
|
+
},
|
|
41
|
+
"encounters": {
|
|
42
|
+
"encounter:head-on": "Head-on",
|
|
43
|
+
"encounter:crossing": "Crossing",
|
|
44
|
+
"encounter:overtaking": "Overtaking",
|
|
45
|
+
"encounter:none": "No encounter"
|
|
46
|
+
},
|
|
47
|
+
"jurisdictions": {
|
|
48
|
+
"intl": "International",
|
|
49
|
+
"us/inland": "US Inland"
|
|
50
|
+
},
|
|
51
|
+
"facts": {
|
|
52
|
+
"propulsion:power": "Power-driven",
|
|
53
|
+
"propulsion:sail": "Sailing",
|
|
54
|
+
"propulsion:oars": "Oars",
|
|
55
|
+
"activity:none": "None",
|
|
56
|
+
"activity:fishing": "Fishing",
|
|
57
|
+
"activity:trawling": "Trawling",
|
|
58
|
+
"activity:towing": "Towing",
|
|
59
|
+
"activity:pushing": "Pushing",
|
|
60
|
+
"activity:being_towed": "Being towed",
|
|
61
|
+
"activity:nuc": "Not under command",
|
|
62
|
+
"activity:ram": "Restricted in her ability to manoeuvre",
|
|
63
|
+
"activity:ram_underwater": "Restricted in her ability to manoeuvre, engaged in underwater operations",
|
|
64
|
+
"activity:cbd": "Constrained by her draught",
|
|
65
|
+
"activity:mine": "Engaged in minesweeping",
|
|
66
|
+
"activity:pilot": "Engaged on pilotage duty",
|
|
67
|
+
"activity:diving": "Engaged in diving operations",
|
|
68
|
+
"position:underway": "Underway",
|
|
69
|
+
"position:anchored": "At anchor",
|
|
70
|
+
"position:aground": "Aground",
|
|
71
|
+
"position:moored": "Moored",
|
|
72
|
+
"rule18_class:nuc": "Not under command",
|
|
73
|
+
"rule18_class:ram": "Restricted in ability to manoeuvre",
|
|
74
|
+
"rule18_class:fishing": "Fishing",
|
|
75
|
+
"rule18_class:wig": "Wing-in-ground craft",
|
|
76
|
+
"rule18_class:cbd": "Constrained by draught",
|
|
77
|
+
"rule18_class:sail": "Sailing vessel",
|
|
78
|
+
"rule18_class:power": "Power-driven vessel",
|
|
79
|
+
"dynamics:tanker": "Tanker",
|
|
80
|
+
"dynamics:cargo": "Cargo ship",
|
|
81
|
+
"dynamics:ferry": "Ferry",
|
|
82
|
+
"dynamics:fishing": "Fishing vessel",
|
|
83
|
+
"dynamics:yacht": "Yacht",
|
|
84
|
+
"dynamics:rib": "RIB",
|
|
85
|
+
"dynamics:unknown": "Unknown",
|
|
86
|
+
"obstruction_side:port": "Port",
|
|
87
|
+
"obstruction_side:starboard": "Starboard",
|
|
88
|
+
"wind_side:port": "Port",
|
|
89
|
+
"wind_side:starboard": "Starboard",
|
|
90
|
+
"wind_side:unknown": "Unknown"
|
|
25
91
|
}
|
|
26
92
|
}
|
|
27
93
|
}
|
package/data/i18n/fi.json
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"review_date": "2026-09-15"
|
|
9
9
|
},
|
|
10
10
|
"strings": {
|
|
11
|
-
"
|
|
11
|
+
"lights": {
|
|
12
12
|
"light:masthead": "Mastovalo",
|
|
13
13
|
"light:sidelight_starboard": "Oikea sivuvalo",
|
|
14
14
|
"light:sidelight_port": "Vasen sivuvalo",
|
|
@@ -16,10 +16,10 @@
|
|
|
16
16
|
"light:towing": "Hinausvalo",
|
|
17
17
|
"light:all_round": "Ympäri näköpiirin näkyvä valo"
|
|
18
18
|
},
|
|
19
|
-
"
|
|
20
|
-
"shall": "Pakollinen",
|
|
21
|
-
"may": "Sallittu",
|
|
22
|
-
"shall-if-practicable": "Pakollinen, jos mahdollista"
|
|
19
|
+
"modalities": {
|
|
20
|
+
"modality:shall": "Pakollinen",
|
|
21
|
+
"modality:may": "Sallittu",
|
|
22
|
+
"modality:shall-if-practicable": "Pakollinen, jos mahdollista"
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
}
|
package/data/lights.json
CHANGED
|
@@ -8,8 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"lights": {
|
|
10
10
|
"light:masthead": {
|
|
11
|
-
"
|
|
12
|
-
"colregs_term": "masthead light",
|
|
11
|
+
"term": "masthead light",
|
|
13
12
|
"cite": "21(a)",
|
|
14
13
|
"color": "white",
|
|
15
14
|
"character": "steady",
|
|
@@ -22,8 +21,7 @@
|
|
|
22
21
|
"rule21": true
|
|
23
22
|
},
|
|
24
23
|
"light:sidelights": {
|
|
25
|
-
"
|
|
26
|
-
"colregs_term": "sidelights",
|
|
24
|
+
"term": "sidelights",
|
|
27
25
|
"cite": "21(b)",
|
|
28
26
|
"character": "steady",
|
|
29
27
|
"composite": true,
|
|
@@ -36,8 +34,7 @@
|
|
|
36
34
|
"rule21": true
|
|
37
35
|
},
|
|
38
36
|
"light:sidelight_starboard": {
|
|
39
|
-
"
|
|
40
|
-
"colregs_term": "sidelight",
|
|
37
|
+
"term": "sidelight",
|
|
41
38
|
"cite": "21(b)",
|
|
42
39
|
"color": "green",
|
|
43
40
|
"character": "steady",
|
|
@@ -50,8 +47,7 @@
|
|
|
50
47
|
"rule21": true
|
|
51
48
|
},
|
|
52
49
|
"light:sidelight_port": {
|
|
53
|
-
"
|
|
54
|
-
"colregs_term": "sidelight",
|
|
50
|
+
"term": "sidelight",
|
|
55
51
|
"cite": "21(b)",
|
|
56
52
|
"color": "red",
|
|
57
53
|
"character": "steady",
|
|
@@ -64,8 +60,7 @@
|
|
|
64
60
|
"rule21": true
|
|
65
61
|
},
|
|
66
62
|
"light:sternlight": {
|
|
67
|
-
"
|
|
68
|
-
"colregs_term": "sternlight",
|
|
63
|
+
"term": "sternlight",
|
|
69
64
|
"cite": "21(c)",
|
|
70
65
|
"color": "white",
|
|
71
66
|
"character": "steady",
|
|
@@ -78,8 +73,7 @@
|
|
|
78
73
|
"rule21": true
|
|
79
74
|
},
|
|
80
75
|
"light:towing": {
|
|
81
|
-
"
|
|
82
|
-
"colregs_term": "towing light",
|
|
76
|
+
"term": "towing light",
|
|
83
77
|
"cite": "21(d)",
|
|
84
78
|
"color": "yellow",
|
|
85
79
|
"character": "steady",
|
|
@@ -92,8 +86,7 @@
|
|
|
92
86
|
"rule21": true
|
|
93
87
|
},
|
|
94
88
|
"light:all_round": {
|
|
95
|
-
"
|
|
96
|
-
"colregs_term": "all-round light",
|
|
89
|
+
"term": "all-round light",
|
|
97
90
|
"cite": "21(e)",
|
|
98
91
|
"color": null,
|
|
99
92
|
"color_note": "colour is fixed by the citing paragraph, not by Rule 21",
|
|
@@ -106,8 +99,7 @@
|
|
|
106
99
|
"rule21": true
|
|
107
100
|
},
|
|
108
101
|
"light:flashing": {
|
|
109
|
-
"
|
|
110
|
-
"colregs_term": "flashing light",
|
|
102
|
+
"term": "flashing light",
|
|
111
103
|
"cite": "21(f)",
|
|
112
104
|
"color": null,
|
|
113
105
|
"color_note": "colour is fixed by the citing paragraph",
|
|
@@ -122,8 +114,7 @@
|
|
|
122
114
|
"rule21": true
|
|
123
115
|
},
|
|
124
116
|
"light:torch": {
|
|
125
|
-
"
|
|
126
|
-
"colregs_term": "electric torch or lighted lantern",
|
|
117
|
+
"term": "electric torch or lighted lantern",
|
|
127
118
|
"cite": "25(d)(i)",
|
|
128
119
|
"color": "white",
|
|
129
120
|
"character": "shown in sufficient time to prevent collision",
|
|
@@ -133,8 +124,7 @@
|
|
|
133
124
|
"note": "Not a fixed navigation light: it is held ready at hand and shown on approach. It has no prescribed arc, range or position."
|
|
134
125
|
},
|
|
135
126
|
"light:deck_lights": {
|
|
136
|
-
"
|
|
137
|
-
"colregs_term": "working or equivalent lights",
|
|
127
|
+
"term": "working or equivalent lights",
|
|
138
128
|
"cite": "30(c)",
|
|
139
129
|
"color": null,
|
|
140
130
|
"character": "steady",
|
package/data/version.json
CHANGED
|
@@ -102,6 +102,8 @@ data one.
|
|
|
102
102
|
international law where a national body deliberately has none. That is
|
|
103
103
|
about deltas, not licences; it survives this ADR untouched and is the live
|
|
104
104
|
blocker on CEVNI. Read the two together or the wrong one gets blamed.
|
|
105
|
+
*(ADR 0018 has since supplied that mechanism; the bar is now "land with
|
|
106
|
+
your tombstones", not "wait".)*
|
|
105
107
|
- `schema/rules.schema.json` carries the conditional: `text` required unless
|
|
106
108
|
`text_status` is `withheld`, in which case it is forbidden and
|
|
107
109
|
`withheld_reason` is required. Only `withheld_reason` is refused on a
|
|
@@ -49,7 +49,7 @@ that is the fixture contract: both fixture files `expect` entry ids.
|
|
|
49
49
|
|
|
50
50
|
### 2. `FactRecord` keeps its name
|
|
51
51
|
|
|
52
|
-
It is colregs' name for the per-vessel record (`
|
|
52
|
+
It is colregs' name for the per-vessel record (`self.fact` is "exactly the
|
|
53
53
|
record above, key for key"). ADR 0005 §2 has the situation wrap two fact
|
|
54
54
|
records; the fact record itself does not widen, and a display consumer never
|
|
55
55
|
sees a situation. A name that hinted at two vessels would describe the wrapper,
|
|
@@ -59,13 +59,13 @@ not the thing.
|
|
|
59
59
|
|
|
60
60
|
colregs states the situation twice: nested by subject and class in
|
|
61
61
|
`facts.json` §`situation.record` and in every fixture case, and flat as
|
|
62
|
-
`
|
|
62
|
+
`self:fact:activity` inside predicates. The engine's public type is the
|
|
63
63
|
**nested** form. The flat form is the predicate namespace and stays internal
|
|
64
64
|
to the walker.
|
|
65
65
|
|
|
66
66
|
```ts
|
|
67
67
|
interface Situation {
|
|
68
|
-
|
|
68
|
+
self: Subject;
|
|
69
69
|
other?: Subject;
|
|
70
70
|
pair?: Pair;
|
|
71
71
|
}
|
|
@@ -73,7 +73,7 @@ interface Subject { fact: FactRecord; kin?: Kinematics; geo?: DirectionalGeometr
|
|
|
73
73
|
interface Pair { geo?: PairGeometry; env?: Environment; }
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
-
- `
|
|
76
|
+
- `self` is required; `other`/`Subject.fact` follow colregs' own fixture schema
|
|
77
77
|
(`situation-fixtures.schema.json`, `0.2.0`) — `other` optional (Rule 19's
|
|
78
78
|
single-vessel scope needs no synthesized one), `fact` required. Every other
|
|
79
79
|
class, and every key inside a class, is optional — absent is absent.
|
|
@@ -98,7 +98,7 @@ interface EncounterEvaluation {
|
|
|
98
98
|
scope: EntryId[];
|
|
99
99
|
encounter?: 'head-on' | 'crossing' | 'overtaking' | 'none';
|
|
100
100
|
risk_of_collision: { asserted: boolean; by: EntryId[] };
|
|
101
|
-
roles: {
|
|
101
|
+
roles: { self: SubjectRole[]; other: SubjectRole[] };
|
|
102
102
|
overridden: { id: EntryId; by: EntryId }[];
|
|
103
103
|
modalities: Record<EntryId, Modality>;
|
|
104
104
|
}
|
|
@@ -164,10 +164,10 @@ resolution, and validation of the situation record.
|
|
|
164
164
|
| `FactRecord` keeps its name | ink | — |
|
|
165
165
|
| `Situation` nested by subject and class, generated from `facts.json` | ink | — |
|
|
166
166
|
| Verb name `evaluateEncounter`; result name `EncounterEvaluation` | ✎ | colregs renaming the `pair` subject or the `encounter` effect |
|
|
167
|
-
| `
|
|
167
|
+
| `self` required, `other`/`Subject.fact` per colregs 0.2.0's fixture schema | ✎ | revised 2026-09-07 from "own/other both required"; Mark to confirm before ink |
|
|
168
168
|
| `appliedEncounterEntries` as the fixture-replay companion | ✎ | the situation-fixture replay being written |
|
|
169
169
|
| Field names snake_case with unit suffixes across both ADRs; `EntryId`/`ParagraphCite` alias `string` for ids and cites | ✎ | the rename's alias window closing; a consumer arguing the compiler should enforce the two apart |
|
|
170
|
-
| `EncounterEvaluation` field set (§4); `categories` and `provenance` added 2026-09-16 beyond the block above, as `DisplayEvaluation` carries them — colregs-engine 0.1.5 built them and ADR 0014's `encounter-evaluation.schema.json` is now the shape | ✎ | building it; Q-35, Q-36, Q-43 in colregs |
|
|
170
|
+
| `EncounterEvaluation` field set (§4); `categories` and `provenance` added 2026-09-16 beyond the block above, as `DisplayEvaluation` carries them — colregs-engine 0.1.5 built them and ADR 0014's `encounter-evaluation.schema.json` is now the shape; `roles` is the pooled two-frame read, ADR 0016 | ✎ | building it; Q-35, Q-36, Q-43 in colregs |
|
|
171
171
|
| `encounter` absent vs the ADR 0005 §5 status alphabet | ✎ | Q-43 |
|
|
172
172
|
| `conduct` is a separate package, not a third verb | ✎ | superseded by ADR 0012: a third and fourth verb, in this package |
|
|
173
173
|
| Geometry-consistency validation (REQ-VERIFY-8) in the engine's validator | ? | deciding whether it is data-suite-only |
|
|
@@ -81,12 +81,12 @@ interface ConductEvaluation {
|
|
|
81
81
|
phases: ConductPhaseChange[];
|
|
82
82
|
}
|
|
83
83
|
interface ConductVerdict {
|
|
84
|
-
id: EntryId; subject: '
|
|
84
|
+
id: EntryId; subject: 'self' | 'other';
|
|
85
85
|
verdict: 'kept' | 'breached' | 'pending';
|
|
86
86
|
attached_at_s?: number; decided_at_s?: number;
|
|
87
87
|
robustness?: { value: number; unit: string };
|
|
88
88
|
}
|
|
89
|
-
interface ConductPhaseChange { subject: '
|
|
89
|
+
interface ConductPhaseChange { subject: 'self' | 'other'; phase: ParagraphCite; at_s: number; }
|
|
90
90
|
```
|
|
91
91
|
|
|
92
92
|
- One **verdict** per applied conduct entry per subject it attached to; an
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# ADR 0016 — An encounter's roles are read from both frames, pooled, then resolved
|
|
2
|
+
|
|
3
|
+
Date: 2026-09-16
|
|
4
|
+
Status: accepted — Solace's ruling on #141, 2026-09-16.
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
Every `precedence` entry in `data/applicability.json` is written from the
|
|
9
|
+
duty-holder's seat: `effect.self` is one of `give-way`, `shall-not-impede`,
|
|
10
|
+
`keep-clear`, `none`, and `effect.other` is `stand-on` or `none`. No entry
|
|
11
|
+
names self stand-on. That is faithful to the text — a paragraph addresses the
|
|
12
|
+
vessel it binds, and the other vessel's stand-on is Rule 17's inference —
|
|
13
|
+
and it is why entry `rule:15a:keep_out_of_the_way` gates self's bearing on the
|
|
14
|
+
starboard half only.
|
|
15
|
+
|
|
16
|
+
ADR 0011 §4 defines `EncounterEvaluation.roles` as a role set for *both*
|
|
17
|
+
subjects: what everyone is to do. Nothing between the two said how an engine
|
|
18
|
+
gets from a one-seat table to a two-seat answer. colregs-engine read the
|
|
19
|
+
situation once, from self's seat, and passed every fixture doing it, because
|
|
20
|
+
`fixtures/situation-fixtures.json` expects one-seat entry ids.
|
|
21
|
+
|
|
22
|
+
Measured on 2026-09-16 (#141), with colregs' own matcher, one-seat against
|
|
23
|
+
pooled:
|
|
24
|
+
|
|
25
|
+
| encounter | one seat tells self | pooled says self is |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| crossing, other on self's port bow | nothing | stand-on |
|
|
28
|
+
| self not under command, ordinary power vessel to starboard | give-way (15(a)) | stand-on (18(a)(i)) |
|
|
29
|
+
| self power-driven, a sailing vessel overtaking her | give-way (18(a)(iv)) | stand-on (13(a)) |
|
|
30
|
+
|
|
31
|
+
The second and third are not gaps but wrong answers: both vessels give-way
|
|
32
|
+
at once. The cause is that every `rel:overrides` edge of the Q-40 family —
|
|
33
|
+
`rule:18a_i` → `rule:15a:keep_out_of_the_way`, `rule:13a` → `rule:18a_iv`,
|
|
34
|
+
`rule:9c` → `rule:18a_iii` and the rest — has its source in one vessel's
|
|
35
|
+
seat and its target in the other's. A one-seat reader never sees the source,
|
|
36
|
+
so the target stands. The suite has pooled both seats since ADR 0005 §4
|
|
37
|
+
(`pooledRoles` in `test/data.test.mjs`); with the swap removed it fails.
|
|
38
|
+
|
|
39
|
+
Two repairs were on the table: reciprocal entries (self stand-on, other
|
|
40
|
+
give-way, gates mirrored), or the pooled read stated as the contract.
|
|
41
|
+
|
|
42
|
+
## Decision
|
|
43
|
+
|
|
44
|
+
1. **`evaluateEncounter` reads two frames.** The situation as given, and its
|
|
45
|
+
swap (`self` and `other` exchanged, `pair` unchanged), are both matched
|
|
46
|
+
against every non-`display` entry. Derived facts are computed per frame.
|
|
47
|
+
2. **Precedence entries are pooled, then resolved.** The precedence entries
|
|
48
|
+
that apply in either frame form one pool, keyed by which vessel each fired
|
|
49
|
+
for. `rel:overrides` is resolved over the pool, so an override may reach
|
|
50
|
+
an entry that fired in the other frame. Only then are roles read.
|
|
51
|
+
3. **`roles.self` and `roles.other` come from the pool.** A vessel's roles are
|
|
52
|
+
every non-`none` role a surviving forceful entry lays on her, from
|
|
53
|
+
`effect.self` where she was the frame's self and from `effect.other` where
|
|
54
|
+
she was the frame's other; `by` names the entry either way.
|
|
55
|
+
4. **`applied`, `scope`, `encounter`, `risk_of_collision`, `modalities` and
|
|
56
|
+
`categories` stay self-frame.** They answer the fixture's entry-id `expect`
|
|
57
|
+
as they do today, and a classification is the pair's already.
|
|
58
|
+
5. **A situation fixture may state `roles`.** `roles: {self: [{role, by}],
|
|
59
|
+
other: [{role, by}]}` is the pooled, resolved answer for both subjects and
|
|
60
|
+
binds an engine to it; `expect` stays the self-frame entry ids.
|
|
61
|
+
6. **No reciprocal entries.** They would double the table and every override
|
|
62
|
+
edge, make `effect.other` redundant, and still need the override to reach
|
|
63
|
+
across the pair.
|
|
64
|
+
|
|
65
|
+
## Consequences
|
|
66
|
+
|
|
67
|
+
- Four binding cases in `fixtures/situation-fixtures.json` carry `roles`:
|
|
68
|
+
the crossing as written and the three encounters in the table above.
|
|
69
|
+
- `INV-PB-roles-exclusive` and `INV-PB-one-role-source` are stated over the
|
|
70
|
+
pooled read, as the suite already asserts them.
|
|
71
|
+
- colregs-engine's `evaluateEncounter` changes to match; colregs-engine#85
|
|
72
|
+
(the 13(d) latch ordering) is independent of this.
|
|
73
|
+
- 8(f)(iii)'s `none`/`none` and Q-35 are untouched: pooling adds no role,
|
|
74
|
+
it only lets every entry see the entries it was written to override.
|
|
75
|
+
|
|
76
|
+
## Register
|
|
77
|
+
|
|
78
|
+
| item | level | what would settle it |
|
|
79
|
+
|---|---|---|
|
|
80
|
+
| Two frames, pooled, resolved, then roles — not reciprocal entries | ink | Solace, 2026-09-16 |
|
|
81
|
+
| Self-frame `applied`; pooled `roles` only | ✎ | a consumer needing the other frame's applied ids |
|
|
82
|
+
| Fixture `roles` as `{role, by}` per subject | ✎ | the engine's conformance replay consuming it |
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# ADR 0017 — Closed vocabularies are prefixed identifiers
|
|
2
|
+
|
|
3
|
+
Date: 2026-09-16
|
|
4
|
+
Status: accepted — Solace's ruling, 2026-09-16
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
Issue #120 built a display catalog for the light, modality, role, encounter
|
|
9
|
+
and jurisdiction vocabularies and, in doing so, named a fiction in
|
|
10
|
+
`docs/identifiers.md`: "What is not an identifier" declared modality, role,
|
|
11
|
+
jurisdiction and encounter values outside `REQ-MODEL-10`'s identifier space,
|
|
12
|
+
on the theory that a closed vocabulary is a different kind of thing from a
|
|
13
|
+
name. The carve-out never reduced what a rename costs. Every one of these
|
|
14
|
+
values is emitted by the engine (`DisplayEvaluation.modality`,
|
|
15
|
+
`EncounterEvaluation.roles[].role`, `.encounter`, `provenance.jurisdictions`,
|
|
16
|
+
`evaluated_categories`) and compared by a consumer the same way an entry id
|
|
17
|
+
or a fact value is — `if (role === 'give-way')` breaks on a rename exactly
|
|
18
|
+
as `if (activity === 'nuc')` would. Declining to promise stability does not
|
|
19
|
+
make the rename cheaper; it only leaves the promise unwritten.
|
|
20
|
+
|
|
21
|
+
The collision that follows from treating them as a separate kind is not
|
|
22
|
+
hypothetical. It has already happened, in the data on `main`:
|
|
23
|
+
|
|
24
|
+
| string | is a … | and also a … |
|
|
25
|
+
|---|---|---|
|
|
26
|
+
| `shall-not-impede` | modality (`modalities`) | role (`effects.roles`) |
|
|
27
|
+
| `none` | role (`effects.roles`) | encounter (`effects.encounters`) |
|
|
28
|
+
|
|
29
|
+
This is the identical shape as `towing`, the collision ADR 0001's `light:`
|
|
30
|
+
prefix and `activity:` axis already resolved: a consumer holding the bare
|
|
31
|
+
string cannot say which field it came out of. The rule already on file —
|
|
32
|
+
"the prefix makes the namespace part of the identifier, which resolves that
|
|
33
|
+
collision by construction rather than by convention" — already applies here;
|
|
34
|
+
it was only not applied.
|
|
35
|
+
|
|
36
|
+
Issue #120's own draft, before this ruling, reached the opposite
|
|
37
|
+
recommendation on its Q2: that prefixing sections into identifiers "later"
|
|
38
|
+
costs the same as doing it now. That held only if the bare strings were not
|
|
39
|
+
identifiers. They are — held and compared by consumers — so the option runs
|
|
40
|
+
one way: pre-1.0, prefixing is a data-layer, one-PR change; post-1.0 it is
|
|
41
|
+
an identifier-layer change REQ-MODEL-10 forbids outright, leaving only the
|
|
42
|
+
two-names-forever shape `docs/identifiers.md` already rejected for
|
|
43
|
+
`fact:own_activity`. Ruled by Solace, 2026-09-16.
|
|
44
|
+
|
|
45
|
+
## Decision
|
|
46
|
+
|
|
47
|
+
1. **Four closed vocabularies take a type prefix**, the same mechanism as
|
|
48
|
+
`light:`, `fact:`, `rel:`: `modality:` (`data/applicability.json`
|
|
49
|
+
`modalities`, every entry's `modality`, `modality_by` branches),
|
|
50
|
+
`role:` (`effects.roles`, `effect.own`/`effect.other`), `encounter:`
|
|
51
|
+
(`effects.encounters`, `effect.encounter`), and `category:` (`categories`,
|
|
52
|
+
every entry's and `represented_paragraphs` record's `category`) — new to
|
|
53
|
+
this list because it is the same class (package-coined, closed, emitted,
|
|
54
|
+
compared) and leaving it bare while prefixing the other three would
|
|
55
|
+
recreate the inconsistency this ADR closes.
|
|
56
|
+
2. **Jurisdiction stays bare.** `intl` and `us/inland` are not names this
|
|
57
|
+
package coined. Jurisdiction is a coordinate with REQ-SCOPE-2's own
|
|
58
|
+
`<body>/<waters>` grammar, its left segment borrowed from ISO 3166, the
|
|
59
|
+
whole value doubling as a corpus key (REQ-LANG-3) and a `data/text/`
|
|
60
|
+
filesystem path — its sibling axis, `language`, is a bare BCP 47 tag for
|
|
61
|
+
the same reason. `jurisdiction:us/inland` would put a colon namespace in
|
|
62
|
+
front of a slash path and claim the package minted `us`, which it did
|
|
63
|
+
not. Its stability is a property of the grammar (REQ-SCOPE-4 makes
|
|
64
|
+
adding a jurisdiction additive; no fact, light or role value can spell
|
|
65
|
+
`us/inland`), not of a prefix — the same reason a paragraph path carries
|
|
66
|
+
none. Jurisdiction moves from "What is not an identifier" to the bare
|
|
67
|
+
class beside paragraph paths in `docs/identifiers.md`: it *is* an
|
|
68
|
+
identifier, immutable under REQ-MODEL-10 exactly like `27(a)(i)`, and it
|
|
69
|
+
just carries no prefix.
|
|
70
|
+
3. **The two live collisions are recorded, not just resolved.**
|
|
71
|
+
`modality:shall-not-impede` and `role:shall-not-impede` are two names;
|
|
72
|
+
`role:none` and `encounter:none` are two names. Each pair collided under
|
|
73
|
+
the old bare scheme and does not under this one.
|
|
74
|
+
|
|
75
|
+
## Consequences
|
|
76
|
+
|
|
77
|
+
- **One-pass rename, one commit.** `data/applicability.json`'s four
|
|
78
|
+
vocabulary maps and every entry field that names a value; both fixture
|
|
79
|
+
files (`applicability-fixtures.json` has no modality field to touch;
|
|
80
|
+
`situation-fixtures.json`'s `expect[].modality`); every schema enum/pattern
|
|
81
|
+
naming one of the four (`applicability.schema.json`,
|
|
82
|
+
`evaluation.schema.json`, `encounter-evaluation.schema.json`,
|
|
83
|
+
`i18n-catalog.schema.json`); the two i18n catalogs' `modality` section
|
|
84
|
+
keys; the test suite's literal comparisons; `docs/identifiers.md`,
|
|
85
|
+
`docs/requirements.md`, `README.md`. Jurisdiction values, patterns and
|
|
86
|
+
every corpus path are untouched.
|
|
87
|
+
- **colregs-engine follow-up.** Its generated types (`DisplayEvaluation`,
|
|
88
|
+
`EncounterEvaluation`, and any hand-written literal comparing against a
|
|
89
|
+
modality, role, encounter or category string) regenerate from this
|
|
90
|
+
package's schemas and wait on a colregs release carrying this ADR. Until
|
|
91
|
+
that release, colregs-engine's own types name the pre-ADR bare values;
|
|
92
|
+
this is a breaking change for it in the ordinary pre-1.0 sense (no
|
|
93
|
+
deprecation window, no shim — `AGENTS.md` "Stage: pre-consumer"), tracked
|
|
94
|
+
as its own follow-up, not blocking this PR.
|
|
95
|
+
- **The i18n catalog keys change shape**, not content: `modality:shall`
|
|
96
|
+
replaces `shall` as the key into `data/i18n/*.json`'s `modality` section;
|
|
97
|
+
the label strings themselves are untouched. A catalog section for role,
|
|
98
|
+
encounter or category is not added by this ADR — only the vocabulary
|
|
99
|
+
values those sections would key against.
|
|
100
|
+
- Rule ids (`rule:15a:crossing`) and shape keys (`when`, `effect`, a
|
|
101
|
+
`category:scope` effect's `part`/`section`/`applies_rules`) are untouched: only
|
|
102
|
+
the four vocabularies' *values* take the prefix, never a key or an id
|
|
103
|
+
that happens to share a word with one.
|
|
104
|
+
|
|
105
|
+
Ruled by Solace, 2026-09-16.
|