colregs 0.2.4 → 0.3.1

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 (57) hide show
  1. package/PROVENANCE.md +3 -3
  2. package/README.md +19 -10
  3. package/data/applicability.json +219 -244
  4. package/data/corpora.json +41 -0
  5. package/data/editions.json +27 -0
  6. package/data/facts.json +4 -4
  7. package/data/geometry.json +8 -8
  8. package/data/i18n/en.json +27 -0
  9. package/data/i18n/fi.json +25 -0
  10. package/data/images.json +32 -32
  11. package/data/operations.json +79 -0
  12. package/data/rules.json +176 -599
  13. package/data/text/intl/2016/en-US.uscg.json +877 -0
  14. package/data/text/intl/2016/es.boe.json +23 -0
  15. package/data/text/intl/2016/fi.finlex.json +22 -0
  16. package/data/text/us/inland/2014/en-US.ecfr.json +22 -0
  17. package/data/version.json +1 -1
  18. package/docs/adr/0001-name-and-jurisdiction-model.md +42 -2
  19. package/docs/adr/0003-language-as-a-dimension.md +8 -7
  20. package/docs/adr/0006-json-schema-and-identifier-diff.md +2 -0
  21. package/docs/adr/0011-api-shape.md +1 -1
  22. package/docs/adr/0012-trace-and-rule2-departure-api.md +3 -4
  23. package/docs/adr/0013-corpus-files-with-editions.md +70 -0
  24. package/docs/adr/0014-engine-interface-owned-by-colregs.md +103 -0
  25. package/docs/adr/0015-rule-ids-are-paragraph-keys.md +191 -0
  26. package/docs/budgets.json +17 -17
  27. package/docs/gates.json +3 -3
  28. package/docs/identifiers.md +49 -51
  29. package/docs/maritime-sources.md +58 -0
  30. package/docs/normative-language.md +103 -0
  31. package/docs/part-b-invariants.md +31 -29
  32. package/docs/requirements.md +118 -97
  33. package/docs/timeline.md +102 -0
  34. package/docs/verification/2026-09-09-text-slug-straw-man.md +3 -2
  35. package/fixtures/applicability-fixtures.json +190 -190
  36. package/fixtures/situation-fixtures.json +287 -287
  37. package/package.json +1 -1
  38. package/schema/applicability-fixtures.schema.json +4 -13
  39. package/schema/applicability.schema.json +27 -27
  40. package/schema/conduct-evaluation.schema.json +135 -0
  41. package/schema/corpora.schema.json +68 -0
  42. package/schema/corpus.schema.json +329 -0
  43. package/schema/display-evaluation.schema.json +146 -0
  44. package/schema/editions.schema.json +47 -0
  45. package/schema/encounter-evaluation.schema.json +109 -0
  46. package/schema/evaluation.schema.json +149 -0
  47. package/schema/fact-record.schema.json +30 -0
  48. package/schema/i18n-catalog.schema.json +47 -0
  49. package/schema/operations.schema.json +124 -0
  50. package/schema/rule2-departure-finding.schema.json +72 -0
  51. package/schema/rule2-departure-model.schema.json +159 -0
  52. package/schema/rules.schema.json +20 -90
  53. package/schema/situation-fixtures.schema.json +10 -146
  54. package/schema/situation.schema.json +72 -0
  55. package/schema/trace.schema.json +33 -0
  56. package/data/deprecated-identifiers.json +0 -7
  57. package/schema/deprecated-identifiers.schema.json +0 -29
