colregs 0.1.0 → 0.2.0
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 +46 -13
- package/README.md +324 -20
- package/data/applicability.json +1247 -236
- package/data/deprecated-identifiers.json +7 -0
- package/data/facts.json +549 -112
- package/data/geometry.json +34 -34
- package/data/images.json +52 -26
- package/data/lights.json +36 -36
- package/data/rules.json +887 -104
- package/docs/adr/0001-name-and-jurisdiction-model.md +194 -0
- package/docs/adr/0002-wig-gate-and-jurisdiction-field.md +77 -0
- 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/conventions.md +27 -0
- package/docs/gates.json +84 -0
- package/docs/identifiers.md +435 -0
- package/docs/requirements.md +1320 -0
- 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 +498 -138
- package/fixtures/situation-fixtures.json +3603 -0
- package/package.json +27 -5
- package/schema/applicability-fixtures.schema.json +47 -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,27 @@
|
|
|
1
|
+
# Conventions
|
|
2
|
+
|
|
3
|
+
## Ink, pencil, open
|
|
4
|
+
|
|
5
|
+
Design documents in this project mark each decision with a confidence
|
|
6
|
+
level. The level is not a comment on the author's certainty; it is the
|
|
7
|
+
**rule for who may change it and on what grounds.** A session reads the
|
|
8
|
+
level before it reads the content, and behaves accordingly.
|
|
9
|
+
|
|
10
|
+
| level | marker | who changes it | on what grounds |
|
|
11
|
+
|---|---|---|---|
|
|
12
|
+
| **Ink** | none (default in ADRs) | Mark | significant evidence: a counterexample, a verification result, a primary source. A session may argue, citing the evidence. It never edits ink itself. |
|
|
13
|
+
| **Pencil** | `✎` | any session | a better idea. Log the change and why. Each pencilled item names what would settle it. |
|
|
14
|
+
| **Open** | `?` | anyone proposes | a proposal moves it to pencil |
|
|
15
|
+
|
|
16
|
+
How to ask a session about them:
|
|
17
|
+
|
|
18
|
+
- "What's in pencil?" — list them, with what would settle each.
|
|
19
|
+
- "Should we change this pencil item?" — give an opinion and change it if
|
|
20
|
+
Mark agrees, or on your own if the doc says the session may.
|
|
21
|
+
- "Should we change this ink item?" — answer only with evidence. If there is
|
|
22
|
+
none, say "no evidence to reopen it" and stop.
|
|
23
|
+
|
|
24
|
+
Ink and pencil are per item, not per document. A document is mostly ink
|
|
25
|
+
with pencilled items, or mostly pencil with a few ink anchors; both are
|
|
26
|
+
normal. Moving an item from pencil to ink is a decision Mark makes, and it
|
|
27
|
+
gets a one-line log entry saying what settled it.
|
package/docs/gates.json
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
{
|
|
2
|
+
"note": "Machine-readable mirror of the reversibility gates in docs/requirements.md \u00a710. Governance metadata about this repository, not COLREGS data \u2014 it deliberately lives in docs/ rather than data/, which is the consumer-facing model. REQ-GATE-3 is enforced against this file by test/data.test.mjs; the prose section is authoritative for the reasoning, this file is authoritative for status.",
|
|
3
|
+
"requirements": [
|
|
4
|
+
"REQ-GATE-1",
|
|
5
|
+
"REQ-GATE-2",
|
|
6
|
+
"REQ-GATE-3",
|
|
7
|
+
"REQ-GATE-4"
|
|
8
|
+
],
|
|
9
|
+
"closing_events": {
|
|
10
|
+
"1.0-tag": "the 1.0 version tag \u2014 REQ-GATE-3 blocks it until every gate carrying this closing event has been re-taken",
|
|
11
|
+
"first-non-english-corpus": "the second corpus of any one jurisdiction, which in practice is the first non-English text",
|
|
12
|
+
"first-community-translation-of-national-corpus": "the first `community`-tier translation of a `national`-tier corpus",
|
|
13
|
+
"first-external-contribution": "the first merged contribution from someone other than the copyright holder",
|
|
14
|
+
"none": "the decline does not get cheaper or dearer with time; recorded so it is not re-read as merely deferred"
|
|
15
|
+
},
|
|
16
|
+
"statuses": {
|
|
17
|
+
"open": "declined for now, not re-taken; adoption still possible at the stated cost",
|
|
18
|
+
"re-taken": "reconsidered deliberately and confirmed as declined, in the cited ADR",
|
|
19
|
+
"adopted": "the declined design was subsequently adopted, in the cited ADR",
|
|
20
|
+
"declined-permanently": "declined on grounds that do not decay; no closing event exists"
|
|
21
|
+
},
|
|
22
|
+
"gates": [
|
|
23
|
+
{
|
|
24
|
+
"id": "GATE-1",
|
|
25
|
+
"title": "`paragraph_id` split from `citation_path`",
|
|
26
|
+
"declined_in": "docs/adr/0003-language-as-a-dimension.md",
|
|
27
|
+
"closing_event": "1.0-tag",
|
|
28
|
+
"trigger": "a paragraph path that keeps its spelling while changing what text it denotes, WITHIN ONE JURISDICTION ACROSS RELEASES. Two routes: an IMO amendment renumbering an `intl` path (verified real \u2014 A.910(22) displaced 23(c) to 23(d); A.464(XII) relettered 24(g) to 24(h)), or a national body renumbering its own amalgamation, so that e.g. `us/inland 24(c)` denotes different text in vN+1 than in vN. Cross-jurisdiction divergence at one point in time is NOT this trigger: the effective identifier is (jurisdiction, path), so the 15 same-path-different-text rows in the 33 CFR 83 diff are REQ-MODEL-1 working as designed.",
|
|
29
|
+
"status": "open",
|
|
30
|
+
"settled_by": null,
|
|
31
|
+
"note": "Open on timing, not outcome: if the trigger fires the split is forced and lands as a major version. Ruling 2026-08-30 (requirements.md \u00a710): does not flip on today's data \u2014 no published path has changed denotation \u2014 but leans adopt, decided in the second-jurisdiction bundle with GATE-2 and Q-10. That bundle is also where the re-check happens; it is justified by Q-11 and GATE-2, not by cross-jurisdiction divergence."
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"id": "GATE-2",
|
|
35
|
+
"title": "instrument \u2192 edition \u2192 corpus as first-class layers",
|
|
36
|
+
"declined_in": "docs/adr/0003-language-as-a-dimension.md",
|
|
37
|
+
"closing_event": "first-non-english-corpus",
|
|
38
|
+
"trigger": "a jurisdiction publishing two editions in force concurrently",
|
|
39
|
+
"status": "open",
|
|
40
|
+
"settled_by": null,
|
|
41
|
+
"note": "Due at translation #1, not at 1.0."
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"id": "GATE-3",
|
|
45
|
+
"title": "legal-status \u00d7 translation-status as two enums",
|
|
46
|
+
"declined_in": "docs/adr/0003-language-as-a-dimension.md",
|
|
47
|
+
"closing_event": "first-community-translation-of-national-corpus",
|
|
48
|
+
"trigger": "a real corpus whose legal tier and translation tier disagree in a way a consumer must filter on and cannot from the metadata as structured",
|
|
49
|
+
"status": "open",
|
|
50
|
+
"settled_by": null,
|
|
51
|
+
"note": "Half-adopted: one tier for legal authority (REQ-LANG-3), translation provenance as structured metadata (REQ-LANG-8, REQ-PROV-6)."
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"id": "GATE-4",
|
|
55
|
+
"title": "a package-encoded language fallback chain",
|
|
56
|
+
"declined_in": "docs/adr/0003-language-as-a-dimension.md",
|
|
57
|
+
"closing_event": "none",
|
|
58
|
+
"trigger": null,
|
|
59
|
+
"status": "declined-permanently",
|
|
60
|
+
"settled_by": "docs/adr/0003-language-as-a-dimension.md",
|
|
61
|
+
"note": "The door opens outward: loosening REQ-LANG-7 later is additive, tightening it later would break consumers. Recorded so the asymmetry is not re-discovered as an argument for adopting early."
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"id": "GATE-5",
|
|
65
|
+
"title": "a CI-enforced terminology glossary",
|
|
66
|
+
"declined_in": "docs/adr/0003-language-as-a-dimension.md",
|
|
67
|
+
"closing_event": "none",
|
|
68
|
+
"trigger": null,
|
|
69
|
+
"status": "declined-permanently",
|
|
70
|
+
"settled_by": "docs/adr/0003-language-as-a-dimension.md",
|
|
71
|
+
"note": "Declined for legal corpora \u2014 it follows from verbatimness (REQ-MODEL-1) and does not decay. For display catalogs it is contributor guidance, gated on contribution docs existing at all."
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"id": "GATE-6",
|
|
75
|
+
"title": "the compilation's outbound licence",
|
|
76
|
+
"declined_in": "docs/adr/0004-licence-layering.md",
|
|
77
|
+
"closing_event": "first-external-contribution",
|
|
78
|
+
"trigger": "deciding the data side wants CC0 or CC BY 4.0 separately from the code (REQ-PROV-4, Q-9)",
|
|
79
|
+
"status": "open",
|
|
80
|
+
"settled_by": null,
|
|
81
|
+
"note": "ADR 0004 settles the code licence (MIT \u2192 Apache-2.0) and leaves the data licence open. Already-published npm versions stay under the licence they shipped with; the gate governs future releases only, and is held open deliberately by REQ-PROV-7's CONTRIBUTING.md terms (DCO-style certification plus a relicensing grant); a CLA-assistant bot is the upgrade path if contributors arrive."
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
}
|