colregs 0.2.3 → 0.3.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.
@@ -0,0 +1,202 @@
1
+ # ADR 0012 — The trace and Rule 2 departure verbs: kinematic and temporal evaluation
2
+
3
+ Date: 2026-09-07
4
+ Origin: colregs-engine, where it was ADR 0002 until 2026-09-09. The
5
+ decision is about colregs-engine's public API; "this package" below is
6
+ colregs-engine. ADRs for the whole family live here (AGENTS.md).
7
+ Status: draft, full stop. It names the verbs and frames their inputs and
8
+ results so the programme has a target to build toward; it does not build
9
+ them, and it expects to be broken while the package is 0.x.
10
+
11
+ ## Context
12
+
13
+ ADR 0011 fixed two verbs, one per input, and put kinematic and temporal
14
+ evaluation out of *that* shape: a different input and a different tool. It did
15
+ not say what the verbs would be, which reads as "never" — the wrong lesson,
16
+ since the formal-methods programme (colregs-engine#1, phases 3–5) exists to do
17
+ this evaluation and colregs' ADR 0005 has fixed most of the input.
18
+
19
+ What the data settles today:
20
+
21
+ - The **situation record** carries `kin:`, `geo:` and `hist:` per subject,
22
+ every threshold a paragraph reads declared once under `situation.constants`.
23
+ - `conduct` is "monitored over a trace, not evaluated at a point";
24
+ `kin:rot_deg_min` and `hist:latched_at_s` are read by no point predicate.
25
+ - Rule 2 is a region of situation space a game solver finds, with a closed
26
+ status alphabet and a fixed output list (ADR 0005 §5, proposal v4 §4).
27
+ R0/R1/R2 are research-ontology labels — colregs' vocabulary, not this API.
28
+
29
+ Not settled: the dynamics model (Q-18), the game's parameters (Q-19, Q-20),
30
+ offline or runtime (Q-22), the conduct effect shape, a trace fixture schema.
31
+
32
+ ## Decision
33
+
34
+ ### 1. Four inputs, four verbs
35
+
36
+ | input | verb | result | status |
37
+ |---|---|---|---|
38
+ | `FactRecord` — one vessel | `evaluateDisplay` | `DisplayEvaluation` | built |
39
+ | `Situation` — two vessels, one instant | `evaluateEncounter` | `EncounterEvaluation` | target |
40
+ | `Trace` — the situation over time | `evaluateConduct` | `ConductEvaluation` | named here |
41
+ | `Situation` under a `Rule2DepartureModel` | `evaluateRule2Departure` | `Rule2DepartureFinding` | named here |
42
+
43
+ The two new verbs live **in this package**, superseding ADR 0011's pencilled
44
+ "separate package": a function of a trace and a lookup in a precomputed grid are
45
+ both pure and total, under the same validator, `opts.data` and `colregs.version`
46
+ provenance. What is *not* pure — the solver, the model checker — stays in
47
+ `research/`.
48
+
49
+ ### 2. `Trace` — the input `conduct` reads
50
+
51
+ ```ts
52
+ interface TraceSample { t_s: number; situation: Situation; }
53
+ /** Non-empty, strictly increasing `t_s`, the same two vessels throughout. */
54
+ interface Trace { samples: TraceSample[]; }
55
+ ```
56
+
57
+ `Trace` is runtime-verification vocabulary — the object an STL monitor reads, a
58
+ checker's counterexample — not the Rules'. `t_s` is seconds on the caller's
59
+ clock; the engine reads differences only. A trace is a window, not a session:
60
+ the caller (searoom, the simulator, a fixture) decides how much history to hand
61
+ over, and the result says what window it saw. An object, so a field can be added
62
+ without breaking a caller. The validator that rejects an unknown fact key
63
+ rejects what it can see — an empty trace, non-increasing `t_s`, `other` in some
64
+ samples and not others. It cannot see identity (a `Situation` names no vessel):
65
+ the pair staying the same is the caller's, like the latch; purity is over
66
+ well-formed input.
67
+
68
+ `hist:was_overtaking` is a snapshot fact the caller supplies and clears, not
69
+ history the engine derives: set when 13(b)'s sector held at an earlier sample,
70
+ because "finally past and clear" is a judgement colregs declines to threshold
71
+ (Q-47). A windowed trace cannot see a latch set before it; time is the caller's.
72
+
73
+ ### 3. `ConductEvaluation` — verdicts over the window
74
+
75
+ ```ts
76
+ interface ConductEvaluation {
77
+ colregs: { version: string; source: 'resolved' | 'caller' };
78
+ window: { from_s: number; to_s: number; samples: number };
79
+ applied: EntryId[];
80
+ verdicts: ConductVerdict[];
81
+ phases: ConductPhaseChange[];
82
+ }
83
+ interface ConductVerdict {
84
+ id: EntryId; subject: 'own' | 'other';
85
+ verdict: 'kept' | 'breached' | 'pending';
86
+ attached_at_s?: number; decided_at_s?: number;
87
+ robustness?: { value: number; unit: string };
88
+ }
89
+ interface ConductPhaseChange { subject: 'own' | 'other'; phase: ParagraphCite; at_s: number; }
90
+ ```
91
+
92
+ - One **verdict** per applied conduct entry per subject it attached to; an
93
+ entry that never attached is absent, as it is from `applied`.
94
+ `kept`/`breached` are decided; `pending` means the window ended before the
95
+ duty could be judged (17(a)(ii)'s "as soon as it becomes apparent" hasn't
96
+ run out). `attached_at_s` is when the entry attached the role judged;
97
+ `decided_at_s` when a breach began or a duty was met. `robustness`, like
98
+ `Trace`, is runtime-verification vocabulary: the STL margin the monitor
99
+ computes, value and unit, so a near miss and a wide pass do not look alike.
100
+ - A **phase** is the Rule 13(d)/17 protocol state: the latch, the stand-on
101
+ vessel passing from 17(a)(i) to 17(a)(ii) to 17(b). `phase` is a
102
+ `ParagraphCite`, never an `EntryId` — ADR 0011 §4's aliases, used throughout
103
+ here, and several phases have no entry — naming the state machine
104
+ programme phase 4's TLA+ or UPPAAL model checks; the monitor refines it.
105
+ - `applied` and the companion `appliedConductEntries(trace)` keep the fixture
106
+ contract; per-sample encounter evaluations are not returned — call `evaluateEncounter`.
107
+
108
+ Vague quantities a conduct paragraph reads — "readily apparent" (8(b)), "ample
109
+ time" (16), "as soon as it becomes apparent" (17(a)(ii)) — are declared once in
110
+ colregs' `situation.constants`, as `appreciable_bearing_change_deg_min` is in
111
+ `facts.json` at 0.2.0. A number a paragraph reads belongs in colregs; one only
112
+ the solver reads, inside the grid.
113
+
114
+ ### 4. `Rule2DepartureModel` and `Rule2DepartureFinding` — the Rule 2 departure finding
115
+
116
+ ```ts
117
+ interface SolverParameters {
118
+ dynamics: string[]; horizon_s: number; cadence_s: number; separation_m: number;
119
+ information: 'full' | 'partial'; adversary: 'compliant' | 'physics';
120
+ }
121
+ interface Rule2DepartureModel extends SolverParameters {
122
+ version: string; colregs_version: string;
123
+ }
124
+ interface Rule2DepartureFinding {
125
+ status: 'not-flagged' | 'model-rule-conflict'
126
+ | 'no-robust-policy-in-model' | 'inconclusive-in-model';
127
+ rules: EncounterEvaluation; advisories: Rule2DepartureAdvisory[];
128
+ model: { version: string; colregs_version: string; parameters: SolverParameters;
129
+ assumptions_violated: string[] };
130
+ }
131
+ interface Rule2DepartureAdvisory {
132
+ action: { alter_deg?: number; sog_kn?: number }; margin_m: number;
133
+ breaches: ParagraphCite[]; envelope: { holds_until_s: number };
134
+ }
135
+ ```
136
+
137
+ The model is required and positional, not an `opts` field: `opts.data` defaults
138
+ to the colregs release this package resolves and a grid has no default. Any
139
+ field beyond `version`, `colregs_version` and the parameters is the artefact's
140
+ own, not API. `SolverParameters` are the axes the sensitivity matrix will vary
141
+ (Q-17 to Q-22): the field set is a claim about what that matrix is. `version`
142
+ names one grid, immutably; `colregs_version` the release it was solved against,
143
+ carried beside `rules.colregs.version` (a mismatch is reported, not refused);
144
+ the finding echoes the parameters, so a consumer can say under which ones it
145
+ holds. `action` and `envelope` take the shapes the worked scenarios used.
146
+ Otherwise the shape follows proposal v4 §4. `not-flagged`, `model-rule-conflict`
147
+ and `no-robust-policy-in-model` name R0, R1 and R2, `inconclusive-in-model`
148
+ none, so a `region` field could only repeat the status or invent one. The
149
+ rule-derived obligations sit in `rules` unchanged, so the reader sees what the
150
+ Rules said; R1's advisories are ranked best margin first, each with the
151
+ paragraphs it breaks; R2's list is empty; `assumptions_violated` is display
152
+ text, never matched on. `not-flagged` means not flagged by this model, never
153
+ "the rules suffice". `breaches` carries `ParagraphCite`s (`17(c)`), as
154
+ `phase` does, never entry ids: a `breaches` entry against a shall-if-practicable
155
+ paragraph is the model's verdict and not the Rules' — the compliance predicate's
156
+ rule for those, and for 17(a)(ii)'s `may`, is open and carded.
157
+
158
+ The guard rail is narrower than "no danger input": the grid asserts departures,
159
+ and a caller who swaps it changes every finding. What the signature buys is that
160
+ no *situation* input names a departure — correcting facts moves the outputs;
161
+ asserting danger is not an input — and that `model.version` rides on every
162
+ finding, so a claim is attributable to a named grid, never to the Rules.
163
+ Disclaimers live in the README and licence, not here.
164
+
165
+ ### 5. How each layer is checked
166
+
167
+ | layer | verb | what discharges it |
168
+ |---|---|---|
169
+ | point | `evaluateDisplay`, `evaluateEncounter` | exhaustive enumeration; Z3; Alloy for the sectors |
170
+ | trace | `evaluateConduct` | STL monitors *are* the verdict function; TLA+/UPPAAL for the 13(d)/17 phase machine |
171
+ | Rule 2 departure | `evaluateRule2Departure` | the game solver, offline; the artefact checked in, its parameters on the model |
172
+
173
+ The tools do not move into `src/`. What moves is their *output*: a counterexample
174
+ trace becomes a fixture; a certified grid, a `Rule2DepartureModel`.
175
+
176
+ ## Consequences
177
+
178
+ - README's roadmap paragraph names all four verbs and their status; ADR 0011
179
+ §5's `conduct` and Rule 2 bullets and its register row point here.
180
+ - Order of work, each step shipping something a consumer can call:
181
+ 1. colregs: a trace fixture schema and the first `conduct` entries (16,
182
+ 17, 8(b)) with an effect shape, as `situation-fixtures.json` did.
183
+ 2. `Trace`, `appliedConductEntries` against those fixtures — phase 3
184
+ starts here.
185
+ 3. `evaluateConduct` with verdicts and phases; the constants above.
186
+ 4. `evaluateRule2Departure` once a grid exists. ~~Until then the name is reserved and nothing exported: a stub answering `inconclusive-in-model` is a stub wearing a status.~~
187
+ **Struck. Reserving the name was never the policy here: a name gets an export the day it is written down, throwing or answering `inconclusive-in-model`. Withholding an export to look careful is the failure mode, not the stub. — Solace, 2026-09-13**
188
+
189
+ ## Register
190
+
191
+ | item | level | what would settle it |
192
+ |---|---|---|
193
+ | Two more verbs, one per input, in this package | ✎ | Mark's confirmation; supersedes ADR 0011's "separate package" row |
194
+ | `Trace` an object over `TraceSample[]`, `t_s` on the caller's clock, pair identity the caller's | ✎ | the first trace fixture |
195
+ | `hist:was_overtaking` a caller-supplied snapshot fact, never engine-derived | ✎ | Q-47; the first conduct monitor |
196
+ | `ConductVerdict` alphabet `kept`/`breached`/`pending`; absent is absent | ✎ | the first STL monitor being written |
197
+ | Field names snake_case with unit suffixes; `EntryId`/`ParagraphCite` per ADR 0011 §4 | ✎ | that ADR's row; the compiler enforces neither |
198
+ | `ConductPhaseChange.phase` values are paragraph cites, not entry ids | ✎ | the programme phase-4 TLA+ or UPPAAL model |
199
+ | Vague-quantity constants live in colregs; `SolverParameters` on the model and echoed on the finding, `colregs_version` naming the release solved against; nothing else on the model is API | ✎ | the first constant a conduct entry reads; Q-19's sensitivity matrix |
200
+ | `Rule2DepartureFinding` field set — `rules`, `Rule2DepartureAdvisory[]`, no banner cite (it is a function of `status`); the status alphabet is colregs' (ADR 0005 §5), not this package's to rename | ✎ | proposal v4 §4's sensitivity matrix; Q-19, Q-20 |
201
+ | No *situation* input names a departure; the grid does, and is named in every finding | ✎ | — |
202
+ | A name is exported the day it is written down, throwing or answering `inconclusive-in-model`; exports carry TSDoc's `@beta` release tag until a fixture backs them | ✎ | — |
@@ -0,0 +1,70 @@
1
+ # ADR 0013 — Rule text as corpus files under an edition registry (GATE-2 adopted)
2
+
3
+ Date: 2026-09-12
4
+ Status: accepted (2026-09-13)
5
+
6
+ ## Context
7
+
8
+ ADR 0003 made language a dimension and sketched its layout, but nothing
9
+ landed, and its GATE-2 (an edition layer between instrument and corpus) is
10
+ due for re-take *before* the first non-English corpus. Finlex (`fi`) and BOE
11
+ (`es`) are both licence-clear (ADR 0001, 2026-09-12), so the next text to
12
+ land forces the key shape. This ADR fixes the keys and leaves the words for
13
+ issue #98. A sibling proposal (branch `multi-jurisdiction-schema`, option A)
14
+ confirms GATE-2 declined instead; the two are meant to be read side by side.
15
+
16
+ ## Decision
17
+
18
+ 1. **`data/rules.json` is the skeleton.** Per paragraph: `path`, `rule`,
19
+ `jurisdiction`, `images`. No `text`, no `rule_title`, no source, and no
20
+ amendment state of its own. It gains `24(g)(i)`: the Convention has the
21
+ path; the USCG page lacks the words, which is the corpus's gap.
22
+ 2. **`data/editions.json` is the registry: jurisdiction → instrument →
23
+ editions.** An edition id is `<jurisdiction>@<tag>` (`intl@2016`,
24
+ `us/inland@2014`), carrying `amended_through`, `in_force` and optionally
25
+ `superseded_by`. **The tag is the `in_force` year, nothing else** — the
26
+ amending instrument (an IMO resolution, a Federal Register cite) belongs
27
+ in `amended_through`, never in the tag. Each jurisdiction names the
28
+ edition the skeleton consolidates. Two editions of
29
+ one jurisdiction may be registered at once, which is GATE-2's trigger
30
+ case expressed as data rather than as a diff between strings.
31
+ 3. **One file per corpus**,
32
+ `data/text/<jurisdiction>/<tag>/<language>.<source_id>.json`, identity
33
+ `<edition>.<language>.<source_id>`. A corpus names its `edition` and
34
+ nothing about jurisdiction or amendment state: both are the edition's.
35
+ CI checks the edition is registered and the filename agrees.
36
+ 4. **A corpus paragraph is `rule_title` + `text`** (or ADR 0010's withheld
37
+ fields). Corpus metadata otherwise as in option A: `tier`,
38
+ `normalization`, `source` (REQ-PROV-6), `rights` (three statements),
39
+ `gaps`, optional `translation_of`.
40
+ 5. **`data/corpora.json` indexes the files**, derived and drift-checked.
41
+ 6. **GATE-2 is re-taken and adopted.** The cost is one registry file and
42
+ one extra path segment; the gain is that "which consolidated state" is an
43
+ identifier compared by equality, not free text compared by eye, and a
44
+ stub must name an edition before it names anything else.
45
+
46
+ ## What the stubs show
47
+
48
+ Four corpora ship: `intl@2016.en-US.uscg` (today's text, moved verbatim),
49
+ `intl@2016.fi.finlex`, `intl@2016.es.boe` and `us/inland@2014.en-US.ecfr`,
50
+ the last three empty. The edition layer forces a claim option A lets a stub
51
+ defer: the Finnish and Spanish stubs assert they reflect `intl@2016` before
52
+ anyone has checked, and the `us/inland@2014` edition is recalled, not
53
+ verified, and says so. That is the honest cost of this option and the reason
54
+ to look at both.
55
+
56
+ ## Consequences
57
+
58
+ - Breaking file layout (REQ-PKG-4): `data/rules.json` no longer carries
59
+ text. colregs-engine's conformance research reads only `paragraphs` keys
60
+ and is unaffected; anything reading `.text` moves to the en-US corpus.
61
+ - ADR 0010's withheld mechanics move to the corpus paragraph, where the
62
+ text is; the skeleton has nothing to withhold.
63
+ - A new IMO amendment is a new registered edition, a new skeleton pointer,
64
+ and new corpus files under a new directory; the old ones may stay until
65
+ the last consumer moves, marked `superseded_by`. Under option A the same
66
+ event is an in-place edit of every corpus's `amendment_state`.
67
+ - REQ-LANG-1, -3, -5, -9, -10 and REQ-PROV-6 move from unimplemented to
68
+ implemented for the data on file; catalogs (REQ-LANG-6) are untouched.
69
+ - Landing Finnish or Spanish words is an edit to one file and one number in
70
+ the index, and still waits on this ADR being accepted (REQ-GATE-2).
package/docs/budgets.json CHANGED
@@ -2,20 +2,28 @@
2
2
  "$comment": "Prose budgets enforced by prose-budget (the engine in dotfiles .local/bin), run locally by npm test and in CI by the shared workflow. Raising a number or adding an exception is a deliberate, reviewable diff.",
3
3
  "lines": {
4
4
  "README.md": 360,
5
- "AGENTS.md": 120,
5
+ "AGENTS.md": 200,
6
6
  "CLAUDE.md": 10,
7
7
  "docs/requirements.md": 1500,
8
8
  "docs/identifiers.md": 450,
9
9
  "docs/part-b-invariants.md": 950,
10
+ "docs/adr/0001-name-and-jurisdiction-model.md": 280,
10
11
  "docs/adr/0007-rule26-overrides-and-aground.md": 90,
11
12
  "docs/adr/0008-mooring-buoy-modifier.md": 90,
12
13
  "docs/adr/0009-data-version-stamp.md": 90,
14
+ "docs/adr/0010-text-withheld-jurisdictions.md": 125,
15
+ "docs/adr/0011-api-shape.md": 173,
16
+ "docs/adr/0012-trace-and-rule2-departure-api.md": 203,
17
+ "docs/verification/2026-09-09-text-slug-straw-man.md": 270,
13
18
  "required": true,
14
- "pending": []
19
+ "pending": [],
20
+ "docs/adr/0013-corpus-files-with-editions.md": 95,
21
+ "docs/timeline.md": 130
15
22
  },
16
23
  "json_prose": {
17
24
  "targets": [
18
25
  "data/*.json",
26
+ "data/text/**/*.json",
19
27
  "fixtures/*.json"
20
28
  ],
21
29
  "keys": [
@@ -82,6 +90,7 @@
82
90
  "AGENTS.md",
83
91
  "CLAUDE.md",
84
92
  "data/*.json",
93
+ "data/text/**/*.json",
85
94
  "fixtures/*.json",
86
95
  "test/*.mjs",
87
96
  "docs/**/*.md"
@@ -92,6 +101,7 @@
92
101
  "AGENTS.md",
93
102
  "CLAUDE.md",
94
103
  "data/*.json",
104
+ "data/text/**/*.json",
95
105
  "fixtures/*.json",
96
106
  "test/*.mjs"
97
107
  ],
@@ -100,6 +110,7 @@
100
110
  "AGENTS.md",
101
111
  "CLAUDE.md",
102
112
  "data/*.json",
113
+ "data/text/**/*.json",
103
114
  "fixtures/*.json",
104
115
  "test/*.mjs"
105
116
  ],
@@ -108,6 +119,7 @@
108
119
  "AGENTS.md",
109
120
  "CLAUDE.md",
110
121
  "data/*.json",
122
+ "data/text/**/*.json",
111
123
  "fixtures/*.json",
112
124
  "test/*.mjs"
113
125
  ],
@@ -116,6 +128,7 @@
116
128
  "AGENTS.md",
117
129
  "CLAUDE.md",
118
130
  "data/*.json",
131
+ "data/text/**/*.json",
119
132
  "fixtures/*.json",
120
133
  "test/*.mjs"
121
134
  ]