@@ -0,0 +1,23 @@
1
+ {
2
+ "id": "intl@2016.es.boe",
3
+ "edition": "intl@2016",
4
+ "edition_status": "claimed",
5
+ "language": "es",
6
+ "source_id": "boe",
7
+ "tier": "national",
8
+ "normalization": "NFC",
9
+ "source": {
10
+ "publisher": "Agencia Estatal Boletín Oficial del Estado",
11
+ "title": "Instrumento de Adhesión de España al Convenio sobre el Reglamento internacional para prevenir los abordajes, 1972",
12
+ "url": "https://www.boe.es/",
13
+ "retrieved": null
14
+ },
15
+ "rights": {
16
+ "source_text": "BOE aviso legal: reuse permitted, commercial and non-commercial; consolidated texts must not be represented as official",
17
+ "redistribution_basis": "BOE aviso legal, with attribution",
18
+ "attribution": "Fuente de los datos: Agencia Estatal Boletín Oficial del Estado",
19
+ "package_licence": "Apache-2.0"
20
+ },
21
+ "note": "Stub: the BOE instrument id is not yet located; resolve it and the retrieval date before any text lands -- Q-7.",
22
+ "paragraphs": {}
23
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "id": "intl@2016.fi.finlex",
3
+ "edition": "intl@2016",
4
+ "edition_status": "claimed",
5
+ "language": "fi",
6
+ "source_id": "finlex",
7
+ "tier": "national",
8
+ "normalization": "NFC",
9
+ "source": {
10
+ "publisher": "Finlex (Oikeusministeriö), Suomen säädöskokoelman sopimussarja",
11
+ "title": "Asetus kansainvälisistä säännöistä yhteentörmäämisen ehkäisemiseksi merellä vuonna 1972 tehdyn yleissopimuksen voimaansaattamisesta, SopS 30/1977",
12
+ "url": "https://www.finlex.fi/fi/sopimukset/sopsteksti/1977/19770030",
13
+ "retrieved": "2026-09-12"
14
+ },
15
+ "rights": {
16
+ "source_text": "Tekijänoikeuslaki 404/1961 §9: no copyright in treaties and their official translations",
17
+ "redistribution_basis": "Finlex FAQ: published materials carry no usage restriction; open data CC BY 4.0",
18
+ "package_licence": "Apache-2.0"
19
+ },
20
+ "note": "Stub: identity and provenance only; the Finnish text is not transcribed yet. GATE-2 is adopted in docs/gates.json, settled by ADR 0013, so nothing structural blocks it.",
21
+ "paragraphs": {}
22
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "id": "us/inland@2014.en-US.ecfr",
3
+ "edition": "us/inland@2014",
4
+ "edition_status": "claimed",
5
+ "language": "en-US",
6
+ "source_id": "ecfr",
7
+ "tier": "national",
8
+ "normalization": "NFC",
9
+ "source": {
10
+ "publisher": "Office of the Federal Register, eCFR",
11
+ "title": "33 CFR Part 83, Navigation Rules (Inland)",
12
+ "url": "https://www.ecfr.gov/current/title-33/chapter-I/subchapter-E/part-83",
13
+ "retrieved": null
14
+ },
15
+ "rights": {
16
+ "source_text": "Work of the United States Government, not subject to copyright in the United States (17 U.S.C. 105)",
17
+ "redistribution_basis": "17 U.S.C. 105",
18
+ "package_licence": "Apache-2.0"
19
+ },
20
+ "note": "Stub: a second jurisdiction, so the key shape is exercised on both axes; the delta itself waits on Q-11 -- REQ-SCOPE-3.",
21
+ "paragraphs": {}
22
+ }
package/data/version.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "0.2.4"
2
+ "version": "0.3.1"
3
3
  }
@@ -1,8 +1,9 @@
1
1
  # ADR 0001 — Package name, and jurisdiction as a dimension
2
2
 
3
3
  Date: 2026-08-29
4
- Status: accepted; amended 2026-09-05 and 2026-09-09 (licence terms verified,
5
- see Amendments). CEVNI's text stays blocked; ADR 0010 unblocks modelling it.
4
+ Status: accepted; amended 2026-09-05, 2026-09-09 and 2026-09-12 (licence
5
+ terms verified, see Amendments). CEVNI's text stays blocked; ADR 0010
6
+ unblocks modelling it.
6
7
 
7
8
  ## Context
8
9
 
@@ -225,3 +226,42 @@ corpus candidates, previously unchecked. Q-6 (authenticity) verified
225
226
  `es` clears REQ-PROV-2; `fi` and `en`/`fr` via UNTS stay open. Updating
226
227
  `docs/requirements.md` Q-7 for this is left as a follow-up (concurrent
227
228
  edits to that file are in flight elsewhere).
