@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/out/load.d.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* The authoring loader: a directory of JSON files in, one {@link GeographicModelDocument} out.
|
|
7
|
+
*
|
|
8
|
+
* **The filesystem layout is authoring convenience and carries no meaning.** A concept means the
|
|
9
|
+
* same thing whichever file it was written in, and a file may hold any subset of the tables. What a
|
|
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.
|
|
12
|
+
*
|
|
13
|
+
* Two properties make the loader safe to build an artifact from:
|
|
14
|
+
*
|
|
15
|
+
* 1. **Enumeration order cannot reach the output.** {@link mergeGeographicModelFiles} sorts the
|
|
16
|
+
* files it was handed before reading any of them, so the merged tables are a function of the file
|
|
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.
|
|
27
|
+
*/
|
|
28
|
+
import type { GeographicModelDocument } from "./schema.ts";
|
|
29
|
+
/**
|
|
30
|
+
* The manifest every model directory carries: the document's `version`, and nothing else.
|
|
31
|
+
*/
|
|
32
|
+
export declare const MODEL_MANIFEST_FILENAME = "model.json";
|
|
33
|
+
/**
|
|
34
|
+
* Every way loading can fail. The document validator's whole vocabulary, plus the one failure only a loader meets: a
|
|
35
|
+
* file that is not JSON at all.
|
|
36
|
+
*/
|
|
37
|
+
export declare const LoadIssueCode: {
|
|
38
|
+
/**
|
|
39
|
+
* A source file could not be parsed as JSON. Emitted by the loader alone; the document validator is handed values,
|
|
40
|
+
* never text.
|
|
41
|
+
*/
|
|
42
|
+
readonly MalformedJSON: "malformed_json";
|
|
43
|
+
readonly MissingField: "missing_field";
|
|
44
|
+
readonly WrongType: "wrong_type";
|
|
45
|
+
readonly EmptyValue: "empty_value";
|
|
46
|
+
readonly EmptyList: "empty_list";
|
|
47
|
+
readonly UnknownField: "unknown_field";
|
|
48
|
+
readonly RankingField: "ranking_field";
|
|
49
|
+
readonly UnknownConceptKind: "unknown_concept_kind";
|
|
50
|
+
readonly UnknownModality: "unknown_modality";
|
|
51
|
+
readonly UnknownRelationSemantics: "unknown_relation_semantics";
|
|
52
|
+
readonly UnknownConceptStatus: "unknown_concept_status";
|
|
53
|
+
readonly UnknownExternalVocabulary: "unknown_external_vocabulary";
|
|
54
|
+
readonly UnknownDerivationInputKind: "unknown_derivation_input_kind";
|
|
55
|
+
readonly DuplicateID: "duplicate_id";
|
|
56
|
+
readonly SelfReference: "self_reference";
|
|
57
|
+
readonly CyclicIsA: "cyclic_isa";
|
|
58
|
+
readonly UnknownConcept: "unknown_concept";
|
|
59
|
+
readonly UnknownRelation: "unknown_relation";
|
|
60
|
+
readonly UnknownDerivationInput: "unknown_derivation_input";
|
|
61
|
+
readonly DomainKindMismatch: "domain_kind_mismatch";
|
|
62
|
+
readonly RangeKindMismatch: "range_kind_mismatch";
|
|
63
|
+
readonly InverseNotReciprocal: "inverse_not_reciprocal";
|
|
64
|
+
readonly InverseKindsMismatch: "inverse_kinds_mismatch";
|
|
65
|
+
readonly TransitiveKindsDisjoint: "transitive_kinds_disjoint";
|
|
66
|
+
readonly MalformedCountry: "malformed_country";
|
|
67
|
+
};
|
|
68
|
+
export type LoadIssueCode = (typeof LoadIssueCode)[keyof typeof LoadIssueCode];
|
|
69
|
+
/**
|
|
70
|
+
* One violation, addressed to the file an author can open.
|
|
71
|
+
*/
|
|
72
|
+
export interface SourcedIssue {
|
|
73
|
+
/**
|
|
74
|
+
* The source file, relative to the model directory, with `/` separators on every platform.
|
|
75
|
+
*/
|
|
76
|
+
file: string;
|
|
77
|
+
/**
|
|
78
|
+
* The JSONPath-style address into the MERGED document, kept so a reader can find the record in the table the
|
|
79
|
+
* validator saw.
|
|
80
|
+
*/
|
|
81
|
+
path: string;
|
|
82
|
+
code: LoadIssueCode;
|
|
83
|
+
message: string;
|
|
84
|
+
/**
|
|
85
|
+
* For a duplicate identifier: the file that claimed it first.
|
|
86
|
+
*/
|
|
87
|
+
otherFile?: string;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* One authoring file: its path relative to the model directory, and its text.
|
|
91
|
+
*/
|
|
92
|
+
export interface GeographicModelSourceFile {
|
|
93
|
+
path: string;
|
|
94
|
+
text: string;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Render every issue as one line, `file:path: message [code]`, in the order the loader produced them.
|
|
98
|
+
*/
|
|
99
|
+
export declare function formatSourcedIssues(issues: readonly SourcedIssue[]): string;
|
|
100
|
+
/**
|
|
101
|
+
* Thrown when a model directory does not load. Carries every issue, and states them all in its message, so a caller
|
|
102
|
+
* that only prints `error.message` still sees the whole list.
|
|
103
|
+
*/
|
|
104
|
+
export declare class GeographicModelLoadError extends Error {
|
|
105
|
+
readonly issues: readonly SourcedIssue[];
|
|
106
|
+
constructor(issues: readonly SourcedIssue[]);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Merge authoring files into one document, and validate the result.
|
|
110
|
+
*
|
|
111
|
+
* The files are sorted by path before anything is read, so any enumeration order produces the same tables in the same
|
|
112
|
+
* order. Throws {@link GeographicModelLoadError} with every issue, each addressed to its source file; returns nothing
|
|
113
|
+
* partial.
|
|
114
|
+
*/
|
|
115
|
+
export declare function mergeGeographicModelFiles(files: readonly GeographicModelSourceFile[]): GeographicModelDocument;
|
|
116
|
+
/**
|
|
117
|
+
* Load a model directory: read every `*.json` file under it, merge them, and validate the result.
|
|
118
|
+
*
|
|
119
|
+
* Throws {@link GeographicModelLoadError} with every issue, each addressed to its source file.
|
|
120
|
+
*/
|
|
121
|
+
export declare function loadGeographicModelDirectory(root: string): GeographicModelDocument;
|
|
122
|
+
//# sourceMappingURL=load.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"load.d.ts","sourceRoot":"","sources":["../load.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAMH,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAA;AAY1D;;GAEG;AACH,eAAO,MAAM,uBAAuB,eAAe,CAAA;AAiBnD;;;GAGG;AACH,eAAO,MAAM,aAAa;IAEzB;;;OAGG;;;;;;;;;;;;;;;;;;;;;;;;;;CAEM,CAAA;AAEV,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,OAAO,aAAa,CAAC,CAAA;AAE9E;;GAEG;AACH,MAAM,WAAW,YAAY;IAC5B;;OAEG;IACH,IAAI,EAAE,MAAM,CAAA;IACZ;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,aAAa,CAAA;IACnB,OAAO,EAAE,MAAM,CAAA;IACf;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACzC,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;CACZ;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,SAAS,YAAY,EAAE,GAAG,MAAM,CAQ3E;AAED;;;GAGG;AACH,qBAAa,wBAAyB,SAAQ,KAAK;IAClD,QAAQ,CAAC,MAAM,EAAE,SAAS,YAAY,EAAE,CAAA;gBAE5B,MAAM,EAAE,SAAS,YAAY,EAAE;CAM3C;AA2KD;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,SAAS,yBAAyB,EAAE,GAAG,uBAAuB,CA4C9G;AA8BD;;;;GAIG;AACH,wBAAgB,4BAA4B,CAAC,IAAI,EAAE,MAAM,GAAG,uBAAuB,CAIlF"}
|
package/out/load.js
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* The authoring loader: a directory of JSON files in, one {@link GeographicModelDocument} out.
|
|
7
|
+
*
|
|
8
|
+
* **The filesystem layout is authoring convenience and carries no meaning.** A concept means the
|
|
9
|
+
* same thing whichever file it was written in, and a file may hold any subset of the tables. What a
|
|
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.
|
|
12
|
+
*
|
|
13
|
+
* Two properties make the loader safe to build an artifact from:
|
|
14
|
+
*
|
|
15
|
+
* 1. **Enumeration order cannot reach the output.** {@link mergeGeographicModelFiles} sorts the
|
|
16
|
+
* files it was handed before reading any of them, so the merged tables are a function of the file
|
|
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.
|
|
27
|
+
*/
|
|
28
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
29
|
+
import { resolve } from "node:path";
|
|
30
|
+
import { compareIdentifiers } from "./artifact.js";
|
|
31
|
+
import { validateGeographicModelDocument } from "./validate.js";
|
|
32
|
+
import { add, checkFieldNames, isPlainObject, readArray, readString, ValidationIssueCode, } from "./validation-issues.js";
|
|
33
|
+
/**
|
|
34
|
+
* The manifest every model directory carries: the document's `version`, and nothing else.
|
|
35
|
+
*/
|
|
36
|
+
export const MODEL_MANIFEST_FILENAME = "model.json";
|
|
37
|
+
/**
|
|
38
|
+
* The keys a source file may use. They are the document's tables, minus the manifest's `version`.
|
|
39
|
+
*/
|
|
40
|
+
const TABLE_FIELDS = ["relations", "concepts", "mappings", "observations", "derivedFacts"];
|
|
41
|
+
const MANIFEST_FIELDS = ["version"];
|
|
42
|
+
/**
|
|
43
|
+
* The document path a record occupies, e.g. `$.concepts[7]` or `$.concepts[7].assertions[1]`. Group 3 is present only
|
|
44
|
+
* for an assertion, which is the one record that nests.
|
|
45
|
+
*/
|
|
46
|
+
const RECORD_PATH_PATTERN = /^\$\.([A-Za-z]+)\[(\d+)\](?:\.assertions\[(\d+)\])?/u;
|
|
47
|
+
/**
|
|
48
|
+
* Every way loading can fail. The document validator's whole vocabulary, plus the one failure only a loader meets: a
|
|
49
|
+
* file that is not JSON at all.
|
|
50
|
+
*/
|
|
51
|
+
export const LoadIssueCode = {
|
|
52
|
+
...ValidationIssueCode,
|
|
53
|
+
/**
|
|
54
|
+
* A source file could not be parsed as JSON. Emitted by the loader alone; the document validator is handed values,
|
|
55
|
+
* never text.
|
|
56
|
+
*/
|
|
57
|
+
MalformedJSON: "malformed_json",
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Render every issue as one line, `file:path: message [code]`, in the order the loader produced them.
|
|
61
|
+
*/
|
|
62
|
+
export function formatSourcedIssues(issues) {
|
|
63
|
+
return issues
|
|
64
|
+
.map((issue) => {
|
|
65
|
+
const claimant = issue.otherFile ? ` — first claimed in ${issue.otherFile}` : "";
|
|
66
|
+
return `${issue.file}:${issue.path}: ${issue.message}${claimant} [${issue.code}]`;
|
|
67
|
+
})
|
|
68
|
+
.join("\n");
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Thrown when a model directory does not load. Carries every issue, and states them all in its message, so a caller
|
|
72
|
+
* that only prints `error.message` still sees the whole list.
|
|
73
|
+
*/
|
|
74
|
+
export class GeographicModelLoadError extends Error {
|
|
75
|
+
issues;
|
|
76
|
+
constructor(issues) {
|
|
77
|
+
super(`geographic-model source does not load (${issues.length} issues)\n${formatSourcedIssues(issues)}`);
|
|
78
|
+
this.name = "GeographicModelLoadError";
|
|
79
|
+
this.issues = issues;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function sourced(file, issues) {
|
|
83
|
+
return issues.map((issue) => ({ file, path: issue.path, code: issue.code, message: issue.message }));
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Parse one source file, or report why it could not be parsed.
|
|
87
|
+
*
|
|
88
|
+
* The house wrapper lives in `@mailwoman/core/objects`, and this package takes no dependency on `@mailwoman/core` — the
|
|
89
|
+
* boundary record keeps world semantics out of core, and a build-time loader is not the reason to reverse it. The
|
|
90
|
+
* parser's own message is also the useful half of the report here, which a wrapper returning a fallback discards.
|
|
91
|
+
*/
|
|
92
|
+
function readSourceJSON(file, issues) {
|
|
93
|
+
try {
|
|
94
|
+
// oxlint-disable-next-line no-restricted-properties -- see the note above.
|
|
95
|
+
return JSON.parse(file.text);
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
issues.push({
|
|
99
|
+
file: file.path,
|
|
100
|
+
path: "$",
|
|
101
|
+
code: LoadIssueCode.MalformedJSON,
|
|
102
|
+
message: error instanceof Error ? error.message : String(error),
|
|
103
|
+
});
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function claim(state, table, id, file) {
|
|
108
|
+
if (id === undefined)
|
|
109
|
+
return;
|
|
110
|
+
const claims = state.firstClaims.get(table) ?? new Map();
|
|
111
|
+
state.firstClaims.set(table, claims);
|
|
112
|
+
if (!claims.has(id)) {
|
|
113
|
+
claims.set(id, file);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function recordID(entry) {
|
|
117
|
+
if (!isPlainObject(entry))
|
|
118
|
+
return undefined;
|
|
119
|
+
return typeof entry.id === "string" ? entry.id : undefined;
|
|
120
|
+
}
|
|
121
|
+
function readManifestFile(state, file, value) {
|
|
122
|
+
const issues = [];
|
|
123
|
+
if (!isPlainObject(value)) {
|
|
124
|
+
add(issues, "$", ValidationIssueCode.WrongType, `\`${MODEL_MANIFEST_FILENAME}\` must be an object`);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
checkFieldNames(issues, "$", value, MANIFEST_FIELDS);
|
|
128
|
+
state.version = readString(issues, "$", value, "version", true);
|
|
129
|
+
}
|
|
130
|
+
state.issues.push(...sourced(file.path, issues));
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Append one file's tables to the merged document, recording where every record came from.
|
|
134
|
+
*/
|
|
135
|
+
function readTableFile(state, file, value) {
|
|
136
|
+
const issues = [];
|
|
137
|
+
if (!isPlainObject(value)) {
|
|
138
|
+
add(issues, "$", ValidationIssueCode.WrongType, "a geographic-model source file must be an object");
|
|
139
|
+
state.issues.push(...sourced(file.path, issues));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
// `version` is admitted to the field check and then refused on its own, so the report names where a version belongs
|
|
143
|
+
// instead of only saying the field is unknown here.
|
|
144
|
+
checkFieldNames(issues, "$", value, [...TABLE_FIELDS, ...MANIFEST_FIELDS]);
|
|
145
|
+
if ("version" in value) {
|
|
146
|
+
add(issues, "$.version", ValidationIssueCode.UnknownField, `the document's \`version\` is authored in \`${MODEL_MANIFEST_FILENAME}\`, not in a table file`);
|
|
147
|
+
}
|
|
148
|
+
for (const table of TABLE_FIELDS) {
|
|
149
|
+
if (!(table in value))
|
|
150
|
+
continue;
|
|
151
|
+
for (const entry of readArray(issues, "$", value, table, false) ?? []) {
|
|
152
|
+
const index = state.tables[table].length;
|
|
153
|
+
const id = recordID(entry);
|
|
154
|
+
const documentPath = `${table}[${index}]`;
|
|
155
|
+
state.tables[table].push(entry);
|
|
156
|
+
state.origins.set(documentPath, { file: file.path, table, id });
|
|
157
|
+
claim(state, table, id, file.path);
|
|
158
|
+
if (table !== "concepts" || !isPlainObject(entry) || !Array.isArray(entry.assertions))
|
|
159
|
+
continue;
|
|
160
|
+
for (const [position, assertion] of entry.assertions.entries()) {
|
|
161
|
+
const assertionID = recordID(assertion);
|
|
162
|
+
state.origins.set(`${documentPath}.assertions[${position}]`, {
|
|
163
|
+
file: file.path,
|
|
164
|
+
table: "assertions",
|
|
165
|
+
id: assertionID,
|
|
166
|
+
});
|
|
167
|
+
claim(state, "assertions", assertionID, file.path);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
state.issues.push(...sourced(file.path, issues));
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Re-address one validation issue from its position in the merged document to the file the record was authored in.
|
|
175
|
+
*/
|
|
176
|
+
function attribute(state, issue) {
|
|
177
|
+
const match = RECORD_PATH_PATTERN.exec(issue.path);
|
|
178
|
+
const nested = match?.[3];
|
|
179
|
+
const key = match ? `${match[1]}[${match[2]}]${nested ? `.assertions[${nested}]` : ""}` : undefined;
|
|
180
|
+
const origin = key ? state.origins.get(key) : undefined;
|
|
181
|
+
// A document-level issue — `$.version`, or the root itself — is about the manifest, which is the only file that
|
|
182
|
+
// contributes anything outside a table.
|
|
183
|
+
const file = origin?.file ?? MODEL_MANIFEST_FILENAME;
|
|
184
|
+
const claimant = issue.code === ValidationIssueCode.DuplicateID && origin?.id
|
|
185
|
+
? state.firstClaims.get(origin.table)?.get(origin.id)
|
|
186
|
+
: undefined;
|
|
187
|
+
return {
|
|
188
|
+
file,
|
|
189
|
+
path: issue.path,
|
|
190
|
+
code: issue.code,
|
|
191
|
+
message: issue.message,
|
|
192
|
+
...(claimant ? { otherFile: claimant } : {}),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Merge authoring files into one document, and validate the result.
|
|
197
|
+
*
|
|
198
|
+
* The files are sorted by path before anything is read, so any enumeration order produces the same tables in the same
|
|
199
|
+
* order. Throws {@link GeographicModelLoadError} with every issue, each addressed to its source file; returns nothing
|
|
200
|
+
* partial.
|
|
201
|
+
*/
|
|
202
|
+
export function mergeGeographicModelFiles(files) {
|
|
203
|
+
const state = {
|
|
204
|
+
issues: [],
|
|
205
|
+
tables: { relations: [], concepts: [], mappings: [], observations: [], derivedFacts: [] },
|
|
206
|
+
origins: new Map(),
|
|
207
|
+
firstClaims: new Map(),
|
|
208
|
+
};
|
|
209
|
+
const ordered = files.toSorted((left, right) => compareIdentifiers(left.path, right.path));
|
|
210
|
+
const manifest = ordered.find((file) => file.path === MODEL_MANIFEST_FILENAME);
|
|
211
|
+
if (!manifest) {
|
|
212
|
+
state.issues.push({
|
|
213
|
+
file: MODEL_MANIFEST_FILENAME,
|
|
214
|
+
path: "$",
|
|
215
|
+
code: LoadIssueCode.MissingField,
|
|
216
|
+
message: `a model directory carries \`${MODEL_MANIFEST_FILENAME}\`, holding the document's \`version\``,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
for (const file of ordered) {
|
|
220
|
+
const value = readSourceJSON(file, state.issues);
|
|
221
|
+
if (value === undefined)
|
|
222
|
+
continue;
|
|
223
|
+
if (file === manifest) {
|
|
224
|
+
readManifestFile(state, file, value);
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
readTableFile(state, file, value);
|
|
228
|
+
}
|
|
229
|
+
// A record issue cannot be addressed to a file that failed to parse, so the structural pass reports alone.
|
|
230
|
+
if (state.issues.length)
|
|
231
|
+
throw new GeographicModelLoadError(state.issues);
|
|
232
|
+
const result = validateGeographicModelDocument({ version: state.version, ...state.tables });
|
|
233
|
+
if (!result.ok) {
|
|
234
|
+
throw new GeographicModelLoadError(result.issues.map((issue) => attribute(state, issue)));
|
|
235
|
+
}
|
|
236
|
+
return result.document;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Every `*.json` file under `root`, relative to it, in code-point order.
|
|
240
|
+
*
|
|
241
|
+
* Directory entries are sorted at each level rather than taken as `readdir` returns them, so the list is a property of
|
|
242
|
+
* the tree and not of the filesystem that stored it. Symbolic links are not followed: a model directory is source, and
|
|
243
|
+
* a link out of it is a record whose home nobody can state.
|
|
244
|
+
*/
|
|
245
|
+
function listSourceFiles(root, prefix = "") {
|
|
246
|
+
const entries = readdirSync(resolve(root, prefix), { withFileTypes: true });
|
|
247
|
+
const found = [];
|
|
248
|
+
for (const entry of entries.toSorted((left, right) => compareIdentifiers(left.name, right.name))) {
|
|
249
|
+
const path = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
250
|
+
if (entry.isDirectory()) {
|
|
251
|
+
found.push(...listSourceFiles(root, path));
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
if (entry.isFile() && entry.name.endsWith(".json")) {
|
|
255
|
+
found.push(path);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return found;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Load a model directory: read every `*.json` file under it, merge them, and validate the result.
|
|
262
|
+
*
|
|
263
|
+
* Throws {@link GeographicModelLoadError} with every issue, each addressed to its source file.
|
|
264
|
+
*/
|
|
265
|
+
export function loadGeographicModelDirectory(root) {
|
|
266
|
+
const files = listSourceFiles(root).map((path) => ({ path, text: readFileSync(resolve(root, path), "utf8") }));
|
|
267
|
+
return mergeGeographicModelFiles(files);
|
|
268
|
+
}
|
|
269
|
+
//# sourceMappingURL=load.js.map
|
package/out/load.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"load.js","sourceRoot":"","sources":["../load.ts"],"names":[],"mappings":"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"}
|
package/out/lookup.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* The read surface over a compiled artifact: every question answered by one map probe, none by
|
|
7
|
+
* walking the graph.
|
|
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.
|
|
19
|
+
*/
|
|
20
|
+
import type { POICategoryID } from "@mailwoman/poi-taxonomy/types";
|
|
21
|
+
import { type CompiledGeographicModel } from "./artifact.ts";
|
|
22
|
+
import type { ConceptID, ConceptRecord, DerivedFactRecord, ExternalVocabulary, RelationID, RelationRecord } from "./schema.ts";
|
|
23
|
+
/**
|
|
24
|
+
* Lookups over one compiled artifact.
|
|
25
|
+
*/
|
|
26
|
+
export interface GeographicModelIndex {
|
|
27
|
+
/**
|
|
28
|
+
* The artifact these lookups read, so a consumer holding the index never has to carry both.
|
|
29
|
+
*/
|
|
30
|
+
readonly model: CompiledGeographicModel;
|
|
31
|
+
/**
|
|
32
|
+
* One concept record, or `undefined` when the artifact does not carry it.
|
|
33
|
+
*/
|
|
34
|
+
concept(id: ConceptID): ConceptRecord | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* One relation definition, or `undefined` when the artifact does not carry it.
|
|
37
|
+
*/
|
|
38
|
+
relation(id: RelationID): RelationRecord | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* Every concept this one is a kind of, transitively, in code-point order. Empty for a concept that is a kind of
|
|
41
|
+
* nothing; `undefined` for a concept the artifact does not carry.
|
|
42
|
+
*/
|
|
43
|
+
ancestorsOf(id: ConceptID): readonly ConceptID[] | undefined;
|
|
44
|
+
/**
|
|
45
|
+
* Every derived fact whose subject is this concept, in artifact order. Empty for a concept nothing was derived about;
|
|
46
|
+
* `undefined` for a concept the artifact does not carry.
|
|
47
|
+
*/
|
|
48
|
+
derivedFactsAbout(id: ConceptID): readonly DerivedFactRecord[] | undefined;
|
|
49
|
+
/**
|
|
50
|
+
* The concepts a mapping translates this external identifier into, in code-point order.
|
|
51
|
+
*
|
|
52
|
+
* An empty list is a true negative rather than an unread answer: the artifact carries every mapping the document
|
|
53
|
+
* authored, so nothing having declared this identifier is the whole of what there is to know about it.
|
|
54
|
+
*/
|
|
55
|
+
conceptsForExternalID(vocabulary: ExternalVocabulary, externalID: POICategoryID): readonly ConceptID[];
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Index a compiled artifact for reading.
|
|
59
|
+
*
|
|
60
|
+
* Every table is walked once here so that no table is ever walked again. Nothing is copied — the records handed back
|
|
61
|
+
* are the artifact's own.
|
|
62
|
+
*/
|
|
63
|
+
export declare function createGeographicModelIndex(model: CompiledGeographicModel): GeographicModelIndex;
|
|
64
|
+
//# sourceMappingURL=lookup.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lookup.d.ts","sourceRoot":"","sources":["../lookup.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAA;AAElE,OAAO,EAAE,KAAK,uBAAuB,EAAsB,MAAM,eAAe,CAAA;AAChF,OAAO,KAAK,EACX,SAAS,EACT,aAAa,EACb,iBAAiB,EACjB,kBAAkB,EAClB,UAAU,EACV,cAAc,EACd,MAAM,aAAa,CAAA;AAEpB;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACpC;;OAEG;IACH,QAAQ,CAAC,KAAK,EAAE,uBAAuB,CAAA;IACvC;;OAEG;IACH,OAAO,CAAC,EAAE,EAAE,SAAS,GAAG,aAAa,GAAG,SAAS,CAAA;IACjD;;OAEG;IACH,QAAQ,CAAC,EAAE,EAAE,UAAU,GAAG,cAAc,GAAG,SAAS,CAAA;IACpD;;;OAGG;IACH,WAAW,CAAC,EAAE,EAAE,SAAS,GAAG,SAAS,SAAS,EAAE,GAAG,SAAS,CAAA;IAC5D;;;OAGG;IACH,iBAAiB,CAAC,EAAE,EAAE,SAAS,GAAG,SAAS,iBAAiB,EAAE,GAAG,SAAS,CAAA;IAC1E;;;;;OAKG;IACH,qBAAqB,CAAC,UAAU,EAAE,kBAAkB,EAAE,UAAU,EAAE,aAAa,GAAG,SAAS,SAAS,EAAE,CAAA;CACtG;AAMD;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,uBAAuB,GAAG,oBAAoB,CAmD/F"}
|
package/out/lookup.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* The read surface over a compiled artifact: every question answered by one map probe, none by
|
|
7
|
+
* walking the graph.
|
|
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.
|
|
19
|
+
*/
|
|
20
|
+
import { compareIdentifiers } from "./artifact.js";
|
|
21
|
+
function externalKey(vocabulary, externalID) {
|
|
22
|
+
return `${vocabulary}\u0000${externalID}`;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Index a compiled artifact for reading.
|
|
26
|
+
*
|
|
27
|
+
* Every table is walked once here so that no table is ever walked again. Nothing is copied — the records handed back
|
|
28
|
+
* are the artifact's own.
|
|
29
|
+
*/
|
|
30
|
+
export function createGeographicModelIndex(model) {
|
|
31
|
+
const concepts = new Map(model.concepts.map((concept) => [String(concept.id), concept]));
|
|
32
|
+
const relations = new Map(model.relations.map((relation) => [String(relation.id), relation]));
|
|
33
|
+
const ancestors = new Map(model.inheritanceClosure.map((entry) => [String(entry.concept), entry.ancestors]));
|
|
34
|
+
const facts = new Map();
|
|
35
|
+
const external = new Map();
|
|
36
|
+
for (const fact of model.derivedFacts) {
|
|
37
|
+
const subject = String(fact.subject);
|
|
38
|
+
const existing = facts.get(subject);
|
|
39
|
+
if (existing) {
|
|
40
|
+
existing.push(fact);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
facts.set(subject, [fact]);
|
|
44
|
+
}
|
|
45
|
+
for (const mapping of model.mappings) {
|
|
46
|
+
const key = externalKey(mapping.vocabulary, String(mapping.externalID));
|
|
47
|
+
const existing = external.get(key);
|
|
48
|
+
if (!existing) {
|
|
49
|
+
external.set(key, [mapping.concept]);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (!existing.includes(mapping.concept)) {
|
|
53
|
+
existing.push(mapping.concept);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
model,
|
|
58
|
+
concept: (id) => concepts.get(String(id)),
|
|
59
|
+
relation: (id) => relations.get(String(id)),
|
|
60
|
+
ancestorsOf: (id) => ancestors.get(String(id)),
|
|
61
|
+
derivedFactsAbout: (id) => (concepts.has(String(id)) ? (facts.get(String(id)) ?? []) : undefined),
|
|
62
|
+
conceptsForExternalID: (vocabulary, externalID) => {
|
|
63
|
+
const matches = external.get(externalKey(vocabulary, String(externalID)));
|
|
64
|
+
return matches ? matches.toSorted(compareIdentifiers) : [];
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=lookup.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lookup.js","sourceRoot":"","sources":["../lookup.ts"],"names":[],"mappings":"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"}
|