@@ -151,6 +164,7 @@
151
164
  "CLAUDE.md",
152
165
  "docs/**/*.md",
153
166
  "data/*.json",
167
+ "data/text/**/*.json",
154
168
  "fixtures/*.json"
155
169
  ]
156
170
  },
package/docs/gates.json CHANGED
@@ -36,9 +36,9 @@
36
36
  "declined_in": "docs/adr/0003-language-as-a-dimension.md",
37
37
  "closing_event": "first-non-english-corpus",
38
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."
39
+ "status": "adopted",
40
+ "settled_by": "docs/adr/0013-corpus-files-with-editions.md",
41
+ "note": "Re-taken and adopted ahead of its due date (translation #1) — ADR 0013 introduces the edition registry directly."
42
42
  },
43
43
  {
44
44
  "id": "GATE-3",
@@ -76,7 +76,9 @@ Neither consumer lives in this repo.
76
76
  (Rule 28 "[Reserved]") means silence-means-inherit would apply
77
77
  international law where the national body deliberately has none, so no
78
78
  non-`intl` jurisdiction lands before an explicit suppression mechanism
79
- exists. A delta that only *adds* entries is exempt: it suppresses nothing,
79
+ exists. That bar is about suppression, not licensing: it is independent of
80
+ REQ-PROV-2 and ADR 0010, and it binds whether a jurisdiction's text ships or
81
+ is withheld. A delta that only *adds* entries is exempt: it suppresses nothing,
80
82
  so the Q-11 hazard cannot arise from it (ADR 0008, `30a-buoy`/`30b-buoy`).
81
83
  A delta that suppresses or replaces an `intl` entry still waits.
82
84
  - **REQ-SCOPE-4** — Adding a jurisdiction MUST be additive. It MUST NOT require
@@ -123,7 +125,12 @@ Four layers, each independently addressable.
123
125
 
124
126
  - **REQ-MODEL-1** — **Rule text**, verbatim, keyed by paragraph path. Text MUST
125
127
  NOT be paraphrased, summarised or reflowed. Where a jurisdiction's text
126
- differs, both MUST be stored, keyed by jurisdiction.
128
+ differs, both MUST be stored, keyed by jurisdiction — unless the paragraph is
129
+ `text_status: withheld` (ADR 0010), which stores no rule text. What a
130
+ withheld paragraph carries instead — citation alone, a digest, a
131
+ deterministic non-prose reduction, or the `intl` equivalent — is pencil in
132
+ ADR 0010, deliberately unsettled. This requirement bars paraphrasing text
133
+ the package *ships*; it does not by itself decide the withheld case.
127
134
  - **REQ-MODEL-2** — **Light definitions** (Rule 21) MUST carry colour, arc of
128
135
  visibility in degrees, and range by length band (Rule 22). Jurisdictions MAY
129
136
  add definitions (e.g. the US special flashing light, Inland 21(g)).
@@ -431,8 +438,7 @@ official languages, and many states gazette their own legally binding
431
438
  translation. (Recalled, not yet verified against the primary sources — Q-6.)
432
439
  See ADR 0003.
433
440
 
434
- - **REQ-LANG-1** **(unimplemented — no language dimension exists in the
435
- data)** — Language MUST be a dimension orthogonal to jurisdiction,
441
+ - **REQ-LANG-1** — Language MUST be a dimension orthogonal to jurisdiction,
436
442
  identified by BCP 47 tags. Which body of rules applies and which text of
437
443
  them is displayed are independent questions; neither MUST ever be inferred
438
444
  from the other, and no property beyond the language of the text — not
@@ -445,8 +451,7 @@ See ADR 0003.
445
451
  and renaming an identifier is a breaking change (REQ-PKG-4). Immutability
446
452
  itself is REQ-MODEL-10; this requirement adds only that identifiers are
447
453
  never localized.
448
- - **REQ-LANG-3** **(unimplemented — rule text is one untagged ruleset, not a
449
- corpus per jurisdiction × language × source)** — Rule text MUST be storable
454
+ - **REQ-LANG-3** — Rule text MUST be storable
450
455
  as a **corpus** per (jurisdiction × language × source), keyed by paragraph
451
456
  path, holding at most one text per path, with corpus-level provenance and
452
457
  one declared status tier:
@@ -461,8 +466,7 @@ See ADR 0003.
461
466
  rule applies per corpus, against that corpus's own source.
462
467
  - **REQ-LANG-4** — Adding a language MUST be additive: no schema change, no
463
468
  edits to existing corpora or catalogs (the language mirror of REQ-SCOPE-4).
464
- - **REQ-LANG-5** **(unimplemented — no coverage declaration, and the CI checks
465
- are unwritten)** — Corpora MAY be partial. Coverage MUST be declared in
469
+ - **REQ-LANG-5** — Corpora MAY be partial. Coverage MUST be declared in
466
470
  machine-readable form, and CI MUST fail on a corpus key that does not
467
471
  resolve to a known paragraph path, and on a corpus filename that disagrees
468
472
  with the file's internal metadata. Silence MUST NOT imply coverage (the
@@ -488,15 +492,13 @@ See ADR 0003.
488
492
  - **REQ-LANG-8** — A `community`-tier corpus MUST record who produced and
489
493
  who reviewed it. Machine translation without named human review MUST NOT
490
494
  be accepted.
491
- - **REQ-LANG-9** **(unimplemented — the shipped text declares no normalization
492
- form)** — Verbatim (REQ-MODEL-1) is defined at the Unicode level: each
495
+ - **REQ-LANG-9** — Verbatim (REQ-MODEL-1) is defined at the Unicode level: each
493
496
  corpus MUST declare the normalization form applied to its text (NFC unless
494
497
  declared otherwise) and MUST NOT insert or strip bidi control characters,
495
498
  localize numerals, punctuation, units or quotation marks, or otherwise "fix"
496
499
  the source text. Rendering direction is a consumer concern and MUST stay out
497
500
  of the data.
498
- - **REQ-LANG-10** **(unimplemented — neither the skeleton nor the ruleset
499
- declares an amendment state)** — The structural skeleton MUST declare, as
501
+ - **REQ-LANG-10** — The structural skeleton MUST declare, as
500
502
  data, the amendment state it consolidates (e.g. "COLREGS 72 as amended
501
503
  through …"). Every corpus MUST declare the amendment state its source
502
504
  reflects. The two MAY differ — a corpus transcribed from an older
@@ -509,9 +511,10 @@ See ADR 0003.
509
511
 
510
512
  - **REQ-PROV-1** — Every text and image asset MUST record its source, the date
511
513
  retrieved, and its licence or public-domain basis.
512
- - **REQ-PROV-2** — A jurisdiction MUST NOT be added until its reproduction terms
513
- have been checked against the primary source and recorded. Recalled or assumed
514
- terms are not sufficient.
514
+ - **REQ-PROV-2** — A jurisdiction's *rule text* MUST NOT be published until its
515
+ reproduction terms have been checked against the primary source and recorded.
516
+ Recalled or assumed terms are not sufficient. This does not bar *modelling* a
517
+ jurisdiction: its structure may ship with the text withheld — ADR 0010.
515
518
  - **REQ-PROV-3** — Where a licence requires attribution (e.g. OGL, CC BY), the
516
519
  attribution text MUST ship in the package, not only in the repo.
517
520
  - **REQ-PROV-4** — Code licence and data licence MUST be stated separately. The
@@ -519,8 +522,7 @@ See ADR 0003.
519
522
  - **REQ-PROV-5** — Images MUST be addressable as data: an image record per file,
520
523
  naming what it illustrates by entry id or paragraph path. Unexplained filename
521
524
  prefixes are a provenance defect.
522
- - **REQ-PROV-6** **(unimplemented — source identity is a prose string and
523
- rights are one flat field)** — Source identity MUST be structured data —
525
+ - **REQ-PROV-6** — Source identity MUST be structured data —
524
526
  publisher, title, edition, publication and effective dates, URL, retrieval
525
527
  date — not a prose string. Rights MUST be recorded separately for the source
526
528
  text, for the basis on which this package redistributes it, and for the
@@ -691,20 +693,21 @@ Each gate names the declined design, the closing event, and the trigger.
691
693
  second-jurisdiction bundle — justified by Q-11 and GATE-2, no longer by
692
694
  a cross-jurisdiction trigger route.
693
695
 
694
- - **GATE-2 — instrument → edition → corpus as first-class layers**
695
- (ADR 0003, declined; the adopted 80% is REQ-LANG-10).
696
- *Closing event*: the second corpus of any one jurisdiction — which,
697
- read against ADR 0003's sequencing, means **the first non-English
698
- corpus**, not a distant milestone. A French or Finnish text of `intl`
699
- is a second corpus of `intl`. With one corpus, re-homing it under an
700
- edition parent is a single file move; the cost scales with
696
+ - ~~**GATE-2 — instrument → edition → corpus as first-class layers**~~
697
+ (ADR 0003, declined; the adopted 80% is REQ-LANG-10). **Re-taken and
698
+ adopted, ADR 0013** — ahead of its due date (translation #1): the
699
+ edition registry lands directly, rather than waiting for the trigger
700
+ below to fire.
701
+ *Closing event, as declined*: the second corpus of any one jurisdiction
702
+ — which, read against ADR 0003's sequencing, means **the first
703
+ non-English corpus**, not a distant milestone. A French or Finnish text
704
+ of `intl` is a second corpus of `intl`. With one corpus, re-homing it
705
+ under an edition parent is a single file move; the cost scales with
701
706
  corpora × languages immediately thereafter.
702
- *Trigger*: a jurisdiction publishing two editions in force
707
+ *Trigger, as declined*: a jurisdiction publishing two editions in force
703
708
  concurrently — an old and a new text running in parallel through a
704
709
  transition period. REQ-LANG-10's declared amendment state makes such a
705
- pair machine-visible, which is what gives this trigger a foothold.
706
- *Re-take required before translation #1 lands* — the edition-layer
707
- decision is due at the first added translation, not at 1.0.
710
+ pair machine-visible, which is what gave this trigger its foothold.
708
711
 
709
712
  - **GATE-3 — legal-status × translation-status as two enums**
710
713
  (ADR 0003, half-adopted: one tier for legal authority in REQ-LANG-3,
@@ -792,6 +795,9 @@ Tracked here until resolved; each becomes an ADR.
792
795
  unece.org was unreachable from the checking host, and the UN's default
793
796
  terms (personal, non-commercial use only) block it until written
794
797
  permission or a national transposition is chosen instead.
798
+ **Ruled 2026-09-09 (ADR 0010):** this blocks CEVNI's *text*, not CEVNI.
799
+ A jurisdiction may be modelled in full with its text withheld, so no
800
+ session should treat `eu/cevni` as unimplementable.
795
801
  - **Q-4** — Two upstream SignalK spec asks are outstanding and independent of
796
802
  this package: a making-way indicator, and `design.maxSpeed`.
797
803
  - **Q-5** — REQ-VERIFY-5 asks for boundary fixtures on every numeric gate.
@@ -831,6 +837,14 @@ Tracked here until resolved; each becomes an ADR.
831
837
  not only as a whole: clearing one candidate source unblocks that corpus
832
838
  alone, which is the cheap path when a demo needs a specific language
833
839
  early.
840
+ **BOE (`es`) and Finlex (`fi`) verified clean, 2026-09-09 and 2026-09-12**
841
+ (ADR 0001 amendments) — both permit the reuse REQ-PROV-2 needs. The UNTS
842
+ deposit (`en`/`fr`) is confirmed **blocked**, not merely unverified: no
843
+ UNTS-specific rights statement exists, and the reachable terms are the
844
+ same personal/non-commercial, no-derivative-works terms that block CEVNI.
845
+ `es` is the cheapest language to ship the first non-`intl` corpus; UNTS
846
+ needs written UN permission or a national republication before `en`/`fr`
847
+ can use the deposit route.
834
848
  - **Q-8** — Does the paragraph path survive the first national amalgamation?
835
849
  GATE-1's accepted risk rests on paragraph paths being immutable
836
850
  (REQ-MODEL-10) — adding and deprecating are fine, but a path that keeps its
@@ -918,7 +932,9 @@ Tracked here until resolved; each becomes an ADR.
918
932
  "this path/entry deliberately does not exist here", distinguishable from
919
933
  "not yet transcribed". Decide the mechanism in the second-jurisdiction
920
934
  bundle (GATE-1 re-take, GATE-2, Q-10); until then no non-`intl`
921
- jurisdiction lands.
935
+ jurisdiction lands. This is the live blocker on CEVNI, and it is *not* the
936
+ licence one: ADR 0010 removed the licence block by permitting structure with
937
+ the text withheld, and left this one standing.
922
938
  Also from the same verification pass, tracked on the global board rather
923
939
  than here: four transcription defects in `data/rules.json` itself
924
940
  (`21(a)`, `21(b)`, `23(b)`, `29(b)`) — a data fix, not a design
@@ -1328,6 +1344,16 @@ written up in `docs/identifiers.md` §"Effects"; what it could not is here.
1328
1344
  be a number invented rather than declared. Settled by whatever settles the
1329
1345
  `conduct` monitors, which are the things that watch a duty end rather than
1330
1346
  begin.
1347
+
1348
+ **Ruled 2026-09-08:** while one vessel holds the latch, 13(a)'s sector
1349
+ test is suppressed on the other, newly-gaining vessel — once overtaking,
1350
+ always overtaking, per 13(d)'s own text, so a geometry test that would
1351
+ newly name a second give-way vessel must not fire while the first still
1352
+ holds the role by history alone. `13a`'s sector branch now also reads
1353
+ `other:hist:was_overtaking: false`; the closing fixture in
1354
+ `fixtures/situation-fixtures.json` is the case this closes — a vessel
1355
+ drops back onto the latch-holder's own stern, and only the latch-holder
1356
+ gives way.
1331
1357
  - **Q-48** — **Nothing checks that a situation is geometrically possible.**
1332
1358
  `own:geo:rel_bearing_deg`, `other:geo:rel_bearing_deg`, the two
1333
1359
  `kin:heading_deg` and the two `kin:sog_kn` are six facts related by two
@@ -1398,16 +1424,25 @@ nothing is blocked while open.
1398
1424
  - **Q-51** — **What arms 13(d)'s latch?** *A:* 13(b)'s deeming, at the first
1399
1425
  state the geometry holds. *B:* 13(a)'s duty actually attaching. They differ
1400
1426
  where the geometry holds but a condition on 13(a) does not — `Q-50`'s
1401
- surface. Default **A**: `hist:was_overtaking` is "was, at some earlier point
1402
- in this encounter, an overtaking vessel" and `13b-overtaking` is geometry.
1403
- No recommendation; downstream of `Q-50`.
1427
+ surface. Pencilled **B** ✎ (Solace, 2026-09-14): the latch arms when the
1428
+ duty attaches. KIVELI [2025] EWHC 1185 (Admlty) holds the analogous Rule 14
1429
+ latch arms on risk of collision and then persists; no authority was found on
1430
+ overtaking geometry with no risk of collision. Free to reverse: four entries
1431
+ (`13a`, `13b-*`, `13d`) and their fixtures, and nothing consumes the one
1432
+ state this changes the answer for. Settled by: a decision on Rule 13 itself,
1433
+ or Cockcroft & Lameijer on whether overtaking status needs risk of
1434
+ collision.
1404
1435
  - **Q-52** — **What does 13(d)'s latch forbid?** *Narrow:* reclassification to
1405
1436
  *crossing* only, as the paragraph says, leaving head-on to Rule 14 on the
1406
1437
  geometry of the moment. *Broad:* the encounter stays an overtaking and no
1407
1438
  other Section II classification attaches. Both preserve the duty; they differ
1408
- on encounter type, which Rule 17's phases and 14(a) hang off. Default
1409
- **broad**: `13d` yields `encounter: overtaking` from history alone, `14b` and
1410
- `15a-crossing` gate on `was_overtaking: false`. No recommendation.
1439
+ on encounter type, which Rule 17's phases and 14(a) hang off.
1440
+ Pencilled **broad** ✎ (Solace, 2026-09-14), which is also the data's
1441
+ default: `13d` yields `encounter: overtaking` from history alone, `14b` and
1442
+ `15a-crossing` gate on `was_overtaking: false`. 13(a)'s "notwithstanding"
1443
+ already displaces Rule 14; eCOLREGs states the broad reading as conventional.
1444
+ Settled by: a case or commentary on an overtaking becoming a head-on; none
1445
+ found.
1411
1446
  - **Q-53** — **Does 17(a)(ii) suspend 17(a)(i)'s duty, or add an exception?**
1412
1447
  *Suspension:* "may, however" lifts the duty once non-compliance is apparent;
1413
1448
  a monitor then flags nothing. *Exception:* the duty stands and a departure is
@@ -1447,3 +1482,10 @@ Two decisions taken in pencil, reversible in one edit: the invariants live in
1447
1482
  their own document under `REQ-INV-1`–`REQ-INV-7` (§4.2 says why), and the id
1448
1483
  scheme is `REQ-INV-2`'s — cheap to change until P4.2 cites an id from a TLA+
1449
1484
  module, expensive after.
1485
+
1486
+ A third, ruled rather than pencil: **`docs/part-b-invariants.md` stays
1487
+ hand-written Markdown; no derived JSON registry.** Ruled 2026-09-08. Revisit
1488
+ only if P4.2's TLA+ needs to cite `INV-` ids mechanically — the id scheme
1489
+ just above is already the cheap-to-change half of that trigger, so the
1490
+ revisit costs one file's worth of tooling, not a re-derivation of the ids
1491
+ themselves.