229
+
230
+ ### 2026-09-12 — UNTS and Finlex resolved (issue #81, Q-7)
231
+
232
+ Re-ran both 2026-09-09 checks with a fetch that renders what looked like a
233
+ JS-only shell. Finlex turned out not to need a browser: its Next.js page
234
+ ships the article text inline in a React Server Components payload that a
235
+ plain fetch already receives, just not as visible HTML.
236
+
237
+ **Finlex (`fi`) — resolved clean.** §9, Tekijänoikeuslaki 404/1961: no
238
+ copyright in laws and decrees; in other documents enacted via the Statutes
239
+ Collection and in treaties and similar instruments containing international
240
+ obligations; in the decisions and statements of an authority or other public
241
+ body; or in official translations of the above. Finlex's own FAQ
242
+ (`finlex.fi/fi/ukk`, Ministry of Justice) states published materials carry
243
+ no usage restrictions, and machine-readable open data is licensed CC BY 4.0.
244
+ EFFI's contested claim concerns the compiled database, not the statute text
245
+ §9 already excludes; Finlex's current primary statement outranks that
246
+ secondary source.
247
+
248
+ **UNTS (`en`/`fr`) — resolved, and it does not clear.** Checked the UNTS
249
+ intro and FAQ pages on treaties.un.org and the copyright and terms-of-use
250
+ pages on un.org directly; none needed a browser. No UNTS-specific rights
251
+ page exists anywhere in that set — every path leads to the same general UN
252
+ terms already found blocking CEVNI: personal, non-commercial use, with no
253
+ right to resell, redistribute or create derivative works. Confirmed
254
+ blocked, not merely ambiguous.
255
+
256
+ Per Q-7's per-language routing, `es` (BOE, cleared 2026-09-09) is the
257
+ corpus that unblocks the first non-`intl` text; `fi` is now open alongside
258
+ it. UNTS stays closed pending written UN permission or a national
259
+ republication of the authentic `en`/`fr` text.
260
+
261
+ Sources: `fi` —
262
+ <https://www.finlex.fi/fi/laki/ajantasa/1961/19610404> (§9);
263
+ <https://www.finlex.fi/fi/ukk> (usage terms, CC BY 4.0). UNTS —
264
+ <https://treaties.un.org/Pages/Content.aspx?path=DB/UNTS/pageIntro_en.xml>;
265
+ <https://treaties.un.org/Pages/Overview.aspx?path=overview/faq/page1_en.xml>;
266
+ <https://www.un.org/en/about-us/copyright>;
267
+ <https://www.un.org/en/about-us/terms-of-use>.
@@ -79,8 +79,10 @@ with BCP 47 codes, structured as three layers:
79
79
  consolidated state*: the skeleton declares, as data, the amendment state
80
80
  it consolidates, and every corpus declares the amendment state its
81
81
  source reflects (REQ-LANG-10). A mismatch is legitimate but
82
- machine-visible — declared staleness, never silence. Historical states
83
- are prior package versions, not an in-data version dimension. A
82
+ machine-visible — declared staleness, never silence. (GATE-2 adopted in
83
+ ADR 0013: `data/editions.json` now registers editions in force
84
+ *concurrently*, so "historical states are prior package versions" holds
85
+ only for a fully superseded edition, not one still in transition.) A
84
86
  renumbering amendment is a major version under REQ-PKG-4 — but it is
85
87
  resolved by *issuing new paragraph paths and deprecating the old ones*
86
88
  (REQ-MODEL-10/11), never by repointing an existing path at different
@@ -204,11 +206,10 @@ check. What was declined, and why — recorded so it isn't re-argued:
204
206
  renumbering lands — or when Q-8's check of the first national
205
207
  amalgamation shows paragraph paths do not survive it.
206
208
  - **A full temporal/legal-version model** (instrument → edition → corpus as
207
- first-class layers; GATE-2). The package models current consolidated law;
208
- history lives in package versions. The cheap 80% — declared amendment
209
- state on skeleton and corpus, machine-visible mismatch — is adopted
210
- instead. If a jurisdiction ever requires multiple concurrent editions,
211
- that is a new ADR.
209
+ first-class layers; GATE-2). Declined *here*; the cheap 80% (declared
210
+ amendment state, machine-visible mismatch) was adopted instead. **Re-taken
211
+ and adopted in ADR 0013**, on the trigger this bullet named: a
212
+ jurisdiction requiring multiple concurrent editions.
212
213
  - **`dir: ltr|rtl` metadata per language.** Derivable from the language
213
214
  tag via CLDR by any consumer that needs it; storing it invites drift.
214
215
 
@@ -1,5 +1,7 @@
1
1
  # ADR 0006 — JSON Schema for structural validation, identifier diff for version discipline
2
2
 
3
+ The identifier-diff half is removed pre-1.0 by ADR 0015 (Solace, 2026-09-16); it returns, if at all, with the 1.0.0 tag.
4
+
3
5
  Date: 2026-09-04
4
6
  Status: accepted
5
7
 
@@ -167,7 +167,7 @@ resolution, and validation of the situation record.
167
167
  | `own` required, `other`/`Subject.fact` per colregs 0.2.0's fixture schema | ✎ | revised 2026-09-07 from "own/other both required"; Mark to confirm before ink |
168
168
  | `appliedEncounterEntries` as the fixture-replay companion | ✎ | the situation-fixture replay being written |
169
169
  | Field names snake_case with unit suffixes across both ADRs; `EntryId`/`ParagraphCite` alias `string` for ids and cites | ✎ | the rename's alias window closing; a consumer arguing the compiler should enforce the two apart |
