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.
Files changed (34) hide show
  1. package/LICENSE +194 -26
  2. package/PROVENANCE.md +46 -13
  3. package/README.md +324 -20
  4. package/data/applicability.json +1247 -236
  5. package/data/deprecated-identifiers.json +7 -0
  6. package/data/facts.json +549 -112
  7. package/data/geometry.json +34 -34
  8. package/data/images.json +52 -26
  9. package/data/lights.json +36 -36
  10. package/data/rules.json +887 -104
  11. package/docs/adr/0001-name-and-jurisdiction-model.md +194 -0
  12. package/docs/adr/0002-wig-gate-and-jurisdiction-field.md +77 -0
  13. package/docs/adr/0003-language-as-a-dimension.md +220 -0
  14. package/docs/adr/0004-licence-layering.md +78 -0
  15. package/docs/adr/0005-rule-categories-and-the-situation-record.md +323 -0
  16. package/docs/adr/0006-json-schema-and-identifier-diff.md +95 -0
  17. package/docs/conventions.md +27 -0
  18. package/docs/gates.json +84 -0
  19. package/docs/identifiers.md +435 -0
  20. package/docs/requirements.md +1320 -0
  21. package/docs/verification/2026-08-30-q6-q8.md +278 -0
  22. package/docs/verification/2026-09-05-rule27f-rule28-anchor-scenarios.md +330 -0
  23. package/fixtures/applicability-fixtures.json +498 -138
  24. package/fixtures/situation-fixtures.json +3603 -0
  25. package/package.json +27 -5
  26. package/schema/applicability-fixtures.schema.json +47 -0
  27. package/schema/applicability.schema.json +401 -0
  28. package/schema/deprecated-identifiers.schema.json +29 -0
  29. package/schema/facts.schema.json +793 -0
  30. package/schema/geometry.schema.json +239 -0
  31. package/schema/images.schema.json +53 -0
  32. package/schema/lights.schema.json +131 -0
  33. package/schema/rules.schema.json +50 -0
  34. 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.
@@ -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
+ }