@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.
- package/README.md +32 -32
- package/data/geographic-model.json +5 -5
- package/data/model/concepts.json +5 -5
- package/lib/artifact.ts +34 -62
- package/lib/compile.ts +52 -67
- package/lib/index.ts +21 -35
- package/lib/load.ts +55 -64
- package/lib/lookup.ts +12 -28
- package/lib/packaged.ts +49 -0
- package/lib/schema.ts +104 -97
- package/lib/validate.ts +24 -393
- package/lib/{validation-issues.ts → validation/issues.ts} +13 -17
- package/lib/validation/references.ts +361 -0
- package/out/artifact.d.ts +28 -45
- package/out/artifact.d.ts.map +1 -1
- package/out/artifact.js +23 -50
- package/out/artifact.js.map +1 -1
- package/out/compile.d.ts +27 -41
- package/out/compile.d.ts.map +1 -1
- package/out/compile.js +41 -63
- package/out/compile.js.map +1 -1
- package/out/index.d.ts +21 -35
- package/out/index.d.ts.map +1 -1
- package/out/index.js +21 -35
- package/out/index.js.map +1 -1
- package/out/load.d.ts +32 -41
- package/out/load.d.ts.map +1 -1
- package/out/load.js +41 -53
- package/out/load.js.map +1 -1
- package/out/lookup.d.ts +12 -28
- package/out/lookup.d.ts.map +1 -1
- package/out/lookup.js +4 -16
- package/out/lookup.js.map +1 -1
- package/out/packaged.d.ts +24 -0
- package/out/packaged.d.ts.map +1 -0
- package/out/packaged.js +41 -0
- package/out/packaged.js.map +1 -0
- package/out/schema.d.ts +104 -97
- package/out/schema.d.ts.map +1 -1
- package/out/schema.js +37 -52
- package/out/schema.js.map +1 -1
- package/out/tools/build-artifact.d.ts +27 -0
- package/out/tools/build-artifact.d.ts.map +1 -0
- package/out/tools/build-artifact.js +44 -0
- package/out/tools/build-artifact.js.map +1 -0
- package/out/validate.d.ts +69 -32
- package/out/validate.d.ts.map +1 -1
- package/out/validate.js +15 -236
- package/out/validate.js.map +1 -1
- package/out/{validation-issues.d.ts → validation/issues.d.ts} +12 -15
- package/out/validation/issues.d.ts.map +1 -0
- package/out/{validation-issues.js → validation/issues.js} +12 -16
- package/out/validation/issues.js.map +1 -0
- package/out/validation/references.d.ts +10 -0
- package/out/validation/references.d.ts.map +1 -0
- package/out/validation/references.js +205 -0
- package/out/validation/references.js.map +1 -0
- package/package.json +34 -75
- package/tools/build-artifact.ts +56 -0
- package/out/scripts/build-artifact.d.ts +0 -51
- package/out/scripts/build-artifact.d.ts.map +0 -1
- package/out/scripts/build-artifact.js +0 -85
- package/out/scripts/build-artifact.js.map +0 -1
- package/out/validation-issues.d.ts.map +0 -1
- 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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
12
|
-
- **Do not depend on version `0.0.0`.**
|
|
13
|
-
-
|
|
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
|
|
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
|
-
- **
|
|
30
|
-
- **Authored, observed, and derived are three types
|
|
31
|
-
- **A derived fact carries its provenance
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)`
|
|
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)`
|
|
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
|
|
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)`
|
|
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
|
|
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,
|
|
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
|
|
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/
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
90
|
+
| Empirical, spatial activity-affordance statistics | Fitted from data elsewhere in the program, against the identifiers owned here |
|
|
91
91
|
|
|
92
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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"
|
package/data/model/concepts.json
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
7
|
-
* bytes back into a typed value.
|
|
6
|
+
* Defines the compiled geographic model artifact, its canonical serialization and its reader.
|
|
8
7
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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
|
|
45
|
-
* was not written for says so instead of quietly reading fields that moved.
|
|
30
|
+
* The artifact format version.
|
|
46
31
|
*
|
|
47
|
-
*
|
|
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
|
|
38
|
+
* One concept's transitive `isA` ancestors.
|
|
53
39
|
*
|
|
54
|
-
* Every concept
|
|
55
|
-
*
|
|
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
|
|
49
|
+
* The compiled artifact.
|
|
65
50
|
*
|
|
66
|
-
* The
|
|
67
|
-
*
|
|
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
|
|
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
|
|
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
|
|
85
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
99
|
+
* Serializes the artifact as tab-indented JSON with a trailing newline and sorted keys.
|
|
121
100
|
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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 ${
|
|
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
|
-
*
|
|
181
|
-
* {@link CompiledGeographicModel}.
|
|
154
|
+
* Reads a value from `JSON.parse` as a {@link CompiledGeographicModel}.
|
|
182
155
|
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
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)
|