170
- | `EncounterEvaluation` field set (§4) | ✎ | building it; Q-35, Q-36, Q-43 in colregs |
170
+ | `EncounterEvaluation` field set (§4); `categories` and `provenance` added 2026-09-16 beyond the block above, as `DisplayEvaluation` carries them — colregs-engine 0.1.5 built them and ADR 0014's `encounter-evaluation.schema.json` is now the shape | ✎ | building it; Q-35, Q-36, Q-43 in colregs |
171
171
  | `encounter` absent vs the ADR 0005 §5 status alphabet | ✎ | Q-43 |
172
172
  | `conduct` is a separate package, not a third verb | ✎ | superseded by ADR 0012: a third and fourth verb, in this package |
173
173
  | Geometry-consistency validation (REQ-VERIFY-8) in the engine's validator | ? | deciding whether it is data-suite-only |
@@ -183,9 +183,8 @@ trace becomes a fixture; a certified grid, a `Rule2DepartureModel`.
183
183
  2. `Trace`, `appliedConductEntries` against those fixtures — phase 3
184
184
  starts here.
185
185
  3. `evaluateConduct` with verdicts and phases; the constants above.
186
- 4. `evaluateRule2Departure` once a grid exists. Until then the name is
187
- reserved and nothing exported: a stub answering `inconclusive-in-model`
188
- is a stub wearing a status.
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**
189
188
 
190
189
  ## Register
191
190
 
@@ -200,4 +199,4 @@ trace becomes a fixture; a certified grid, a `Rule2DepartureModel`.
200
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 |
201
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 |
202
201
  | No *situation* input names a departure; the grid does, and is named in every finding | ✎ | — |
