@mailwoman/geographic-model 10.0.0 → 10.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +32 -32
  2. package/data/geographic-model.json +5 -5
  3. package/data/model/concepts.json +5 -5
  4. package/lib/artifact.ts +37 -65
  5. package/lib/compile.ts +55 -70
  6. package/lib/index.ts +22 -35
  7. package/lib/load.ts +60 -72
  8. package/lib/lookup.ts +12 -28
  9. package/lib/packaged.ts +50 -0
  10. package/lib/schema.ts +104 -97
  11. package/lib/validate.ts +32 -56
  12. package/lib/validation/issues.ts +32 -36
  13. package/lib/validation/references.ts +24 -27
  14. package/out/artifact.d.ts +28 -45
  15. package/out/artifact.d.ts.map +1 -1
  16. package/out/artifact.js +25 -52
  17. package/out/artifact.js.map +1 -1
  18. package/out/compile.d.ts +27 -41
  19. package/out/compile.d.ts.map +1 -1
  20. package/out/compile.js +42 -64
  21. package/out/compile.js.map +1 -1
  22. package/out/index.d.ts +22 -35
  23. package/out/index.d.ts.map +1 -1
  24. package/out/index.js +22 -35
  25. package/out/index.js.map +1 -1
  26. package/out/load.d.ts +30 -39
  27. package/out/load.d.ts.map +1 -1
  28. package/out/load.js +42 -56
  29. package/out/load.js.map +1 -1
  30. package/out/lookup.d.ts +12 -28
  31. package/out/lookup.d.ts.map +1 -1
  32. package/out/lookup.js +4 -16
  33. package/out/lookup.js.map +1 -1
  34. package/out/packaged.d.ts +24 -0
  35. package/out/packaged.d.ts.map +1 -0
  36. package/out/packaged.js +42 -0
  37. package/out/packaged.js.map +1 -0
  38. package/out/schema.d.ts +104 -97
  39. package/out/schema.d.ts.map +1 -1
  40. package/out/schema.js +37 -52
  41. package/out/schema.js.map +1 -1
  42. package/out/tools/build-artifact.d.ts +27 -0
  43. package/out/tools/build-artifact.d.ts.map +1 -0
  44. package/out/tools/build-artifact.js +44 -0
  45. package/out/tools/build-artifact.js.map +1 -0
  46. package/out/validate.d.ts +29 -50
  47. package/out/validate.d.ts.map +1 -1
  48. package/out/validate.js +12 -35
  49. package/out/validate.js.map +1 -1
  50. package/out/validation/issues.d.ts +17 -20
  51. package/out/validation/issues.d.ts.map +1 -1
  52. package/out/validation/issues.js +25 -29
  53. package/out/validation/issues.js.map +1 -1
  54. package/out/validation/references.d.ts.map +1 -1
  55. package/out/validation/references.js +20 -23
  56. package/out/validation/references.js.map +1 -1
  57. package/package.json +35 -76
  58. package/tools/build-artifact.ts +56 -0
  59. package/out/scripts/build-artifact.d.ts +0 -51
  60. package/out/scripts/build-artifact.d.ts.map +0 -1
  61. package/out/scripts/build-artifact.js +0 -85
  62. package/out/scripts/build-artifact.js.map +0 -1
package/lib/schema.ts CHANGED
@@ -3,144 +3,129 @@
3
3
  * @license AGPL-3.0
4
4
  * @author Teffen Ellis, et al.
5
5
  *
6
- * The authored-record schema for `@mailwoman/geographic-model`: the stable identifiers, the closed
7
- * vocabularies, and the record shapes for concepts, relations, external-vocabulary mappings, source
8
- * observations, and derived facts.
6
+ * Defines the authored record schema for `@mailwoman/geographic-model`: identifiers, closed
7
+ * vocabularies and the shapes of concepts, relations, mappings, observations and derived facts.
9
8
  *
10
- * Three properties hold by construction, and they are why the file reads the way it does.
9
+ * The schema has no numeric fields. {@link Modality} is a vocabulary of words with no exported order,
10
+ * because ranking weights belong to `@mailwoman/resolver` and `@mailwoman/neural`.
11
11
  *
