@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/compile.ts CHANGED
@@ -3,32 +3,17 @@
3
3
  * @license AGPL-3.0
4
4
  * @author Teffen Ellis, et al.
5
5
  *
6
- * Deterministic compilation of a validated {@link GeographicModelDocument} into the runtime artifact.
6
+ * Compiles a validated {@link GeographicModelDocument} into the runtime artifact deterministically.
7
7
  *
8
- * The compiler validates by DELEGATION: `parseGeographicModelDocument` is the only thing that
9
- * decides whether a document is well formed, and it throws with every violation before a single byte
10
- * is computed. There is no second validator here, and no partial artifact on failure — a compile
11
- * either produces the whole artifact or produces nothing.
8
+ * `parseGeographicModelDocument` performs all document validation and throws before compilation
9
+ * starts. A compile either returns the whole artifact or throws.
12
10
  *
13
- * **`isA` alone defines semantic inheritance.** Two things follow from it, and they are the whole
14
- * derivation surface:
11
+ * Only `isA` defines inheritance. The compiler materializes the transitive `isA` closure for each
12
+ * concept. It copies each ancestor's assertions onto descendants as {@link DerivedFactRecord}
13
+ * entries. The compiler does not close over relations marked `transitive` or `inverse`.
15
14
  *
16
- * 1. The transitive `isA` closure is materialized per concept, so a consumer asks "what is this a
17
- * kind of" with one lookup rather than by walking the graph at query time.
18
- * 2. Every assertion an ancestor carries is materialized onto its descendants as a
19
- * {@link DerivedFactRecord}, naming {@link DERIVATION_ISA_INHERITANCE} and every record the
20
- * derivation read. Without this a consumer would still be traversing — the closure alone tells it
21
- * which concepts to go and read, which is the traversal it was supposed to be spared.
22
- *
23
- * A relation declaring `transitive` or `inverse` is NOT closed over. Those fields are vocabulary
24
- * describing what the relation means; materializing them is a reasoning step no executable need has
25
- * asked for, and the boundary record excludes general reasoning from this package. The day one is
26
- * needed it arrives as its own named derivation beside this one.
27
- *
28
- * Cycles never reach the derivations: `parseGeographicModelDocument` refuses a direct or indirect
29
- * `isA` cycle and names the trail it followed, so a cyclic document fails as a validation error
30
- * rather than as a hang. The walk below is breadth-first over a visited set regardless, which makes
31
- * it total for any graph rather than for the graphs the validator happens to admit.
15
+ * The validator rejects `isA` cycles, but the ancestor walk also tracks visited concepts, so it
16
+ * terminates on any graph.
32
17
  */
33
18
 
34
19
  import { compareByCodePoint as compareIdentifiers } from "@mailwoman/core/strings/compare"
@@ -48,34 +33,38 @@ import {
48
33
  } from "#schema"
49
34
  import { parseGeographicModelDocument } from "#validate"
50
35
  /**
51
- * The name a fact derived by `isA` inheritance carries in its `derivation` field. A consumer branches on this rather
52
- * than on where the record sits.
36
+ * The `derivation` value of a fact derived by `isA` inheritance.
53
37
  */
54
38
  export const DERIVATION_ISA_INHERITANCE = "isa-assertion-inheritance"
55
39
 
56
40
  /**
57
- * Every way compilation can refuse a document the validator accepted.
41
+ * Reasons that compilation rejects a document that passed validation.
58
42
  *
59
- * Both are discovered while writing derived records, which is why the validator cannot report them: they are properties
60
- * of what the compiler is about to write, not of what the author wrote.
43
+ * Both problems concern the derived records.
44
+ * The validator never sees those records.
61
45
  */
62
46
  export const CompileIssueCode = {
63
47
  /**
64
- * An inherited assertion would land on a concept whose kind the relation does not accept on the asserting side.
65
- * Emitting it would put a record in the artifact that the document validator would reject if it were authored.
48
+ * An inherited assertion would apply to a concept whose kind the relation's `domainKinds` excludes.
66
49
  */
67
50
  InheritedDomainKindMismatch: "inherited_domain_kind_mismatch",
68
51
  /**
69
- * Two derived facts claim one identifier. Reachable when an authored derived fact takes an identifier a derivation
70
- * also produces, or when authored identifiers carry the separators the derived form is built from.
52
+ * Two derived facts share one identifier.
53
+ *
54
+ * This happens when an authored derived fact uses an identifier that a derivation also
55
+ * produces, or when authored identifiers contain the separators of the derived form.
71
56
  */
72
57
  DuplicateDerivedFactID: "duplicate_derived_fact_id",
73
58
  } as const
74
59
 
60
+ /**
61
+ * A {@link CompileIssueCode} value.
62
+ */
63
+
75
64
  export type CompileIssueCode = (typeof CompileIssueCode)[keyof typeof CompileIssueCode]
