@mailwoman/geographic-model 10.0.0 → 10.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -32
- package/data/geographic-model.json +5 -5
- package/data/model/concepts.json +5 -5
- package/lib/artifact.ts +37 -65
- package/lib/compile.ts +55 -70
- package/lib/index.ts +22 -35
- package/lib/load.ts +60 -72
- package/lib/lookup.ts +12 -28
- package/lib/packaged.ts +50 -0
- package/lib/schema.ts +104 -97
- package/lib/validate.ts +32 -56
- package/lib/validation/issues.ts +32 -36
- package/lib/validation/references.ts +24 -27
- package/out/artifact.d.ts +28 -45
- package/out/artifact.d.ts.map +1 -1
- package/out/artifact.js +25 -52
- 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 +42 -64
- package/out/compile.js.map +1 -1
- package/out/index.d.ts +22 -35
- package/out/index.d.ts.map +1 -1
- package/out/index.js +22 -35
- package/out/index.js.map +1 -1
- package/out/load.d.ts +30 -39
- package/out/load.d.ts.map +1 -1
- package/out/load.js +42 -56
- 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 +42 -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 +29 -50
- package/out/validate.d.ts.map +1 -1
- package/out/validate.js +12 -35
- package/out/validate.js.map +1 -1
- package/out/validation/issues.d.ts +17 -20
- package/out/validation/issues.d.ts.map +1 -1
- package/out/validation/issues.js +25 -29
- package/out/validation/issues.js.map +1 -1
- package/out/validation/references.d.ts.map +1 -1
- package/out/validation/references.js +20 -23
- package/out/validation/references.js.map +1 -1
- package/package.json +35 -76
- 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/lib/load.ts
CHANGED
|
@@ -3,27 +3,17 @@
|
|
|
3
3
|
* @license AGPL-3.0
|
|
4
4
|
* @author Teffen Ellis, et al.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* Loads a directory of JSON files into one {@link GeographicModelDocument}.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* directory does carry is one manifest — `model.json`, holding the document's `version` — because a
|
|
11
|
-
* version assembled from whichever fragment happened to declare one is a version nobody chose.
|
|
8
|
+
* File layout does not assign meaning to a file. Any file may hold any subset of the tables.
|
|
9
|
+
* Only `model.json` may declare the document's `version`.
|
|
12
10
|
*
|
|
13
|
-
*
|
|
11
|
+
* The loader sorts files by path before reading them, so directory enumeration order never affects
|
|
12
|
+
* the output. It records the source file of every record and maps each validation issue back to that
|
|
13
|
+
* file. A duplicate-identifier issue also reports the file that used the identifier first.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* NAMES and their contents. `readdir` order, and therefore the filesystem, is out of the answer.
|
|
18
|
-
* 2. **Every issue names the file it came from.** The document validator addresses a record by its
|
|
19
|
-
* position in the merged table (`$.concepts[7].kind`), which is the one thing an author cannot
|
|
20
|
-
* see; the loader keeps a per-record origin and re-addresses each issue to its source file. A
|
|
21
|
-
* duplicate identifier names both files — the one that claimed it and the one that claimed it
|
|
22
|
-
* first — because "already used" is unactionable without the other half.
|
|
23
|
-
*
|
|
24
|
-
* Validation itself is delegated whole to `./validate.ts`. What the loader checks on its own is only
|
|
25
|
-
* what the validator cannot see: whether a file parses, whether it is an object, and whether the
|
|
26
|
-
* keys it uses are tables.
|
|
15
|
+
* `./validate.ts` performs document validation. The loader checks only that each file parses, holds
|
|
16
|
+
* an object and uses known table keys.
|
|
27
17
|
*/
|
|
28
18
|
|
|
29
19
|
import { readLocalTextFile } from "@mailwoman/core/fs/readers"
|
|
@@ -43,12 +33,13 @@ import {
|
|
|
43
33
|
ValidationIssueCode,
|
|
44
34
|
} from "#validation/issues"
|
|
45
35
|
/**
|
|
46
|
-
* The manifest every model directory
|
|
36
|
+
* The manifest file that every model directory contains.
|
|
37
|
+
* It holds only the document's `version`.
|
|
47
38
|
*/
|
|
48
39
|
export const MODEL_MANIFEST_FILENAME = "model.json"
|
|
49
40
|
|
|
50
41
|
/**
|
|
51
|
-
* The keys a source file may use.
|
|
42
|
+
* The table keys that a non-manifest source file may use.
|
|
52
43
|
*/
|
|
53
44
|
const TABLE_FIELDS = ["relations", "concepts", "mappings", "observations", "derivedFacts"] as const
|
|
54
45
|
|
|
@@ -57,49 +48,49 @@ type TableField = (typeof TABLE_FIELDS)[number]
|
|
|
57
48
|
const MANIFEST_FIELDS = ["version"] as const
|
|
58
49
|
|
|
59
50
|
/**
|
|
60
|
-
*
|
|
61
|
-
*
|
|
51
|
+
* Matches a record's path in the merged document, such as `$.concepts[7]` or `$.concepts[7].assertions[1]`.
|
|
52
|
+
* Group 3 matches only for a nested assertion.
|
|
62
53
|
*/
|
|
63
54
|
const RECORD_PATH_PATTERN = /^\$\.([A-Za-z]+)\[(\d+)\](?:\.assertions\[(\d+)\])?/u
|
|
64
55
|
|
|
65
56
|
/**
|
|
66
|
-
*
|
|
67
|
-
* file that is not JSON at all.
|
|
57
|
+
* Load issue codes: every validation code plus `MalformedJSON`.
|
|
68
58
|
*/
|
|
69
59
|
export const LoadIssueCode = {
|
|
70
60
|
...ValidationIssueCode,
|
|
71
61
|
/**
|
|
72
|
-
* A source file
|
|
73
|
-
* never text.
|
|
62
|
+
* A source file is not valid JSON.
|
|
74
63
|
*/
|
|
75
64
|
MalformedJSON: "malformed_json",
|
|
76
65
|
} as const
|
|
77
66
|
|
|
67
|
+
/**
|
|
68
|
+
* A {@link LoadIssueCode} value.
|
|
69
|
+
*/
|
|
78
70
|
export type LoadIssueCode = (typeof LoadIssueCode)[keyof typeof LoadIssueCode]
|
|
79
71
|
|
|
80
72
|
/**
|
|
81
|
-
* One
|
|
73
|
+
* One load issue, attributed to its source file.
|
|
82
74
|
*/
|
|
83
75
|
export interface SourcedIssue {
|
|
84
76
|
/**
|
|
85
|
-
* The source file
|
|
77
|
+
* The source file path relative to the model directory, with `/` separators on every platform.
|
|
86
78
|
*/
|
|
87
79
|
file: string
|
|
88
80
|
/**
|
|
89
|
-
* The JSONPath-style
|
|
90
|
-
* validator saw.
|
|
81
|
+
* The JSONPath-style location in the merged document.
|
|
91
82
|
*/
|
|
92
83
|
path: string
|
|
93
84
|
code: LoadIssueCode
|
|
94
85
|
message: string
|
|
95
86
|
/**
|
|
96
|
-
* For a duplicate identifier
|
|
87
|
+
* For a duplicate identifier, the file that used the identifier first.
|
|
97
88
|
*/
|
|
98
|
-
otherFile
|
|
89
|
+
otherFile: string | null
|
|
99
90
|
}
|
|
100
91
|
|
|
101
92
|
/**
|
|
102
|
-
*
|
|
93
|
+
* A source file's path relative to the model directory, plus its text.
|
|
103
94
|
*/
|
|
104
95
|
export interface GeographicModelSourceFile {
|
|
105
96
|
path: string
|
|
@@ -107,7 +98,7 @@ export interface GeographicModelSourceFile {
|
|
|
107
98
|
}
|
|
108
99
|
|
|
109
100
|
/**
|
|
110
|
-
*
|
|
101
|
+
* Formats each issue as one `file:path: message [code]` line, in input order.
|
|
111
102
|
*/
|
|
112
103
|
export function formatSourcedIssues(issues: readonly SourcedIssue[]): string {
|
|
113
104
|
return issues
|
|
@@ -120,8 +111,8 @@ export function formatSourcedIssues(issues: readonly SourcedIssue[]): string {
|
|
|
120
111
|
}
|
|
121
112
|
|
|
122
113
|
/**
|
|
123
|
-
*
|
|
124
|
-
*
|
|
114
|
+
* The error thrown when a model directory does not load.
|
|
115
|
+
* Its message lists every issue.
|
|
125
116
|
*/
|
|
126
117
|
export class GeographicModelLoadError extends Error {
|
|
127
118
|
readonly issues: readonly SourcedIssue[]
|
|
@@ -135,16 +126,16 @@ export class GeographicModelLoadError extends Error {
|
|
|
135
126
|
}
|
|
136
127
|
|
|
137
128
|
/**
|
|
138
|
-
*
|
|
129
|
+
* The source of one record in the merged document.
|
|
139
130
|
*/
|
|
140
131
|
interface RecordOrigin {
|
|
141
132
|
file: string
|
|
142
133
|
/**
|
|
143
|
-
* The
|
|
144
|
-
*
|
|
134
|
+
* The record's table, or `assertions` for an assertion nested in a concept.
|
|
135
|
+
* Identifiers are unique within a table.
|
|
145
136
|
*/
|
|
146
137
|
table: string
|
|
147
|
-
id
|
|
138
|
+
id: string | null
|
|
148
139
|
}
|
|
149
140
|
|
|
150
141
|
interface MergeState {
|
|
@@ -152,23 +143,21 @@ interface MergeState {
|
|
|
152
143
|
tables: Record<TableField, unknown[]>
|
|
153
144
|
origins: Map<string, RecordOrigin>
|
|
154
145
|
/**
|
|
155
|
-
*
|
|
156
|
-
*
|
|
146
|
+
* Maps each table to its identifiers and the file that used each identifier first.
|
|
147
|
+
* The validator reports only the second use.
|
|
157
148
|
*/
|
|
158
149
|
firstClaims: Map<string, Map<string, string>>
|
|
159
|
-
version
|
|
150
|
+
version: string | null
|
|
160
151
|
}
|
|
161
152
|
|
|
162
153
|
function sourced(file: string, issues: readonly ValidationIssue[]): SourcedIssue[] {
|
|
163
|
-
return issues.map((issue) => ({ file, path: issue.path, code: issue.code, message: issue.message }))
|
|
154
|
+
return issues.map((issue) => ({ file, path: issue.path, code: issue.code, message: issue.message, otherFile: null }))
|
|
164
155
|
}
|
|
165
156
|
|
|
166
157
|
/**
|
|
167
|
-
*
|
|
158
|
+
* Parses one source file, or records the parse failure and returns `undefined`.
|
|
168
159
|
*
|
|
169
|
-
* The
|
|
170
|
-
* boundary record keeps world semantics out of core, and a build-time loader is not the reason to reverse it. The
|
|
171
|
-
* parser's own message is also the useful half of the report here, which a wrapper returning a fallback discards.
|
|
160
|
+
* The loader calls `JSON.parse` directly because the report needs the parser's error message.
|
|
172
161
|
*/
|
|
173
162
|
function readSourceJSON(file: GeographicModelSourceFile, issues: SourcedIssue[]): unknown {
|
|
174
163
|
try {
|
|
@@ -180,14 +169,15 @@ function readSourceJSON(file: GeographicModelSourceFile, issues: SourcedIssue[])
|
|
|
180
169
|
path: "$",
|
|
181
170
|
code: LoadIssueCode.MalformedJSON,
|
|
182
171
|
message: error instanceof Error ? error.message : String(error),
|
|
172
|
+
otherFile: null,
|
|
183
173
|
})
|
|
184
174
|
|
|
185
175
|
return undefined
|
|
186
176
|
}
|
|
187
177
|
}
|
|
188
178
|
|
|
189
|
-
function claim(state: MergeState, table: string, id: string |
|
|
190
|
-
if (id
|
|
179
|
+
function claim(state: MergeState, table: string, id: string | null, file: string): void {
|
|
180
|
+
if (!id) return
|
|
191
181
|
|
|
192
182
|
const claims = state.firstClaims.get(table) ?? new Map<string, string>()
|
|
193
183
|
|
|
@@ -198,10 +188,10 @@ function claim(state: MergeState, table: string, id: string | undefined, file: s
|
|
|
198
188
|
}
|
|
199
189
|
}
|
|
200
190
|
|
|
201
|
-
function recordID(entry: unknown): string |
|
|
202
|
-
if (!isPlainObject(entry)) return
|
|
191
|
+
function recordID(entry: unknown): string | null {
|
|
192
|
+
if (!isPlainObject(entry)) return null
|
|
203
193
|
|
|
204
|
-
return typeof entry.id === "string" ? entry.id :
|
|
194
|
+
return typeof entry.id === "string" ? entry.id : null
|
|
205
195
|
}
|
|
206
196
|
|
|
207
197
|
function readManifestFile(state: MergeState, file: GeographicModelSourceFile, value: unknown): void {
|
|
@@ -219,7 +209,7 @@ function readManifestFile(state: MergeState, file: GeographicModelSourceFile, va
|
|
|
219
209
|
}
|
|
220
210
|
|
|
221
211
|
/**
|
|
222
|
-
*
|
|
212
|
+
* Appends one file's tables to the merged document and records each record's origin.
|
|
223
213
|
*/
|
|
224
214
|
function readTableFile(state: MergeState, file: GeographicModelSourceFile, value: unknown): void {
|
|
225
215
|
const issues: ValidationIssue[] = []
|
|
@@ -232,8 +222,7 @@ function readTableFile(state: MergeState, file: GeographicModelSourceFile, value
|
|
|
232
222
|
return
|
|
233
223
|
}
|
|
234
224
|
|
|
235
|
-
//
|
|
236
|
-
// instead of only saying the field is unknown here.
|
|
225
|
+
// The field check allows `version` so that the specific error below can say where it belongs.
|
|
237
226
|
checkFieldNames(issues, "$", value, [...TABLE_FIELDS, ...MANIFEST_FIELDS])
|
|
238
227
|
|
|
239
228
|
if ("version" in value) {
|
|
@@ -277,7 +266,7 @@ function readTableFile(state: MergeState, file: GeographicModelSourceFile, value
|
|
|
277
266
|
}
|
|
278
267
|
|
|
279
268
|
/**
|
|
280
|
-
*
|
|
269
|
+
* Attributes one validation issue to the file that contains the record.
|
|
281
270
|
*/
|
|
282
271
|
function attribute(state: MergeState, issue: ValidationIssue): SourcedIssue {
|
|
283
272
|
const match = RECORD_PATH_PATTERN.exec(issue.path)
|
|
@@ -285,30 +274,29 @@ function attribute(state: MergeState, issue: ValidationIssue): SourcedIssue {
|
|
|
285
274
|
const key = match ? `${match[1]}[${match[2]}]${nested ? `.assertions[${nested}]` : ""}` : undefined
|
|
286
275
|
const origin = key ? state.origins.get(key) : undefined
|
|
287
276
|
|
|
288
|
-
//
|
|
289
|
-
// contributes anything outside a table.
|
|
277
|
+
// Only the manifest contributes fields outside the tables, so it owns document-level issues.
|
|
290
278
|
const file = origin?.file ?? MODEL_MANIFEST_FILENAME
|
|
291
279
|
|
|
292
280
|
const claimant =
|
|
293
281
|
issue.code === ValidationIssueCode.DuplicateID && origin?.id
|
|
294
282
|
? state.firstClaims.get(origin.table)?.get(origin.id)
|
|
295
|
-
:
|
|
283
|
+
: null
|
|
296
284
|
|
|
297
285
|
return {
|
|
298
286
|
file,
|
|
299
287
|
path: issue.path,
|
|
300
288
|
code: issue.code,
|
|
301
289
|
message: issue.message,
|
|
302
|
-
|
|
290
|
+
otherFile: claimant ?? null,
|
|
303
291
|
}
|
|
304
292
|
}
|
|
305
293
|
|
|
306
294
|
/**
|
|
307
|
-
*
|
|
295
|
+
* Merges source files into one document and validates it.
|
|
296
|
+
*
|
|
297
|
+
* The function sorts files by path first, so the input order never affects the result.
|
|
308
298
|
*
|
|
309
|
-
*
|
|
310
|
-
* order. Throws {@link GeographicModelLoadError} with every issue, each addressed to its source file; returns nothing
|
|
311
|
-
* partial.
|
|
299
|
+
* @throws {GeographicModelLoadError} With every issue, each attributed to its source file.
|
|
312
300
|
*/
|
|
313
301
|
export function mergeGeographicModelFiles(files: readonly GeographicModelSourceFile[]): GeographicModelDocument {
|
|
314
302
|
const state: MergeState = {
|
|
@@ -316,6 +304,7 @@ export function mergeGeographicModelFiles(files: readonly GeographicModelSourceF
|
|
|
316
304
|
tables: { relations: [], concepts: [], mappings: [], observations: [], derivedFacts: [] },
|
|
317
305
|
origins: new Map(),
|
|
318
306
|
firstClaims: new Map(),
|
|
307
|
+
version: null,
|
|
319
308
|
}
|
|
320
309
|
|
|
321
310
|
const ordered = files.toSorted((left, right) => compareIdentifiers(left.path, right.path))
|
|
@@ -327,6 +316,7 @@ export function mergeGeographicModelFiles(files: readonly GeographicModelSourceF
|
|
|
327
316
|
path: "$",
|
|
328
317
|
code: LoadIssueCode.MissingField,
|
|
329
318
|
message: `a model directory carries \`${MODEL_MANIFEST_FILENAME}\`, holding the document's \`version\``,
|
|
319
|
+
otherFile: null,
|
|
330
320
|
})
|
|
331
321
|
}
|
|
332
322
|
|
|
@@ -344,7 +334,7 @@ export function mergeGeographicModelFiles(files: readonly GeographicModelSourceF
|
|
|
344
334
|
readTableFile(state, file, value)
|
|
345
335
|
}
|
|
346
336
|
|
|
347
|
-
//
|
|
337
|
+
// Validation issues cannot be attributed reliably after a structural failure, so those failures stop here.
|
|
348
338
|
if (state.issues.length) throw new GeographicModelLoadError(state.issues)
|
|
349
339
|
|
|
350
340
|
const result = validateGeographicModelDocument({ version: state.version, ...state.tables })
|
|
@@ -357,11 +347,9 @@ export function mergeGeographicModelFiles(files: readonly GeographicModelSourceF
|
|
|
357
347
|
}
|
|
358
348
|
|
|
359
349
|
/**
|
|
360
|
-
*
|
|
350
|
+
* Lists every `*.json` file under `root` as a relative path, sorted by code point at each level.
|
|
361
351
|
*
|
|
362
|
-
*
|
|
363
|
-
* the tree and not of the filesystem that stored it. Symbolic links are not followed: a model directory is source, and
|
|
364
|
-
* a link out of it is a record whose home nobody can state.
|
|
352
|
+
* Symbolic links are skipped because only regular files and directories match.
|
|
365
353
|
*/
|
|
366
354
|
async function listSourceFiles(root: string, prefix = ""): Promise<string[]> {
|
|
367
355
|
const entries = await Globerator.from("*", {
|
|
@@ -390,9 +378,9 @@ async function listSourceFiles(root: string, prefix = ""): Promise<string[]> {
|
|
|
390
378
|
}
|
|
391
379
|
|
|
392
380
|
/**
|
|
393
|
-
*
|
|
381
|
+
* Reads every `*.json` file under `root`, merges the files and validates the merged document.
|
|
394
382
|
*
|
|
395
|
-
*
|
|
383
|
+
* @throws {GeographicModelLoadError} With every issue, each attributed to its source file.
|
|
396
384
|
*/
|
|
397
385
|
export async function loadGeographicModelDirectory(root: string): Promise<GeographicModelDocument> {
|
|
398
386
|
const paths = await listSourceFiles(root)
|
package/lib/lookup.ts
CHANGED
|
@@ -3,19 +3,8 @@
|
|
|
3
3
|
* @license AGPL-3.0
|
|
4
4
|
* @author Teffen Ellis, et al.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* The shape is deliberate. `@mailwoman/geographic-model` compiles authored records into an artifact
|
|
10
|
-
* precisely so a runtime consumer never traverses anything — so the index exposes lookups
|
|
11
|
-
* (`concept`, `ancestorsOf`, `derivedFactsAbout`, `conceptsForExternalID`) and no walk, no cursor,
|
|
12
|
-
* and no query language.
|
|
13
|
-
*
|
|
14
|
-
* Two absences are distinguished everywhere, because they are different answers. A lookup for a
|
|
15
|
-
* concept the artifact does not carry returns `undefined` — the model cannot speak to it. A lookup
|
|
16
|
-
* for a concept it does carry, which nothing was derived about, returns an empty list — the model
|
|
17
|
-
* carries it and states nothing. A reader that collapsed the two would report "no ancestors" for a
|
|
18
|
-
* concept it had never heard of.
|
|
6
|
+
* Read-only lookups over a compiled artifact. Queries use prebuilt maps rather than traversing the graph.
|
|
7
|
+
* The lookup returns `undefined` for unknown concepts and an empty list for known concepts with no related facts.
|
|
19
8
|
*/
|
|
20
9
|
|
|
21
10
|
import { compareByCodePoint as compareIdentifiers } from "@mailwoman/core/strings/compare"
|
|
@@ -32,36 +21,32 @@ import type {
|
|
|
32
21
|
} from "#schema"
|
|
33
22
|
|
|
34
23
|
/**
|
|
35
|
-
*
|
|
24
|
+
* Lookup methods for one compiled artifact.
|
|
36
25
|
*/
|
|
37
26
|
export interface GeographicModelIndex {
|
|
38
27
|
/**
|
|
39
|
-
*
|
|
28
|
+
* Compiled artifact used by this index.
|
|
40
29
|
*/
|
|
41
30
|
readonly model: CompiledGeographicModel
|
|
42
31
|
/**
|
|
43
|
-
*
|
|
32
|
+
* Concept record, or `undefined` if absent.
|
|
44
33
|
*/
|
|
45
34
|
concept(id: ConceptID): ConceptRecord | undefined
|
|
46
35
|
/**
|
|
47
|
-
*
|
|
36
|
+
* Relation definition, or `undefined` if absent.
|
|
48
37
|
*/
|
|
49
38
|
relation(id: RelationID): RelationRecord | undefined
|
|
50
39
|
/**
|
|
51
|
-
*
|
|
52
|
-
* nothing; `undefined` for a concept the artifact does not carry.
|
|
40
|
+
* Transitive ancestors in code-point order; `undefined` if the concept is absent.
|
|
53
41
|
*/
|
|
54
42
|
ancestorsOf(id: ConceptID): readonly ConceptID[] | undefined
|
|
55
43
|
/**
|
|
56
|
-
*
|
|
57
|
-
* `undefined` for a concept the artifact does not carry.
|
|
44
|
+
* Derived facts in artifact order; `undefined` if the concept is absent.
|
|
58
45
|
*/
|
|
59
46
|
derivedFactsAbout(id: ConceptID): readonly DerivedFactRecord[] | undefined
|
|
60
47
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* An empty list is a true negative rather than an unread answer: the artifact carries every mapping the document
|
|
64
|
-
* authored, so nothing having declared this identifier is the whole of what there is to know about it.
|
|
48
|
+
* Concepts mapped from this external ID, in code-point order.
|
|
49
|
+
* An empty list means no mapping is recorded.
|
|
65
50
|
*/
|
|
66
51
|
conceptsForExternalID(vocabulary: ExternalVocabulary, externalID: POICategoryID): readonly ConceptID[]
|
|
67
52
|
}
|
|
@@ -71,10 +56,9 @@ function externalKey(vocabulary: ExternalVocabulary, externalID: string): string
|
|
|
71
56
|
}
|
|
72
57
|
|
|
73
58
|
/**
|
|
74
|
-
*
|
|
59
|
+
* Build lookup maps over a compiled artifact.
|
|
75
60
|
*
|
|
76
|
-
*
|
|
77
|
-
* are the artifact's own.
|
|
61
|
+
* Return references to the artifact's records without copying them.
|
|
78
62
|
*/
|
|
79
63
|
export function createGeographicModelIndex(model: CompiledGeographicModel): GeographicModelIndex {
|
|
80
64
|
const concepts = new Map<string, ConceptRecord>(model.concepts.map((concept) => [String(concept.id), concept]))
|
package/lib/packaged.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
* @file Reads the compiled model this package ships in `data/geographic-model.json`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readLocalTextFile } from "@mailwoman/core/fs/readers"
|
|
9
|
+
import { pathExists } from "@mailwoman/core/fs/readers/stat"
|
|
10
|
+
import { resolvePackagePath } from "@mailwoman/core/module/resolvers"
|
|
11
|
+
import { resolvePath } from "path-ts"
|
|
12
|
+
|
|
13
|
+
import { type CompiledGeographicModel, parseCompiledGeographicModel } from "#artifact"
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The authoring directory and the artifact it compiles to.
|
|
17
|
+
*
|
|
18
|
+
* Probes for `model/model.json`, so a genuinely missing `data/` throws naming the path.
|
|
19
|
+
*/
|
|
20
|
+
export async function packagedModelPaths(): Promise<{ source: string; artifact: string }> {
|
|
21
|
+
const candidates = [resolvePackagePath("@mailwoman/geographic-model", "data")]
|
|
22
|
+
const probes: Array<[string, boolean]> = []
|
|
23
|
+
|
|
24
|
+
for (const candidate of candidates) {
|
|
25
|
+
probes.push([candidate, await pathExists(resolvePath(candidate, "model/model.json"))])
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const found = probes.find(([, exists]) => exists)?.[0]
|
|
29
|
+
|
|
30
|
+
if (!found) {
|
|
31
|
+
throw new Error(`geographic-model: could not find data/model — looked in ${candidates.join(", ")}`)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return { source: resolvePath(found, "model"), artifact: resolvePath(found, "geographic-model.json") }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Read the committed artifact.
|
|
39
|
+
*
|
|
40
|
+
* The reader checks the format version.
|
|
41
|
+
* It trusts the records because the loader validated them on input.
|
|
42
|
+
*/
|
|
43
|
+
export async function readCompiledGeographicModel(): Promise<CompiledGeographicModel> {
|
|
44
|
+
const text = await readLocalTextFile((await packagedModelPaths()).artifact)
|
|
45
|
+
|
|
46
|
+
// A corrupt committed artifact is a broken build and the `SyntaxError` names the offset.
|
|
47
|
+
// The package's parse wrappers live in `@mailwoman/core`, which this package deliberately does not depend on.
|
|
48
|
+
// oxlint-disable-next-line no-restricted-properties -- see the note above.
|
|
49
|
+
return parseCompiledGeographicModel(JSON.parse(text))
|
|
50
|
+
}
|