12
- * **No numeric field exists anywhere in this schema** — not a strength, not a confidence, not a
13
- * count. {@link Modality} is an ordinal vocabulary of WORDS and this module exports no order over
14
- * it, because a number attached to an authored relationship is a ranking weight whatever it is
15
- * called, and ranking belongs to `@mailwoman/resolver` and `@mailwoman/neural`.
12
+ * Authored assertions, source observations and derived facts are separate types with separately
13
+ * branded identifiers. A derived fact has no {@link SourceProvenance}. Its `derivation` and `inputs`
14
+ * serve as its provenance.
16
15
  *
17
- * **An authored assertion, a source observation, and a derived fact are three different types.** A
18
- * {@link RelationAssertion} is authored by a curator and lives on the concept it is about; a
19
- * {@link SourceObservationRecord} records what a named external source states and never enters the
20
- * concept table; a {@link DerivedFactRecord} names the procedure that produced it and every input
21
- * that procedure read. Their identifiers are separately branded, so one is not assignable where
22
- * another is expected.
23
- *
24
- * **Every record carries provenance, and a derived fact carries it structurally.** A derived fact
25
- * has no {@link SourceProvenance} of its own: its `derivation` plus its resolved `inputs` ARE its
26
- * provenance, and each input carries source provenance in turn.
27
- *
28
- * Identifier namespaces are per-table. A {@link ConceptID} and a {@link RelationID} may read the
29
- * same string without colliding; the brands are what keep them apart in a consumer.
30
- *
31
- * Consumed by `./validate.ts` (which refuses a document violating any rule above), by #1926's
32
- * deterministic compiler, and by the first authored document in #1927.
33
- *
34
- * Boundary record: `docs/superpowers/specs/2026-08-26-geographic-model-boundaries.md` (#1917).
16
+ * Each table has its own identifier namespace, so a {@link ConceptID} and a {@link RelationID} may hold
17
+ * the same string.
35
18
  */
36
19
 
37
20
  import type { POICategoryID } from "@mailwoman/poi-taxonomy/types"
38
21
  import type { Tagged } from "type-fest"
39
22
 
40
23
  /**
41
- * A concept identifier, e.g. `pharmacy`, `obtain_medication`. Branded — convert via {@link toConceptID}.
24
+ * A branded concept identifier, such as `pharmacy` or `obtain_medication`.
42
25
  */
43
26
  export type ConceptID = Tagged<string, "GeographicConceptID">
44
27
 
45
28
  /**
46
- * Brand a raw string as a {@link ConceptID}. Purely a compile-time assertion; the string is unchanged.
29
+ * Brands a string as a {@link ConceptID} without changing it.
47
30
  */
48
31
  export function toConceptID(id: string): ConceptID {
49
32
  return id as ConceptID
50
33
  }
51
34
 
52
35
  /**
53
- * A relation identifier, e.g. `affords`. Branded — convert via {@link toRelationID}.
36
+ * A branded relation identifier, such as `affords`.
54
37
  */
55
38
  export type RelationID = Tagged<string, "GeographicRelationID">
56
39
 
57
40
  /**
58
- * Brand a raw string as a {@link RelationID}. Purely a compile-time assertion; the string is unchanged.
41
+ * Brands a string as a {@link RelationID} without changing it.
59
42
  */
60
43
  export function toRelationID(id: string): RelationID {
61
44
  return id as RelationID
62
45
  }
63
46
 
64
47
  /**
65
- * The identifier of one authored {@link RelationAssertion}. Branded — convert via {@link toRuleID}.
48
+ * The branded identifier of one {@link RelationAssertion}.
66
49
  */
67
50
  export type RuleID = Tagged<string, "GeographicRuleID">
68
51
 
69
52
  /**
70
- * Brand a raw string as a {@link RuleID}. Purely a compile-time assertion; the string is unchanged.
53
+ * Brands a string as a {@link RuleID} without changing it.
71
54
  */
72
55
  export function toRuleID(id: string): RuleID {
73
56
  return id as RuleID
74
57
  }
75
58
 
76
59
  /**
77
- * The identifier of one {@link ExternalMappingRecord}. Branded — convert via {@link toMappingID}.
60
+ * The branded identifier of one {@link ExternalMappingRecord}.
78
61
  */
79
62
  export type MappingID = Tagged<string, "GeographicMappingID">
80
63
 
81
64
  /**
82
- * Brand a raw string as a {@link MappingID}. Purely a compile-time assertion; the string is unchanged.
65
+ * Brands a string as a {@link MappingID} without changing it.
83
66
  */
84
67
  export function toMappingID(id: string): MappingID {
85
68
  return id as MappingID
86
69
  }
87
70
 
88
71
  /**
89
- * The identifier of one {@link SourceObservationRecord}. Branded — convert via {@link toObservationID}.
72
+ * The branded identifier of one {@link SourceObservationRecord}.
90
73
  */
91
74
  export type ObservationID = Tagged<string, "GeographicObservationID">
92
75
 
93
76
  /**
94
- * Brand a raw string as an {@link ObservationID}. Purely a compile-time assertion; the string is unchanged.
77
+ * Brands a string as an {@link ObservationID} without changing it.
95
78
  */
96
79
  export function toObservationID(id: string): ObservationID {
97
80
  return id as ObservationID
98
81
  }
99
82
 
100
83
  /**
101
- * The identifier of one {@link DerivedFactRecord}. Branded — convert via {@link toDerivedFactID}.
84
+ * The branded identifier of one {@link DerivedFactRecord}.
102
85
  */
103
86
  export type DerivedFactID = Tagged<string, "GeographicDerivedFactID">
104
87
 
105
88
  /**
106
- * Brand a raw string as a {@link DerivedFactID}. Purely a compile-time assertion; the string is unchanged.
89
+ * Brands a string as a {@link DerivedFactID} without changing it.
107
90
  */
108
91
  export function toDerivedFactID(id: string): DerivedFactID {
109
92
  return id as DerivedFactID
110
93
  }
111
94
 
112
95
  /**
113
- * The complete set of concept kinds. Three, deliberately: the first executable slice needs an establishment class, an
114
- * activity, and the place class an establishment is sited in. Widening this vocabulary is a reviewed schema revision,
115
- * which is the review cost the program accepted in exchange for refusing speculative upper-ontology breadth.
96
+ * The concept kinds.
97
+ * A new kind requires a reviewed schema revision.
116
98
  */
117
99
  export const ConceptKind = {
118
100
  /**
119
- * A geographic place — an area or a point that other things are sited in or near.
101
+ * A geographic area or point where other things are located.
120
102
  */
121
103
  Place: "place",
122
104
  /**
123
- * A class of premises a person can go to, e.g. `pharmacy`.
105
+ * A class of premises that a person can visit, such as `pharmacy`.
124
106
  */
125
107
  Establishment: "establishment",
126
108
  /**
127
- * Something a person does, e.g. `obtain_medication`. The identifier is owned here; any statistics fitted against it
128
- * are owned by #1683.
109
+ * A kind of activity, such as `obtain_medication`.
129
110
  */
130
111
  Activity: "activity",
131
112
  } as const
132
113
 
114
+ /**
115
+ * A {@link ConceptKind} value.
116
+ */
133
117
  export type ConceptKind = (typeof ConceptKind)[keyof typeof ConceptKind]
134
118
 
135
119
  /**
136
- * How strongly an assertion, an observation, or a derived fact claims its proposition holds.
120
+ * How strongly an assertion, observation or derived fact claims that its proposition holds.
137
121
  *
138
- * The vocabulary is ordinal in meaning and this module exports no order over it, on purpose. An exported rank would be
139
- * one arithmetic step from a weight, and an authored weight is the single thing the package boundary forbids.
122
+ * The values are ordinal in meaning, but the module exports no order
123
+ * so callers do not convert them into weights.
140
124
  */
141
125
  export const Modality = {
142
126
  /**
143
- * Holds in every instance; a counter-example falsifies the record rather than qualifying it.
127
+ * Holds in every instance.
128
+ * A counterexample falsifies the record.
144
129
  */
145
130
  Necessary: "necessary",
146
131
  /**
@@ -151,13 +136,16 @@ export const Modality = {
151
136
  Expected: "expected",
152
137
  WeaklyExpected: "weakly_expected",
153
138
  /**
154
- * Consistent with the concept and asserts nothing about how often it holds.
139
+ * Consistent with the concept, with no claim about frequency.
155
140
  */
156
141
  Possible: "possible",
157
142
  Unusual: "unusual",
158
143
  StronglyUnusual: "strongly_unusual",
159
144
  } as const
160
145
 
146
+ /**
147
+ * A {@link Modality} value.
148
+ */
161
149
  export type Modality = (typeof Modality)[keyof typeof Modality]
162
150
 
163
151
  /**
@@ -174,67 +162,81 @@ export const RelationSemantics = {
174
162
  Defeasible: "defeasible",
175
163
  } as const
176
164
 
165
+ /**
166
+ * A {@link RelationSemantics} value.
167
+ */
177
168
  export type RelationSemantics = (typeof RelationSemantics)[keyof typeof RelationSemantics]
178
169
 
179
170
  /**
180
- * A concept's authoring lifecycle. It says whether a consumer should read the record, and nothing about how much the
181
- * record is worth.
171
+ * A concept's review status.
172
+ * It tells consumers whether to use the record.
182
173
  */
183
174
  export const ConceptStatus = {
184
175
  /**
185
- * Authored, not yet reviewed. A compiler may refuse to emit it.
176
+ * Authored but not yet reviewed.
186
177
  */
187
178
  Draft: "draft",
188
179
  Active: "active",
189
180
  /**
190
- * Kept so existing references resolve; no new reference should be authored against it.
181
+ * Retained so that existing references resolve.
182
+ * New references should avoid it.
191
183
  */
192
184
  Deprecated: "deprecated",
193
185
  } as const
194
186
 
187
+ /**
188
+ * A {@link ConceptStatus} value.
189
+ */
195
190
  export type ConceptStatus = (typeof ConceptStatus)[keyof typeof ConceptStatus]
196
191
 
197
192
  /**
198
- * The external vocabularies a concept can be mapped into. One today, and the mapping record below is typed against it
199
- * directly; a second vocabulary turns {@link ExternalMappingRecord} into a union discriminated on `vocabulary`.
193
+ * External vocabularies that concepts can map to.
194
+ *
195
+ * {@link ExternalMappingRecord} assumes a single vocabulary.
196
+ * A second vocabulary would require a union discriminated on `vocabulary`.
200
197
  */
201
198
  export const ExternalVocabulary = {
202
199
  /**
203
- * `@mailwoman/poi-taxonomy` category identifiers — the owner of external and curated POI categories.
200
+ * Category identifiers from `@mailwoman/poi-taxonomy`.
204
201
  */
205
202
  POITaxonomy: "poi-taxonomy",
206
203
  } as const
207
204
 
205
+ /**
206
+ * An {@link ExternalVocabulary} value.
207
+ */
208
208
  export type ExternalVocabulary = (typeof ExternalVocabulary)[keyof typeof ExternalVocabulary]
209
209
 
210
210
  /**
211
- * Where a record came from. `source` is required and non-empty on every record that carries provenance: a record whose
212
- * source is blank is indistinguishable from a record nobody stands behind.
211
+ * The origin of a record.
212
+ * `source` must be non-empty.
213
213
  */
214
214
  export interface SourceProvenance {
215
215
  /**
216
- * The naming authority, dataset, publication, or curator, e.g. `overture-places` or `mailwoman-curated`.
216
+ * The authority, dataset, publication or curator, such as `overture-places` or `mailwoman-curated`.
217
217
  */
218
218
  source: string
219
219
  /**
220
- * The source's own version or release string, when it has one, e.g. an Overture release.
220
+ * The source's version or release string, such as an Overture release.
221
221
  */
222
222
  sourceVersion?: string
223
223
  /**
224
- * The identifier of the specific record within the source, when the claim is traceable to one.
224
+ * The identifier of the specific source record that supports the claim.
225
225
  */
226
226
  sourceRecord?: string
227
227
  sourceURL?: string
228
228
  /**
229
- * ISO 8601 calendar date the record was authored, `YYYY-MM-DD`.
229
+ * ISO 8601 date on which the record was authored, as `YYYY-MM-DD`.
230
230
  */
231
231
  authoredAt?: string
232
232
  notes?: string
233
233
  }
234
234
 
235
235
  /**
236
- * One relation's definition. Relations are vocabulary, not claims: the record says what the relation MEANS and which
237
- * concept kinds may stand on either side of it, and asserts nothing about any particular pair.
236
+ * One relation's definition.
237
+ *
238
+ * A relation record defines meaning and the concept kinds allowed on each side.
239
+ * It makes no claim about any particular pair.
238
240
  */
239
241
  export interface RelationRecord {
240
242
  id: RelationID
@@ -251,16 +253,16 @@ export interface RelationRecord {
251
253
  transitive: boolean
252
254
  symmetric: boolean
253
255
  /**
254
- * The relation reading the same edge in the other direction. When present it must resolve, and the relation it names
255
- * must name this one back.
256
+ * The relation that reads the same edge in the opposite direction.
257
+ *
258
+ * When set, it must exist and declare this relation as its own inverse.
256
259
  */
257
260
  inverse?: RelationID
258
261
  semantics: RelationSemantics
259
262
  }
260
263
 
261
264
  /**
262
- * One authored claim, attached to the concept it is about. This is curated semantics — what a curator states holds, not
263
- * what a dataset was observed to contain.
265
+ * One curated claim, stored on the concept that it describes.
264
266
  */
265
267
  export interface RelationAssertion {
266
268
  id: RuleID
@@ -268,15 +270,17 @@ export interface RelationAssertion {
268
270
  target: ConceptID
269
271
  modality: Modality
270
272
  /**
271
- * ISO 3166-1 alpha-2 codes the claim is scoped to. Absent means the curator scoped it to nowhere in particular, which
272
- * is a weaker statement than scoping it to everywhere.
273
+ * ISO 3166-1 alpha-2 codes that scope the claim.
274
+ *
275
+ * An absent list means the claim has no country scope.
276
+ * This is weaker than a claim about every country.
273
277
  */
274
278
  countries?: string[]
275
279
  provenance: SourceProvenance
276
280
  }
277
281
 
278
282
  /**
279
- * One concept, and everything authored about it.
283
+ * One concept and its authored assertions.
280
284
  */
281
285
  export interface ConceptRecord {
282
286
  id: ConceptID
@@ -284,7 +288,8 @@ export interface ConceptRecord {
284
288
  description: string
285
289
  kind: ConceptKind
286
290
  /**
287
- * Broader concepts this one is a kind of. May be empty; may not name this concept, directly or around a cycle.
291
+ * Broader concepts that this concept is a kind of.
292
+ * The list may be empty but must not form a cycle.
288
293
  */
289
294
  isA: ConceptID[]
290
295
  assertions: RelationAssertion[]
@@ -293,27 +298,27 @@ export interface ConceptRecord {
293
298
  }
294
299
 
295
300
  /**
296
- * A translation from an external vocabulary's identifier into a concept owned here. It carries no semantics of its own:
297
- * it says which external identifier names the same thing, and on whose authority.
301
+ * Maps an external vocabulary identifier to a concept.
302
+ * The mapping adds no semantics.
298
303
  */
299
304
  export interface ExternalMappingRecord {
300
305
  id: MappingID
301
306
  concept: ConceptID
302
307
  vocabulary: ExternalVocabulary
303
308
  /**
304
- * The identifier in the external vocabulary. Typed as a `POICategoryID` because `poi-taxonomy` is the only member of
305
- * {@link ExternalVocabulary}; a second member makes this field a per-vocabulary type.
309
+ * The identifier in the external vocabulary.
310
+ *
311
+ * The type is `POICategoryID` because `poi-taxonomy` is the only external vocabulary.
306
312
  */
307
313
  externalID: POICategoryID
308
314
  provenance: SourceProvenance
309
315
  }
310
316
 
311
317
  /**
312
- * A proposition a named external source states, recorded in this model's vocabulary.
318
+ * A proposition stated by an identified external source, expressed in this model's vocabulary.
313
319
  *
314
- * It is kept out of the concept table on purpose. An observation is evidence about the world that someone else
315
- * collected; promoting one into an authored assertion is a curation decision that has to be made and provenanced
316
- * explicitly, never by the record sitting in a convenient place.
320
+ * Observations stay out of the concept table.
321
+ * An authored assertion requires an explicit curation decision with its own provenance.
317
322
  */
318
323
  export interface SourceObservationRecord {
319
324
  id: ObservationID
@@ -322,14 +327,14 @@ export interface SourceObservationRecord {
322
327
  object: ConceptID
323
328
  modality: Modality
324
329
  /**
325
- * ISO 3166-1 alpha-2 codes the source scoped its statement to.
330
+ * ISO 3166-1 alpha-2 codes that scope the source's statement.
326
331
  */
327
332
  countries?: string[]
328
333
  provenance: SourceProvenance
329
334
  }
330
335
 
331
336
  /**
332
- * Which table a {@link DerivationInput} points into.
337
+ * The table that a {@link DerivationInput} refers to.
333
338
  */
334
339
  export const DerivationInputKind = {
335
340
  Concept: "concept",
@@ -340,11 +345,14 @@ export const DerivationInputKind = {
340
345
  DerivedFact: "derived_fact",
341
346
  } as const
342
347
 
348
+ /**
349
+ * A {@link DerivationInputKind} value.
350
+ */
343
351
  export type DerivationInputKind = (typeof DerivationInputKind)[keyof typeof DerivationInputKind]
344
352
 
345
353
  /**
346
- * One record a derivation read. The union is discriminated on `kind` so the identifier's brand matches the table it
347
- * resolves against.
354
+ * One record that a derivation read.
355
+ * The `kind` discriminant fixes the identifier's brand.
348
356
  */
349
357
  export type DerivationInput =
350
358
  | { kind: typeof DerivationInputKind.Concept; id: ConceptID }
@@ -355,21 +363,21 @@ export type DerivationInput =
355
363
  | { kind: typeof DerivationInputKind.DerivedFact; id: DerivedFactID }
356
364
 
357
365
  /**
358
- * A fact a named procedure computed from named inputs. Never hand-authored: #1926's compiler writes this table, and the
359
- * validator refuses a fact whose derivation is unnamed or whose inputs do not resolve.
366
+ * A fact that an identified procedure computed from specific input records.
360
367
  *
361
- * There is no provenance field. The derivation plus the inputs is the provenance and the stronger kind — a source
362
- * string can be copied onto a record that did not come from it, while an input list either resolves or the document
363
- * does not validate.
368
+ * The record has no provenance field.
369
+ * Its `derivation` and `inputs` serve as provenance.
370
+ * The validator rejects a fact whose inputs do not resolve.
364
371
  */
365
372
  export interface DerivedFactRecord {
366
373
  id: DerivedFactID
367
374
  /**
368
- * The deterministic procedure that produced this fact, named so a reader can re-run it.
375
+ * The name of the deterministic procedure that produced this fact.
369
376
  */
370
377
  derivation: string
371
378
  /**
372
- * Every record the derivation read. At least one, and each must resolve.
379
+ * Every record that the derivation read.
380
+ * The list must be non-empty and every input must resolve.
373
381
  */
374
382
  inputs: DerivationInput[]
375
383
  subject: ConceptID
@@ -380,15 +388,14 @@ export interface DerivedFactRecord {
380
388
  }
381
389
 
382
390
  /**
383
- * One authored document: the whole record set a validator, and later a compiler, reads at once.
391
+ * One complete authored document.
384
392
  *
385
- * All six fields are required, `derivedFacts` included. A hand-authored file therefore writes `"derivedFacts": []`,
386
- * which is the point — an absent table and an empty table are different claims, and the format that allows the first to
387
- * stand in for the second is the format where a dropped table reads as a world with no derived facts in it.
393
+ * Every field is required, including `derivedFacts`, so that a dropped table fails
394
+ * validation instead of reading as an empty one.
388
395
  */
389
396
  export interface GeographicModelDocument {
390
397
  /**
391
- * The document's own schema/data version.
398
+ * The document's data version.
392
399
  */
393
400
  version: string
394
401
  relations: RelationRecord[]
package/lib/validate.ts CHANGED
@@ -3,28 +3,11 @@
3
3
  * @license AGPL-3.0
4
4
  * @author Teffen Ellis, et al.
5
5
  *
6
- * Deterministic validation of a {@link GeographicModelDocument}. Plain TypeScript: no reasoner, no
7
- * query engine, no schema library, and no I/O — the same input always produces the same issue list.
6
+ * Deterministically validate a {@link GeographicModelDocument} without I/O or external schema libraries. Report all
7
+ * issues in traversal order and return either the complete valid document or the full issue list.
8
8
  *
9
- * The validator reports EVERY violation it finds. A record set is authored by hand and read by a
10
- * compiler, so a validator that stops at the first problem hands its author one defect per run. It
11
- * also never returns a partial document: either the input satisfies every rule and is returned
12
- * whole, or nothing is returned and every issue is named with the path it was found at. There is no
13
- * third answer in which some records were dropped quietly, because a dropped record is
14
- * indistinguishable from a world that does not contain it.
15
- *
16
- * Two passes, in this order, and both always run:
17
- *
18
- * 1. **Shape.** Field presence, field types, closed-vocabulary membership, and unknown keys. An
19
- * unknown key whose name announces ranking policy — a boost, a penalty, a weight, a rank, a
20
- * score — is reported under its own code rather than as an anonymous stray field, because that
21
- * is the one authoring mistake this package exists to refuse.
22
- * 2. **Whole-table references.** Duplicate identifiers, `isA` self-reference and cycles, relation
23
- * and concept resolution, relation domain and range kinds, inverse reciprocity, and derivation
24
- * inputs. These are answerable only once every table has been read, which is why they are
25
- * reported after the shape issues rather than interleaved with them.
26
- *
27
- * Consumed by #1926's compiler, which validates before it emits, and by #1927's authored document.
9
+ * Validate record shapes first, then whole-table references such as duplicate IDs, inheritance cycles, relation
10
+ * constraints, inverses and derivation inputs.
28
11
  */
29
12
 
30
13
  import { isPlainObject } from "@mailwoman/core/objects"
@@ -52,8 +35,6 @@ import {
52
35
  } from "#validation/issues"
53
36
  import { checkReferences } from "#validation/references"
54
37
 
55
- export { type ValidationIssue, ValidationIssueCode } from "#validation/issues"
56
-
57
38
  /**
58
39
  * The whole document, or every reason it is not one.
59
40
  */
@@ -96,54 +77,53 @@ const DERIVED_FACT_FIELDS = [
96
77
  const DERIVATION_INPUT_FIELDS = ["kind", "id"] as const
97
78
 
98
79
  /**
99
- * ISO 3166-1 alpha-2, upper case. A lower-case or three-letter value is an authoring mistake that would otherwise scope
100
- * a claim to a country nothing else in the system names.
80
+ * Uppercase ISO 3166-1 alpha-2 country code.
101
81
  */
102
82
  const COUNTRY_PATTERN = /^[A-Z]{2}$/
103
83
 
104
84
  export interface AssertionView {
105
85
  path: string
106
- id?: string
107
- relation?: string
108
- target?: string
86
+ id: string | null
87
+ relation: string | null
88
+ target: string | null
109
89
  }
110
90
 
111
91
  export interface ConceptView {
112
92
  path: string
113
- id?: string
114
- kind?: ConceptKind
115
- isA?: string[]
93
+ id: string | null
94
+ kind: ConceptKind | null
95
+ isA: string[] | null
116
96
  assertions: AssertionView[]
117
97
  }
118
98
 
119
99
  export interface RelationView {
120
100
  path: string
121
- id?: string
122
- domainKinds?: ConceptKind[]
123
- rangeKinds?: ConceptKind[]
124
- transitive?: boolean
125
- symmetric?: boolean
126
- inverse?: string
101
+ id: string | null
102
+ domainKinds: ConceptKind[] | null
103
+ rangeKinds: ConceptKind[] | null
104
+ transitive: boolean | null
105
+ symmetric: boolean | null
106
+ inverse: string | null
127
107
  }
128
108
 
129
109
  export interface MappingView {
130
110
  path: string
131
- id?: string
132
- concept?: string
111
+ id: string | null
112
+ concept: string | null
133
113
  }
134
114
 
135
115
  export interface TripleView {
136
116
  path: string
137
- id?: string
138
- subject?: string
139
- relation?: string
140
- object?: string
117
+ id: string | null
118
+ subject: string | null
119
+ relation: string | null
120
+ object: string | null
141
121
  }
142
122
 
143
123
  export interface DerivationInputView {
144
124
  path: string
145
- kind?: DerivationInputKind
146
- id?: string
125
+ kind: DerivationInputKind | null
126
+ id: string | null
147
127
  }
148
128
 
149
129
  export interface DerivedFactView extends TripleView {
@@ -209,7 +189,7 @@ function readProvenance(issues: ValidationIssue[], path: string, container: Reco
209
189
  }
210
190
 
211
191
  /**
212
- * Read the `label` and `description` an authored record carries.
192
+ * Read required label and description fields.
213
193
  */
214
194
  function readNarration(issues: ValidationIssue[], path: string, value: Record<string, unknown>): void {
215
195
  readString(issues, path, value, "label", true)
@@ -450,29 +430,26 @@ function collectIssues(input: unknown): ValidationIssue[] {
450
430
  /**
451
431
  * Validate an authored geographic-model document.
452
432
  *
453
- * Returns the document whole, or every reason it is not one. Issues arrive in traversal order — shape issues per record
454
- * in table order, then whole-table reference issues — so two runs over the same input produce the same list.
433
+ * @returns The full document or every validation issue in deterministic order.
455
434
  */
456
435
  export function validateGeographicModelDocument(input: unknown): ValidationResult {
457
436
  const issues = collectIssues(input)
458
437
 
459
438
  if (issues.length) return { ok: false, issues }
460
439
 
461
- // A clean input IS the document — the validator reads, it never rewrites. Keeping the assertion in this function,
462
- // where `input` is still `unknown`, is what makes it a single step rather than a cast through `unknown`.
440
+ // Return the original document without rewriting it.
463
441
  return { ok: true, document: input as GeographicModelDocument }
464
442
  }
465
443
 
466
444
  /**
467
- * Render every issue as one line, `path: message [code]`, in the order the validator produced them.
445
+ * Format issues as `path: message [code]`, preserving their order.
468
446
  */
469
447
  export function formatValidationIssues(issues: readonly ValidationIssue[]): string {
470
448
  return issues.map((issue) => `${issue.path}: ${issue.message} [${issue.code}]`).join("\n")
471
449
  }
472
450
 
473
451
  /**
474
- * Thrown by {@link parseGeographicModelDocument}. Carries the whole issue list, and states the whole issue list in its
475
- * message, so a caller that only ever prints `error.message` still sees every violation.
452
+ * Validation error carrying the complete issue list in both `issues` and `message`.
476
453
  */
477
454
  export class GeographicModelValidationError extends Error {
478
455
  readonly issues: readonly ValidationIssue[]
@@ -486,9 +463,8 @@ export class GeographicModelValidationError extends Error {
486
463
  }
487
464
 
488
465
  /**
489
- * Validate and return an authored document, throwing {@link GeographicModelValidationError} with every violation if it
490
- * does not validate. The throwing form is for callers with no partial-result behavior to offer — a compiler, a build
491
- * step, a test.
466
+ * Validate and return the document, or throw with all issues.
467
+ * Use when partial results are not meaningful.
492
468
  */
493
469
  export function parseGeographicModelDocument(input: unknown): GeographicModelDocument {
494
470
  const result = validateGeographicModelDocument(input)