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.
- package/LICENSE +194 -26
- package/PROVENANCE.md +45 -12
- package/README.md +222 -23
- package/data/applicability.json +1252 -190
- package/data/deprecated-identifiers.json +7 -0
- package/data/facts.json +556 -113
- package/data/geometry.json +33 -33
- package/data/images.json +51 -25
- package/data/lights.json +35 -35
- package/data/rules.json +787 -3
- package/docs/adr/0001-name-and-jurisdiction-model.md +119 -1
- package/docs/adr/0003-language-as-a-dimension.md +220 -0
- package/docs/adr/0004-licence-layering.md +78 -0
- package/docs/adr/0005-rule-categories-and-the-situation-record.md +323 -0
- package/docs/adr/0006-json-schema-and-identifier-diff.md +95 -0
- package/docs/adr/0007-rule26-overrides-and-aground.md +68 -0
- package/docs/adr/0008-mooring-buoy-modifier.md +87 -0
- package/docs/budgets.json +162 -0
- package/docs/conventions.md +27 -0
- package/docs/gates.json +84 -0
- package/docs/identifiers.md +435 -0
- package/docs/part-b-invariants.md +922 -0
- package/docs/requirements.md +1251 -23
- package/docs/verification/2026-08-30-q6-q8.md +278 -0
- package/docs/verification/2026-09-05-rule27f-rule28-anchor-scenarios.md +330 -0
- package/fixtures/applicability-fixtures.json +361 -227
- package/fixtures/situation-fixtures.json +3673 -0
- package/package.json +16 -4
- package/schema/applicability-fixtures.schema.json +49 -0
- package/schema/applicability.schema.json +401 -0
- package/schema/deprecated-identifiers.schema.json +29 -0
- package/schema/facts.schema.json +793 -0
- package/schema/geometry.schema.json +239 -0
- package/schema/images.schema.json +53 -0
- package/schema/lights.schema.json +131 -0
- package/schema/rules.schema.json +50 -0
- package/schema/situation-fixtures.schema.json +325 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
# ADR 0005 — Rule categories, the situation record, and the Rule 2 region
|
|
2
|
+
|
|
3
|
+
Date: 2026-09-04
|
|
4
|
+
Status: accepted, **all of it in pencil**
|
|
5
|
+
|
|
6
|
+
Everything this ADR records is `✎` **pencil** under
|
|
7
|
+
[`docs/conventions.md`](../conventions.md): any session may change any of it
|
|
8
|
+
for a better idea, logging the change and what would settle the item. That is
|
|
9
|
+
the decision — proceed on this shape — not a claim that the shape is right.
|
|
10
|
+
The house default for an ADR is ink; this one inverts it deliberately and
|
|
11
|
+
says so once here rather than marking every paragraph.
|
|
12
|
+
|
|
13
|
+
## Context
|
|
14
|
+
|
|
15
|
+
Part C (lights and shapes) was modelled first because it is the easy half:
|
|
16
|
+
one vessel's facts in, a set of lights out. `REQ-PART-4` then declared Part B
|
|
17
|
+
— the steering and sailing rules — out of scope and possibly permanent:
|
|
18
|
+
"they govern conduct between two vessels, not the appearance of one, and the
|
|
19
|
+
fact record is single-vessel by construction."
|
|
20
|
+
|
|
21
|
+
The second clause is the real one, and it is a statement about the *input*,
|
|
22
|
+
not about whether Part B can be modelled. Proposal
|
|
23
|
+
`2026-09-04-rule-categories-v4.md` in `colregs-engine`
|
|
24
|
+
(`docs/proposals/`) works out what input Part B actually needs, and what
|
|
25
|
+
happens to the rest of the model once the answer is "a pair of vessels, with
|
|
26
|
+
kinematics". It went through seven outside reviews across four drafts (three
|
|
27
|
+
Claude subagents, codex, Gemini 3.1 Pro); the reviews are in that repo. Mark
|
|
28
|
+
approved it on 2026-09-04 with everything in pencil, API breaks allowed
|
|
29
|
+
while the package is 0.x.
|
|
30
|
+
|
|
31
|
+
This ADR records that approval and amends `REQ-PART-4`. It does not
|
|
32
|
+
re-argue the proposal; the proposal is the reasoning, this is the decision.
|
|
33
|
+
|
|
34
|
+
## Decision
|
|
35
|
+
|
|
36
|
+
### 1. Every rule paragraph carries exactly one category
|
|
37
|
+
|
|
38
|
+
Nine values, a closed set:
|
|
39
|
+
|
|
40
|
+
| category | reads | produces | who evaluates it |
|
|
41
|
+
|---|---|---|---|
|
|
42
|
+
| `definition` | — | vocabulary | n/a |
|
|
43
|
+
| `standard` | — | technical values (ranges, heights) | n/a |
|
|
44
|
+
| `scope` | visibility, in-sight, jurisdiction | which sections apply | engine |
|
|
45
|
+
| `display` | one vessel's facts | signals + modality | engine |
|
|
46
|
+
| `classification` | relative geometry, history | encounter type, risk of collision | engine |
|
|
47
|
+
| `precedence` | two vessels' facts + encounter type | give-way / stand-on / shall-not-impede / none | engine |
|
|
48
|
+
| `conduct` | encounter + role + phase + kinematics + observations | obligated or prohibited action | monitored over a trace, not evaluated at a point |
|
|
49
|
+
| `care` | anything | residual responsibility | represented, never independently evaluated |
|
|
50
|
+
| `meta` | the region state (§4 below) | banner + advisories | computed by a solver, reviewed by humans |
|
|
51
|
+
|
|
52
|
+
The unit is the **paragraph**, as everywhere else in this package
|
|
53
|
+
(ADR 0001). `care` is Rule 2(a) alone; `meta` is Rule 2(b) alone. Part D
|
|
54
|
+
(Rules 32–37) has no category yet and stays blocked on `REQ-PART-3`'s ADR.
|
|
55
|
+
|
|
56
|
+
One category per paragraph holds because dual roles are *relations*, not
|
|
57
|
+
second categories: 13(a) is `classification`, and its effect on Rule 18 is
|
|
58
|
+
`rel:overrides` from 13 to 18. The proposal's first-cut membership table is
|
|
59
|
+
pencil paragraph by paragraph; the nine names are pencil as a set.
|
|
60
|
+
|
|
61
|
+
`display` is the default, so every entry in `data/applicability.json` today
|
|
62
|
+
is already correctly categorised without being touched.
|
|
63
|
+
|
|
64
|
+
### 2. Part B reads a situation; the fact record does not change
|
|
65
|
+
|
|
66
|
+
The per-vessel fact record — `data/facts.json`, the three axes, the
|
|
67
|
+
scalars — is unchanged, and nothing in this ADR renames or repoints an
|
|
68
|
+
identifier in it (`REQ-MODEL-10`).
|
|
69
|
+
|
|
70
|
+
A **situation record** wraps it: two fact records, a kinematic state per
|
|
71
|
+
vessel, relative geometry, and history. **Kinematic state is a new fact
|
|
72
|
+
class** — position, heading, speed, rate of turn, and a dynamics class
|
|
73
|
+
(tanker, ferry, yacht, …) — not an extension of the existing one. A
|
|
74
|
+
consumer that only shows lights never constructs a situation and never
|
|
75
|
+
reads a kinematic fact.
|
|
76
|
+
|
|
77
|
+
The fixture format cannot carry a situation, so a fixture-schema step
|
|
78
|
+
precedes any two-subject data. Existing fixtures are untouched and stay
|
|
79
|
+
valid.
|
|
80
|
+
|
|
81
|
+
### 3. `care` and `meta` are not entries
|
|
82
|
+
|
|
83
|
+
Rules 2(a) and 2(b) do not produce lights, and evaluating them is precisely
|
|
84
|
+
what this package must not do. They go in a **sibling registry beside
|
|
85
|
+
`known_omissions`** in `data/applicability.json` — recorded, addressable,
|
|
86
|
+
citable, and deliberately not evaluable. A registry entry is the machine-
|
|
87
|
+
visible statement "this paragraph exists, this package represents it, and
|
|
88
|
+
nothing here computes it".
|
|
89
|
+
|
|
90
|
+
### 4. Two new modalities, and a sixth relation
|
|
91
|
+
|
|
92
|
+
`shall-not` and `shall-not-impede` join `shall`, `may`,
|
|
93
|
+
`shall-if-practicable`, `conditional` and `exempt`. Both are needed by
|
|
94
|
+
Part B and neither is expressible as a negated `shall`: `shall-not-impede`
|
|
95
|
+
is a distinct duty in the rules' own vocabulary (18(d), 8(f)), and 8(f)(iii)
|
|
96
|
+
restores full Section II duties once risk of collision exists.
|
|
97
|
+
|
|
98
|
+
`rel:overrides` joins the five relations of `REQ-MODEL-7` as a sixth. It is
|
|
99
|
+
the superiority relation — "this paragraph's requirement prevails over
|
|
100
|
+
that one's" — carrying Rule 18's "except where Rules 9, 10 and 13 otherwise
|
|
101
|
+
require". It is checkable: a solver looks for cycles, and for two `shall`
|
|
102
|
+
norms in conflict with no override between them.
|
|
103
|
+
|
|
104
|
+
### 5. The status alphabet, and the R0/R1/R2 ontology as the research goal
|
|
105
|
+
|
|
106
|
+
The engine's output carries one of four statuses, fixed as a closed
|
|
107
|
+
alphabet: `not-flagged`, `model-rule-conflict`, `no-robust-policy-in-model`,
|
|
108
|
+
`inconclusive-in-model`. Each says what the *model* knows and nothing about
|
|
109
|
+
what the law concludes; `not-flagged` means "not flagged by this model",
|
|
110
|
+
never "the rules suffice".
|
|
111
|
+
|
|
112
|
+
The alphabet is the surface of an ontology this project states as its
|
|
113
|
+
**research goal**, not as an implemented feature. With `Safe(s, σown,
|
|
114
|
+
σother)` meaning no invariant violation within a horizon *T*, and *A* a
|
|
115
|
+
declared set of admissible other-vessel strategies:
|
|
116
|
+
|
|
117
|
+
| region | definition |
|
|
118
|
+
|---|---|
|
|
119
|
+
| **R0** rules-suffice | ∃ compliant σown ∀ σother ∈ A: Safe |
|
|
120
|
+
| **R1** departure-required-in-model | ¬R0 ∧ ∃ σown ∀ σother ∈ A: Safe |
|
|
121
|
+
| **R2** unwinnable-in-model | ¬∃ σown ∀ σother ∈ A: Safe |
|
|
122
|
+
|
|
123
|
+
R1 is a model finding bearing on Rule 2(b), not a legal classification.
|
|
124
|
+
Rule 2 is not evaluated at runtime as a predicate; it names a *region* of
|
|
125
|
+
situation space that a game solver computes offline, and Rule 2 is
|
|
126
|
+
deliberately absent from the compliance predicate or the definition is
|
|
127
|
+
circular.
|
|
128
|
+
|
|
129
|
+
None of this lands in `colregs`. The solver, the region grid and the output
|
|
130
|
+
envelope live in `colregs-engine`; what this package owes them is the
|
|
131
|
+
category field, the registry, the modalities and `rel:overrides`. The
|
|
132
|
+
ontology is recorded here so the data decisions above have a stated purpose
|
|
133
|
+
and so a later reader can see what they were shaped for.
|
|
134
|
+
|
|
135
|
+
### 6. `REQ-PART-4` is superseded, not deleted
|
|
136
|
+
|
|
137
|
+
`REQ-PART-4` said Part B is out of v1 scope and may never be modelled. The
|
|
138
|
+
scope half stands: Part B is out of v1, Part C for `intl` completes first
|
|
139
|
+
(`REQ-PART-1`), and nothing in this ADR ships data. The "may never" half
|
|
140
|
+
does not: the obstacle it named — a single-vessel fact record — is
|
|
141
|
+
addressed by the situation record without changing that record. The
|
|
142
|
+
requirement is struck through in place and replaced by `REQ-CAT-1..5`, per
|
|
143
|
+
the ID-stability rule in the requirements preamble. No requirement ID is
|
|
144
|
+
reused.
|
|
145
|
+
|
|
146
|
+
## Pencil changed since
|
|
147
|
+
|
|
148
|
+
Logged here rather than left to a commit message, as `docs/conventions.md`
|
|
149
|
+
requires of a pencil change.
|
|
150
|
+
|
|
151
|
+
- **2026-09-04, PR #24 — 13(a) is `precedence`, not `classification`.** §1
|
|
152
|
+
above gives 13(a) to `classification` and says its effect on Rule 18 is a
|
|
153
|
+
relation. The relation part held: entry `13a` carries `rel:overrides`
|
|
154
|
+
against every Rule 18 entry. The category did not. 13(a) is the one
|
|
155
|
+
paragraph of Rule 13 that assigns a role — the overtaking vessel keeps out
|
|
156
|
+
of the way — and a `classification` entry produces an encounter type, with
|
|
157
|
+
nowhere to put a role. 13(b) is the classification: it is the sector test
|
|
158
|
+
that sets the `hist:was_overtaking` latch, and it is not yet written.
|
|
159
|
+
Tracked as `Q-37`; it does not disturb the one-category-per-paragraph rule,
|
|
160
|
+
which is what §1 was really defending.
|
|
161
|
+
- **2026-09-04, PR #24 — a fifth fact class, `env`, `pair`-only.** §2 names
|
|
162
|
+
four classes. Rules 9 and 10 are scoped to *places*, and a narrow channel is
|
|
163
|
+
neither a vessel's fact nor the pair's geometry. `pair:env:narrow_channel`
|
|
164
|
+
and `pair:env:traffic_lane` are the addition; `fixtures/situation-fixtures.json`
|
|
165
|
+
had already recorded the gap before the class existed.
|
|
166
|
+
- **2026-09-04, PR #24 — `effect` is the fourth new field.** §4 widens
|
|
167
|
+
`modality` and adds `rel:overrides`; it does not say what a norm that
|
|
168
|
+
produces no lights produces instead. `effect` is that, written up in
|
|
169
|
+
`docs/identifiers.md` §"Effects" and required by `REQ-CAT-8`.
|
|
170
|
+
|
|
171
|
+
- **2026-09-04, PR #25 — the predicate language grows `not` and `any_of`.**
|
|
172
|
+
`Q-33`. ADR 0005 says nothing about the predicate language because it did not
|
|
173
|
+
expect to need to; the first two-subject data found that a conjunction of
|
|
174
|
+
equalities cannot say "any vessel other than …" (18(d)(i)) or "under 20 m or
|
|
175
|
+
a sailing vessel" (9(b), 10(j)). Both constructs live in `satisfies` and one
|
|
176
|
+
shared walker, so both evaluators get them. The absent-fact rule is extended
|
|
177
|
+
rather than excepted: `not` over an absent fact is unsatisfied, so a
|
|
178
|
+
predicate never fires on silence. `9b-small`/`9b-sail` and
|
|
179
|
+
`10j-small`/`10j-sail` collapse to `9b` and `10j`; the four suffixed ids are
|
|
180
|
+
retired and recorded in `retired_entry_ids`, and never reused
|
|
181
|
+
(`REQ-MODEL-10`). Nothing has shipped a Part B entry, so retirement is free
|
|
182
|
+
once and will not be again.
|
|
183
|
+
- **2026-09-04, PR #25 — a fact may be derived.** `Q-32`. §2 says the
|
|
184
|
+
per-vessel fact record does not change, and it does not: `fact:rule18_class`
|
|
185
|
+
is a new key in a new `derived` section, computed by this package from the
|
|
186
|
+
record rather than supplied with it, with a decode table as its definition in
|
|
187
|
+
the style `signalk_navigation_state` already set. It is the answer to the
|
|
188
|
+
largest strain the first two-subject data found — that `fact:activity` is a
|
|
189
|
+
display axis and Rule 18's rank is not the same thing — and it is what lets a
|
|
190
|
+
`precedence` entry stop enumerating activity values it must be edited to keep
|
|
191
|
+
correct. One new boolean, `fact:tow_restricts_deviation` (27(c)), because the
|
|
192
|
+
rank genuinely needs a fact the record did not carry.
|
|
193
|
+
|
|
194
|
+
- **2026-09-04, PR #26 — a `classification` effect has two shapes, and Rule 12
|
|
195
|
+
is not one of them.** §1 gives `classification` two outputs at once —
|
|
196
|
+
"encounter type, risk of collision" — and §4 says nothing about what a
|
|
197
|
+
classification entry produces. It produces an `effect` with exactly one key:
|
|
198
|
+
`{"encounter": "head-on"|"crossing"|"overtaking"|"none"}` for Rules 13, 14 and
|
|
199
|
+
15, or `{"risk_of_collision": true}` for 7(d)(i). Two shapes rather than one
|
|
200
|
+
merged shape, because a merged one would have made every encounter entry state
|
|
201
|
+
a risk it does not decide. There is no `false`: 7(a) deems risk to exist in
|
|
202
|
+
any doubt, so an entry may add a ground for risk and nothing here may deny
|
|
203
|
+
one. Written up in `docs/identifiers.md` §"Effects" and required by
|
|
204
|
+
`REQ-CAT-8`.
|
|
205
|
+
**Rule 12 departs from §1's table** the way 13(a) did in PR #24 and for the
|
|
206
|
+
same reason: 12(a) assigns a role, and a classification effect has nowhere to
|
|
207
|
+
put one. `12a1`–`12a3` are `precedence`; 12(b) is a `definition` and is the
|
|
208
|
+
cite on the new `kin:wind_side` fact rather than an entry. `Q-40`.
|
|
209
|
+
- **2026-09-04, PR #26 — the partition is a property of the data, not of the
|
|
210
|
+
engine.** The proposal's verification column gives `classification` an "Alloy
|
|
211
|
+
partition". The data-level half of it is asserted here: 13(b)'s sector is one
|
|
212
|
+
constraint object, Rule 15's crossing is `not` over that same object, and Rule
|
|
213
|
+
14's cone is negated the same way inside an `any_of`, so no crossing sector is
|
|
214
|
+
enumerated and the three encounter types cannot overlap or leave a gap unless
|
|
215
|
+
someone edits one of two constraints without the other. The suite sweeps both
|
|
216
|
+
subjects' bearings in half-degree steps over all 518 400 points. Recorded as
|
|
217
|
+
`REQ-CAT-9`, which also requires that a threshold the Rules do not state be
|
|
218
|
+
declared once in `data/facts.json` under `situation.constants` and read from
|
|
219
|
+
there — `appreciable_bearing_change_deg_min` (pencil, 1.0 °/min) and
|
|
220
|
+
`head_on_half_angle_deg` (pencil, 11.25° — one point of the compass, the same
|
|
221
|
+
angular vocabulary 13(b)'s two points come from) are the two that were
|
|
222
|
+
invented; the two `overtaking_sector_*_deg` are ink, because 13(b) states
|
|
223
|
+
them.
|
|
224
|
+
- **2026-09-04, PR #26 — the `ram` refinement was a special case and is now a
|
|
225
|
+
property of the value.** Not a design decision so much as a defect PR #25
|
|
226
|
+
recorded and this one repaid: `activity:ram_underwater` matched a predicate
|
|
227
|
+
written for `activity:ram` under scalar equality and, silently, not under list
|
|
228
|
+
membership. The `fact:rule18_class` decode carried an `any_of` written to work
|
|
229
|
+
around it. The refinement now expands the value once, so equality, membership
|
|
230
|
+
and each `any_of` disjunct read it alike, `not` still negates the refined
|
|
231
|
+
reading, and the decode row is a bare list again.
|
|
232
|
+
|
|
233
|
+
Sixteen things the model could not express are recorded as `Q-31`–`Q-48` in
|
|
234
|
+
`docs/requirements.md` §11 rather than bent into the data. The largest is
|
|
235
|
+
`Q-32`: `fact:activity` is a display axis, and a vessel's rank under Rule 18
|
|
236
|
+
is not the same thing as the lights she shows.
|
|
237
|
+
- **2026-09-05, PR #35 — Rules 13 and 18 override Rule 12.** §4's sixth
|
|
238
|
+
relation was written down for Rules 9, 10 and 13 over Rule 18, and for 18(f)
|
|
239
|
+
within it. Rule 18's chapeau excepts only those three, so where Rule 18 and
|
|
240
|
+
Rule 12 are both in force — two sailing vessels, one of them fishing or not
|
|
241
|
+
under command — Rule 18 governs: `18b1`–`18b3` and `18c1`–`18c2` carry
|
|
242
|
+
`rel:overrides` against `12a1`–`12a3`, and `13a` does too. The Rule 12
|
|
243
|
+
entries now read 3(c) rather than the Rule 18 rank, which is what made the
|
|
244
|
+
relation necessary. `Q-40`.
|
|
245
|
+
- **2026-09-05, PR #36 — the head-on classification cites 14(b).** §1 gives
|
|
246
|
+
each paragraph one category. 14(a) is `conduct` — it prescribes the
|
|
247
|
+
alteration to starboard — and 14(b) is the deeming test, which is what a
|
|
248
|
+
`classification` entry evaluates, as 13(b) is for overtaking. Entry `14a` is
|
|
249
|
+
retired and `14b` cites the paragraph it reads. `Q-45`.
|
|
250
|
+
- **2026-09-05, PR #46 — Rule 18 overrides Rule 15, and Rule 15 reads 3(b).**
|
|
251
|
+
The other half of the same question. `15a-give-way` kept the four Rule 18
|
|
252
|
+
ranks — NUC, RAM, fishing and WIG — out of Rule 15 by negating them on both
|
|
253
|
+
subjects in its own predicate. That is the sixth relation written as a gate:
|
|
254
|
+
it produced the right roles wherever Rule 18 spoke, and the wrong ones where
|
|
255
|
+
Rule 18 is silent, because two vessels engaged in fishing under power, or a
|
|
256
|
+
NUC and a RAM under power, took no helm role from any entry at all. The gate
|
|
257
|
+
is gone; `18a1`–`18a3`, `18c1`–`18c2` and `18f1` carry `rel:overrides`
|
|
258
|
+
against `15a-give-way`. Where Rule 18 does not order the pair, Rule 15 now
|
|
259
|
+
does — including onto a vessel that may be unable to discharge the duty,
|
|
260
|
+
which is §5's Rule 2 region and is recorded rather than gated. `13a` needs no
|
|
261
|
+
override here: `15a-give-way` excludes every overtaking by the `hist`
|
|
262
|
+
latch and by 13(b)'s sector, and a test pins that so the absence stays a
|
|
263
|
+
reason rather than an oversight. `Q-40`.
|
|
264
|
+
|
|
265
|
+
The line this draws, since it is the second time the question has come up:
|
|
266
|
+
a condition on whether a paragraph *applies at all* belongs in the
|
|
267
|
+
predicate, and a condition on which of two applicable paragraphs *wins*
|
|
268
|
+
belongs in a relation. Rule 15 does apply to a fishing vessel under power;
|
|
269
|
+
what Rule 18 does is displace the role it assigns. Writing that as a gate
|
|
270
|
+
did not merely put the reason somewhere a test could not read it — it lost
|
|
271
|
+
the cases where the displacing paragraph is silent.
|
|
272
|
+
|
|
273
|
+
## What is unchanged
|
|
274
|
+
|
|
275
|
+
Stated plainly, because the value of the design above is mostly in what it
|
|
276
|
+
does not disturb:
|
|
277
|
+
|
|
278
|
+
- **The light rules.** Every entry in `data/applicability.json`, every
|
|
279
|
+
light, every relation, every predicate: unchanged.
|
|
280
|
+
- **Every fixture.** `fixtures/applicability-fixtures.json` is untouched;
|
|
281
|
+
the cross-implementation contract (`REQ-VERIFY-1`) still holds exactly.
|
|
282
|
+
- **Every test.** `npm test` passes on this PR with no data change at all.
|
|
283
|
+
- **The fact record.** Kinematics are a new class beside it, not a change
|
|
284
|
+
to it.
|
|
285
|
+
- **Identifiers.** Nothing renamed, nothing repointed (`REQ-MODEL-10`).
|
|
286
|
+
|
|
287
|
+
This PR is documentation only. The data changes it authorises land later,
|
|
288
|
+
additively, in the sequence the proposal sets out.
|
|
289
|
+
|
|
290
|
+
## v0.x compatibility
|
|
291
|
+
|
|
292
|
+
Concretely, for this package: `category` and `subjects` have defaults
|
|
293
|
+
(`display`, `1`), so adding them is additive and no existing entry needs
|
|
294
|
+
editing. The two new modality values and `rel:overrides` widen closed
|
|
295
|
+
vocabularies, which is additive for a consumer that reads them and a
|
|
296
|
+
breaking change only for one that exhaustively switches on the old set.
|
|
297
|
+
|
|
298
|
+
Beyond that, while the package is `0.x`: **an API break is allowed if the
|
|
299
|
+
design is wrong.** Field names, the situation record's shape, the registry's
|
|
300
|
+
file and schema, and the fixture schema for situations are all pencil, and
|
|
301
|
+
correcting one of them is worth a version bump rather than a compatibility
|
|
302
|
+
shim. `REQ-PKG-4`'s major-version rule and `REQ-MODEL-10`'s immutability
|
|
303
|
+
both continue to apply on their own terms — additive is still additive, and
|
|
304
|
+
no identifier is ever repointed — but nothing here is a stability promise to
|
|
305
|
+
consumers, and the README should not read as one until 1.0.
|
|
306
|
+
|
|
307
|
+
## Consequences
|
|
308
|
+
|
|
309
|
+
- `docs/requirements.md` gains `REQ-CAT-1..5` (§4.1) and `REQ-PART-4` is
|
|
310
|
+
struck through citing this ADR.
|
|
311
|
+
- Eighteen pencilled items from the proposal are recorded as open questions
|
|
312
|
+
`Q-13`–`Q-30`, each with what settles it. Most are `colregs-engine`'s to
|
|
313
|
+
settle; they are listed here because this ADR is what makes them live.
|
|
314
|
+
- `docs/conventions.md` is copied into this repo so the ink/pencil marker
|
|
315
|
+
read above is resolvable from inside it, and `CLAUDE.md` points at it.
|
|
316
|
+
- No gate opens or closes. The category field, the registry and
|
|
317
|
+
`rel:overrides` are additive; none of them declines a design that gets
|
|
318
|
+
more expensive to adopt later, which is what `REQ-GATE-1` records.
|
|
319
|
+
`Q-10`'s `rel:conditional_includes` question gains a sixth verb to weigh
|
|
320
|
+
against, and is still decided in the second-jurisdiction bundle.
|
|
321
|
+
- The proposal's step 1 is this ADR. Steps 2–4 (rule text for Rules 1–19,
|
|
322
|
+
the `care`/`meta` registry with the kinematic fact class, the engine
|
|
323
|
+
output envelope) can run in parallel once this lands.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# ADR 0006 — JSON Schema for structural validation, identifier diff for version discipline
|
|
2
|
+
|
|
3
|
+
Date: 2026-09-04
|
|
4
|
+
Status: accepted
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
`test/data.test.mjs` is the entire verification surface. It enforces
|
|
9
|
+
referential integrity exhaustively — every cite resolves to `rules.json`,
|
|
10
|
+
every light id to `lights.json`, every fact key to `facts.json`, every
|
|
11
|
+
cross-reference to an entry — and it runs the fixture replay and drift
|
|
12
|
+
test (REQ-VERIFY-1/2). What it does not check is *shape*: a misspelled
|
|
13
|
+
key (`modaltiy`), a `count` given as a string, an unexpected top-level
|
|
14
|
+
field, or a malformed `{gte, lt}` predicate all pass silently until some
|
|
15
|
+
consumer trips on them.
|
|
16
|
+
|
|
17
|
+
The package exists to be consumed without a JavaScript runtime
|
|
18
|
+
(REQ-PKG-2). A consumer in Python or Rust cannot run the Node suite. It
|
|
19
|
+
has no machine-readable statement of what a well-formed entry looks like;
|
|
20
|
+
today that lives in prose in README.md and in the tests.
|
|
21
|
+
|
|
22
|
+
A third question was raised at the same time: whether semantic versioning
|
|
23
|
+
could be derived from a schema, so that a breaking change is detected
|
|
24
|
+
mechanically rather than by the committer choosing a conventional-commit
|
|
25
|
+
prefix.
|
|
26
|
+
|
|
27
|
+
Alternatives considered and declined:
|
|
28
|
+
|
|
29
|
+
- **CUE, TypeSpec, JSON-LD/SHACL.** Each can express more than JSON Schema
|
|
30
|
+
(CUE in particular can state cross-file constraints). None has a
|
|
31
|
+
validator a consumer of this package already runs, and each adds a
|
|
32
|
+
toolchain to a repo whose contract is "plain JSON, nothing else". The
|
|
33
|
+
extra expressiveness would only re-encode checks the test suite already
|
|
34
|
+
performs.
|
|
35
|
+
- **Schema-derived semver** (`json-schema-diff` or equivalent in CI).
|
|
36
|
+
REQ-PKG-4 defines a breaking change as removal of an entry id, a fact
|
|
37
|
+
vocabulary value, or a change in relation semantics. All three are data
|
|
38
|
+
changes that leave the schema untouched. A schema diff would therefore
|
|
39
|
+
miss nearly every real break and flag only the rare structural one.
|
|
40
|
+
- **Generating enums from data into the schema at test time.** Keeps the
|
|
41
|
+
schema "complete" but makes it a build product rather than a document,
|
|
42
|
+
and duplicates checks the suite already does. Declined; the schema
|
|
43
|
+
stays static and hand-written.
|
|
44
|
+
|
|
45
|
+
## Decision
|
|
46
|
+
|
|
47
|
+
**Ship hand-written JSON Schema 2020-12**, one schema per data file plus
|
|
48
|
+
one for `fixtures/applicability-fixtures.json`, under `schema/`, listed in
|
|
49
|
+
`package.json` `files` so consumers get it with the data.
|
|
50
|
+
|
|
51
|
+
The schema covers **structure only**: required keys, types,
|
|
52
|
+
`additionalProperties: false` at every object level, the id patterns from
|
|
53
|
+
`docs/identifiers.md`, the closed enums for `modality` and the five
|
|
54
|
+
relation verbs, the shape of predicate operators (`gte`/`gt`/`lte`/`lt`,
|
|
55
|
+
list membership, equality), and the shape of a light reference
|
|
56
|
+
(`light`/`position`/`count`).
|
|
57
|
+
|
|
58
|
+
The schema does **not** attempt cross-file references. Cite-to-rule,
|
|
59
|
+
light-to-definition, fact-to-vocabulary and entry-to-entry resolution
|
|
60
|
+
stay in `test/data.test.mjs`. JSON Schema has no cross-document
|
|
61
|
+
reference mechanism; a partial imitation would be a second, weaker copy
|
|
62
|
+
of checks that already exist.
|
|
63
|
+
|
|
64
|
+
**Validate in the suite with Ajv as a devDependency.** REQ-PKG-1 (zero
|
|
65
|
+
runtime dependencies) governs what ships, not what tests. The published
|
|
66
|
+
package stays dependency-free.
|
|
67
|
+
|
|
68
|
+
**Version discipline comes from an identifier diff, not the schema.** A
|
|
69
|
+
test extracts every published identifier — entry ids, fact axis values,
|
|
70
|
+
light ids, relation verbs — from the last release tag (or the published
|
|
71
|
+
npm tarball) and compares them with HEAD. Any removal not accompanied by
|
|
72
|
+
a deprecation marker fails the build. This mechanises REQ-MODEL-10 and
|
|
73
|
+
the "removal is major" clause of REQ-PKG-4. release-please still owns
|
|
74
|
+
the version number; the test only refuses to let a silent removal reach
|
|
75
|
+
it.
|
|
76
|
+
|
|
77
|
+
## Consequences
|
|
78
|
+
|
|
79
|
+
- A new file under `data/` needs a schema before it can merge, and the
|
|
80
|
+
schema needs a test that loads it. Schema and data change together.
|
|
81
|
+
- `additionalProperties: false` means an experimental key in an entry
|
|
82
|
+
fails the build until the schema admits it. That is intended: the
|
|
83
|
+
schema is the record of what the shape is, and a key nobody wrote down
|
|
84
|
+
is a key nobody will maintain.
|
|
85
|
+
- Consumers in other languages can validate a vendored copy of the data
|
|
86
|
+
against `schema/` with any 2020-12 validator. The schema is now part of
|
|
87
|
+
the public contract and is subject to REQ-PKG-4: removing a property or
|
|
88
|
+
narrowing a type is a major version.
|
|
89
|
+
- The identifier-diff test needs the previous release available at test
|
|
90
|
+
time. In CI that is a `git fetch --tags`; locally it is whatever tag is
|
|
91
|
+
present. A missing baseline must fail loudly, not skip.
|
|
92
|
+
- If a future jurisdiction or Part D landing needs a constraint that
|
|
93
|
+
spans files — "every `us/inland` entry overrides an `intl` id that
|
|
94
|
+
exists" — it goes in the test suite, not the schema. This ADR is the
|
|
95
|
+
answer to the next "should we use CUE" question.
|
|
@@ -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.
|