@mailwoman/geographic-model 0.0.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 +155 -0
- package/artifact.ts +198 -0
- package/compile.ts +350 -0
- package/data/geographic-model.json +172 -0
- package/data/model/concepts.json +114 -0
- package/data/model/mappings.json +18 -0
- package/data/model/model.json +3 -0
- package/data/model/relations.json +14 -0
- package/index.ts +51 -0
- package/load.ts +396 -0
- package/lookup.ts +129 -0
- package/out/artifact.d.ts +106 -0
- package/out/artifact.d.ts.map +1 -0
- package/out/artifact.js +135 -0
- package/out/artifact.js.map +1 -0
- package/out/compile.d.ts +84 -0
- package/out/compile.d.ts.map +1 -0
- package/out/compile.js +259 -0
- package/out/compile.js.map +1 -0
- package/out/index.d.ts +51 -0
- package/out/index.d.ts.map +1 -0
- package/out/index.js +51 -0
- package/out/index.js.map +1 -0
- package/out/load.d.ts +122 -0
- package/out/load.d.ts.map +1 -0
- package/out/load.js +269 -0
- package/out/load.js.map +1 -0
- package/out/lookup.d.ts +64 -0
- package/out/lookup.d.ts.map +1 -0
- package/out/lookup.js +68 -0
- package/out/lookup.js.map +1 -0
- package/out/schema.d.ts +366 -0
- package/out/schema.d.ts.map +1 -0
- package/out/schema.js +166 -0
- package/out/schema.js.map +1 -0
- package/out/scripts/build-artifact.d.ts +51 -0
- package/out/scripts/build-artifact.d.ts.map +1 -0
- package/out/scripts/build-artifact.js +78 -0
- package/out/scripts/build-artifact.js.map +1 -0
- package/out/validate.d.ts +67 -0
- package/out/validate.d.ts.map +1 -0
- package/out/validate.js +465 -0
- package/out/validate.js.map +1 -0
- package/out/validation-issues.d.ts +84 -0
- package/out/validation-issues.d.ts.map +1 -0
- package/out/validation-issues.js +190 -0
- package/out/validation-issues.js.map +1 -0
- package/package.json +120 -0
- package/schema.ts +399 -0
- package/validate.ts +845 -0
- package/validation-issues.ts +305 -0
package/compile.ts
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* Deterministic compilation of a validated {@link GeographicModelDocument} into the runtime artifact.
|
|
7
|
+
*
|
|
8
|
+
* The compiler validates by DELEGATION: `parseGeographicModelDocument` is the only thing that
|
|
9
|
+
* decides whether a document is well formed, and it throws with every violation before a single byte
|
|
10
|
+
* is computed. There is no second validator here, and no partial artifact on failure — a compile
|
|
11
|
+
* either produces the whole artifact or produces nothing.
|
|
12
|
+
*
|
|
13
|
+
* **`isA` alone defines semantic inheritance.** Two things follow from it, and they are the whole
|
|
14
|
+
* derivation surface:
|
|
15
|
+
*
|
|
16
|
+
* 1. The transitive `isA` closure is materialized per concept, so a consumer asks "what is this a
|
|
17
|
+
* kind of" with one lookup rather than by walking the graph at query time.
|
|
18
|
+
* 2. Every assertion an ancestor carries is materialized onto its descendants as a
|
|
19
|
+
* {@link DerivedFactRecord}, naming {@link DERIVATION_ISA_INHERITANCE} and every record the
|
|
20
|
+
* derivation read. Without this a consumer would still be traversing — the closure alone tells it
|
|
21
|
+
* which concepts to go and read, which is the traversal it was supposed to be spared.
|
|
22
|
+
*
|
|
23
|
+
* A relation declaring `transitive` or `inverse` is NOT closed over. Those fields are vocabulary
|
|
24
|
+
* describing what the relation means; materializing them is a reasoning step no executable need has
|
|
25
|
+
* asked for, and the boundary record excludes general reasoning from this package. The day one is
|
|
26
|
+
* needed it arrives as its own named derivation beside this one.
|
|
27
|
+
*
|
|
28
|
+
* Cycles never reach the derivations: `parseGeographicModelDocument` refuses a direct or indirect
|
|
29
|
+
* `isA` cycle and names the trail it followed, so a cyclic document fails as a validation error
|
|
30
|
+
* rather than as a hang. The walk below is breadth-first over a visited set regardless, which makes
|
|
31
|
+
* it total for any graph rather than for the graphs the validator happens to admit.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import {
|
|
35
|
+
ARTIFACT_SCHEMA_VERSION,
|
|
36
|
+
type CompiledGeographicModel,
|
|
37
|
+
compareIdentifiers,
|
|
38
|
+
type InheritanceClosureEntry,
|
|
39
|
+
} from "./artifact.ts"
|
|
40
|
+
import {
|
|
41
|
+
type ConceptID,
|
|
42
|
+
type ConceptRecord,
|
|
43
|
+
type DerivationInput,
|
|
44
|
+
DerivationInputKind,
|
|
45
|
+
type DerivedFactRecord,
|
|
46
|
+
type GeographicModelDocument,
|
|
47
|
+
type RelationAssertion,
|
|
48
|
+
type RelationRecord,
|
|
49
|
+
toConceptID,
|
|
50
|
+
toDerivedFactID,
|
|
51
|
+
} from "./schema.ts"
|
|
52
|
+
import { parseGeographicModelDocument } from "./validate.ts"
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The name a fact derived by `isA` inheritance carries in its `derivation` field. A consumer branches on this rather
|
|
56
|
+
* than on where the record sits.
|
|
57
|
+
*/
|
|
58
|
+
export const DERIVATION_ISA_INHERITANCE = "isa-assertion-inheritance"
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Every way compilation can refuse a document the validator accepted.
|
|
62
|
+
*
|
|
63
|
+
* Both are discovered while writing derived records, which is why the validator cannot report them: they are properties
|
|
64
|
+
* of what the compiler is about to write, not of what the author wrote.
|
|
65
|
+
*/
|
|
66
|
+
export const CompileIssueCode = {
|
|
67
|
+
/**
|
|
68
|
+
* An inherited assertion would land on a concept whose kind the relation does not accept on the asserting side.
|
|
69
|
+
* Emitting it would put a record in the artifact that the document validator would reject if it were authored.
|
|
70
|
+
*/
|
|
71
|
+
InheritedDomainKindMismatch: "inherited_domain_kind_mismatch",
|
|
72
|
+
/**
|
|
73
|
+
* Two derived facts claim one identifier. Reachable when an authored derived fact takes an identifier a derivation
|
|
74
|
+
* also produces, or when authored identifiers carry the separators the derived form is built from.
|
|
75
|
+
*/
|
|
76
|
+
DuplicateDerivedFactID: "duplicate_derived_fact_id",
|
|
77
|
+
} as const
|
|
78
|
+
|
|
79
|
+
export type CompileIssueCode = (typeof CompileIssueCode)[keyof typeof CompileIssueCode]
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* One reason a document that validates does not compile.
|
|
83
|
+
*/
|
|
84
|
+
export interface CompileIssue {
|
|
85
|
+
code: CompileIssueCode
|
|
86
|
+
message: string
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Thrown by {@link compileGeographicModel}. Carries every reason at once, and states them all in its message, so a
|
|
91
|
+
* caller that only prints `error.message` still sees the whole list.
|
|
92
|
+
*/
|
|
93
|
+
export class GeographicModelCompileError extends Error {
|
|
94
|
+
readonly issues: readonly CompileIssue[]
|
|
95
|
+
|
|
96
|
+
constructor(issues: readonly CompileIssue[]) {
|
|
97
|
+
const detail = issues.map((issue) => `${issue.message} [${issue.code}]`).join("\n")
|
|
98
|
+
|
|
99
|
+
super(`geographic-model document does not compile (${issues.length} issues)\n${detail}`)
|
|
100
|
+
|
|
101
|
+
this.name = "GeographicModelCompileError"
|
|
102
|
+
this.issues = issues
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The order derivation inputs are listed in. Grouping by table first keeps a long input list readable; the identifier
|
|
108
|
+
* breaks ties inside a table.
|
|
109
|
+
*/
|
|
110
|
+
const DERIVATION_INPUT_ORDER: readonly DerivationInputKind[] = [
|
|
111
|
+
DerivationInputKind.Concept,
|
|
112
|
+
DerivationInputKind.Relation,
|
|
113
|
+
DerivationInputKind.Assertion,
|
|
114
|
+
DerivationInputKind.Mapping,
|
|
115
|
+
DerivationInputKind.Observation,
|
|
116
|
+
DerivationInputKind.DerivedFact,
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The separator for the compound keys this module groups by. `U+0000` cannot appear in a readable identifier without
|
|
121
|
+
* being visible in it, so two different key tuples cannot collapse onto one string. The derived IDENTIFIERS written
|
|
122
|
+
* into the artifact use readable separators instead, and are checked for collisions once they are all built.
|
|
123
|
+
*/
|
|
124
|
+
const KEY_SEPARATOR = "\u0000"
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The separator between country codes inside a derived identifier.
|
|
128
|
+
*/
|
|
129
|
+
const COUNTRY_SEPARATOR = "+"
|
|
130
|
+
|
|
131
|
+
function compareByID(left: { id: string }, right: { id: string }): number {
|
|
132
|
+
return compareIdentifiers(left.id, right.id)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Walk `isA` upward from one concept and return every concept reachable, in code-point order.
|
|
137
|
+
*
|
|
138
|
+
* Breadth-first over a visited set: each concept is expanded once, so the walk terminates on any graph and the answer
|
|
139
|
+
* does not depend on how the parents were authored. The concept itself is never in its own list — it could only get
|
|
140
|
+
* there around a cycle, and the validator refuses those before the compiler runs.
|
|
141
|
+
*/
|
|
142
|
+
function ancestorsOfConcept(
|
|
143
|
+
conceptID: ConceptID,
|
|
144
|
+
parents: ReadonlyMap<string, readonly ConceptID[]>
|
|
145
|
+
): readonly ConceptID[] {
|
|
146
|
+
const visited = new Set<string>()
|
|
147
|
+
const frontier: ConceptID[] = [...(parents.get(conceptID) ?? [])]
|
|
148
|
+
|
|
149
|
+
// An array iterator reads entries appended during the walk, which is what makes this breadth-first rather than a
|
|
150
|
+
// pass over the direct parents.
|
|
151
|
+
for (const next of frontier) {
|
|
152
|
+
if (next === conceptID || visited.has(next)) continue
|
|
153
|
+
|
|
154
|
+
visited.add(next)
|
|
155
|
+
frontier.push(...(parents.get(next) ?? []))
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const reachable = [...visited].toSorted(compareIdentifiers)
|
|
159
|
+
|
|
160
|
+
return reachable.map(toConceptID)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* One derived fact under construction. Drafts are keyed by the proposition they state, so two ancestors asserting the
|
|
165
|
+
* same thing produce ONE fact naming both of them as inputs, while two ancestors asserting the same triple under
|
|
166
|
+
* different modality produce two facts — a contradiction a consumer can see, rather than a silent choice between them.
|
|
167
|
+
*/
|
|
168
|
+
interface DerivedDraft {
|
|
169
|
+
subject: ConceptID
|
|
170
|
+
assertion: RelationAssertion
|
|
171
|
+
countries?: readonly string[]
|
|
172
|
+
inputs: Map<string, DerivationInput>
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function addInput(draft: DerivedDraft, input: DerivationInput): void {
|
|
176
|
+
draft.inputs.set(`${input.kind}${KEY_SEPARATOR}${input.id}`, input)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function draftInputs(draft: DerivedDraft): DerivationInput[] {
|
|
180
|
+
const inputs = [...draft.inputs.values()]
|
|
181
|
+
|
|
182
|
+
return inputs.toSorted((left, right) => {
|
|
183
|
+
const byKind = DERIVATION_INPUT_ORDER.indexOf(left.kind) - DERIVATION_INPUT_ORDER.indexOf(right.kind)
|
|
184
|
+
|
|
185
|
+
return byKind === 0 ? compareIdentifiers(left.id, right.id) : byKind
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* The identifier a derived fact carries. Built from the proposition it states, so it is stable across edits elsewhere
|
|
191
|
+
* in the document, and readable, so a reader meeting one in a diff can tell what it says.
|
|
192
|
+
*/
|
|
193
|
+
function derivedFactID(draft: DerivedDraft): string {
|
|
194
|
+
const scope = draft.countries?.length ? `:${draft.countries.join(COUNTRY_SEPARATOR)}` : ""
|
|
195
|
+
const { relation, target, modality } = draft.assertion
|
|
196
|
+
|
|
197
|
+
return `${DERIVATION_ISA_INHERITANCE}:${draft.subject}:${relation}:${target}:${modality}${scope}`
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* The relation and target one assertion is about — the pair a descendant's own assertion speaks for.
|
|
202
|
+
*/
|
|
203
|
+
function edgeKey(assertion: RelationAssertion): string {
|
|
204
|
+
return `${assertion.relation}${KEY_SEPARATOR}${assertion.target}`
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function draftKey(subject: ConceptID, assertion: RelationAssertion, countries: readonly string[] | undefined): string {
|
|
208
|
+
const scope = countries?.join(COUNTRY_SEPARATOR) ?? ""
|
|
209
|
+
|
|
210
|
+
return `${subject}${KEY_SEPARATOR}${edgeKey(assertion)}${KEY_SEPARATOR}${assertion.modality}${KEY_SEPARATOR}${scope}`
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Materialize every ancestor's assertions onto their descendants.
|
|
215
|
+
*
|
|
216
|
+
* A concept that authors its own assertion for the same relation and target inherits nothing for that pair. The
|
|
217
|
+
* authored record is the more specific one, which is what `isA` means, and re-stating the pair would put two modalities
|
|
218
|
+
* for one proposition into the artifact with no rule saying which of them holds.
|
|
219
|
+
*/
|
|
220
|
+
function deriveInheritedFacts(
|
|
221
|
+
concepts: readonly ConceptRecord[],
|
|
222
|
+
closure: readonly InheritanceClosureEntry[],
|
|
223
|
+
relations: readonly RelationRecord[],
|
|
224
|
+
issues: CompileIssue[]
|
|
225
|
+
): DerivedFactRecord[] {
|
|
226
|
+
const conceptByID = new Map(concepts.map((concept) => [String(concept.id), concept]))
|
|
227
|
+
const relationByID = new Map(relations.map((relation) => [String(relation.id), relation]))
|
|
228
|
+
const drafts = new Map<string, DerivedDraft>()
|
|
229
|
+
|
|
230
|
+
for (const entry of closure) {
|
|
231
|
+
const concept = conceptByID.get(String(entry.concept))
|
|
232
|
+
|
|
233
|
+
if (!concept) continue
|
|
234
|
+
|
|
235
|
+
const authored = new Set(concept.assertions.map(edgeKey))
|
|
236
|
+
|
|
237
|
+
for (const ancestorID of entry.ancestors) {
|
|
238
|
+
const ancestor = conceptByID.get(String(ancestorID))
|
|
239
|
+
|
|
240
|
+
// Validation refuses an `isA` naming an undeclared concept, and an assertion naming an undeclared relation,
|
|
241
|
+
// so both resolve for any document that reached the compiler. The guards keep the walk total; they do not
|
|
242
|
+
// describe a state the artifact can hold.
|
|
243
|
+
if (!ancestor) continue
|
|
244
|
+
|
|
245
|
+
for (const assertion of ancestor.assertions) {
|
|
246
|
+
if (authored.has(edgeKey(assertion))) continue
|
|
247
|
+
|
|
248
|
+
const relation = relationByID.get(String(assertion.relation))
|
|
249
|
+
|
|
250
|
+
if (!relation) continue
|
|
251
|
+
|
|
252
|
+
if (!relation.domainKinds.includes(concept.kind)) {
|
|
253
|
+
const accepted = relation.domainKinds.map((kind) => `\`${kind}\``).join(", ")
|
|
254
|
+
|
|
255
|
+
issues.push({
|
|
256
|
+
code: CompileIssueCode.InheritedDomainKindMismatch,
|
|
257
|
+
message: `\`${concept.id}\` is a \`${concept.kind}\` and is a kind of \`${ancestor.id}\`, whose assertion \`${assertion.id}\` uses relation \`${relation.id}\` — which accepts ${accepted} on the asserting side`,
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
continue
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const countries = assertion.countries?.length ? assertion.countries.toSorted(compareIdentifiers) : undefined
|
|
264
|
+
const key = draftKey(concept.id, assertion, countries)
|
|
265
|
+
const existing = drafts.get(key)
|
|
266
|
+
const draft: DerivedDraft = existing ?? { subject: concept.id, assertion, countries, inputs: new Map() }
|
|
267
|
+
|
|
268
|
+
addInput(draft, { kind: DerivationInputKind.Concept, id: concept.id })
|
|
269
|
+
addInput(draft, { kind: DerivationInputKind.Concept, id: ancestor.id })
|
|
270
|
+
addInput(draft, { kind: DerivationInputKind.Relation, id: relation.id })
|
|
271
|
+
addInput(draft, { kind: DerivationInputKind.Assertion, id: assertion.id })
|
|
272
|
+
|
|
273
|
+
if (!existing) {
|
|
274
|
+
drafts.set(key, draft)
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return [...drafts.values()].map((draft) => ({
|
|
281
|
+
id: toDerivedFactID(derivedFactID(draft)),
|
|
282
|
+
derivation: DERIVATION_ISA_INHERITANCE,
|
|
283
|
+
inputs: draftInputs(draft),
|
|
284
|
+
subject: draft.subject,
|
|
285
|
+
relation: draft.assertion.relation,
|
|
286
|
+
object: draft.assertion.target,
|
|
287
|
+
modality: draft.assertion.modality,
|
|
288
|
+
...(draft.countries ? { countries: [...draft.countries] } : {}),
|
|
289
|
+
}))
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function checkDerivedIdentifiers(facts: readonly DerivedFactRecord[], issues: CompileIssue[]): void {
|
|
293
|
+
const seen = new Set<string>()
|
|
294
|
+
|
|
295
|
+
for (const fact of facts) {
|
|
296
|
+
const id = String(fact.id)
|
|
297
|
+
|
|
298
|
+
if (seen.has(id)) {
|
|
299
|
+
issues.push({
|
|
300
|
+
code: CompileIssueCode.DuplicateDerivedFactID,
|
|
301
|
+
message: `two derived facts claim \`${id}\` — an authored derived fact and a derivation cannot share an identifier`,
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
continue
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
seen.add(id)
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Compile an authored geographic-model document into its runtime artifact.
|
|
313
|
+
*
|
|
314
|
+
* Throws `GeographicModelValidationError` with every violation if the input is not a valid document, and
|
|
315
|
+
* {@link GeographicModelCompileError} with every reason if it validates but its derivations cannot be written. Nothing
|
|
316
|
+
* partial is ever returned.
|
|
317
|
+
*
|
|
318
|
+
* The document is read, never rewritten: the artifact's tables are new arrays holding the authored records themselves,
|
|
319
|
+
* ordered by identifier, and the derived tables are new records built beside them.
|
|
320
|
+
*/
|
|
321
|
+
export function compileGeographicModel(input: unknown): CompiledGeographicModel {
|
|
322
|
+
const document: GeographicModelDocument = parseGeographicModelDocument(input)
|
|
323
|
+
const concepts = document.concepts.toSorted(compareByID)
|
|
324
|
+
const relations = document.relations.toSorted(compareByID)
|
|
325
|
+
const parents = new Map(concepts.map((concept) => [String(concept.id), concept.isA]))
|
|
326
|
+
|
|
327
|
+
const inheritanceClosure: InheritanceClosureEntry[] = concepts.map((concept) => ({
|
|
328
|
+
concept: concept.id,
|
|
329
|
+
ancestors: ancestorsOfConcept(concept.id, parents),
|
|
330
|
+
}))
|
|
331
|
+
|
|
332
|
+
const issues: CompileIssue[] = []
|
|
333
|
+
const derived = deriveInheritedFacts(concepts, inheritanceClosure, relations, issues)
|
|
334
|
+
const derivedFacts = [...document.derivedFacts, ...derived].toSorted(compareByID)
|
|
335
|
+
|
|
336
|
+
checkDerivedIdentifiers(derivedFacts, issues)
|
|
337
|
+
|
|
338
|
+
if (issues.length) throw new GeographicModelCompileError(issues)
|
|
339
|
+
|
|
340
|
+
return {
|
|
341
|
+
schemaVersion: ARTIFACT_SCHEMA_VERSION,
|
|
342
|
+
modelVersion: document.version,
|
|
343
|
+
relations,
|
|
344
|
+
concepts,
|
|
345
|
+
mappings: document.mappings.toSorted(compareByID),
|
|
346
|
+
observations: document.observations.toSorted(compareByID),
|
|
347
|
+
inheritanceClosure,
|
|
348
|
+
derivedFacts,
|
|
349
|
+
}
|
|
350
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
{
|
|
2
|
+
"concepts": [
|
|
3
|
+
{
|
|
4
|
+
"assertions": [],
|
|
5
|
+
"description": "Something a person does.",
|
|
6
|
+
"id": "activity",
|
|
7
|
+
"isA": [],
|
|
8
|
+
"kind": "activity",
|
|
9
|
+
"label": "activity",
|
|
10
|
+
"provenance": {
|
|
11
|
+
"authoredAt": "2026-08-26",
|
|
12
|
+
"notes": "The class the `affords` relation accepts on the target side.",
|
|
13
|
+
"source": "mailwoman-curated",
|
|
14
|
+
"sourceRecord": "docs/superpowers/specs/2026-08-26-geographic-model-boundaries.md §4",
|
|
15
|
+
"sourceURL": "https://github.com/sister-software/mailwoman/issues/1927"
|
|
16
|
+
},
|
|
17
|
+
"status": "active"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"assertions": [],
|
|
21
|
+
"description": "A class of premises a person can go to.",
|
|
22
|
+
"id": "establishment",
|
|
23
|
+
"isA": ["place"],
|
|
24
|
+
"kind": "establishment",
|
|
25
|
+
"label": "establishment",
|
|
26
|
+
"provenance": {
|
|
27
|
+
"authoredAt": "2026-08-26",
|
|
28
|
+
"notes": "The kind the `affords` relation accepts on the asserting side. Its own `isA` is what puts `place` in every establishment's compiled ancestry.",
|
|
29
|
+
"source": "mailwoman-curated",
|
|
30
|
+
"sourceRecord": "docs/superpowers/specs/2026-08-26-geographic-model-boundaries.md §4",
|
|
31
|
+
"sourceURL": "https://github.com/sister-software/mailwoman/issues/1927"
|
|
32
|
+
},
|
|
33
|
+
"status": "active"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"assertions": [],
|
|
37
|
+
"description": "An establishment class whose premises exist to provide healthcare.",
|
|
38
|
+
"id": "healthcare_facility",
|
|
39
|
+
"isA": ["establishment"],
|
|
40
|
+
"kind": "establishment",
|
|
41
|
+
"label": "healthcare facility",
|
|
42
|
+
"provenance": {
|
|
43
|
+
"authoredAt": "2026-08-26",
|
|
44
|
+
"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.",
|
|
45
|
+
"source": "mailwoman-curated",
|
|
46
|
+
"sourceRecord": "docs/superpowers/specs/2026-08-26-geographic-model-boundaries.md §4",
|
|
47
|
+
"sourceURL": "https://github.com/sister-software/mailwoman/issues/1927"
|
|
48
|
+
},
|
|
49
|
+
"status": "active"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"assertions": [],
|
|
53
|
+
"description": "The activity of obtaining medication a person is entitled to, whether dispensed against a prescription or bought over the counter.",
|
|
54
|
+
"id": "obtain_medication",
|
|
55
|
+
"isA": ["activity"],
|
|
56
|
+
"kind": "activity",
|
|
57
|
+
"label": "obtaining medication",
|
|
58
|
+
"provenance": {
|
|
59
|
+
"authoredAt": "2026-08-26",
|
|
60
|
+
"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.",
|
|
61
|
+
"source": "mailwoman-curated",
|
|
62
|
+
"sourceRecord": "docs/superpowers/specs/2026-08-26-geographic-model-boundaries.md §4",
|
|
63
|
+
"sourceURL": "https://github.com/sister-software/mailwoman/issues/1927"
|
|
64
|
+
},
|
|
65
|
+
"status": "active"
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"assertions": [
|
|
69
|
+
{
|
|
70
|
+
"id": "pharmacy-affords-obtain-medication",
|
|
71
|
+
"modality": "necessary",
|
|
72
|
+
"provenance": {
|
|
73
|
+
"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, which is the same claim `affords` makes by declaring `hard` semantics — a counter-example falsifies this record instead of qualifying it. No `countries` scope: the country-conditional part of the question is which OTHER establishment classes afford the activity, and the slice authors no second class.",
|
|
75
|
+
"source": "mailwoman-curated",
|
|
76
|
+
"sourceRecord": "docs/superpowers/specs/2026-08-26-geographic-model-boundaries.md §4",
|
|
77
|
+
"sourceURL": "https://github.com/sister-software/mailwoman/issues/1927"
|
|
78
|
+
},
|
|
79
|
+
"relation": "affords",
|
|
80
|
+
"target": "obtain_medication"
|
|
81
|
+
}
|
|
82
|
+
],
|
|
83
|
+
"description": "A healthcare facility that dispenses medication to the public.",
|
|
84
|
+
"id": "pharmacy",
|
|
85
|
+
"isA": ["healthcare_facility"],
|
|
86
|
+
"kind": "establishment",
|
|
87
|
+
"label": "pharmacy",
|
|
88
|
+
"provenance": {
|
|
89
|
+
"authoredAt": "2026-08-26",
|
|
90
|
+
"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.",
|
|
91
|
+
"source": "mailwoman-curated",
|
|
92
|
+
"sourceRecord": "docs/superpowers/specs/2026-08-26-geographic-model-boundaries.md §4",
|
|
93
|
+
"sourceURL": "https://github.com/sister-software/mailwoman/issues/1927"
|
|
94
|
+
},
|
|
95
|
+
"status": "active"
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"assertions": [],
|
|
99
|
+
"description": "A geographic place — an area or a point that other things are sited in or near.",
|
|
100
|
+
"id": "place",
|
|
101
|
+
"isA": [],
|
|
102
|
+
"kind": "place",
|
|
103
|
+
"label": "place",
|
|
104
|
+
"provenance": {
|
|
105
|
+
"authoredAt": "2026-08-26",
|
|
106
|
+
"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.",
|
|
107
|
+
"source": "mailwoman-curated",
|
|
108
|
+
"sourceRecord": "docs/superpowers/specs/2026-08-26-geographic-model-boundaries.md §4",
|
|
109
|
+
"sourceURL": "https://github.com/sister-software/mailwoman/issues/1927"
|
|
110
|
+
},
|
|
111
|
+
"status": "active"
|
|
112
|
+
}
|
|
113
|
+
],
|
|
114
|
+
"derivedFacts": [],
|
|
115
|
+
"inheritanceClosure": [
|
|
116
|
+
{
|
|
117
|
+
"ancestors": [],
|
|
118
|
+
"concept": "activity"
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"ancestors": ["place"],
|
|
122
|
+
"concept": "establishment"
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"ancestors": ["establishment", "place"],
|
|
126
|
+
"concept": "healthcare_facility"
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
"ancestors": ["activity"],
|
|
130
|
+
"concept": "obtain_medication"
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
"ancestors": ["establishment", "healthcare_facility", "place"],
|
|
134
|
+
"concept": "pharmacy"
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
"ancestors": [],
|
|
138
|
+
"concept": "place"
|
|
139
|
+
}
|
|
140
|
+
],
|
|
141
|
+
"mappings": [
|
|
142
|
+
{
|
|
143
|
+
"concept": "pharmacy",
|
|
144
|
+
"externalID": "pharmacy",
|
|
145
|
+
"id": "poi-taxonomy-pharmacy",
|
|
146
|
+
"provenance": {
|
|
147
|
+
"authoredAt": "2026-08-26",
|
|
148
|
+
"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`.",
|
|
149
|
+
"source": "mailwoman-curated",
|
|
150
|
+
"sourceRecord": "packages/poi-taxonomy/data/curated-overlay.json categories[pharmacy]",
|
|
151
|
+
"sourceURL": "https://github.com/sister-software/mailwoman/issues/1927",
|
|
152
|
+
"sourceVersion": "poi-taxonomy table 0.4.0"
|
|
153
|
+
},
|
|
154
|
+
"vocabulary": "poi-taxonomy"
|
|
155
|
+
}
|
|
156
|
+
],
|
|
157
|
+
"modelVersion": "0.1.0",
|
|
158
|
+
"observations": [],
|
|
159
|
+
"relations": [
|
|
160
|
+
{
|
|
161
|
+
"description": "The establishment class makes the activity available to a person who goes there. It states what a person can do at premises of that class, and nothing about how a candidate of that class should be ordered, scored, or preferred.",
|
|
162
|
+
"domainKinds": ["establishment"],
|
|
163
|
+
"id": "affords",
|
|
164
|
+
"label": "affords",
|
|
165
|
+
"rangeKinds": ["activity"],
|
|
166
|
+
"semantics": "hard",
|
|
167
|
+
"symmetric": false,
|
|
168
|
+
"transitive": false
|
|
169
|
+
}
|
|
170
|
+
],
|
|
171
|
+
"schemaVersion": 1
|
|
172
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
{
|
|
2
|
+
"concepts": [
|
|
3
|
+
{
|
|
4
|
+
"id": "place",
|
|
5
|
+
"label": "place",
|
|
6
|
+
"description": "A geographic place — an area or a point that other things are sited in or near.",
|
|
7
|
+
"kind": "place",
|
|
8
|
+
"isA": [],
|
|
9
|
+
"assertions": [],
|
|
10
|
+
"provenance": {
|
|
11
|
+
"source": "mailwoman-curated",
|
|
12
|
+
"sourceRecord": "docs/superpowers/specs/2026-08-26-geographic-model-boundaries.md §4",
|
|
13
|
+
"sourceURL": "https://github.com/sister-software/mailwoman/issues/1927",
|
|
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."
|
|
16
|
+
},
|
|
17
|
+
"status": "active"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"id": "establishment",
|
|
21
|
+
"label": "establishment",
|
|
22
|
+
"description": "A class of premises a person can go to.",
|
|
23
|
+
"kind": "establishment",
|
|
24
|
+
"isA": ["place"],
|
|
25
|
+
"assertions": [],
|
|
26
|
+
"provenance": {
|
|
27
|
+
"source": "mailwoman-curated",
|
|
28
|
+
"sourceRecord": "docs/superpowers/specs/2026-08-26-geographic-model-boundaries.md §4",
|
|
29
|
+
"sourceURL": "https://github.com/sister-software/mailwoman/issues/1927",
|
|
30
|
+
"authoredAt": "2026-08-26",
|
|
31
|
+
"notes": "The kind the `affords` relation accepts on the asserting side. Its own `isA` is what puts `place` in every establishment's compiled ancestry."
|
|
32
|
+
},
|
|
33
|
+
"status": "active"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"id": "healthcare_facility",
|
|
37
|
+
"label": "healthcare facility",
|
|
38
|
+
"description": "An establishment class whose premises exist to provide healthcare.",
|
|
39
|
+
"kind": "establishment",
|
|
40
|
+
"isA": ["establishment"],
|
|
41
|
+
"assertions": [],
|
|
42
|
+
"provenance": {
|
|
43
|
+
"source": "mailwoman-curated",
|
|
44
|
+
"sourceRecord": "docs/superpowers/specs/2026-08-26-geographic-model-boundaries.md §4",
|
|
45
|
+
"sourceURL": "https://github.com/sister-software/mailwoman/issues/1927",
|
|
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."
|
|
48
|
+
},
|
|
49
|
+
"status": "active"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"id": "pharmacy",
|
|
53
|
+
"label": "pharmacy",
|
|
54
|
+
"description": "A healthcare facility that dispenses medication to the public.",
|
|
55
|
+
"kind": "establishment",
|
|
56
|
+
"isA": ["healthcare_facility"],
|
|
57
|
+
"assertions": [
|
|
58
|
+
{
|
|
59
|
+
"id": "pharmacy-affords-obtain-medication",
|
|
60
|
+
"relation": "affords",
|
|
61
|
+
"target": "obtain_medication",
|
|
62
|
+
"modality": "necessary",
|
|
63
|
+
"provenance": {
|
|
64
|
+
"source": "mailwoman-curated",
|
|
65
|
+
"sourceRecord": "docs/superpowers/specs/2026-08-26-geographic-model-boundaries.md §4",
|
|
66
|
+
"sourceURL": "https://github.com/sister-software/mailwoman/issues/1927",
|
|
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, which is the same claim `affords` makes by declaring `hard` semantics — a counter-example falsifies this record instead of qualifying it. No `countries` scope: the country-conditional part of the question is which OTHER establishment classes afford the activity, and the slice authors no second class."
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
],
|
|
72
|
+
"provenance": {
|
|
73
|
+
"source": "mailwoman-curated",
|
|
74
|
+
"sourceRecord": "docs/superpowers/specs/2026-08-26-geographic-model-boundaries.md §4",
|
|
75
|
+
"sourceURL": "https://github.com/sister-software/mailwoman/issues/1927",
|
|
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."
|
|
78
|
+
},
|
|
79
|
+
"status": "active"
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"id": "activity",
|
|
83
|
+
"label": "activity",
|
|
84
|
+
"description": "Something a person does.",
|
|
85
|
+
"kind": "activity",
|
|
86
|
+
"isA": [],
|
|
87
|
+
"assertions": [],
|
|
88
|
+
"provenance": {
|
|
89
|
+
"source": "mailwoman-curated",
|
|
90
|
+
"sourceRecord": "docs/superpowers/specs/2026-08-26-geographic-model-boundaries.md §4",
|
|
91
|
+
"sourceURL": "https://github.com/sister-software/mailwoman/issues/1927",
|
|
92
|
+
"authoredAt": "2026-08-26",
|
|
93
|
+
"notes": "The class the `affords` relation accepts on the target side."
|
|
94
|
+
},
|
|
95
|
+
"status": "active"
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"id": "obtain_medication",
|
|
99
|
+
"label": "obtaining medication",
|
|
100
|
+
"description": "The activity of obtaining medication a person is entitled to, whether dispensed against a prescription or bought over the counter.",
|
|
101
|
+
"kind": "activity",
|
|
102
|
+
"isA": ["activity"],
|
|
103
|
+
"assertions": [],
|
|
104
|
+
"provenance": {
|
|
105
|
+
"source": "mailwoman-curated",
|
|
106
|
+
"sourceRecord": "docs/superpowers/specs/2026-08-26-geographic-model-boundaries.md §4",
|
|
107
|
+
"sourceURL": "https://github.com/sister-software/mailwoman/issues/1927",
|
|
108
|
+
"authoredAt": "2026-08-26",
|
|
109
|
+
"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."
|
|
110
|
+
},
|
|
111
|
+
"status": "active"
|
|
112
|
+
}
|
|
113
|
+
]
|
|
114
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"mappings": [
|
|
3
|
+
{
|
|
4
|
+
"id": "poi-taxonomy-pharmacy",
|
|
5
|
+
"concept": "pharmacy",
|
|
6
|
+
"vocabulary": "poi-taxonomy",
|
|
7
|
+
"externalID": "pharmacy",
|
|
8
|
+
"provenance": {
|
|
9
|
+
"source": "mailwoman-curated",
|
|
10
|
+
"sourceVersion": "poi-taxonomy table 0.4.0",
|
|
11
|
+
"sourceRecord": "packages/poi-taxonomy/data/curated-overlay.json categories[pharmacy]",
|
|
12
|
+
"sourceURL": "https://github.com/sister-software/mailwoman/issues/1927",
|
|
13
|
+
"authoredAt": "2026-08-26",
|
|
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
|
+
}
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"relations": [
|
|
3
|
+
{
|
|
4
|
+
"id": "affords",
|
|
5
|
+
"label": "affords",
|
|
6
|
+
"description": "The establishment class makes the activity available to a person who goes there. It states what a person can do at premises of that class, and nothing about how a candidate of that class should be ordered, scored, or preferred.",
|
|
7
|
+
"domainKinds": ["establishment"],
|
|
8
|
+
"rangeKinds": ["activity"],
|
|
9
|
+
"transitive": false,
|
|
10
|
+
"symmetric": false,
|
|
11
|
+
"semantics": "hard"
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
}
|