76
65
 
77
66
  /**
78
- * One reason a document that validates does not compile.
67
+ * One reason that a valid document does not compile.
79
68
  */
80
69
  export interface CompileIssue {
81
70
  code: CompileIssueCode
@@ -83,8 +72,8 @@ export interface CompileIssue {
83
72
  }
84
73
 
85
74
  /**
86
- * Thrown by {@link compileGeographicModel}. Carries every reason at once, and states them all in its message, so a
87
- * caller that only prints `error.message` still sees the whole list.
75
+ * The error that {@link compileGeographicModel} throws.
76
+ * Its message lists every issue.
88
77
  */
89
78
  export class GeographicModelCompileError extends Error {
90
79
  readonly issues: readonly CompileIssue[]
@@ -100,8 +89,8 @@ export class GeographicModelCompileError extends Error {
100
89
  }
101
90
 
102
91
  /**
103
- * The order derivation inputs are listed in. Grouping by table first keeps a long input list readable; the identifier
104
- * breaks ties inside a table.
92
+ * Sort order of derivation inputs by kind.
93
+ * Identifiers break ties within a kind.
105
94
  */
106
95
  const DERIVATION_INPUT_ORDER: readonly DerivationInputKind[] = [
107
96
  DerivationInputKind.Concept,
@@ -113,14 +102,14 @@ const DERIVATION_INPUT_ORDER: readonly DerivationInputKind[] = [
113
102
  ]
114
103
 
115
104
  /**
116
- * The separator for the compound keys this module groups by. `U+0000` cannot appear in a readable identifier without
117
- * being visible in it, so two different key tuples cannot collapse onto one string. The derived IDENTIFIERS written
118
- * into the artifact use readable separators instead, and are checked for collisions once they are all built.
105
+ * Separator for internal grouping keys.
106
+ *
107
+ * Identifiers never contain `U+0000`, so distinct key tuples cannot produce the same string.
119
108
  */
120
109
  const KEY_SEPARATOR = "\u0000"
121
110
 
122
111
  /**
123
- * The separator between country codes inside a derived identifier.
112
+ * Separator between country codes in a derived identifier.
124
113
  */
125
114
  const COUNTRY_SEPARATOR = "+"
126
115
 
@@ -129,11 +118,8 @@ function compareByID(left: { id: string }, right: { id: string }): number {
129
118
  }
130
119
 
131
120
  /**
132
- * Walk `isA` upward from one concept and return every concept reachable, in code-point order.
133
- *
134
- * Breadth-first over a visited set: each concept is expanded once, so the walk terminates on any graph and the answer
135
- * does not depend on how the parents were authored. The concept itself is never in its own list — it could only get
136
- * there around a cycle, and the validator refuses those before the compiler runs.
121
+ * Returns every concept reachable from `conceptID` through `isA`, sorted by code point.
122
+ * The result excludes the concept itself.
137
123
  */
138
124
  function ancestorsOfConcept(
139
125
  conceptID: ConceptID,
@@ -142,8 +128,8 @@ function ancestorsOfConcept(
142
128
  const visited = new Set<string>()
143
129
  const frontier: ConceptID[] = [...(parents.get(conceptID) ?? [])]
144
130
 
145
- // An array iterator reads entries appended during the walk, which is what makes this breadth-first rather than a
146
- // pass over the direct parents.
131
+ // The array iterator also visits entries pushed during the loop.
132
+ // This makes the walk breadth-first.
147
133
  for (const next of frontier) {
148
134
  if (next === conceptID || visited.has(next)) continue
149
135
 
@@ -157,14 +143,17 @@ function ancestorsOfConcept(
157
143
  }
158
144
 
159
145
  /**
160
- * One derived fact under construction. Drafts are keyed by the proposition they state, so two ancestors asserting the
161
- * same thing produce ONE fact naming both of them as inputs, while two ancestors asserting the same triple under
162
- * different modality produce two facts — a contradiction a consumer can see, rather than a silent choice between them.
146
+ * One derived fact under construction.
147
+ *
148
+ * Drafts are keyed by subject, relation, target, modality and countries.
149
+ * Two ancestors with the same assertion produce one fact that lists both as inputs.
150
+ *
151
+ * Assertions that differ only in modality produce separate facts, so a contradiction stays visible.
163
152
  */
164
153
  interface DerivedDraft {
165
154
  subject: ConceptID
166
155
  assertion: RelationAssertion
167
- countries?: readonly string[]
156
+ countries: readonly string[] | null
168
157
  inputs: Map<string, DerivationInput>
169
158
  }
170
159
 
@@ -183,8 +172,9 @@ function draftInputs(draft: DerivedDraft): DerivationInput[] {
183
172
  }
184
173
 
185
174
  /**
186
- * The identifier a derived fact carries. Built from the proposition it states, so it is stable across edits elsewhere
187
- * in the document, and readable, so a reader meeting one in a diff can tell what it says.
175
+ * Builds a readable derived-fact identifier from the fact's proposition.
176
+ *
177
+ * The identifier stays stable when unrelated parts of the document change.
188
178
  */
189
179
  function derivedFactID(draft: DerivedDraft): string {
190
180
  const scope = draft.countries?.length ? `:${draft.countries.join(COUNTRY_SEPARATOR)}` : ""
@@ -194,24 +184,22 @@ function derivedFactID(draft: DerivedDraft): string {
194
184
  }
195
185
 
196
186
  /**
197
- * The relation and target one assertion is about — the pair a descendant's own assertion speaks for.
187
+ * Returns a key for an assertion's relation and target.
198
188
  */
199
189
  function edgeKey(assertion: RelationAssertion): string {
200
190
  return `${assertion.relation}${KEY_SEPARATOR}${assertion.target}`
201
191
  }
202
192
 
203
- function draftKey(subject: ConceptID, assertion: RelationAssertion, countries: readonly string[] | undefined): string {
193
+ function draftKey(subject: ConceptID, assertion: RelationAssertion, countries: readonly string[] | null): string {
204
194
  const scope = countries?.join(COUNTRY_SEPARATOR) ?? ""
205
195
 
206
196
  return `${subject}${KEY_SEPARATOR}${edgeKey(assertion)}${KEY_SEPARATOR}${assertion.modality}${KEY_SEPARATOR}${scope}`
207
197
  }
208
198
 
209
199
  /**
210
- * Materialize every ancestor's assertions onto their descendants.
200
+ * Copies each ancestor's assertions onto its descendants as derived facts.
211
201
  *
212
- * A concept that authors its own assertion for the same relation and target inherits nothing for that pair. The
213
- * authored record is the more specific one, which is what `isA` means, and re-stating the pair would put two modalities
214
- * for one proposition into the artifact with no rule saying which of them holds.
202
+ * A concept's own assertion for a relation and target overrides every inherited assertion for that pair.
215
203
  */
216
204
  function deriveInheritedFacts(
217
205
  concepts: readonly ConceptRecord[],
@@ -233,9 +221,8 @@ function deriveInheritedFacts(
233
221
  for (const ancestorID of entry.ancestors) {
234
222
  const ancestor = conceptByID.get(String(ancestorID))
235
223
 
236
- // Validation refuses an `isA` naming an undeclared concept, and an assertion naming an undeclared relation,
237
- // so both resolve for any document that reached the compiler. The guards keep the walk total; they do not
238
- // describe a state the artifact can hold.
224
+ // Validation guarantees that every referenced concept and relation exists.
225
+ // These guards only satisfy the type checker.
239
226
  if (!ancestor) continue
240
227
 
241
228
  for (const assertion of ancestor.assertions) {
@@ -256,7 +243,7 @@ function deriveInheritedFacts(
256
243
  continue
257
244
  }
258
245
 
259
- const countries = assertion.countries?.length ? assertion.countries.toSorted(compareIdentifiers) : undefined
246
+ const countries = assertion.countries?.length ? assertion.countries.toSorted(compareIdentifiers) : null
260
247
  const key = draftKey(concept.id, assertion, countries)
261
248
  const existing = drafts.get(key)
262
249
  const draft: DerivedDraft = existing ?? { subject: concept.id, assertion, countries, inputs: new Map() }
@@ -305,14 +292,12 @@ function checkDerivedIdentifiers(facts: readonly DerivedFactRecord[], issues: Co
305
292
  }
306
293
 
307
294
  /**
308
- * Compile an authored geographic-model document into its runtime artifact.
295
+ * Compiles an authored geographic-model document into its runtime artifact.
309
296
  *
310
- * Throws `GeographicModelValidationError` with every violation if the input is not a valid document, and
311
- * {@link GeographicModelCompileError} with every reason if it validates but its derivations cannot be written. Nothing
312
- * partial is ever returned.
297
+ * The artifact's tables are new sorted arrays that hold the authored records unchanged.
313
298
  *
314
- * The document is read, never rewritten: the artifact's tables are new arrays holding the authored records themselves,
315
- * ordered by identifier, and the derived tables are new records built beside them.
299
+ * @throws {GeographicModelValidationError} When the input is not a valid document.
300
+ * @throws {GeographicModelCompileError} When the document is valid but its derived facts are invalid.
316
301
  */
317
302
  export function compileGeographicModel(input: unknown): CompiledGeographicModel {
318
303
  const document: GeographicModelDocument = parseGeographicModelDocument(input)
package/lib/index.ts CHANGED
@@ -3,45 +3,31 @@
3
3
  * @license AGPL-3.0
4
4
  * @author Teffen Ellis, et al.
5
5
  *
6
- * `@mailwoman/geographic-model` — the world-semantic layer: stable concepts beyond the POI
7
- * vocabulary, relation definitions, activities and affordances, rule modality, source
8
- * observations, derived facts, derivation provenance, deterministic compilation and validation,
9
- * and mappings from external vocabularies into world concepts.
6
+ * `@mailwoman/geographic-model` — the world-semantic layer: stable concepts beyond the POI
7
+ * vocabulary, relation definitions, activities, affordances, rule modality, source observations,
8
+ * derived facts, derivation provenance, deterministic compilation, plus validation.
10
9
  *
11
- * The public surface is the authored-record schema (`./schema.ts`), its deterministic validator
12
- * (`./validate.ts`), the compiler that turns a validated document into the runtime artifact
13
- * (`./compile.ts`), the artifact's shape and canonical bytes (`./artifact.ts`), and the lookups a
14
- * runtime consumer reads it through (`./lookup.ts`). All five are re-exported here and all five are
15
- * reachable as curated subpaths.
10
+ * The authoring loader (`./load.ts`, the `./load` subpath) is deliberately not re-exported here: it is
11
+ * the only module in the package that touches a filesystem and belongs to the build step rather than
12
+ * to the consumers that read an artifact.
16
13
  *
17
- * The authoring loader (`./load.ts`, the `./load` subpath) is deliberately NOT re-exported here. It
18
- * is the only module in the package that touches a filesystem, and it belongs to the build step that
19
- * produces an artifact rather than to the consumers that read one. The first authored document
20
- * arrives with #1927; this entry point carries no data.
14
+ * Four things this package must never hold, each owned elsewhere:
21
15
  *
22
- * Four things this package must never hold, each owned elsewhere and each a rule the review
23
- * applies rather than a preference:
16
+ * 1. The resolver owns ranking policy. It defines weights, boosts, penalties and candidate order.
17
+ * The resolver package also owns rank order. The neural package and `@mailwoman/core/decoder` own
18
+ * the decode objective. This package creates observations and leaves candidate order to the resolver.
19
+ * 2. A second POI vocabulary — external and curated POI categories, their containment hierarchy, the
20
+ * Overture-leaf translation, the query-phrase lexicon and the brand table belong to
21
+ * `@mailwoman/poi-taxonomy`; this package maps into those identifiers.
22
+ * 3. A second coverage register — dataset identity and coverage epistemics belong to
23
+ * `@mailwoman/core/layers`, and an expected-but-absent observation becomes negative evidence only
24
+ * where `supportsExclusion` permits it there.
25
+ * 4. Empirical affordance statistics — this package owns the stable activity and affordance identifiers.
26
+ * It does not own numeric values fitted against those identifiers.
24
27
  *
25
- * 1. **Ranking policy.** No weights, boosts, penalties, or candidate-ordering API — not as a
26
- * function, not as a type. Candidate ordering belongs to `@mailwoman/resolver`, and the decode
27
- * objective to `@mailwoman/neural` plus `@mailwoman/core/decoder`. Knowledge here creates
28
- * observations; it never overrides learned interpretation.
29
- * 2. **A second POI vocabulary.** External and curated POI categories, their containment
30
- * hierarchy, the Overture-leaf translation, the query-phrase lexicon and the brand table all
31
- * belong to `@mailwoman/poi-taxonomy`. This package maps INTO those identifiers.
32
- * 3. **A second coverage register.** Dataset identity and coverage epistemics belong to
33
- * `@mailwoman/core/layers`. An expected-but-absent observation becomes negative evidence only
34
- * where `supportsExclusion` permits it there.
35
- * 4. **Empirical affordance statistics.** #1683 fits those. This package owns the stable
36
- * activity and affordance identifiers they are fitted against, and nothing numeric about them.
37
- *
38
- * `@mailwoman/core` must not depend on this package. Core ships the pipeline contract and roughly
39
- * 9 MB of reference data to every consumer, so a world-semantics dependency there is one every
40
- * drop-in API inherits without asking for it. Reversing that direction is an explicit amendment to
41
- * the boundary record, not a convenience during implementation.
42
- *
43
- * Boundary record: `docs/superpowers/specs/2026-08-26-geographic-model-boundaries.md` (#1917).
44
- * Program parent: #1916.
28
+ * `@mailwoman/core` must not depend on this package: core ships the pipeline interface and roughly
29
+ * 9 MB of reference data to every consumer, so a world-semantics dependency there is one every
30
+ * drop-in API inherits without asking.
45
31
  */
46
32
 
47
33
  export * from "#artifact"
@@ -49,3 +35,4 @@ export * from "#compile"
49
35
  export * from "#lookup"
50
36
  export * from "#schema"
51
37
  export * from "#validate"
38
+ export { type ValidationIssue, ValidationIssueCode } from "#validation/issues"