203
- | Nothing exported until a fixture backs it; exports then carry TSDoc's `@beta` release tag | ✎ | — |
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).
@@ -0,0 +1,103 @@
1
+ # ADR 0014 — The engine interface is colregs' to own: an operations manifest and result schemas
2
+
3
+ Date: 2026-09-16
4
+ Status: proposed. Solace ordered options 2 and 3 built on 2026-09-16; the
5
+ register marks what settles each row.
6
+
7
+ ## Context
8
+
9
+ ADR 0011 and ADR 0012 fix the engine's verbs and result envelopes, in prose
10
+ and TypeScript blocks, in this repository. The only machine-readable copy
11
+ lives in colregs-engine's `src/types.ts`, so the interface is de facto
12
+ colregs-owned and de jure the engine's: a second engine, in any language,
13
+ would transcribe the ADRs by hand and drift the way the engine's own
14
+ generator was written to stop.
15
+
16
+ colregs already owns *shapes* in production: the engine compiles every
17
+ `schema/*.schema.json` into generated types and derives `FactRecord` and
18
+ `Situation` from `data/facts.json`. What it does not own is the *operations*
19
+ — which verb reads which input and answers which envelope — and the fixture
20
+ files bind to verbs only by convention.
21
+
22
+ Ten routes were surveyed on 2026-09-16 for making the interface a colregs
23
+ artefact, the Java sense of interface against implementation:
24
+
25
+ | # | route | what it gives, what it costs |
26
+ |---|---|---|
27
+ | 1 | result schemas only, verbs stay prose | shapes checkable, operations still hand-read |
28
+ | 2 | **operations manifest**: verb → inputs → result → companion, in JSON | language-neutral; any engine derives its own binding |
29
+ | 3 | **fixtures bound to verbs** in the manifest | the conformance replay becomes the behavioural contract |
30
+ | 4 | hand-written `.d.ts` in colregs | TypeScript-only; the transcription the engine's generator exists to avoid |
31
+ | 5 | `x-operation` keywords inside the existing schemas | option 2 spread over eleven files |
32
+ | 6 | OpenAPI / AsyncAPI | a projection generable *from* 2, not a source |
33
+ | 7 | Protobuf / Smithy | model operations natively; a toolchain a data-only package does not have |
34
+ | 8 | TypeSpec | one source projecting JSON Schema and OpenAPI; same toolchain cost |
35
+ | 9 | a separate `colregs-api` package | a third release train for two files |
36
+ | 10 | status quo | the drift above |
37
+
38
+ ## Decision
39
+
40
+ 1. **`data/operations.json` is the interface.** One operation per verb ADR
41
+ 0011 and ADR 0012 name, each with positional `inputs` (name and schema),
42
+ an `output` schema, the entry-id `companion` verb where one exists, and
43
+ the `fixtures` that exercise it. `schema/operations.schema.json` checks
44
+ its shape; `test/data.test.mjs` checks that every reference resolves.
45
+ 2. **Result envelopes are schemas under `schema/`:** `display-evaluation`,
46
+ `encounter-evaluation`, `conduct-evaluation`, `rule2-departure-finding`,
47
+ transcribed from the engine's `src/types.ts` at colregs-engine 0.1.5.
48
+ JSON-Schema-expressible only: `Record<EntryId, Modality>` becomes
49
+ `patternProperties`, the `EntryId` and `ParagraphCite` aliases become
50
+ `$defs`, the two deprecated camelCase aliases are marked `deprecated` and
51
+ optional. From here the schema is the normative statement of each
52
+ envelope; the TypeScript blocks in ADR 0011 §4 and ADR 0012 §2–4 are
53
+ illustrations of record, and an envelope changes here first, the engine
54
+ second.
55
+ 3. **Inputs get schemas too:** `fact-record`, `situation`, `trace`,
56
+ `rule2-departure-model`. The two fixture schemas `$ref` the first two
57
+ for a case's `facts` and `situation` instead of carrying a copy; a fixture
58
+ case is the verb's input, and the tests validate every bound case against
59
+ it.
60
+ 4. **Schema files compose by `$ref`,** relative to their `$id`
61
+ (`applicability.schema.json#/$defs/ruleId`). ADR 0006's "no cross-file
62
+ references" is about data references — cite to `rules.json` — which stay in
63
+ the tests; a `$ref` between two schema files is one shape reused, not a
64
+ data reference. The suite registers every schema by `$id` before compiling.
65
+ What every envelope shares — the `colregs` stamp, `provenance`, the
66
+ `ruleId` and `paragraphCite` vocabularies — is `schema/evaluation.schema.json`,
67
+ `$defs` only, so moving one later is never a two-repository change.
68
+ 5. **A fixture file is bound to a verb** by `fixtures[].file` and
69
+ `case_inputs`, one case key per positional input, with the answer under
70
+ `expect`: the companion's entry ids unless the binding names another
71
+ schema. A case's `status` and `jurisdiction` are the fixture file's own.
72
+ Every file under `fixtures/` must be bound; `evaluateConduct` and
73
+ `evaluateRule2Departure` bind nothing yet, and say so with an empty list.
74
+ 6. **Not in the manifest:** trailing options a binding accepts (`opts.data`),
75
+ which verbs are built, and what a verb throws. Build status is ADR 0011's
76
+ and ADR 0012's tables; errors are the binding's.
77
+
78
+ ## Consequences
79
+
80
+ - Ten new files under `schema/`. The engine's `generate-schema-types.ts`
81
+ throws on any schema its `ROOT_NAMES` does not list, so the next colregs
82
+ bump fails its build until colregs-engine#86 lands: emit
83
+ `interface ColregsEngine` from `operations.json`, `satisfies ColregsEngine`
84
+ at the engine's root, settle the `fact-record.ts` / `situation.ts`
85
+ collision with the types it derives from `facts.json` today, and validate
86
+ real output against these schemas for every fixture case.
87
+ - A YAML rendering of the manifest is lossless; XML is a generated view (JSON
88
+ Schema to XSD). Authoring the interface in TypeScript first is the one route
89
+ that forecloses both.
90
+ - The 2026-09-16 hand-off named the finding's schema `rule2-departure`; it
91
+ lands as `rule2-departure-finding` beside `rule2-departure-model`, since
92
+ the verb has two shapes to name.
93
+
94
+ ## Register
95
+
96
+ | item | level | what would settle it |
97
+ |---|---|---|
98
+ | Options 2 and 3, not 1 or 4–10 | ✎ | this ADR accepted |
99
+ | Manifest shape: positional `inputs`, `output`, `companion`, `fixtures[].case_inputs` and `expect` | ✎ | the engine generator consuming it; the first conduct or Rule 2 fixture |
100
+ | Envelope schemas transcribed from `types.ts`, envelope changes land here first | ✎ | the first envelope change after the follow-up |
101
+ | Cross-file `$ref` between schema files | ✎ | a consumer whose validator cannot register a schema set |
102
+ | Deprecated aliases optional in the schema, required in the engine | ✎ | the engine dropping them |
103
+ | `opts` and thrown errors stay out of the manifest | ✎ | a second binding needing either |
@@ -0,0 +1,191 @@
1
+ # ADR 0015 — Rule ids are paragraph keys in the `rule:` namespace
2
+
3
+ Date: 2026-09-16
4
+ Status: accepted — Solace's ruling, 2026-09-16
5
+
6
+ ## Context
7
+
8
+ Issue #121 asked what an applicability entry should be called, and ruling C
9
+ was read as *names, not citations*. The first draft of this ADR wrote that
10
+ reading out: every id became a coined name in an `entry:` namespace —
11
+ `entry:anchored`, `entry:towing_mastheads_long_tow`, `entry:being_overtaken`.
12
+ It never landed and never shipped. Solace superseded that reading on
13
+ 2026-09-16 and this file is the replacement.
14
+
15
+ What the rename was buying was insulation: `cite` is a data field, revised
16
+ whenever the package reads the Rules better, and `14a` had already become
17
+ `14b` when the reading of *which paragraph deems a head-on* moved. An
18
+ identifier containing a data field gets renamed every time the field is.
19
+
20
+ What it cost is the vocabulary. An entry is a norm out of the Convention,
21
+ and the people who read these ids — and the consumers that store them
22
+ alongside a `cite` anyway — already have a name for every norm in the
23
+ package: the Rule. `rule:13b` is *Rule 13(b)* in the mariner's mouth. A
24
+ coined name is a second vocabulary beside that one, invented here,
25
+ unguessable from the Rules, and needing a lookup in this file to be read
26
+ back as the paragraph it came from. The insulation is worth one rename in
27
+ a pre-1.0 package; the second vocabulary is permanent. Ruled by Solace,
28
+ 2026-09-16.
29
+
30
+ ## Decision
31
+
32
+ 1. **The namespace noun is `rule:`.** It replaces `entry:` everywhere. The
33
+ overload with the Convention's Rules is the point, not an accident to
34
+ be designed around.
35
+ 2. **An id is `rule:<paragraph-slug>`.** The slug is the cite with its
36
+ punctuation dropped: rule number, paragraph letter attached, each roman
37
+ subparagraph joined by `_`. `27(a)(i)` → `27a_i`; `13(b)` → `13b`;
38
+ `11` → `11`; `23(a)(iii)-(iv)` → `23a_iii_iv`.
39
+ 3. **A bare id is the paragraph's principal norm.** Where the text yields
40
+ a further norm from the same paragraph, it takes a third segment after
41
+ a colon, named in the text's own words: `rule:24a_i` is 24(a)(i)'s two
42
+ masthead lights and `rule:24a_i:exceeds_200m` its three, the threshold
43
+ being the Convention's own, which is when a number may appear in a name.
44
+ 4. **Where one sentence fuses a deeming test and a duty, both norms are
45
+ named.** 15(a) gives `rule:15a:crossing` — the classification, effect
46
+ `encounter: crossing` — and `rule:15a:keep_out_of_the_way` — the
47
+ precedence, own give-way. `avoid_crossing_ahead` is reserved for 15(a)'s
48
+ second duty when the `conduct` shape exists; it is not in use.
49
+ 5. **A Convention subparagraph keeps its own cite.** 30(d)'s chapeau is
50
+ `rule:30d`, citing `30(d)`, importing the 30(a)/(b) anchor lights,
51
+ modality `shall`; the two all-round red lights are `rule:30d_i`, citing
52
+ `30(d)(i)`, modality `shall-if-practicable` because the paragraph says
53
+ "if practicable". `30(d)(i)` joins `rules.json` as a skeleton path with
54
+ this change. `rule:30d_ii` is reserved for the three balls, and lands
55
+ when day shapes do.
56
+ 6. **A jurisdiction override is named by its difference**, and jurisdiction
57
+ stays a field: `rule:30a:mooring_buoy` and `rule:30b:mooring_buoy`, both
58
+ `us/inland`. The name says what 33 CFR 90.5 adds, not who adds it.
59
+ 7. **13(b) is one symmetric entry.** The two entries that preceded it were
60
+ identical but for which subject's `geo:rel_bearing_deg` fell in
61
+ (112.5, 247.5), and both produced `encounter: overtaking`. `rule:13b`
62
+ reads the same sector object on either subject under `any_of` — the
63
+ pattern 13(d) already used over `hist:was_overtaking` — because the
64
+ encounter type belongs to the pair. One paragraph, one norm, one id.
65
+ 8. **Every other entry with a unique cite takes the bare slug of it.**
66
+ 9. **`represented_paragraphs` ids take the prefix too** — `2a` becomes
67
+ `rule:2a` — and keep their own list. They no longer need a schema
68
+ definition of their own: the id pattern is the same one.
69
+ 10. **The schema pattern is**
70
+ `^rule:[0-9]+[a-z]?(_[ivx]+)*(:[a-z][a-z0-9_]*)?$`.
71
+ **An id is opaque to a consumer.** It looks like a citation and is not
72
+ one: a consumer that wants the paragraph reads `cite`, and never splits
73
+ the id to find it. The resemblance is for the human reading a trace;
74
+ the `cite` field is the machine-readable link to the Convention, and it
75
+ is the field that moves when the package's reading of a paragraph moves.
76
+ 11. **The identifier-diff mechanism is removed, not left dormant.**
77
+ `retired_entry_ids`, `data/deprecated-identifiers.json`, its schema, the
78
+ dormant diff test and the retired-id half of the REQ-MODEL-10 collision
79
+ test all go. Nothing pre-1.0 is immutable, so there was nothing for the
80
+ registry to hold and no diff for the test to run; a mechanism that
81
+ cannot fire is a mechanism nobody maintains. It returns, if at all, with
82
+ the 1.0.0 tag that REQ-MODEL-10's baseline names. Ruled by Solace,
83
+ 2026-09-16.
84
+
85
+ ## The names
86
+
87
+ `was` is the `entry:` name from the superseded draft of this ADR, which
88
+ shipped in no release. The citation-derived ids that preceded *it* —
89
+ `24a-m2`, `23a1`, `30d-red` — were never names either and are not carried
90
+ forward.
91
+
92
+ | was | is | cite |
93
+ |---|---|---|
94
+ | `entry:power_forward_masthead` | `rule:23a_i` | 23(a)(i) |
95
+ | `entry:power_second_masthead` | `rule:23a_ii` | 23(a)(ii) |
96
+ | `entry:power_sidelights_sternlight` | `rule:23a_iii_iv` | 23(a)(iii)-(iv) |
97
+ | `entry:air_cushion_non_displacement` | `rule:23b` | 23(b) |
98
+ | `entry:wig_near_surface` | `rule:23c` | 23(c) |
99
+ | `entry:power_under_12m` | `rule:23d_i` | 23(d)(i) |
100
+ | `entry:power_under_7m_slow` | `rule:23d_ii` | 23(d)(ii) |
101
+ | `entry:towing_mastheads` | `rule:24a_i` | 24(a)(i) |
102
+ | `entry:towing_mastheads_long_tow` | `rule:24a_i:exceeds_200m` | 24(a)(i) |
103
+ | `entry:towing_light` | `rule:24a_ii_iv` | 24(a)(ii)-(iv) |
104
+ | `entry:pushing_composite_unit` | `rule:24b` | 24(b) |
105
+ | `entry:pushing_ahead_or_alongside` | `rule:24c` | 24(c) |
106
+ | `entry:being_towed` | `rule:24e` | 24(e) |
107
+ | `entry:sail_sidelights_sternlight` | `rule:25a` | 25(a) |
108
+ | `entry:sail_combined_lantern` | `rule:25b` | 25(b) |
109
+ | `entry:sail_red_over_green` | `rule:25c` | 25(c) |
110
+ | `entry:sail_under_7m` | `rule:25d_i` | 25(d)(i) |
111
+ | `entry:under_oars` | `rule:25d_ii` | 25(d)(ii) |
112
+ | `entry:trawling_green_over_white` | `rule:26b_i` | 26(b)(i) |
113
+ | `entry:trawling_masthead` | `rule:26b_ii` | 26(b)(ii) |
114
+ | `entry:trawling_making_way` | `rule:26b_iii` | 26(b)(iii) |
115
+ | `entry:fishing_red_over_white` | `rule:26c_i` | 26(c)(i) |
116
+ | `entry:fishing_outlying_gear` | `rule:26c_ii` | 26(c)(ii) |
117
+ | `entry:fishing_making_way` | `rule:26c_iii` | 26(c)(iii) |
118
+ | `entry:nuc_red_over_red` | `rule:27a_i` | 27(a)(i) |
119
+ | `entry:nuc_making_way` | `rule:27a_iii` | 27(a)(iii) |
120
+ | `entry:ram_red_white_red` | `rule:27b_i` | 27(b)(i) |
121
+ | `entry:ram_making_way` | `rule:27b_iii` | 27(b)(iii) |
122
+ | `entry:ram_anchored` | `rule:27b_iv` | 27(b)(iv) |
123
+ | `entry:ram_underwater_obstruction` | `rule:27d` | 27(d) |
124
+ | `entry:diving` | `rule:27e_i` | 27(e)(i) |
125
+ | `entry:mine_clearance` | `rule:27f` | 27(f) |
126
+ | `entry:cbd` | `rule:28` | 28 |
127
+ | `entry:pilot` | `rule:29a` | 29(a) |
128
+ | `entry:anchored` | `rule:30a` | 30(a) |
129
+ | `entry:anchored_under_50m` | `rule:30b` | 30(b) |
130
+ | `entry:anchored_deck_lights` | `rule:30c` | 30(c) |
131
+ | `entry:aground_anchor_lights` | `rule:30d` | 30(d) |
132
+ | `entry:aground_red_over_red` | `rule:30d_i` | 30(d)(i) |
133
+ | `entry:anchored_under_7m_not_near_channel` | `rule:30e` | 30(e) |
134
+ | `entry:any_visibility` | `rule:4` | 4 |
135
+ | `entry:in_sight` | `rule:11` | 11 |
136
+ | `entry:restricted_visibility` | `rule:19a` | 19(a) |
137
+ | `entry:power_gives_way_to_nuc` | `rule:18a_i` | 18(a)(i) |
138
+ | `entry:power_gives_way_to_ram` | `rule:18a_ii` | 18(a)(ii) |
139
+ | `entry:power_gives_way_to_fishing` | `rule:18a_iii` | 18(a)(iii) |
140
+ | `entry:power_gives_way_to_sail` | `rule:18a_iv` | 18(a)(iv) |
141
+ | `entry:sail_gives_way_to_nuc` | `rule:18b_i` | 18(b)(i) |
142
+ | `entry:sail_gives_way_to_ram` | `rule:18b_ii` | 18(b)(ii) |
143
+ | `entry:sail_gives_way_to_fishing` | `rule:18b_iii` | 18(b)(iii) |
144
+ | `entry:fishing_gives_way_to_nuc` | `rule:18c_i` | 18(c)(i) |
145
+ | `entry:fishing_gives_way_to_ram` | `rule:18c_ii` | 18(c)(ii) |
146
+ | `entry:avoid_impeding_cbd` | `rule:18d_i` | 18(d)(i) |
147
+ | `entry:wig_keeps_well_clear` | `rule:18f_i` | 18(f)(i) |
148
+ | `entry:not_impeded_remains_obliged` | `rule:8f_iii` | 8(f)(iii) |
149
+ | `entry:overtaking_gives_way` | `rule:13a` | 13(a) |
150
+ | `entry:narrow_channel_small_or_sail` | `rule:9b` | 9(b) |
151
+ | `entry:narrow_channel_fishing` | `rule:9c` | 9(c) |
152
+ | `entry:traffic_lane_fishing` | `rule:10i` | 10(i) |
153
+ | `entry:traffic_lane_small_or_sail` | `rule:10j` | 10(j) |
154
+ | `entry:steady_bearing` | `rule:7d_i` | 7(d)(i) |
155
+ | `entry:sail_port_tack_gives_way` | `rule:12a_i` | 12(a)(i) |
156
+ | `entry:sail_windward_gives_way` | `rule:12a_ii` | 12(a)(ii) |
157
+ | `entry:sail_port_tack_uncertain_gives_way` | `rule:12a_iii` | 12(a)(iii) |
158
+ | `entry:overtaking` | `rule:13b` | 13(b) |
159
+ | `entry:being_overtaken` | `rule:13b` | 13(b) |
160
+ | `entry:overtaking_until_past_and_clear` | `rule:13d` | 13(d) |
161
+ | `entry:head_on` | `rule:14b` | 14(b) |
162
+ | `entry:crossing` | `rule:15a:crossing` | 15(a) |
163
+ | `entry:crossing_gives_way` | `rule:15a:keep_out_of_the_way` | 15(a) |
164
+ | `entry:mooring_buoy` | `rule:30a:mooring_buoy` | 30(a) |
165
+ | `entry:mooring_buoy_under_50m` | `rule:30b:mooring_buoy` | 30(b) |
166
+
167
+ ## Consequences
168
+
169
+ - One pass, one commit: `data/applicability.json`, both fixture files,
170
+ `data/geometry.json`, `data/images.json`, `data/facts.json`,
171
+ `data/rules.json`, the schemas, the suite and the living docs. The
172
+ schema `$defs` that named the type follow the noun: `entryId` is
173
+ `ruleId`, `entryIdList` is `ruleIdList`.
174
+ - Two entries may still share a cite — `rule:24a_i` and
175
+ `rule:24a_i:exceeds_200m`, the two halves of 15(a), the two mooring-buoy
176
+ deltas. What they never share is an id, and the third segment says which
177
+ norm out of the paragraph the entry carries.
178
+ - Earlier ADRs and closed questions cite entries by the superseded names;
179
+ the table above is the map and stays in this file permanently. ADR 0006
180
+ carries a dated note that its identifier-diff half is gone.
181
+ - `represented_paragraphs` is now a second list of records that look like
182
+ entries, differing only in carrying no `when`. Folding it into `entries`
183
+ with a `care`/`meta` category is the obvious follow-up and is not done
184
+ here.
185
+ - Open, and not ruled: whether `fact:rule18_class` should be renamed. It
186
+ carries a rule number inside a fact name, which is the shape this ADR
187
+ moved *entry* ids towards and says nothing about for facts. Left visibly
188
+ open.
189
+ - An encounter with more than two vessels is issue #135; nothing in this
190
+ scheme is aimed at it, and a third subject would need a segment none of
191
+ these ids have.