colregs 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +13 -11
  2. package/data/applicability.json +478 -503
  3. package/data/facts.json +22 -22
  4. package/data/geometry.json +8 -8
  5. package/data/i18n/en.json +27 -0
  6. package/data/i18n/fi.json +25 -0
  7. package/data/images.json +32 -32
  8. package/data/operations.json +79 -0
  9. package/data/rules.json +5 -0
  10. package/data/version.json +1 -1
  11. package/docs/adr/0006-json-schema-and-identifier-diff.md +2 -0
  12. package/docs/adr/0011-api-shape.md +7 -7
  13. package/docs/adr/0012-trace-and-rule2-departure-api.md +2 -2
  14. package/docs/adr/0014-engine-interface-owned-by-colregs.md +103 -0
  15. package/docs/adr/0015-rule-ids-are-paragraph-keys.md +191 -0
  16. package/docs/adr/0016-encounter-roles-are-pooled-across-frames.md +82 -0
  17. package/docs/adr/0017-closed-vocabularies-are-prefixed-identifiers.md +105 -0
  18. package/docs/budgets.json +8 -15
  19. package/docs/decisions.md +6 -0
  20. package/docs/identifiers.md +121 -109
  21. package/docs/maritime-sources.md +58 -0
  22. package/docs/normative-language.md +103 -0
  23. package/docs/part-b-invariants.md +48 -45
  24. package/docs/requirements.md +147 -138
  25. package/fixtures/applicability-fixtures.json +190 -190
  26. package/fixtures/situation-fixtures.json +870 -599
  27. package/package.json +1 -1
  28. package/schema/applicability-fixtures.schema.json +4 -13
  29. package/schema/applicability.schema.json +77 -77
  30. package/schema/conduct-evaluation.schema.json +135 -0
  31. package/schema/display-evaluation.schema.json +146 -0
  32. package/schema/encounter-evaluation.schema.json +109 -0
  33. package/schema/evaluation.schema.json +149 -0
  34. package/schema/fact-record.schema.json +30 -0
  35. package/schema/facts.schema.json +4 -4
  36. package/schema/i18n-catalog.schema.json +47 -0
  37. package/schema/operations.schema.json +124 -0
  38. package/schema/rule2-departure-finding.schema.json +72 -0
  39. package/schema/rule2-departure-model.schema.json +159 -0
  40. package/schema/situation-fixtures.schema.json +30 -127
  41. package/schema/situation.schema.json +72 -0
  42. package/schema/trace.schema.json +33 -0
  43. package/data/deprecated-identifiers.json +0 -7
  44. package/schema/deprecated-identifiers.schema.json +0 -29
@@ -49,7 +49,7 @@ that is the fixture contract: both fixture files `expect` entry ids.
49
49
 
50
50
  ### 2. `FactRecord` keeps its name
51
51
 
52
- It is colregs' name for the per-vessel record (`own.fact` is "exactly the
52
+ It is colregs' name for the per-vessel record (`self.fact` is "exactly the
53
53
  record above, key for key"). ADR 0005 §2 has the situation wrap two fact
54
54
  records; the fact record itself does not widen, and a display consumer never
55
55
  sees a situation. A name that hinted at two vessels would describe the wrapper,
@@ -59,13 +59,13 @@ not the thing.
59
59
 
60
60
  colregs states the situation twice: nested by subject and class in
61
61
  `facts.json` §`situation.record` and in every fixture case, and flat as
62
- `own:fact:activity` inside predicates. The engine's public type is the
62
+ `self:fact:activity` inside predicates. The engine's public type is the
63
63
  **nested** form. The flat form is the predicate namespace and stays internal
64
64
  to the walker.
65
65
 
66
66
  ```ts
67
67
  interface Situation {
68
- own: Subject;
68
+ self: Subject;
69
69
  other?: Subject;
70
70
  pair?: Pair;
71
71
  }
@@ -73,7 +73,7 @@ interface Subject { fact: FactRecord; kin?: Kinematics; geo?: DirectionalGeometr
73
73
  interface Pair { geo?: PairGeometry; env?: Environment; }
74
74
  ```
75
75
 
76
- - `own` is required; `other`/`Subject.fact` follow colregs' own fixture schema
76
+ - `self` is required; `other`/`Subject.fact` follow colregs' own fixture schema
77
77
  (`situation-fixtures.schema.json`, `0.2.0`) — `other` optional (Rule 19's
78
78
  single-vessel scope needs no synthesized one), `fact` required. Every other
