@mailwoman/geographic-model 0.0.0 → 9.3.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 +34 -78
- package/data/geographic-model.json +54 -3
- package/data/model/concepts.json +34 -1
- package/data/model/mappings.json +14 -0
- package/data/model/model.json +1 -1
- package/data/model/relations.json +1 -1
- package/{artifact.ts → lib/artifact.ts} +8 -11
- package/{compile.ts → lib/compile.ts} +3 -3
- package/{index.ts → lib/index.ts} +5 -5
- package/{load.ts → lib/load.ts} +17 -12
- package/{lookup.ts → lib/lookup.ts} +2 -2
- package/{schema.ts → lib/schema.ts} +3 -3
- package/{validate.ts → lib/validate.ts} +5 -4
- package/{validation-issues.ts → lib/validation-issues.ts} +0 -4
- package/out/artifact.d.ts +2 -2
- package/out/artifact.d.ts.map +1 -1
- package/out/artifact.js +6 -10
- package/out/artifact.js.map +1 -1
- package/out/compile.d.ts +1 -1
- package/out/compile.d.ts.map +1 -1
- package/out/compile.js +3 -3
- package/out/compile.js.map +1 -1
- package/out/index.d.ts +5 -5
- package/out/index.d.ts.map +1 -1
- package/out/index.js +5 -5
- package/out/index.js.map +1 -1
- package/out/load.d.ts +2 -2
- package/out/load.d.ts.map +1 -1
- package/out/load.js +15 -10
- package/out/load.js.map +1 -1
- package/out/lookup.d.ts +2 -2
- package/out/lookup.d.ts.map +1 -1
- package/out/lookup.js +1 -1
- package/out/lookup.js.map +1 -1
- package/out/schema.d.ts +3 -3
- package/out/schema.d.ts.map +1 -1
- package/out/schema.js.map +1 -1
- package/out/scripts/build-artifact.d.ts +6 -6
- package/out/scripts/build-artifact.d.ts.map +1 -1
- package/out/scripts/build-artifact.js +26 -19
- package/out/scripts/build-artifact.js.map +1 -1
- package/out/validate.d.ts +3 -3
- package/out/validate.d.ts.map +1 -1
- package/out/validate.js +4 -3
- package/out/validate.js.map +1 -1
- package/out/validation-issues.d.ts +0 -1
- package/out/validation-issues.d.ts.map +1 -1
- package/out/validation-issues.js +0 -3
- package/out/validation-issues.js.map +1 -1
- package/package.json +37 -5
package/README.md
CHANGED
|
@@ -1,21 +1,18 @@
|
|
|
1
1
|
# @mailwoman/geographic-model
|
|
2
2
|
|
|
3
|
-
|
|
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.
|
|
4
4
|
|
|
5
|
-
|
|
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.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## Should you install this?
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Probably not directly — not yet.
|
|
10
10
|
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
- Source observations, kept separate from derived facts.
|
|
15
|
-
- Derivation provenance on every mapping and every derived fact.
|
|
16
|
-
- Deterministic compilation of the authored records into runtime artifacts, and the validation that refuses a record set which does not compile.
|
|
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.
|
|
17
14
|
|
|
18
|
-
## The
|
|
15
|
+
## The shape of the data
|
|
19
16
|
|
|
20
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.
|
|
21
18
|
|
|
@@ -33,64 +30,31 @@ Three properties hold by construction:
|
|
|
33
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.
|
|
34
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.
|
|
35
32
|
|
|
36
|
-
Identifiers are branded through `type-fest`'s `Tagged` and converted explicitly — `toConceptID`, `toRelationID`, `toRuleID`, `toMappingID`, `toObservationID`, `toDerivedFactID
|
|
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.
|
|
37
34
|
|
|
38
35
|
## The validator
|
|
39
36
|
|
|
40
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.
|
|
41
38
|
|
|
42
|
-
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
|
|
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.
|
|
43
40
|
|
|
44
|
-
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`,
|
|
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.
|
|
45
42
|
|
|
46
|
-
It is plain deterministic TypeScript with no I/O
|
|
43
|
+
It is plain deterministic TypeScript with no I/O: no reasoner, no query engine, no schema library.
|
|
47
44
|
|
|
48
|
-
## The loader
|
|
45
|
+
## The loader, the compiler, and the artifact
|
|
49
46
|
|
|
50
|
-
`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.
|
|
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.
|
|
51
48
|
|
|
52
|
-
|
|
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.
|
|
53
50
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
- **Enumeration order cannot reach the output.** The files are sorted by path before any of them is read, so the merged tables are a function of the file names and their contents, never of `readdir` order. `mergeGeographicModelFiles(files)` states the same property without a filesystem, which is how it is tested: any order in, one document out.
|
|
57
|
-
- **Every issue names the file it came from.** The validator addresses a record by its position in the merged table (`$.concepts[7].kind`) — the one address an author cannot see — so the loader keeps a per-record origin and re-addresses each issue. A duplicate identifier names **both** files, the one that claimed it and the one that claimed it first, because "already used" is unactionable without the other half.
|
|
58
|
-
|
|
59
|
-
What the loader checks on its own is only what the validator cannot see: whether a file parses, whether it is an object, and whether the keys it uses are tables. Everything else is delegated whole.
|
|
60
|
-
|
|
61
|
-
## The compiler
|
|
62
|
-
|
|
63
|
-
`compileGeographicModel(input)` validates by delegation — `parseGeographicModelDocument` decides whether a document is well formed, and throws with every violation before a byte is computed — then compiles it into a `CompiledGeographicModel`. There is no second validator, and no partial artifact: a compile produces the whole thing or produces nothing.
|
|
64
|
-
|
|
65
|
-
**`isA` alone defines semantic inheritance**, and two derivations follow from it:
|
|
66
|
-
|
|
67
|
-
| In the artifact | What it is |
|
|
68
|
-
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
69
|
-
| `inheritanceClosure` | Every concept's transitive `isA` ancestors, deduplicated and ordered. One entry per concept, empty list included — a concept that is a kind of nothing says so |
|
|
70
|
-
| `derivedFacts` | Every ancestor's assertions materialized onto its descendants as `DerivedFactRecord`s, naming the derivation and every record it read |
|
|
71
|
-
|
|
72
|
-
Materializing the assertions is what makes the artifact answer a question rather than point at one: the closure alone would tell a consumer which concepts to go and read, which is the traversal it was supposed to be spared. A descendant that states the same relation and target itself inherits nothing for that pair — the authored record is the more specific one, which is what `isA` means.
|
|
73
|
-
|
|
74
|
-
A relation declaring `transitive` or `inverse` is **not** closed over. Those fields say what the relation means; materializing them is a reasoning step no executable need has asked for, and general reasoning is excluded from this package. The day one is needed it arrives as its own named derivation beside this one.
|
|
75
|
-
|
|
76
|
-
Compilation refuses two things the validator cannot see, both about records the compiler is about to write, and both reported with every offending record named: an inherited assertion whose subject kind the relation does not accept, and two derived facts claiming one identifier.
|
|
77
|
-
|
|
78
|
-
## The artifact
|
|
79
|
-
|
|
80
|
-
`serializeCompiledModel(model)` produces the canonical bytes. Two rules define them:
|
|
81
|
-
|
|
82
|
-
- **Every object's keys are emitted in code-point order**, at every depth. A rule that canonicalizes by itself beats a hand-kept field order, which drifts the first time the schema gains a field.
|
|
83
|
-
- **Every table is ordered by identifier**, under `compareIdentifiers` — code point, never `localeCompare`, whose answer depends on the machine's collation. Arrays inside a record keep the order they were authored in.
|
|
84
|
-
|
|
85
|
-
Nothing records when compilation ran: `modelVersion` is the authored document's own version, so two builds of one document are byte-identical and a regenerate is a diff only when the records changed. `schemaVersion` is the artifact FORMAT version, and `parseCompiledGeographicModel` refuses an artifact declaring another one rather than reading fields that may have moved.
|
|
86
|
-
|
|
87
|
-
A committed artifact is these bytes run through `oxfmt`, which inlines short arrays — the same convention `taxonomy.json` follows. So a freshness check compares the **parsed** artifact against a fresh compile, and a byte comparison compares two compiles.
|
|
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.
|
|
88
52
|
|
|
89
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.
|
|
90
54
|
|
|
91
|
-
##
|
|
55
|
+
## What is authored today
|
|
92
56
|
|
|
93
|
-
|
|
57
|
+
The first slice, in [`data/model/`](./data/model/):
|
|
94
58
|
|
|
95
59
|
```text
|
|
96
60
|
place
|
|
@@ -100,23 +64,21 @@ pharmacy isA healthcare_facility affords obtain_medicatio
|
|
|
100
64
|
activity
|
|
101
65
|
obtain_medication isA activity
|
|
102
66
|
|
|
103
|
-
affords establishment → activity,
|
|
67
|
+
affords establishment → activity, not transitive, not symmetric
|
|
104
68
|
poi-taxonomy pharmacy → the pharmacy concept
|
|
105
69
|
```
|
|
106
70
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
Deliberately absent, and each absence is a statement rather than an omission: no source observations, no hand-authored derived facts, no `countries` scope, and no second establishment class. `isA` inheritance materializes nothing here, because the one assertion sits on `pharmacy` and `pharmacy` has no descendants.
|
|
110
|
-
|
|
111
|
-
[`data/geographic-model.json`](./data/geographic-model.json) is the committed compilation of those records. **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:
|
|
112
72
|
|
|
113
73
|
```bash
|
|
114
|
-
node packages/geographic-model/scripts/build-artifact.ts && npx oxfmt packages/geographic-model/data/geographic-model.json
|
|
74
|
+
node packages/geographic-model/lib/scripts/build-artifact.ts && npx oxfmt packages/geographic-model/data/geographic-model.json
|
|
115
75
|
```
|
|
116
76
|
|
|
117
|
-
[`data/PROVENANCE.md`](./data/PROVENANCE.md) records what each file states
|
|
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.
|
|
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.
|
|
118
80
|
|
|
119
|
-
##
|
|
81
|
+
## Design commitments
|
|
120
82
|
|
|
121
83
|
Each of these is owned elsewhere, and naming the owner is what keeps a second copy from growing here.
|
|
122
84
|
|
|
@@ -125,30 +87,24 @@ Each of these is owned elsewhere, and naming the owner is what keeps a second co
|
|
|
125
87
|
| Relevance weights, boosts, penalties, any candidate-ordering API — even a type | `@mailwoman/resolver` (ordering), `@mailwoman/neural` + `@mailwoman/core/decoder` (the decode objective) |
|
|
126
88
|
| POI categories, their containment hierarchy, the Overture-leaf translation, the query-phrase lexicon, brands | `@mailwoman/poi-taxonomy` |
|
|
127
89
|
| Dataset identity and coverage epistemics | `@mailwoman/core/layers` |
|
|
128
|
-
| Empirical, spatial activity-affordance statistics |
|
|
129
|
-
|
|
130
|
-
Two rules follow from that table and are enforced rather than assumed:
|
|
90
|
+
| Empirical, spatial activity-affordance statistics | Fitted from data elsewhere in the program — against the identifiers owned here |
|
|
131
91
|
|
|
132
|
-
|
|
133
|
-
- **The public surface carries no ranking policy.** The same test reads whatever the entry point exports and refuses a binding whose name announces a boost, a penalty, a weight, a rank, a score, or an ordering.
|
|
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.
|
|
134
93
|
|
|
135
|
-
The operating rule for the whole boundary is one sentence
|
|
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.
|
|
136
95
|
|
|
137
|
-
Architecturally excluded for the life of the
|
|
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.
|
|
138
97
|
|
|
139
|
-
##
|
|
98
|
+
## Where the full design lives
|
|
140
99
|
|
|
141
|
-
|
|
100
|
+
This README is the package-local summary. The authoritative documents are in the mailwoman repository:
|
|
142
101
|
|
|
143
|
-
The
|
|
144
|
-
|
|
145
|
-
When the package is blessed and its Trusted Publisher is on file, the change is three edits in one commit: add `packages/geographic-model` to `.release-it.json`, remove its entry from `SANCTIONED_RELEASE_ABSENCES`, and update the arithmetic in `AGENTS.md` and `scripts/release-stage.test.ts`.
|
|
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)
|
|
103
|
+
- The program that governs growth, with its decision points: [sister-software/mailwoman#1916](https://github.com/sister-software/mailwoman/issues/1916)
|
|
146
104
|
|
|
147
105
|
## Layout
|
|
148
106
|
|
|
149
|
-
Source lives at the workspace root
|
|
150
|
-
|
|
151
|
-
The manifest's `files` array already declares `data/**/*.json`. Authored records land there, and a glob written now cannot be the one a later publish forgets: `**/*.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. 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.
|
|
152
108
|
|
|
153
109
|
## License
|
|
154
110
|
|
|
@@ -16,6 +16,39 @@
|
|
|
16
16
|
},
|
|
17
17
|
"status": "active"
|
|
18
18
|
},
|
|
19
|
+
{
|
|
20
|
+
"assertions": [
|
|
21
|
+
{
|
|
22
|
+
"countries": ["US"],
|
|
23
|
+
"id": "drugstore-affords-obtain-medication",
|
|
24
|
+
"modality": "strongly_expected",
|
|
25
|
+
"provenance": {
|
|
26
|
+
"authoredAt": "2026-08-27",
|
|
27
|
+
"notes": "The committed curator statement `{ \"phrase\": \"drugstore\", \"categoryID\": \"pharmacy\", \"locales\": [\"en-US\"] }` says that in en-US this class names the pharmacy category. `strongly_expected` rather than `necessary`: the statement says a US drugstore characteristically dispenses, and neither a locale-scoped synonym nor a row count is a census of dispensing, so a counter-example qualifies this record rather than falsifying it. Scoped to `US` because that is the only country a committed record scopes the class to — CA (369 rows), GB (117) and MX (3) bound a population and assert nothing about what those premises afford, and FR is a measured zero on the shipped layer, so there would be nothing there for the claim to range over.",
|
|
28
|
+
"source": "mailwoman-curated",
|
|
29
|
+
"sourceRecord": "packages/poi-taxonomy/data/curated-overlay.json synonyms[drugstore → pharmacy, locales en-US]",
|
|
30
|
+
"sourceURL": "https://github.com/sister-software/mailwoman/issues/1963",
|
|
31
|
+
"sourceVersion": "poi-taxonomy table 0.4.0"
|
|
32
|
+
},
|
|
33
|
+
"relation": "affords",
|
|
34
|
+
"target": "obtain_medication"
|
|
35
|
+
}
|
|
36
|
+
],
|
|
37
|
+
"description": "A retail establishment class whose premises characteristically include a counter that dispenses medication to the public.",
|
|
38
|
+
"id": "drugstore",
|
|
39
|
+
"isA": ["establishment"],
|
|
40
|
+
"kind": "establishment",
|
|
41
|
+
"label": "drugstore",
|
|
42
|
+
"provenance": {
|
|
43
|
+
"authoredAt": "2026-08-27",
|
|
44
|
+
"notes": "Admitted by the wave-1 amendment against a measured recall gap, re-taken on the shipped layer before this record was frozen: one `group by category_id, country` pass over `poi.db` at manifest 2026-07-22.0 counts 7,168 rows under `retail > drugstore` against 82,168 under `health_and_medical > pharmacy` — 8.0% of the two leaves overall and 12.9% within the US (6,679 of 51,624), all of them out of reach of a `pharmacy` query because the branches are disjoint. A kind of `establishment` directly and not of `healthcare_facility`: the external hierarchy puts it under `retail`, and `healthcare_facility` is premises that exist to provide healthcare, which retail premises with a dispensing counter do not. The `poi-taxonomy` mapping the amendment admits as W1-3 is authored beside this record in `mappings.json`, so `obtain_medication` reaches two mapped kinds and the POI branch searches both.",
|
|
45
|
+
"source": "mailwoman-curated",
|
|
46
|
+
"sourceRecord": "docs/superpowers/specs/2026-08-26-geographic-model-boundaries.md §4.1 W1-1",
|
|
47
|
+
"sourceURL": "https://github.com/sister-software/mailwoman/issues/1963",
|
|
48
|
+
"sourceVersion": "poi.db layer_manifest 2026-07-22.0"
|
|
49
|
+
},
|
|
50
|
+
"status": "active"
|
|
51
|
+
},
|
|
19
52
|
{
|
|
20
53
|
"assertions": [],
|
|
21
54
|
"description": "A class of premises a person can go to.",
|
|
@@ -71,7 +104,7 @@
|
|
|
71
104
|
"modality": "necessary",
|
|
72
105
|
"provenance": {
|
|
73
106
|
"authoredAt": "2026-08-26",
|
|
74
|
-
"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
|
|
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.",
|
|
75
108
|
"source": "mailwoman-curated",
|
|
76
109
|
"sourceRecord": "docs/superpowers/specs/2026-08-26-geographic-model-boundaries.md §4",
|
|
77
110
|
"sourceURL": "https://github.com/sister-software/mailwoman/issues/1927"
|
|
@@ -117,6 +150,10 @@
|
|
|
117
150
|
"ancestors": [],
|
|
118
151
|
"concept": "activity"
|
|
119
152
|
},
|
|
153
|
+
{
|
|
154
|
+
"ancestors": ["establishment", "place"],
|
|
155
|
+
"concept": "drugstore"
|
|
156
|
+
},
|
|
120
157
|
{
|
|
121
158
|
"ancestors": ["place"],
|
|
122
159
|
"concept": "establishment"
|
|
@@ -139,6 +176,20 @@
|
|
|
139
176
|
}
|
|
140
177
|
],
|
|
141
178
|
"mappings": [
|
|
179
|
+
{
|
|
180
|
+
"concept": "drugstore",
|
|
181
|
+
"externalID": "drugstore",
|
|
182
|
+
"id": "poi-taxonomy-drugstore",
|
|
183
|
+
"provenance": {
|
|
184
|
+
"authoredAt": "2026-08-28",
|
|
185
|
+
"notes": "W1-3 of the wave-1 amendment, read back from the committed table at version 0.4.0 (Overture schema v1.17.0): `{ id: drugstore, hierarchy: [retail, drugstore], source: overture }`. The category declares no `overtureCategories`, so `resolveOvertureCategories(\"drugstore\")` is the identity `[\"drugstore\"]`, and the leaf is disjoint from `health_and_medical > pharmacy` — which is why 7,168 rows were out of reach of a `pharmacy` query. With this record `obtain_medication` reaches two mapped kinds; that is the SET the POI branch searches, and no field here states a preference between them. The mapping states that the external identifier names this concept and nothing else.",
|
|
186
|
+
"source": "mailwoman-curated",
|
|
187
|
+
"sourceRecord": "packages/poi-taxonomy/data/taxonomy.json categories[drugstore]",
|
|
188
|
+
"sourceURL": "https://github.com/sister-software/mailwoman/issues/1980",
|
|
189
|
+
"sourceVersion": "poi-taxonomy table 0.4.0"
|
|
190
|
+
},
|
|
191
|
+
"vocabulary": "poi-taxonomy"
|
|
192
|
+
},
|
|
142
193
|
{
|
|
143
194
|
"concept": "pharmacy",
|
|
144
195
|
"externalID": "pharmacy",
|
|
@@ -154,7 +205,7 @@
|
|
|
154
205
|
"vocabulary": "poi-taxonomy"
|
|
155
206
|
}
|
|
156
207
|
],
|
|
157
|
-
"modelVersion": "0.
|
|
208
|
+
"modelVersion": "0.3.0",
|
|
158
209
|
"observations": [],
|
|
159
210
|
"relations": [
|
|
160
211
|
{
|
|
@@ -163,7 +214,7 @@
|
|
|
163
214
|
"id": "affords",
|
|
164
215
|
"label": "affords",
|
|
165
216
|
"rangeKinds": ["activity"],
|
|
166
|
-
"semantics": "
|
|
217
|
+
"semantics": "defeasible",
|
|
167
218
|
"symmetric": false,
|
|
168
219
|
"transitive": false
|
|
169
220
|
}
|
package/data/model/concepts.json
CHANGED
|
@@ -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
|
|
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."
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
],
|
|
@@ -78,6 +78,39 @@
|
|
|
78
78
|
},
|
|
79
79
|
"status": "active"
|
|
80
80
|
},
|
|
81
|
+
{
|
|
82
|
+
"id": "drugstore",
|
|
83
|
+
"label": "drugstore",
|
|
84
|
+
"description": "A retail establishment class whose premises characteristically include a counter that dispenses medication to the public.",
|
|
85
|
+
"kind": "establishment",
|
|
86
|
+
"isA": ["establishment"],
|
|
87
|
+
"assertions": [
|
|
88
|
+
{
|
|
89
|
+
"id": "drugstore-affords-obtain-medication",
|
|
90
|
+
"relation": "affords",
|
|
91
|
+
"target": "obtain_medication",
|
|
92
|
+
"modality": "strongly_expected",
|
|
93
|
+
"countries": ["US"],
|
|
94
|
+
"provenance": {
|
|
95
|
+
"source": "mailwoman-curated",
|
|
96
|
+
"sourceVersion": "poi-taxonomy table 0.4.0",
|
|
97
|
+
"sourceRecord": "packages/poi-taxonomy/data/curated-overlay.json synonyms[drugstore → pharmacy, locales en-US]",
|
|
98
|
+
"sourceURL": "https://github.com/sister-software/mailwoman/issues/1963",
|
|
99
|
+
"authoredAt": "2026-08-27",
|
|
100
|
+
"notes": "The committed curator statement `{ \"phrase\": \"drugstore\", \"categoryID\": \"pharmacy\", \"locales\": [\"en-US\"] }` says that in en-US this class names the pharmacy category. `strongly_expected` rather than `necessary`: the statement says a US drugstore characteristically dispenses, and neither a locale-scoped synonym nor a row count is a census of dispensing, so a counter-example qualifies this record rather than falsifying it. Scoped to `US` because that is the only country a committed record scopes the class to — CA (369 rows), GB (117) and MX (3) bound a population and assert nothing about what those premises afford, and FR is a measured zero on the shipped layer, so there would be nothing there for the claim to range over."
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
],
|
|
104
|
+
"provenance": {
|
|
105
|
+
"source": "mailwoman-curated",
|
|
106
|
+
"sourceVersion": "poi.db layer_manifest 2026-07-22.0",
|
|
107
|
+
"sourceRecord": "docs/superpowers/specs/2026-08-26-geographic-model-boundaries.md §4.1 W1-1",
|
|
108
|
+
"sourceURL": "https://github.com/sister-software/mailwoman/issues/1963",
|
|
109
|
+
"authoredAt": "2026-08-27",
|
|
110
|
+
"notes": "Admitted by the wave-1 amendment against a measured recall gap, re-taken on the shipped layer before this record was frozen: one `group by category_id, country` pass over `poi.db` at manifest 2026-07-22.0 counts 7,168 rows under `retail > drugstore` against 82,168 under `health_and_medical > pharmacy` — 8.0% of the two leaves overall and 12.9% within the US (6,679 of 51,624), all of them out of reach of a `pharmacy` query because the branches are disjoint. A kind of `establishment` directly and not of `healthcare_facility`: the external hierarchy puts it under `retail`, and `healthcare_facility` is premises that exist to provide healthcare, which retail premises with a dispensing counter do not. The `poi-taxonomy` mapping the amendment admits as W1-3 is authored beside this record in `mappings.json`, so `obtain_medication` reaches two mapped kinds and the POI branch searches both."
|
|
111
|
+
},
|
|
112
|
+
"status": "active"
|
|
113
|
+
},
|
|
81
114
|
{
|
|
82
115
|
"id": "activity",
|
|
83
116
|
"label": "activity",
|
package/data/model/mappings.json
CHANGED
|
@@ -13,6 +13,20 @@
|
|
|
13
13
|
"authoredAt": "2026-08-26",
|
|
14
14
|
"notes": "Read from the committed table at version 0.4.0 (Overture schema v1.17.0): `{ id: pharmacy, label: Pharmacy, hierarchy: [health_and_medical, pharmacy], basicLabel: Pharmacy, osmTag: amenity=pharmacy, source: overture }`. The category declares no `overtureCategories`, so `resolveOvertureCategories(\"pharmacy\")` is the identity `[\"pharmacy\"]`. The mapping states that the external identifier names this concept, and nothing else — the containment hierarchy, the Overture-leaf translation and the phrase lexicon stay owned by `@mailwoman/poi-taxonomy`."
|
|
15
15
|
}
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"id": "poi-taxonomy-drugstore",
|
|
19
|
+
"concept": "drugstore",
|
|
20
|
+
"vocabulary": "poi-taxonomy",
|
|
21
|
+
"externalID": "drugstore",
|
|
22
|
+
"provenance": {
|
|
23
|
+
"source": "mailwoman-curated",
|
|
24
|
+
"sourceVersion": "poi-taxonomy table 0.4.0",
|
|
25
|
+
"sourceRecord": "packages/poi-taxonomy/data/taxonomy.json categories[drugstore]",
|
|
26
|
+
"sourceURL": "https://github.com/sister-software/mailwoman/issues/1980",
|
|
27
|
+
"authoredAt": "2026-08-28",
|
|
28
|
+
"notes": "W1-3 of the wave-1 amendment, read back from the committed table at version 0.4.0 (Overture schema v1.17.0): `{ id: drugstore, hierarchy: [retail, drugstore], source: overture }`. The category declares no `overtureCategories`, so `resolveOvertureCategories(\"drugstore\")` is the identity `[\"drugstore\"]`, and the leaf is disjoint from `health_and_medical > pharmacy` — which is why 7,168 rows were out of reach of a `pharmacy` query. With this record `obtain_medication` reaches two mapped kinds; that is the SET the POI branch searches, and no field here states a preference between them. The mapping states that the external identifier names this concept and nothing else."
|
|
29
|
+
}
|
|
16
30
|
}
|
|
17
31
|
]
|
|
18
32
|
}
|
package/data/model/model.json
CHANGED
|
@@ -27,6 +27,10 @@
|
|
|
27
27
|
* Boundary record: `docs/superpowers/specs/2026-08-26-geographic-model-boundaries.md` (#1917).
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
|
+
import { prettyJSON } from "@mailwoman/core/json"
|
|
31
|
+
import { isPlainObject } from "@mailwoman/core/objects"
|
|
32
|
+
import { compareByCodePoint } from "@mailwoman/core/strings/compare"
|
|
33
|
+
|
|
30
34
|
import type {
|
|
31
35
|
ConceptID,
|
|
32
36
|
ConceptRecord,
|
|
@@ -34,8 +38,7 @@ import type {
|
|
|
34
38
|
ExternalMappingRecord,
|
|
35
39
|
RelationRecord,
|
|
36
40
|
SourceObservationRecord,
|
|
37
|
-
} from "
|
|
38
|
-
import { isPlainObject } from "./validation-issues.ts"
|
|
41
|
+
} from "#schema"
|
|
39
42
|
|
|
40
43
|
/**
|
|
41
44
|
* The artifact FORMAT version — incremented by hand when the compiled shape changes, so a reader meeting an artifact it
|
|
@@ -90,13 +93,7 @@ export interface CompiledGeographicModel {
|
|
|
90
93
|
* `String.prototype.localeCompare` is the trap this exists to avoid — its answer depends on the machine's collation, so
|
|
91
94
|
* an artifact ordered with it is reproducible only on the machine that built it.
|
|
92
95
|
*/
|
|
93
|
-
export
|
|
94
|
-
if (left < right) return -1
|
|
95
|
-
|
|
96
|
-
if (left > right) return 1
|
|
97
|
-
|
|
98
|
-
return 0
|
|
99
|
-
}
|
|
96
|
+
export { compareByCodePoint as compareIdentifiers } from "@mailwoman/core/strings/compare"
|
|
100
97
|
|
|
101
98
|
/**
|
|
102
99
|
* Rebuild `value` with every object's keys in code-point order, at every depth. Arrays keep their order — ordering
|
|
@@ -109,7 +106,7 @@ function canonicalize(value: unknown): unknown {
|
|
|
109
106
|
|
|
110
107
|
const canonical: Record<string, unknown> = {}
|
|
111
108
|
|
|
112
|
-
for (const key of Object.keys(value).toSorted(
|
|
109
|
+
for (const key of Object.keys(value).toSorted(compareByCodePoint)) {
|
|
113
110
|
const entry = value[key]
|
|
114
111
|
|
|
115
112
|
if (entry === undefined) continue
|
|
@@ -129,7 +126,7 @@ function canonicalize(value: unknown): unknown {
|
|
|
129
126
|
* two compiles.
|
|
130
127
|
*/
|
|
131
128
|
export function serializeCompiledModel(model: CompiledGeographicModel): string {
|
|
132
|
-
return
|
|
129
|
+
return prettyJSON(canonicalize(model))
|
|
133
130
|
}
|
|
134
131
|
|
|
135
132
|
/**
|
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
type CompiledGeographicModel,
|
|
37
37
|
compareIdentifiers,
|
|
38
38
|
type InheritanceClosureEntry,
|
|
39
|
-
} from "
|
|
39
|
+
} from "#artifact"
|
|
40
40
|
import {
|
|
41
41
|
type ConceptID,
|
|
42
42
|
type ConceptRecord,
|
|
@@ -48,8 +48,8 @@ import {
|
|
|
48
48
|
type RelationRecord,
|
|
49
49
|
toConceptID,
|
|
50
50
|
toDerivedFactID,
|
|
51
|
-
} from "
|
|
52
|
-
import { parseGeographicModelDocument } from "
|
|
51
|
+
} from "#schema"
|
|
52
|
+
import { parseGeographicModelDocument } from "#validate"
|
|
53
53
|
|
|
54
54
|
/**
|
|
55
55
|
* The name a fact derived by `isA` inheritance carries in its `derivation` field. A consumer branches on this rather
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
* Program parent: #1916.
|
|
45
45
|
*/
|
|
46
46
|
|
|
47
|
-
export * from "
|
|
48
|
-
export * from "
|
|
49
|
-
export * from "
|
|
50
|
-
export * from "
|
|
51
|
-
export * from "
|
|
47
|
+
export * from "#artifact"
|
|
48
|
+
export * from "#compile"
|
|
49
|
+
export * from "#lookup"
|
|
50
|
+
export * from "#schema"
|
|
51
|
+
export * from "#validate"
|
package/{load.ts → lib/load.ts}
RENAMED
|
@@ -26,21 +26,21 @@
|
|
|
26
26
|
* keys it uses are tables.
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
29
|
+
import { readDirectoryEntries, readLocalTextFile } from "@mailwoman/core/fs/readers"
|
|
30
|
+
import { isPlainObject } from "@mailwoman/core/objects"
|
|
31
|
+
import { resolvePath } from "path-ts"
|
|
31
32
|
|
|
32
|
-
import { compareIdentifiers } from "
|
|
33
|
-
import type { GeographicModelDocument } from "
|
|
34
|
-
import { validateGeographicModelDocument } from "
|
|
33
|
+
import { compareIdentifiers } from "#artifact"
|
|
34
|
+
import type { GeographicModelDocument } from "#schema"
|
|
35
|
+
import { validateGeographicModelDocument } from "#validate"
|
|
35
36
|
import {
|
|
36
37
|
add,
|
|
37
38
|
checkFieldNames,
|
|
38
|
-
isPlainObject,
|
|
39
39
|
readArray,
|
|
40
40
|
readString,
|
|
41
41
|
type ValidationIssue,
|
|
42
42
|
ValidationIssueCode,
|
|
43
|
-
} from "
|
|
43
|
+
} from "#validation-issues"
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
46
|
* The manifest every model directory carries: the document's `version`, and nothing else.
|
|
@@ -363,15 +363,15 @@ export function mergeGeographicModelFiles(files: readonly GeographicModelSourceF
|
|
|
363
363
|
* the tree and not of the filesystem that stored it. Symbolic links are not followed: a model directory is source, and
|
|
364
364
|
* a link out of it is a record whose home nobody can state.
|
|
365
365
|
*/
|
|
366
|
-
function listSourceFiles(root: string, prefix = ""): string[] {
|
|
367
|
-
const entries =
|
|
366
|
+
async function listSourceFiles(root: string, prefix = ""): Promise<string[]> {
|
|
367
|
+
const entries = await readDirectoryEntries(resolvePath(root, prefix))
|
|
368
368
|
const found: string[] = []
|
|
369
369
|
|
|
370
370
|
for (const entry of entries.toSorted((left, right) => compareIdentifiers(left.name, right.name))) {
|
|
371
371
|
const path = prefix ? `${prefix}/${entry.name}` : entry.name
|
|
372
372
|
|
|
373
373
|
if (entry.isDirectory()) {
|
|
374
|
-
found.push(...listSourceFiles(root, path))
|
|
374
|
+
found.push(...(await listSourceFiles(root, path)))
|
|
375
375
|
|
|
376
376
|
continue
|
|
377
377
|
}
|
|
@@ -389,8 +389,13 @@ function listSourceFiles(root: string, prefix = ""): string[] {
|
|
|
389
389
|
*
|
|
390
390
|
* Throws {@link GeographicModelLoadError} with every issue, each addressed to its source file.
|
|
391
391
|
*/
|
|
392
|
-
export function loadGeographicModelDirectory(root: string): GeographicModelDocument {
|
|
393
|
-
const
|
|
392
|
+
export async function loadGeographicModelDirectory(root: string): Promise<GeographicModelDocument> {
|
|
393
|
+
const paths = await listSourceFiles(root)
|
|
394
|
+
const files: GeographicModelSourceFile[] = []
|
|
395
|
+
|
|
396
|
+
for (const path of paths) {
|
|
397
|
+
files.push({ path, text: await readLocalTextFile(resolvePath(root, path)) })
|
|
398
|
+
}
|
|
394
399
|
|
|
395
400
|
return mergeGeographicModelFiles(files)
|
|
396
401
|
}
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
import type { POICategoryID } from "@mailwoman/poi-taxonomy/types"
|
|
22
22
|
|
|
23
|
-
import { type CompiledGeographicModel, compareIdentifiers } from "
|
|
23
|
+
import { type CompiledGeographicModel, compareIdentifiers } from "#artifact"
|
|
24
24
|
import type {
|
|
25
25
|
ConceptID,
|
|
26
26
|
ConceptRecord,
|
|
@@ -28,7 +28,7 @@ import type {
|
|
|
28
28
|
ExternalVocabulary,
|
|
29
29
|
RelationID,
|
|
30
30
|
RelationRecord,
|
|
31
|
-
} from "
|
|
31
|
+
} from "#schema"
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
34
|
* Lookups over one compiled artifact.
|
|
@@ -358,9 +358,9 @@ export type DerivationInput =
|
|
|
358
358
|
* A fact a named procedure computed from named inputs. Never hand-authored: #1926's compiler writes this table, and the
|
|
359
359
|
* validator refuses a fact whose derivation is unnamed or whose inputs do not resolve.
|
|
360
360
|
*
|
|
361
|
-
* There is no provenance field. The derivation plus the inputs is the provenance
|
|
362
|
-
*
|
|
363
|
-
*
|
|
361
|
+
* There is no provenance field. The derivation plus the inputs is the provenance and the stronger kind — a source
|
|
362
|
+
* string can be copied onto a record that did not come from it, while an input list either resolves or the document
|
|
363
|
+
* does not validate.
|
|
364
364
|
*/
|
|
365
365
|
export interface DerivedFactRecord {
|
|
366
366
|
id: DerivedFactID
|
|
@@ -27,6 +27,8 @@
|
|
|
27
27
|
* Consumed by #1926's compiler, which validates before it emits, and by #1927's authored document.
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
|
+
import { isPlainObject } from "@mailwoman/core/objects"
|
|
31
|
+
|
|
30
32
|
import {
|
|
31
33
|
ConceptKind,
|
|
32
34
|
ConceptStatus,
|
|
@@ -35,11 +37,10 @@ import {
|
|
|
35
37
|
type GeographicModelDocument,
|
|
36
38
|
Modality,
|
|
37
39
|
RelationSemantics,
|
|
38
|
-
} from "
|
|
40
|
+
} from "#schema"
|
|
39
41
|
import {
|
|
40
42
|
add,
|
|
41
43
|
checkFieldNames,
|
|
42
|
-
isPlainObject,
|
|
43
44
|
listVocabulary,
|
|
44
45
|
readArray,
|
|
45
46
|
readBoolean,
|
|
@@ -49,9 +50,9 @@ import {
|
|
|
49
50
|
readVocabularyValue,
|
|
50
51
|
type ValidationIssue,
|
|
51
52
|
ValidationIssueCode,
|
|
52
|
-
} from "
|
|
53
|
+
} from "#validation-issues"
|
|
53
54
|
|
|
54
|
-
export { type ValidationIssue, ValidationIssueCode } from "
|
|
55
|
+
export { type ValidationIssue, ValidationIssueCode } from "#validation-issues"
|
|
55
56
|
|
|
56
57
|
/**
|
|
57
58
|
* The whole document, or every reason it is not one.
|