@mailwoman/geographic-model 9.4.0 → 10.1.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 (65) 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 +34 -62
  5. package/lib/compile.ts +52 -67
  6. package/lib/index.ts +21 -35
  7. package/lib/load.ts +55 -64
  8. package/lib/lookup.ts +12 -28
  9. package/lib/packaged.ts +49 -0
  10. package/lib/schema.ts +104 -97
  11. package/lib/validate.ts +24 -393
  12. package/lib/{validation-issues.ts → validation/issues.ts} +13 -17
  13. package/lib/validation/references.ts +361 -0
  14. package/out/artifact.d.ts +28 -45
  15. package/out/artifact.d.ts.map +1 -1
  16. package/out/artifact.js +23 -50
  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 +41 -63
  21. package/out/compile.js.map +1 -1
  22. package/out/index.d.ts +21 -35
  23. package/out/index.d.ts.map +1 -1
  24. package/out/index.js +21 -35
  25. package/out/index.js.map +1 -1
  26. package/out/load.d.ts +32 -41
  27. package/out/load.d.ts.map +1 -1
  28. package/out/load.js +41 -53
  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 +41 -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 +69 -32
  47. package/out/validate.d.ts.map +1 -1
  48. package/out/validate.js +15 -236
  49. package/out/validate.js.map +1 -1
  50. package/out/{validation-issues.d.ts → validation/issues.d.ts} +12 -15
  51. package/out/validation/issues.d.ts.map +1 -0
  52. package/out/{validation-issues.js → validation/issues.js} +12 -16
  53. package/out/validation/issues.js.map +1 -0
  54. package/out/validation/references.d.ts +10 -0
  55. package/out/validation/references.d.ts.map +1 -0
  56. package/out/validation/references.js +205 -0
  57. package/out/validation/references.js.map +1 -0
  58. package/package.json +34 -75
  59. package/tools/build-artifact.ts +56 -0
  60. package/out/scripts/build-artifact.d.ts +0 -51
  61. package/out/scripts/build-artifact.d.ts.map +0 -1
  62. package/out/scripts/build-artifact.js +0 -85
  63. package/out/scripts/build-artifact.js.map +0 -1
  64. package/out/validation-issues.d.ts.map +0 -1
  65. package/out/validation-issues.js.map +0 -1
package/README.md CHANGED
@@ -1,20 +1,20 @@
1
1
  # @mailwoman/geographic-model
2
2
 
