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
@@ -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.