79
79
  class, and every key inside a class, is optional — absent is absent.
@@ -98,7 +98,7 @@ interface EncounterEvaluation {
98
98
  scope: EntryId[];
99
99
  encounter?: 'head-on' | 'crossing' | 'overtaking' | 'none';
100
100
  risk_of_collision: { asserted: boolean; by: EntryId[] };
101
- roles: { own: SubjectRole[]; other: SubjectRole[] };
101
+ roles: { self: SubjectRole[]; other: SubjectRole[] };
102
102
  overridden: { id: EntryId; by: EntryId }[];
103
103
  modalities: Record<EntryId, Modality>;
104
104
  }
@@ -164,10 +164,10 @@ resolution, and validation of the situation record.
164
164
  | `FactRecord` keeps its name | ink | — |
165
165
  | `Situation` nested by subject and class, generated from `facts.json` | ink | — |
166
166
  | Verb name `evaluateEncounter`; result name `EncounterEvaluation` | ✎ | colregs renaming the `pair` subject or the `encounter` effect |
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 |
167
+ | `self` 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; `roles` is the pooled two-frame read, ADR 0016 | ✎ | 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 |
@@ -81,12 +81,12 @@ interface ConductEvaluation {
81
81
  phases: ConductPhaseChange[];
82
82
  }
83
83
  interface ConductVerdict {
84
- id: EntryId; subject: 'own' | 'other';
84
+ id: EntryId; subject: 'self' | 'other';
85
85
  verdict: 'kept' | 'breached' | 'pending';
86
86
  attached_at_s?: number; decided_at_s?: number;
87
87
  robustness?: { value: number; unit: string };
88
88
  }