3
- Authored geographic semantics for the [mailwoman](https://www.npmjs.com/package/mailwoman) geocoder: stable concepts (`pharmacy`, `obtain_medication`), the relations a curator can state between them (`pharmacy affords obtain_medication`), mappings into external vocabularies, source observations, and derived facts — every record carrying provenance, and all of it compiled deterministically into a lookup artifact the runtime reads.
3
+ Authored geographic semantics for the [mailwoman](https://www.npmjs.com/package/mailwoman) geocoder: stable concepts (`pharmacy`, `obtain_medication`), the relations a curator can state between them (`pharmacy affords obtain_medication`), mappings into external vocabularies, source observations, and derived facts. Every record carries provenance, and the package compiles all of it deterministically into a lookup artifact that the runtime reads.
4
4
 
5
- The design problem it exists to solve: a geocoder benefits from knowing that a pharmacy is a place where you obtain medication, but the moment that knowledge becomes a ranking rule, authored opinion starts overriding what the models learned from data. This package holds the knowledge in a shape that CANNOT become ranking policy — no numeric field exists anywhere in the schema, and the compiled artifact answers lookups, never orderings.
5
+ A geocoder benefits from knowing that a pharmacy is a place where you obtain medication. Once that knowledge becomes a ranking rule, however, authored opinion starts overriding what the models learned from data. This package stores the knowledge in a shape that cannot become ranking policy. The schema has no numeric field, and the compiled artifact answers lookups and never orderings.
6
6
 
7
7
  ## Should you install this?
8
8
 
9
- Probably not directly — not yet.
9
+ You probably should not install it directly yet.
10
10
 
11
- - If you want a geocoder, install [`mailwoman`](https://www.npmjs.com/package/mailwoman). It consumes this package where its experiments call for it.
12
- - **Do not depend on version `0.0.0`.** It exists to establish the package name. The first supported release ships with the next coordinated mailwoman release, and the API is unstable until a `1.x`.
13
- - What you can evaluate today: the schema, the validator, the compiler, and the committed artifact — all shipped as readable TypeScript source and JSON in this repository.
11
+ - If you want a geocoder, install [`mailwoman`](https://www.npmjs.com/package/mailwoman). It uses this package where its experiments need it.
12
+ - **Do not depend on version `0.0.0`.** That version exists to reserve the package name. The first supported release ships with the next coordinated mailwoman release, and the API is unstable until a `1.x`.
13
+ - You can evaluate the schema, the validator, the compiler, and the committed artifact today. All of them ship as readable TypeScript source and JSON in this repository.
14
14
 
15
15
  ## The shape of the data
16
16
 
17
- One document holds six tables, and all six are required — a hand-authored file writes `"derivedFacts": []` rather than leaving the table out, because an absent table and an empty table are different claims.
17
+ One document holds six tables, and all six are required. A hand-authored file writes `"derivedFacts": []` instead of leaving the table out, because an absent table and an empty table make different claims.
18
18
 
19
19
  | Table | Record | What it is |
20
20
  | -------------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
@@ -26,35 +26,35 @@ One document holds six tables, and all six are required — a hand-authored file
26
26
 
27
27
  Three properties hold by construction:
28
28
 
29
- - **No numeric field exists anywhere.** Not a strength, not a confidence, not a count. `Modality` is an ordinal vocabulary of words (`necessary`, `prohibited`, `strongly_expected`, … `strongly_unusual`) and the package exports no order over it, because a number on an authored relationship is a ranking weight whatever it is called.
30
- - **Authored, observed, and derived are three types, not three uses of one type.** Their identifiers carry separate brands, so one is not assignable where another is expected, and they live in separate tables so a curation decision has to be made deliberately rather than by a record sitting in a convenient place.
31
- - **A derived fact carries its provenance structurally.** It has no `source` field: its `derivation` plus its `inputs` are the provenance, and every input carries source provenance in turn. A source string can be copied onto a record that did not come from it; an input list either resolves or the document does not validate.
29
+ - **The schema has no numeric field.** It has no strength, confidence, or count. `Modality` is an ordinal vocabulary of words (`necessary`, `prohibited`, `strongly_expected`, … `strongly_unusual`), and the package exports no order over it, because a number on an authored relationship acts as a ranking weight whatever it is called.
30
+ - **Authored, observed, and derived records are three distinct types.** Their identifiers carry separate brands, so one cannot be assigned where another is expected. They live in separate tables, so a curator must make each curation decision deliberately instead of relying on where a record happens to sit.
31
+ - **A derived fact carries its provenance in its structure.** It has no `source` field. Its `derivation` and its `inputs` form the provenance, and every input carries source provenance in turn. A source string can be copied onto a record that did not come from that source, but an input list must resolve or the document fails validation.
32
32
 
33
- Identifiers are branded through `type-fest`'s `Tagged` and converted explicitly — `toConceptID`, `toRelationID`, `toRuleID`, `toMappingID`, `toObservationID`, `toDerivedFactID`. The brands are compile-time only; the strings survive JSON untouched.
33
+ Identifiers are branded through `type-fest`'s `Tagged` and converted explicitly with `toConceptID`, `toRelationID`, `toRuleID`, `toMappingID`, `toObservationID`, and `toDerivedFactID`. The brands exist only at compile time, and the strings pass through JSON unchanged.
34
34
 
35
35
  ## The validator
36
36
 
37
- `validateGeographicModelDocument(input)` returns the whole document or every reason it is not one. `parseGeographicModelDocument(input)` is the throwing form, and its `GeographicModelValidationError` states every violation in `error.message` as well as on `error.issues`, so a caller that only prints the message still sees all of them.
37
+ `validateGeographicModelDocument(input)` returns either the whole document or every reason it is invalid. `parseGeographicModelDocument(input)` is the throwing form. Its `GeographicModelValidationError` states every violation in `error.message` as well as on `error.issues`, so a caller that only prints the message still sees all of them.
38
38
 
39
- It reports **every** violation, each addressed by a JSONPath-style location such as `$.concepts[0].assertions[1].modality`. It never returns a partial document: a validator that drops the records it could not read, without reporting them, is a validator whose output is indistinguishable from a world that does not contain them.
39
+ The validator reports **every** violation, each with a JSONPath-style location such as `$.concepts[0].assertions[1].modality`. It never returns a partial document. If a validator silently dropped the records it could not read, its output would look the same as a world that did not contain those records.
40
40
 
41
- Two passes, both of which always run. **Shape** covers field presence and types, closed-vocabulary membership, and unknown keys — with a field whose name announces ranking policy (`score`, `boost`, `penalty`, `rankWeight`, and anything else matching the same fragments) reported under its own code rather than as an anonymous stray field. **Whole-table references** covers duplicate identifiers, `isA` self-reference and cycles, relation and concept resolution, relation domain and range kinds, inverse reciprocity, and derivation inputs.
41
+ The validator runs two passes, and both always run. The **shape** pass covers field presence and types, closed-vocabulary membership, and unknown keys. A field whose name suggests ranking policy (`score`, `boost`, `penalty`, `rankWeight`, and anything else matching the same fragments) is reported under its own code instead of as an anonymous stray field. The **whole-table references** pass covers duplicate identifiers, `isA` self-reference and cycles, relation and concept resolution, relation domain and range kinds, inverse reciprocity, and derivation inputs.
42
42
 
43
- It is plain deterministic TypeScript with no I/O: no reasoner, no query engine, no schema library.
43
+ The validator is plain deterministic TypeScript with no I/O. It uses no reasoner, query engine, or schema library.
44
44
 
45
45
  ## The loader, the compiler, and the artifact
46
46
 
47
- `loadGeographicModelDirectory(root)` (the `./load` subpath, the one module here that touches a filesystem) reads every `*.json` file under a directory and merges them into one document. The file layout is authoring convenience and carries no meaning; enumeration order cannot reach the output, and every validation issue names the file it came from.
47
+ `loadGeographicModelDirectory(root)` reads every `*.json` file under a directory and merges them into one document. It lives in the `./load` subpath and is the only module here that touches a filesystem. The file layout is an authoring convenience and carries no meaning. Enumeration order cannot affect the output, and every validation issue identifies the file it came from.
48
48
 
49
- `compileGeographicModel(input)` validates by delegation, then compiles. **`isA` alone defines semantic inheritance**: the artifact carries every concept's transitive ancestors and every ancestor's assertions materialized onto descendants as derived facts naming their derivation and inputs. A relation declaring `transitive` or `inverse` is **not** closed over — those fields say what the relation means, and general reasoning is excluded from this package for its lifetime.
49
+ `compileGeographicModel(input)` delegates validation and then compiles. **`isA` alone defines semantic inheritance.** The artifact carries every concept's transitive ancestors, and it copies every ancestor's assertions onto descendants as derived facts that record their derivation and inputs. The compiler does **not** compute closures for a relation that declares `transitive` or `inverse`. Those fields describe what the relation means, and general reasoning is excluded from this package for its lifetime.
50
50
 
51
- `serializeCompiledModel(model)` produces canonical bytes: keys in code-point order at every depth, tables ordered by identifier, and nothing recording when compilation ran — two builds of one document are byte-identical, so a regenerate is a diff only when the records changed.
51
+ `serializeCompiledModel(model)` produces canonical bytes. Keys appear in code-point order at every depth, tables are ordered by identifier, and no field records when compilation ran. Two builds of one document are byte-identical, so regenerating produces a diff only when the records changed.
52
52
 
53
- `createGeographicModelIndex(model)` (the `./lookup` subpath) is the read surface: `concept`, `relation`, `ancestorsOf`, `derivedFactsAbout`, `conceptsForExternalID`. Lookups only — no walk, no cursor, no query language, because removing query-time traversal is the reason the artifact exists. Two absences stay distinguishable throughout: a concept the artifact does not carry answers `undefined`, and a concept it carries with nothing derived about it answers an empty list.
53
+ `createGeographicModelIndex(model)` in the `./lookup` subpath is the read interface: `concept`, `relation`, `ancestorsOf`, `derivedFactsAbout`, `conceptsForExternalID`. It supports lookups only, without walks, cursors, or a query language, because the artifact exists to remove query-time traversal. Two kinds of absence stay distinguishable. A concept that the artifact does not carry answers `undefined`, and a carried concept with no derived facts answers an empty list.
54
54
 
55
55
  ## What is authored today
56
56
 
57
- The first slice, in [`data/model/`](./data/model/):
57
+ The first record set is in [`data/model/`](./data/model/):
58
58
 
59
59
  ```text
60
60
  place
@@ -64,47 +64,47 @@ pharmacy isA healthcare_facility affords obtain_medicatio
64
64
  activity
65
65
  obtain_medication isA activity
66
66
 
67
- affords establishment → activity, not transitive, not symmetric
67
+ affords establishment → activity, neither transitive nor symmetric
68
68
  poi-taxonomy pharmacy → the pharmacy concept
69
69
  ```
70
70
 
71
- [`data/geographic-model.json`](./data/geographic-model.json) is the committed compilation. **Do not hand-edit it** — regenerate:
71
+ [`data/geographic-model.json`](./data/geographic-model.json) is the committed compilation. **Do not hand-edit it.** Regenerate it instead:
72
72
 
73
73
  ```bash
74
- node packages/geographic-model/lib/scripts/build-artifact.ts && npx oxfmt packages/geographic-model/data/geographic-model.json
74
+ node packages/geographic-model/tools/build-artifact.ts && npx oxfmt packages/geographic-model/data/geographic-model.json
75
75
  ```
76
76
 
77
- [`data/PROVENANCE.md`](./data/PROVENANCE.md) records what each file states and where each external category id was read from. A reviewed amendment admitted a first breadth wave — a `drugstore` concept, a US-scoped `affords` assertion on it, and a second `poi-taxonomy` mapping — and all three are authored, under the same provenance discipline. One activity therefore reaches two establishment classes, which is what the schema was shaped to express and what the POI branch searches as a union.
77
+ [`data/PROVENANCE.md`](./data/PROVENANCE.md) records what each file states and where each external category id was read from. A reviewed amendment added a first breadth wave: a `drugstore` concept, a US-scoped `affords` assertion on it, and a second `poi-taxonomy` mapping. All three are authored under the same provenance rules. One activity therefore reaches two establishment classes. The schema was designed to express that case, and the POI branch searches the two classes as a union.
78
78
 
79
- Measured, not promised: with this one proposition injected behind an off-by-default flag, activity-phrased queries against the live geocoder moved from 0 of 4 answered to 3 of 4 (a pharmacy 0.41 km from the Denver anchor), with all 6 control queries unchanged. That measurement — pre-registered before the code existed, frozen by hash, decided against committed thresholds — is why the package continues to grow.
79
+ In a measurement, this one proposition was injected behind an off-by-default flag. Activity-phrased queries against the live geocoder went from 0 of 4 answered to 3 of 4 (a pharmacy 0.41 km from the Denver anchor), and all 6 control queries were unchanged. The measurement was pre-registered before the code existed, frozen by hash, and judged against committed thresholds. Its result is why the package continues to grow.
80
80
 
81
81
  ## Design commitments
82
82
 
83
- Each of these is owned elsewhere, and naming the owner is what keeps a second copy from growing here.
83
+ Each of these concerns belongs to another package. The ownership list prevents a second copy from growing here.
84
84
 
85
85
  | Not here | Owner |
86
86
  | ------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------- |
87
87
  | Relevance weights, boosts, penalties, any candidate-ordering API — even a type | `@mailwoman/resolver` (ordering), `@mailwoman/neural` + `@mailwoman/core/decoder` (the decode objective) |
88
88
  | POI categories, their containment hierarchy, the Overture-leaf translation, the query-phrase lexicon, brands | `@mailwoman/poi-taxonomy` |
89
89
  | Dataset identity and coverage epistemics | `@mailwoman/core/layers` |
90
- | Empirical, spatial activity-affordance statistics | Fitted from data elsewhere in the program — against the identifiers owned here |
90
+ | Empirical, spatial activity-affordance statistics | Fitted from data elsewhere in the program, against the identifiers owned here |
91
91
 
92
- Two rules are enforced by tests rather than assumed: `@mailwoman/core` must not depend on this package (core ships the pipeline contract plus ~9 MB of reference data to every consumer, and a world-semantics dependency there is one every drop-in API inherits without asking), and the public surface carries no ranking policy — a binding whose name announces a boost, penalty, weight, rank, score, or ordering fails the suite.
92
+ Tests enforce two rules. First, `@mailwoman/core` must not depend on this package. Core ships the pipeline interface plus ~9 MB of reference data to every consumer, so every drop-in API would inherit a world-semantics dependency added there. Second, the public API carries no ranking policy. A binding whose name suggests a boost, penalty, weight, rank, score, or ordering fails the suite.
93
93
 
94
- The operating rule for the whole boundary is one sentence: **knowledge creates observations; it never overrides learned interpretation.** A record here may create a fact, an anomaly, a contradiction, or a coverage-qualified absence. It may not create an imperative.
94
+ One rule governs the whole boundary: **knowledge creates observations; it never overrides learned interpretation.** A record here may create a fact, an anomaly, a contradiction, or a coverage-qualified absence. It may not create an imperative.
95
95
 
96
- Architecturally excluded for the life of the package: an OWL/DL reasoner, a SPARQL endpoint, a triplestore, a general-purpose knowledge-graph service, and any query-time traversal of the authoring JSON. Authored records are source material compiled into artifacts — artifacts, not a service.
96
+ The package architecture excludes the following for its whole life: an OWL/DL reasoner, a SPARQL endpoint, a triplestore, a general-purpose knowledge-graph service, and any query-time traversal of the authoring JSON. Authored records are source material that compiles into artifacts, and the package provides no service.
97
97
 
98
98
  ## Where the full design lives
99
99
 
100
100
  This README is the package-local summary. The authoritative documents are in the mailwoman repository:
101
101
 
102
- - The ownership boundary and the frozen first slice: [`docs/superpowers/specs/2026-08-26-geographic-model-boundaries.md`](https://github.com/sister-software/mailwoman/blob/main/docs/superpowers/specs/2026-08-26-geographic-model-boundaries.md)
102
+ - The ownership boundary and the frozen first record set: [`docs/superpowers/specs/2026-08-26-geographic-model-boundaries.md`](https://github.com/sister-software/mailwoman/blob/main/docs/superpowers/specs/2026-08-26-geographic-model-boundaries.md)
103
103
  - The program that governs growth, with its decision points: [sister-software/mailwoman#1916](https://github.com/sister-software/mailwoman/issues/1916)
104
104
 
105
105
  ## Layout
106
106
 
107
- Source lives at the workspace root. Tests live under `test/unit/` and reach the package through its package name, never a relative path. The manifest's `files` array declares `data/**/*.json` explicitly — `**/*.ts` does not cover JSON, and a data file absent from `files` is a package that installs without the data it exists to carry.
107
+ Source lives at the workspace root. Each test sits beside the module it covers under `lib/`. The manifest's `files` array declares `data/**/*.json` explicitly. `**/*.ts` does not match JSON, and a data file missing from `files` would leave the installed package without the data it exists to carry.
108
108
 
109
109
  ## License
110
110
 
@@ -74,7 +74,7 @@
74
74
  "label": "healthcare facility",
75
75
  "provenance": {
76
76
  "authoredAt": "2026-08-26",
77
- "notes": "The one intermediate class the frozen slice names between `pharmacy` and `establishment`. It carries no assertion of its own: a claim authored here would be inherited by every later healthcare class, which is breadth the program has not decided on.",
77
+ "notes": "The one intermediate class the frozen record set names between `pharmacy` and `establishment`. It carries no assertion of its own: a claim authored here would be inherited by every later healthcare class, which is breadth the program has not decided on.",
78
78
  "source": "mailwoman-curated",
79
79
  "sourceRecord": "docs/superpowers/specs/2026-08-26-geographic-model-boundaries.md §4",
80
80
  "sourceURL": "https://github.com/sister-software/mailwoman/issues/1927"
@@ -90,7 +90,7 @@
90
90
  "label": "obtaining medication",
91
91
  "provenance": {
92
92
  "authoredAt": "2026-08-26",
93
- "notes": "The frozen slice's activity, and a stable identifier this package owns. Issue #1683 may later fit empirical statistics against it; nothing numeric about it is authored here.",
93
+ "notes": "The frozen record set's activity, and a stable identifier this package owns. Issue #1683 may later fit empirical statistics against it; nothing numeric about it is authored here.",
94
94
  "source": "mailwoman-curated",
95
95
  "sourceRecord": "docs/superpowers/specs/2026-08-26-geographic-model-boundaries.md §4",
96
96
  "sourceURL": "https://github.com/sister-software/mailwoman/issues/1927"
@@ -104,7 +104,7 @@
104
104
  "modality": "necessary",
105
105
  "provenance": {
106
106
  "authoredAt": "2026-08-26",
107
- "notes": "The proposition the first slice exists to state. `necessary` because dispensing medication to the public is what makes premises a pharmacy rather than a neighbouring retail class — a counter-example falsifies this record instead of qualifying it. The relation is `defeasible`, which says whether `affords` assertions admit exceptions at all and is what lets a `strongly_expected` record sit beside this one coherently; it does not weaken this claim. No `countries` scope: the country-conditional part of the question is which OTHER establishment classes afford the activity, and each of those carries its own scope.",
107
+ "notes": "The proposition the first record set exists to state. `necessary` because dispensing medication to the public is what makes premises a pharmacy rather than a neighbouring retail class — a counter-example falsifies this record instead of qualifying it. The relation is `defeasible`, which says whether `affords` assertions admit exceptions at all and is what lets a `strongly_expected` record sit beside this one coherently; it does not weaken this claim. No `countries` scope: the country-conditional part of the question is which OTHER establishment classes afford the activity, and each of those carries its own scope.",
108
108
  "source": "mailwoman-curated",
109
109
  "sourceRecord": "docs/superpowers/specs/2026-08-26-geographic-model-boundaries.md §4",
110
110
  "sourceURL": "https://github.com/sister-software/mailwoman/issues/1927"
@@ -120,7 +120,7 @@
120
120
  "label": "pharmacy",
121
121
  "provenance": {
122
122
  "authoredAt": "2026-08-26",
123
- "notes": "The frozen slice's entity kind. Mapped into `@mailwoman/poi-taxonomy`'s `pharmacy` category by `mappings.json`; the mapping carries the external identifier, and this record carries what the class affords.",
123
+ "notes": "The frozen record set's entity kind. Mapped into `@mailwoman/poi-taxonomy`'s `pharmacy` category by `mappings.json`; the mapping carries the external identifier, and this record carries what the class affords.",
124
124
  "source": "mailwoman-curated",
125
125
  "sourceRecord": "docs/superpowers/specs/2026-08-26-geographic-model-boundaries.md §4",
126
126
  "sourceURL": "https://github.com/sister-software/mailwoman/issues/1927"
@@ -136,7 +136,7 @@
136
136
  "label": "place",
137
137
  "provenance": {
138
138
  "authoredAt": "2026-08-26",
139
- "notes": "The class an establishment is sited in. Authored because the frozen slice's entity kind needs one, and no wider upper ontology is authored around it.",
139
+ "notes": "The class an establishment is sited in. Authored because the frozen record set's entity kind needs one, and no wider upper ontology is authored around it.",
140
140
  "source": "mailwoman-curated",
141
141
  "sourceRecord": "docs/superpowers/specs/2026-08-26-geographic-model-boundaries.md §4",
142
142
  "sourceURL": "https://github.com/sister-software/mailwoman/issues/1927"
@@ -12,7 +12,7 @@
12
12
  "sourceRecord": "docs/superpowers/specs/2026-08-26-geographic-model-boundaries.md §4",
13
13
  "sourceURL": "https://github.com/sister-software/mailwoman/issues/1927",
14
14
  "authoredAt": "2026-08-26",
15
- "notes": "The class an establishment is sited in. Authored because the frozen slice's entity kind needs one, and no wider upper ontology is authored around it."
15
+ "notes": "The class an establishment is sited in. Authored because the frozen record set's entity kind needs one, and no wider upper ontology is authored around it."
16
16
  },
17
17
  "status": "active"
18
18
  },
@@ -44,7 +44,7 @@
44
44
  "sourceRecord": "docs/superpowers/specs/2026-08-26-geographic-model-boundaries.md §4",
45
45
  "sourceURL": "https://github.com/sister-software/mailwoman/issues/1927",
46
46
  "authoredAt": "2026-08-26",
47
- "notes": "The one intermediate class the frozen slice names between `pharmacy` and `establishment`. It carries no assertion of its own: a claim authored here would be inherited by every later healthcare class, which is breadth the program has not decided on."
47
+ "notes": "The one intermediate class the frozen record set names between `pharmacy` and `establishment`. It carries no assertion of its own: a claim authored here would be inherited by every later healthcare class, which is breadth the program has not decided on."
48
48
  },
49
49
  "status": "active"
50
50
  },
@@ -65,7 +65,7 @@
65
65
  "sourceRecord": "docs/superpowers/specs/2026-08-26-geographic-model-boundaries.md §4",
66
66
  "sourceURL": "https://github.com/sister-software/mailwoman/issues/1927",
67
67
  "authoredAt": "2026-08-26",
68
- "notes": "The proposition the first slice exists to state. `necessary` because dispensing medication to the public is what makes premises a pharmacy rather than a neighbouring retail class — a counter-example falsifies this record instead of qualifying it. The relation is `defeasible`, which says whether `affords` assertions admit exceptions at all and is what lets a `strongly_expected` record sit beside this one coherently; it does not weaken this claim. No `countries` scope: the country-conditional part of the question is which OTHER establishment classes afford the activity, and each of those carries its own scope."
68
+ "notes": "The proposition the first record set exists to state. `necessary` because dispensing medication to the public is what makes premises a pharmacy rather than a neighbouring retail class — a counter-example falsifies this record instead of qualifying it. The relation is `defeasible`, which says whether `affords` assertions admit exceptions at all and is what lets a `strongly_expected` record sit beside this one coherently; it does not weaken this claim. No `countries` scope: the country-conditional part of the question is which OTHER establishment classes afford the activity, and each of those carries its own scope."
69
69
  }
70
70
  }
71
71
  ],
@@ -74,7 +74,7 @@
74
74
  "sourceRecord": "docs/superpowers/specs/2026-08-26-geographic-model-boundaries.md §4",
75
75
  "sourceURL": "https://github.com/sister-software/mailwoman/issues/1927",
76
76
  "authoredAt": "2026-08-26",
77
- "notes": "The frozen slice's entity kind. Mapped into `@mailwoman/poi-taxonomy`'s `pharmacy` category by `mappings.json`; the mapping carries the external identifier, and this record carries what the class affords."
77
+ "notes": "The frozen record set's entity kind. Mapped into `@mailwoman/poi-taxonomy`'s `pharmacy` category by `mappings.json`; the mapping carries the external identifier, and this record carries what the class affords."
78
78
  },
79
79
  "status": "active"
80
80
  },
@@ -139,7 +139,7 @@
139
139
  "sourceRecord": "docs/superpowers/specs/2026-08-26-geographic-model-boundaries.md §4",
140
140
  "sourceURL": "https://github.com/sister-software/mailwoman/issues/1927",
141
141
  "authoredAt": "2026-08-26",
142
- "notes": "The frozen slice's activity, and a stable identifier this package owns. Issue #1683 may later fit empirical statistics against it; nothing numeric about it is authored here."
142
+ "notes": "The frozen record set's activity, and a stable identifier this package owns. Issue #1683 may later fit empirical statistics against it; nothing numeric about it is authored here."
143
143
  },
144
144
  "status": "active"
145
145
  }
package/lib/artifact.ts CHANGED
@@ -3,31 +3,17 @@
3
3
  * @license AGPL-3.0
4
4
  * @author Teffen Ellis, et al.
5
5
  *
6
- * The compiled runtime artifact: its shape, its canonical bytes, and the reader that turns those
7
- * bytes back into a typed value.
6
+ * Defines the compiled geographic model artifact, its canonical serialization and its reader.
8
7
  *
9
- * A runtime consumer reads THIS and never the authored records. That is the whole point of the
10
- * artifact, and it is why the shape carries every table plus the materialized `isA` closure: a
11
- * consumer that had to walk `concepts[].isA` to answer a question about one concept would be doing
12
- * query-time traversal, which the boundary record excludes.
8
+ * Runtime consumers read the artifact instead of the authored records. The artifact includes the
9
+ * materialized `isA` closure so that consumers never traverse `concepts[].isA` at query time.
13
10
  *
14
- * Two determinism rules define the bytes, and both are stated so a reader can check them:
15
- *
16
- * 1. **Every object's keys are emitted in code-point order**, at every depth. A rule that
17
- * canonicalizes by itself beats a hand-kept field order, which drifts the first time the schema
18
- * gains a field.
19
- * 2. **Every table is ordered by identifier**, under {@link compareIdentifiers} — code point, never
20
- * `localeCompare`, whose answer depends on the machine's collation. Arrays INSIDE a record keep
21
- * the order they were authored in; the compiler writes the tables, so it orders those.
22
- *
23
- * Nothing here records when compilation ran. `modelVersion` is the authored document's own version,
24
- * so two builds of one document are byte-identical, and a clock in the artifact would make every
25
- * regenerate a diff.
26
- *
27
- * Boundary record: `docs/superpowers/specs/2026-08-26-geographic-model-boundaries.md` (#1917).
11
+ * Serialization is deterministic. Object keys use code-point order at every depth.
12
+ * The compiler sorts each table by identifier in code-point order. Arrays inside a record keep authored order.
13
+ * The artifact records no build time. Two builds of one document are byte-identical.
28
14
  */
29
15
 
30
- import { prettyJSON } from "@mailwoman/core/json"
16
+ import { prettyJSON, stringifyJSON } from "@mailwoman/core/json"
31
17
  import { isPlainObject } from "@mailwoman/core/objects"
32
18
  import { compareByCodePoint } from "@mailwoman/core/strings/compare"
33
19
 
@@ -41,19 +27,18 @@ import type {
41
27
  } from "#schema"
42
28
 
43
29
  /**
44
- * The artifact FORMAT version — incremented by hand when the compiled shape changes, so a reader meeting an artifact it
45
- * was not written for says so instead of quietly reading fields that moved.
30
+ * The artifact format version.
46
31
  *
47
- * It is not data about the world, which is why a number is allowed here and nowhere in `./schema.ts`.
32
+ * Increment it whenever the compiled shape changes, so that a reader rejects
33
+ * an artifact it cannot interpret.
48
34
  */
49
35
  export const ARTIFACT_SCHEMA_VERSION = 1
50
36
 
51
37
  /**
52
- * One concept's transitive `isA` ancestors, materialized so a consumer answers "what is this a kind of" with a lookup.
38
+ * One concept's transitive `isA` ancestors.
53
39
  *
54
- * Every concept gets an entry, including one with no ancestors at all: an entry with an empty list says the concept was
55
- * compiled and is a kind of nothing, and a missing entry would say the same thing while also being what an absent
56
- * concept looks like.
40
+ * Every compiled concept has an entry, even with an empty ancestor list,
41
+ * so a missing entry always means an unknown concept.
57
42
  */
58
43
  export interface InheritanceClosureEntry {
59
44
  concept: ConceptID
@@ -61,18 +46,18 @@ export interface InheritanceClosureEntry {
61
46
  }
62
47
 
63
48
  /**
64
- * The whole compiled artifact.
49
+ * The compiled artifact.
65
50
  *
66
- * The five authored tables travel unchanged — the compiler orders them and rewrites no record — beside the two tables
67
- * it computes: {@link InheritanceClosureEntry} per concept, and the derived facts the closure materializes.
51
+ * The compiler sorts the authored tables without changing any record.
52
+ * It adds the inheritance closure and the derived facts that it computes.
68
53
  */
69
54
  export interface CompiledGeographicModel {
70
55
  /**
71
- * The format version of this artifact — {@link ARTIFACT_SCHEMA_VERSION} at the time it was written.
56
+ * The format version, equal to {@link ARTIFACT_SCHEMA_VERSION} when the artifact was written.
72
57
  */
73
58
  schemaVersion: number
74
59
  /**
75
- * The authored document's own `version`. Never a build date.
60
+ * The authored document's `version`.
76
61
  */
77
62
  modelVersion: string
78
63
  relations: readonly RelationRecord[]
@@ -81,22 +66,16 @@ export interface CompiledGeographicModel {
81
66
  observations: readonly SourceObservationRecord[]
82
67
  inheritanceClosure: readonly InheritanceClosureEntry[]
83
68
  /**
84
- * The authored derived facts, plus every fact the compiler's derivations produced. Each names the procedure that
85
- * wrote it and every record that procedure read.
69
+ * The authored derived facts plus the facts that the compiler derived.
70
+ *
71
+ * Each fact records its procedure and every record that the procedure read.
86
72
  */
87
73
  derivedFacts: readonly DerivedFactRecord[]
88
74
  }
89
75
 
90
76
  /**
91
- * The artifact's stated order: UTF-16 code point, ascending.
92
- *
93
- * `String.prototype.localeCompare` is the trap this exists to avoid — its answer depends on the machine's collation, so
94
- * an artifact ordered with it is reproducible only on the machine that built it.
95
- */
96
-
97
- /**
98
- * Rebuild `value` with every object's keys in code-point order, at every depth. Arrays keep their order — ordering
99
- * those is the compiler's job, and doing it here would silently reorder authored data.
77
+ * Copies `value` with object keys in code-point order at every depth and drops `undefined` entries.
78
+ * Arrays keep their order.
100
79
  */
101
80
  function canonicalize(value: unknown): unknown {
102
81
  if (Array.isArray(value)) return value.map(canonicalize)
@@ -117,12 +96,11 @@ function canonicalize(value: unknown): unknown {
117
96
  }
118
97
 
119
98
  /**
120
- * The artifact's canonical bytes: tab-indented, one trailing newline, keys in code-point order.
99
+ * Serializes the artifact as tab-indented JSON with a trailing newline and sorted keys.
121
100
  *
122
- * Tab indentation and the trailing newline match the repository's other committed JSON tables (`taxonomy.json`,
123
- * `brands.json`). A committed copy of these bytes is the generator's output run through `oxfmt`, which inlines short
124
- * arrays — so a freshness check compares the PARSED artifact against a fresh compile, and a byte comparison compares
125
- * two compiles.
101
+ * The committed copy also passes through `oxfmt`, which inlines short arrays.
102
+ * A freshness check compares parsed values.
103
+ * A byte comparison applies only to two compiler outputs.
126
104
  */
127
105
  export function serializeCompiledModel(model: CompiledGeographicModel): string {
128
106
  return prettyJSON(canonicalize(model))
@@ -140,8 +118,7 @@ export class GeographicModelArtifactError extends Error {
140
118
  }
141
119
 
142
120
  /**
143
- * The tables every artifact carries. A reader that finds one missing refuses rather than answering every question about
144
- * that table with an empty list.
121
+ * The tables that every artifact must contain.
145
122
  */
146
123
  const ARTIFACT_TABLES = [
147
124
  "concepts",
@@ -153,16 +130,13 @@ const ARTIFACT_TABLES = [
153
130
  ] as const
154
131
 
155
132
  /**
156
- * Why `value` is not a compiled artifact, or nothing when it is one.
157
- *
158
- * Kept separate from {@link parseCompiledGeographicModel} so the reader casts the value it was HANDED, still typed
159
- * `unknown`, rather than a narrowed shape it would have to launder through a second cast to widen again.
133
+ * Returns the reason `value` is not a compiled artifact, or `undefined` when it is one.
160
134
  */
161
135
  function artifactProblem(value: unknown): string | undefined {
162
136
  if (!isPlainObject(value)) return "a compiled geographic model must be an object"
163
137
 
164
138
  if (value.schemaVersion !== ARTIFACT_SCHEMA_VERSION) {
165
- return `this reader understands artifact schema version ${ARTIFACT_SCHEMA_VERSION}; the artifact declares ${JSON.stringify(value.schemaVersion)}`
139
+ return `this reader understands artifact schema version ${ARTIFACT_SCHEMA_VERSION}; the artifact declares ${stringifyJSON(value.schemaVersion)}`
166
140
  }
167
141
 
168
142
  if (typeof value.modelVersion !== "string" || !value.modelVersion.trim().length) {
@@ -177,13 +151,11 @@ function artifactProblem(value: unknown): string | undefined {
177
151
  }
178
152
 
179
153
  /**
180
- * Read a parsed artifact — the value `JSON.parse` produced from the compiled bytes — as a
181
- * {@link CompiledGeographicModel}.
154
+ * Reads a value from `JSON.parse` as a {@link CompiledGeographicModel}.
182
155
  *
183
- * It checks the format version and the presence of every table, and does NOT re-validate the records. An artifact is
184
- * generated from a document that `parseGeographicModelDocument` already accepted; re-checking every record here would
185
- * be a second validator, and the version check is what catches the failure this reader can actually meet — an artifact
186
- * written by a different compiler.
156
+ * The reader checks the format version, the model version and the presence of every table.
157
+ * It does not revalidate records, because the compiler accepts only documents
158
+ * that already passed validation.
187
159
  */
188
160
  export function parseCompiledGeographicModel(input: unknown): CompiledGeographicModel {
189
161
  const problem = artifactProblem(input)