89
- interface ConductPhaseChange { subject: 'own' | 'other'; phase: ParagraphCite; at_s: number; }
89
+ interface ConductPhaseChange { subject: 'self' | 'other'; phase: ParagraphCite; at_s: number; }
90
90
  ```
91
91
 
92
92
  - One **verdict** per applied conduct entry per subject it attached to; an
@@ -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.
@@ -0,0 +1,82 @@
1
+ # ADR 0016 — An encounter's roles are read from both frames, pooled, then resolved
2
+
3
+ Date: 2026-09-16
4
+ Status: accepted — Solace's ruling on #141, 2026-09-16.
5
+
6
+ ## Context
7
+
8
+ Every `precedence` entry in `data/applicability.json` is written from the
9
+ duty-holder's seat: `effect.self` is one of `give-way`, `shall-not-impede`,
10
+ `keep-clear`, `none`, and `effect.other` is `stand-on` or `none`. No entry
11
+ names self stand-on. That is faithful to the text — a paragraph addresses the
12
+ vessel it binds, and the other vessel's stand-on is Rule 17's inference —
13
+ and it is why entry `rule:15a:keep_out_of_the_way` gates self's bearing on the
14
+ starboard half only.
15
+
16
+ ADR 0011 §4 defines `EncounterEvaluation.roles` as a role set for *both*
17
+ subjects: what everyone is to do. Nothing between the two said how an engine
18
+ gets from a one-seat table to a two-seat answer. colregs-engine read the
19
+ situation once, from self's seat, and passed every fixture doing it, because
20
+ `fixtures/situation-fixtures.json` expects one-seat entry ids.
21
+
22
+ Measured on 2026-09-16 (#141), with colregs' own matcher, one-seat against
23
+ pooled:
24
+
25
+ | encounter | one seat tells self | pooled says self is |
26
+ |---|---|---|
27
+ | crossing, other on self's port bow | nothing | stand-on |
28
+ | self not under command, ordinary power vessel to starboard | give-way (15(a)) | stand-on (18(a)(i)) |
29
+ | self power-driven, a sailing vessel overtaking her | give-way (18(a)(iv)) | stand-on (13(a)) |
30
+
31
+ The second and third are not gaps but wrong answers: both vessels give-way
32
+ at once. The cause is that every `rel:overrides` edge of the Q-40 family —
33
+ `rule:18a_i` → `rule:15a:keep_out_of_the_way`, `rule:13a` → `rule:18a_iv`,
34
+ `rule:9c` → `rule:18a_iii` and the rest — has its source in one vessel's
35
+ seat and its target in the other's. A one-seat reader never sees the source,
36
+ so the target stands. The suite has pooled both seats since ADR 0005 §4
37
+ (`pooledRoles` in `test/data.test.mjs`); with the swap removed it fails.
38
+
39
+ Two repairs were on the table: reciprocal entries (self stand-on, other
40
+ give-way, gates mirrored), or the pooled read stated as the contract.
41
+
42
+ ## Decision
43
+
44
+ 1. **`evaluateEncounter` reads two frames.** The situation as given, and its
45
+ swap (`self` and `other` exchanged, `pair` unchanged), are both matched
46
+ against every non-`display` entry. Derived facts are computed per frame.
47
+ 2. **Precedence entries are pooled, then resolved.** The precedence entries
48
+ that apply in either frame form one pool, keyed by which vessel each fired
49
+ for. `rel:overrides` is resolved over the pool, so an override may reach
50
+ an entry that fired in the other frame. Only then are roles read.
51
+ 3. **`roles.self` and `roles.other` come from the pool.** A vessel's roles are
52
+ every non-`none` role a surviving forceful entry lays on her, from
53
+ `effect.self` where she was the frame's self and from `effect.other` where
54
+ she was the frame's other; `by` names the entry either way.
55
+ 4. **`applied`, `scope`, `encounter`, `risk_of_collision`, `modalities` and
56
+ `categories` stay self-frame.** They answer the fixture's entry-id `expect`
57
+ as they do today, and a classification is the pair's already.
58
+ 5. **A situation fixture may state `roles`.** `roles: {self: [{role, by}],
59
+ other: [{role, by}]}` is the pooled, resolved answer for both subjects and
60
+ binds an engine to it; `expect` stays the self-frame entry ids.
61
+ 6. **No reciprocal entries.** They would double the table and every override
62
+ edge, make `effect.other` redundant, and still need the override to reach
63
+ across the pair.
64
+
65
+ ## Consequences
66
+
67
+ - Four binding cases in `fixtures/situation-fixtures.json` carry `roles`:
68
+ the crossing as written and the three encounters in the table above.
69
+ - `INV-PB-roles-exclusive` and `INV-PB-one-role-source` are stated over the
70
+ pooled read, as the suite already asserts them.
71
+ - colregs-engine's `evaluateEncounter` changes to match; colregs-engine#85
72
+ (the 13(d) latch ordering) is independent of this.
73
+ - 8(f)(iii)'s `none`/`none` and Q-35 are untouched: pooling adds no role,
74
+ it only lets every entry see the entries it was written to override.
75
+
76
+ ## Register
77
+
78
+ | item | level | what would settle it |
79
+ |---|---|---|
80
+ | Two frames, pooled, resolved, then roles — not reciprocal entries | ink | Solace, 2026-09-16 |
81
+ | Self-frame `applied`; pooled `roles` only | ✎ | a consumer needing the other frame's applied ids |
82
+ | Fixture `roles` as `{role, by}` per subject | ✎ | the engine's conformance replay consuming it |
@@ -0,0 +1,105 @@
1
+ # ADR 0017 — Closed vocabularies are prefixed identifiers
2
+
3
+ Date: 2026-09-16
4
+ Status: accepted — Solace's ruling, 2026-09-16
5
+
6
+ ## Context
7
+
8
+ Issue #120 built a display catalog for the light, modality, role, encounter
9
+ and jurisdiction vocabularies and, in doing so, named a fiction in
10
+ `docs/identifiers.md`: "What is not an identifier" declared modality, role,
11
+ jurisdiction and encounter values outside `REQ-MODEL-10`'s identifier space,
12
+ on the theory that a closed vocabulary is a different kind of thing from a
13
+ name. The carve-out never reduced what a rename costs. Every one of these
14
+ values is emitted by the engine (`DisplayEvaluation.modality`,
15
+ `EncounterEvaluation.roles[].role`, `.encounter`, `provenance.jurisdictions`,
16
+ `evaluated_categories`) and compared by a consumer the same way an entry id
17
+ or a fact value is — `if (role === 'give-way')` breaks on a rename exactly
18
+ as `if (activity === 'nuc')` would. Declining to promise stability does not
19
+ make the rename cheaper; it only leaves the promise unwritten.
20
+
21
+ The collision that follows from treating them as a separate kind is not
22
+ hypothetical. It has already happened, in the data on `main`:
23
+
24
+ | string | is a … | and also a … |
25
+ |---|---|---|
26
+ | `shall-not-impede` | modality (`modalities`) | role (`effects.roles`) |
27
+ | `none` | role (`effects.roles`) | encounter (`effects.encounters`) |
28
+
29
+ This is the identical shape as `towing`, the collision ADR 0001's `light:`
30
+ prefix and `activity:` axis already resolved: a consumer holding the bare
31
+ string cannot say which field it came out of. The rule already on file —
32
+ "the prefix makes the namespace part of the identifier, which resolves that
33
+ collision by construction rather than by convention" — already applies here;
34
+ it was only not applied.
35
+
36
+ Issue #120's own draft, before this ruling, reached the opposite
37
+ recommendation on its Q2: that prefixing sections into identifiers "later"
38
+ costs the same as doing it now. That held only if the bare strings were not
39
+ identifiers. They are — held and compared by consumers — so the option runs
40
+ one way: pre-1.0, prefixing is a data-layer, one-PR change; post-1.0 it is
41
+ an identifier-layer change REQ-MODEL-10 forbids outright, leaving only the
42
+ two-names-forever shape `docs/identifiers.md` already rejected for
43
+ `fact:own_activity`. Ruled by Solace, 2026-09-16.
44
+
45
+ ## Decision
46
+
47
+ 1. **Four closed vocabularies take a type prefix**, the same mechanism as
48
+ `light:`, `fact:`, `rel:`: `modality:` (`data/applicability.json`
49
+ `modalities`, every entry's `modality`, `modality_by` branches),
50
+ `role:` (`effects.roles`, `effect.own`/`effect.other`), `encounter:`
51
+ (`effects.encounters`, `effect.encounter`), and `category:` (`categories`,
52
+ every entry's and `represented_paragraphs` record's `category`) — new to
53
+ this list because it is the same class (package-coined, closed, emitted,
54
+ compared) and leaving it bare while prefixing the other three would
55
+ recreate the inconsistency this ADR closes.
56
+ 2. **Jurisdiction stays bare.** `intl` and `us/inland` are not names this
57
+ package coined. Jurisdiction is a coordinate with REQ-SCOPE-2's own
58
+ `<body>/<waters>` grammar, its left segment borrowed from ISO 3166, the
59
+ whole value doubling as a corpus key (REQ-LANG-3) and a `data/text/`
60
+ filesystem path — its sibling axis, `language`, is a bare BCP 47 tag for
61
+ the same reason. `jurisdiction:us/inland` would put a colon namespace in
62
+ front of a slash path and claim the package minted `us`, which it did
63
+ not. Its stability is a property of the grammar (REQ-SCOPE-4 makes
64
+ adding a jurisdiction additive; no fact, light or role value can spell
65
+ `us/inland`), not of a prefix — the same reason a paragraph path carries
66
+ none. Jurisdiction moves from "What is not an identifier" to the bare
67
+ class beside paragraph paths in `docs/identifiers.md`: it *is* an
68
+ identifier, immutable under REQ-MODEL-10 exactly like `27(a)(i)`, and it
69
+ just carries no prefix.
70
+ 3. **The two live collisions are recorded, not just resolved.**
71
+ `modality:shall-not-impede` and `role:shall-not-impede` are two names;
72
+ `role:none` and `encounter:none` are two names. Each pair collided under
73
+ the old bare scheme and does not under this one.
74
+
75
+ ## Consequences
76
+
77
+ - **One-pass rename, one commit.** `data/applicability.json`'s four
78
+ vocabulary maps and every entry field that names a value; both fixture
79
+ files (`applicability-fixtures.json` has no modality field to touch;
80
+ `situation-fixtures.json`'s `expect[].modality`); every schema enum/pattern
81
+ naming one of the four (`applicability.schema.json`,
82
+ `evaluation.schema.json`, `encounter-evaluation.schema.json`,
83
+ `i18n-catalog.schema.json`); the two i18n catalogs' `modality` section
84
+ keys; the test suite's literal comparisons; `docs/identifiers.md`,
85
+ `docs/requirements.md`, `README.md`. Jurisdiction values, patterns and
86
+ every corpus path are untouched.
87
+ - **colregs-engine follow-up.** Its generated types (`DisplayEvaluation`,
88
+ `EncounterEvaluation`, and any hand-written literal comparing against a
89
+ modality, role, encounter or category string) regenerate from this
90
+ package's schemas and wait on a colregs release carrying this ADR. Until
91
+ that release, colregs-engine's own types name the pre-ADR bare values;
92
+ this is a breaking change for it in the ordinary pre-1.0 sense (no
93
+ deprecation window, no shim — `AGENTS.md` "Stage: pre-consumer"), tracked
94
+ as its own follow-up, not blocking this PR.
95
+ - **The i18n catalog keys change shape**, not content: `modality:shall`
96
+ replaces `shall` as the key into `data/i18n/*.json`'s `modality` section;
97
+ the label strings themselves are untouched. A catalog section for role,
98
+ encounter or category is not added by this ADR — only the vocabulary
99
+ values those sections would key against.
100
+ - Rule ids (`rule:15a:crossing`) and shape keys (`when`, `effect`, a
101
+ `category:scope` effect's `part`/`section`/`applies_rules`) are untouched: only
102
+ the four vocabularies' *values* take the prefix, never a key or an id
103
+ that happens to share a word with one.
104
+
105
+ Ruled by Solace, 2026-09-16.
package/docs/budgets.json CHANGED
@@ -2,6 +2,7 @@
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
+ "docs/normative-language.md": 130,
5
6
  "AGENTS.md": 200,
6
7
  "CLAUDE.md": 10,
7
8
  "docs/requirements.md": 1500,
@@ -18,7 +19,13 @@
18
19
  "required": true,
19
20
  "pending": [],
20
21
  "docs/adr/0013-corpus-files-with-editions.md": 95,
21
- "docs/timeline.md": 130
22
+ "docs/adr/0014-engine-interface-owned-by-colregs.md": 110,
23
+ "docs/adr/0015-rule-ids-are-paragraph-keys.md": 210,
24
+ "docs/adr/0016-encounter-roles-are-pooled-across-frames.md": 90,
25
+ "docs/adr/0017-closed-vocabularies-are-prefixed-identifiers.md": 120,
26
+ "docs/timeline.md": 130,
27
+ "docs/maritime-sources.md": 130,
28
+ "docs/decisions.md": 40
22
29
  },
23
30
  "json_prose": {
24
31
  "targets": [
@@ -55,14 +62,10 @@
55
62
  "data/applicability.json#/entries/62/note",
56
63
  "data/applicability.json#/entries/63/note",
57
64
  "data/applicability.json#/entries/64/note",
58
- "data/applicability.json#/entries/64/gap",
59
65
  "data/applicability.json#/entries/65/note",
60
- "data/applicability.json#/entries/65/gap",
61
66
  "data/applicability.json#/entries/66/note",
62
67
  "data/applicability.json#/entries/67/note",
63
68
  "data/applicability.json#/entries/68/note",
64
- "data/applicability.json#/entries/68/gap",
65
- "data/applicability.json#/entries/69/note",
66
69
  "data/facts.json#/derived/note",
67
70
  "data/facts.json#/derived/fact:rule18_class/note",
68
71
  "data/facts.json#/derived/fact:rule18_class/decode/1/note",
@@ -134,16 +137,6 @@
134
137
  ]
135
138
  },
136
139
  "grandfathered": [
137
- {
138
- "file": "data/applicability.json",
139
- "hash": "447b3984d57b",
140
- "match": "PR #24"
141
- },
142
- {
143
- "file": "data/applicability.json",
144
- "hash": "5ff9d39319b8",
145
- "match": "PR #24"
146
- },
147
140
  {
148
141
  "file": "docs/conventions.md",
149
142
  "hash": "d1f1ed997431",
@@ -0,0 +1,6 @@
1
+ # Decisions
2
+
3
+ Rulings from `/sequence` sessions and other closed judgment calls. One line
4
+ each: date, short name, the answer, a link to the argument.
5
+
6
+ - 2026-09-16 — own vs self: rename the subject segment `own` to `self` across the situation record (fact keys, precedence effect keys, facts.json, fixtures, docs/identifiers.md). Work spawned as [colregs#138](https://github.com/mark-brannan/colregs/issues/138).