@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/validation/issues.ts
CHANGED
|
@@ -3,20 +3,17 @@
|
|
|
3
3
|
* @license AGPL-3.0
|
|
4
4
|
* @author Teffen Ellis, et al.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* The issue vocabulary and the primitive field readers `./validate.ts` is built from.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* validation boundary is the same as inventing one. Returning nothing, having said why, lets the
|
|
12
|
-
* caller skip the checks that depended on the field and keep running the ones that did not.
|
|
13
|
-
*
|
|
14
|
-
* `./validate.ts` re-exports {@link ValidationIssueCode} and {@link ValidationIssue}; the readers
|
|
15
|
-
* stay internal to the package.
|
|
8
|
+
* Every reader appends issues to the supplied list and returns `null` when it cannot read a field.
|
|
9
|
+
* The pass records each defect and continues.
|
|
10
|
+
* A default would turn an unreadable field into a value and invent data at the validation boundary.
|
|
16
11
|
*/
|
|
17
12
|
|
|
18
13
|
/**
|
|
19
|
-
* Every way a document can fail validation.
|
|
14
|
+
* Every way a document can fail validation.
|
|
15
|
+
*
|
|
16
|
+
* The set is closed, so a consumer branches on the code rather than on message prose.
|
|
20
17
|
*/
|
|
21
18
|
export const ValidationIssueCode = {
|
|
22
19
|
MissingField: "missing_field",
|
|
@@ -26,12 +23,12 @@ export const ValidationIssueCode = {
|
|
|
26
23
|
*/
|
|
27
24
|
EmptyValue: "empty_value",
|
|
28
25
|
/**
|
|
29
|
-
* A list that must
|
|
26
|
+
* A list that must contain entries was empty.
|
|
30
27
|
*/
|
|
31
28
|
EmptyList: "empty_list",
|
|
32
29
|
UnknownField: "unknown_field",
|
|
33
30
|
/**
|
|
34
|
-
* A field whose name announces ranking policy
|
|
31
|
+
* A field whose name announces ranking policy, refused wherever it appears at any depth.
|
|
35
32
|
*/
|
|
36
33
|
RankingField: "ranking_field",
|
|
37
34
|
UnknownConceptKind: "unknown_concept_kind",
|
|
@@ -64,8 +61,8 @@ export type ValidationIssueCode = (typeof ValidationIssueCode)[keyof typeof Vali
|
|
|
64
61
|
*/
|
|
65
62
|
export interface ValidationIssue {
|
|
66
63
|
/**
|
|
67
|
-
* A JSONPath-style address into the validated input
|
|
68
|
-
*
|
|
64
|
+
* A JSONPath-style address into the validated input whose root is `$`,
|
|
65
|
+
* e.g. `$.concepts[0].assertions[1].modality`.
|
|
69
66
|
*/
|
|
70
67
|
path: string
|
|
71
68
|
code: ValidationIssueCode
|
|
@@ -73,9 +70,8 @@ export interface ValidationIssue {
|
|
|
73
70
|
}
|
|
74
71
|
|
|
75
72
|
/**
|
|
76
|
-
* Name fragments that announce ranking policy
|
|
77
|
-
*
|
|
78
|
-
* than by an enumeration that a seventh spelling walks past.
|
|
73
|
+
* Name fragments that announce ranking policy, matched case-insensitively against every field name
|
|
74
|
+
* at every depth so a new spelling is refused by one rule rather than added to an enumeration.
|
|
79
75
|
*/
|
|
80
76
|
const RANKING_FIELD_FRAGMENTS = [
|
|
81
77
|
"boost",
|
|
@@ -138,7 +134,7 @@ export function readString(
|
|
|
138
134
|
container: Record<string, unknown>,
|
|
139
135
|
key: string,
|
|
140
136
|
required: boolean
|
|
141
|
-
): string |
|
|
137
|
+
): string | null {
|
|
142
138
|
const value = container[key]
|
|
143
139
|
const fieldPath = `${path}.${key}`
|
|
144
140
|
|
|
@@ -147,19 +143,19 @@ export function readString(
|
|
|
147
143
|
add(issues, fieldPath, ValidationIssueCode.MissingField, `\`${key}\` is required`)
|
|
148
144
|
}
|
|
149
145
|
|
|
150
|
-
return
|
|
146
|
+
return null
|
|
151
147
|
}
|
|
152
148
|
|
|
153
149
|
if (typeof value !== "string") {
|
|
154
150
|
add(issues, fieldPath, ValidationIssueCode.WrongType, `\`${key}\` must be a string`)
|
|
155
151
|
|
|
156
|
-
return
|
|
152
|
+
return null
|
|
157
153
|
}
|
|
158
154
|
|
|
159
|
-
if (!value.trim()
|
|
155
|
+
if (!value.trim()) {
|
|
160
156
|
add(issues, fieldPath, ValidationIssueCode.EmptyValue, `\`${key}\` must not be blank`)
|
|
161
157
|
|
|
162
|
-
return
|
|
158
|
+
return null
|
|
163
159
|
}
|
|
164
160
|
|
|
165
161
|
return value
|
|
@@ -170,20 +166,20 @@ export function readBoolean(
|
|
|
170
166
|
path: string,
|
|
171
167
|
container: Record<string, unknown>,
|
|
172
168
|
key: string
|
|
173
|
-
): boolean |
|
|
169
|
+
): boolean | null {
|
|
174
170
|
const value = container[key]
|
|
175
171
|
const fieldPath = `${path}.${key}`
|
|
176
172
|
|
|
177
173
|
if (value === undefined) {
|
|
178
174
|
add(issues, fieldPath, ValidationIssueCode.MissingField, `\`${key}\` is required`)
|
|
179
175
|
|
|
180
|
-
return
|
|
176
|
+
return null
|
|
181
177
|
}
|
|
182
178
|
|
|
183
179
|
if (typeof value !== "boolean") {
|
|
184
180
|
add(issues, fieldPath, ValidationIssueCode.WrongType, `\`${key}\` must be a boolean`)
|
|
185
181
|
|
|
186
|
-
return
|
|
182
|
+
return null
|
|
187
183
|
}
|
|
188
184
|
|
|
189
185
|
return value
|
|
@@ -195,7 +191,7 @@ export function readArray(
|
|
|
195
191
|
container: Record<string, unknown>,
|
|
196
192
|
key: string,
|
|
197
193
|
required: boolean
|
|
198
|
-
): unknown[] |
|
|
194
|
+
): unknown[] | null {
|
|
199
195
|
const value = container[key]
|
|
200
196
|
const fieldPath = `${path}.${key}`
|
|
201
197
|
|
|
@@ -204,13 +200,13 @@ export function readArray(
|
|
|
204
200
|
add(issues, fieldPath, ValidationIssueCode.MissingField, `\`${key}\` is required`)
|
|
205
201
|
}
|
|
206
202
|
|
|
207
|
-
return
|
|
203
|
+
return null
|
|
208
204
|
}
|
|
209
205
|
|
|
210
206
|
if (!Array.isArray(value)) {
|
|
211
207
|
add(issues, fieldPath, ValidationIssueCode.WrongType, `\`${key}\` must be an array`)
|
|
212
208
|
|
|
213
|
-
return
|
|
209
|
+
return null
|
|
214
210
|
}
|
|
215
211
|
|
|
216
212
|
return value
|
|
@@ -222,10 +218,10 @@ export function readStringArray(
|
|
|
222
218
|
container: Record<string, unknown>,
|
|
223
219
|
key: string,
|
|
224
220
|
required: boolean
|
|
225
|
-
): string[] |
|
|
221
|
+
): string[] | null {
|
|
226
222
|
const entries = readArray(issues, path, container, key, required)
|
|
227
223
|
|
|
228
|
-
if (!entries) return
|
|
224
|
+
if (!entries) return null
|
|
229
225
|
|
|
230
226
|
const values: string[] = []
|
|
231
227
|
|
|
@@ -238,7 +234,7 @@ export function readStringArray(
|
|
|
238
234
|
continue
|
|
239
235
|
}
|
|
240
236
|
|
|
241
|
-
if (!entry.trim()
|
|
237
|
+
if (!entry.trim()) {
|
|
242
238
|
add(issues, entryPath, ValidationIssueCode.EmptyValue, "must not be blank")
|
|
243
239
|
|
|
244
240
|
continue
|
|
@@ -257,10 +253,10 @@ export function readVocabularyValue<T extends string>(
|
|
|
257
253
|
key: string,
|
|
258
254
|
allowed: readonly T[],
|
|
259
255
|
code: ValidationIssueCode
|
|
260
|
-
): T |
|
|
256
|
+
): T | null {
|
|
261
257
|
const value = readString(issues, path, container, key, true)
|
|
262
258
|
|
|
263
|
-
if (value
|
|
259
|
+
if (!value) return null
|
|
264
260
|
|
|
265
261
|
const match = allowed.find((candidate) => candidate === value)
|
|
266
262
|
|
|
@@ -268,7 +264,7 @@ export function readVocabularyValue<T extends string>(
|
|
|
268
264
|
add(issues, `${path}.${key}`, code, `\`${value}\` is not one of ${listVocabulary(allowed)}`)
|
|
269
265
|
}
|
|
270
266
|
|
|
271
|
-
return match
|
|
267
|
+
return match ?? null
|
|
272
268
|
}
|
|
273
269
|
|
|
274
270
|
export function readVocabularyArray<T extends string>(
|
|
@@ -278,10 +274,10 @@ export function readVocabularyArray<T extends string>(
|
|
|
278
274
|
key: string,
|
|
279
275
|
allowed: readonly T[],
|
|
280
276
|
code: ValidationIssueCode
|
|
281
|
-
): T[] |
|
|
277
|
+
): T[] | null {
|
|
282
278
|
const values = readStringArray(issues, path, container, key, true)
|
|
283
279
|
|
|
284
|
-
if (!values) return
|
|
280
|
+
if (!values) return null
|
|
285
281
|
|
|
286
282
|
const matched: T[] = []
|
|
287
283
|
|
|
@@ -18,9 +18,10 @@ import type {
|
|
|
18
18
|
import { add, listVocabulary, type ValidationIssue, ValidationIssueCode } from "#validation/issues"
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
-
* Index a table by identifier, reporting every record after the first that
|
|
21
|
+
* Index a table by identifier, reporting every record after the first that
|
|
22
|
+
* claims an identifier already taken.
|
|
22
23
|
*/
|
|
23
|
-
function indexByID<T extends { path: string; id
|
|
24
|
+
function indexByID<T extends { path: string; id: string | null }>(
|
|
24
25
|
issues: ValidationIssue[],
|
|
25
26
|
records: readonly T[],
|
|
26
27
|
label: string
|
|
@@ -28,7 +29,7 @@ function indexByID<T extends { path: string; id?: string }>(
|
|
|
28
29
|
const index = new Map<string, T>()
|
|
29
30
|
|
|
30
31
|
for (const record of records) {
|
|
31
|
-
if (record.id
|
|
32
|
+
if (!record.id) continue
|
|
32
33
|
|
|
33
34
|
if (index.has(record.id)) {
|
|
34
35
|
add(
|
|
@@ -48,24 +49,22 @@ function indexByID<T extends { path: string; id?: string }>(
|
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
interface EdgeCheck {
|
|
51
|
-
subjectKind
|
|
52
|
+
subjectKind: ConceptKind | null
|
|
52
53
|
subjectPath: string
|
|
53
|
-
relationID
|
|
54
|
+
relationID: string | null
|
|
54
55
|
relationPath: string
|
|
55
|
-
objectID
|
|
56
|
+
objectID: string | null
|
|
56
57
|
objectPath: string
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
/**
|
|
60
|
-
* Resolve one subject–relation–object edge and check it against the relation's declared domain
|
|
61
|
-
*
|
|
62
|
-
* Shared by authored assertions, source observations, and derived facts. The three differ in who stands behind them and
|
|
63
|
-
* in what provenance they carry, and the structural question asked of them is the same one.
|
|
61
|
+
* Resolve one subject–relation–object edge and check it against the relation's declared domain
|
|
62
|
+
* and range kinds shared by authored assertions, source observations and derived facts.
|
|
64
63
|
*/
|
|
65
64
|
function checkEdge(issues: ValidationIssue[], edge: EdgeCheck, tables: ReferenceTables): void {
|
|
66
|
-
const relation = edge.relationID
|
|
65
|
+
const relation = edge.relationID ? tables.relations.get(edge.relationID) : null
|
|
67
66
|
|
|
68
|
-
if (edge.relationID
|
|
67
|
+
if (edge.relationID && !relation) {
|
|
69
68
|
add(
|
|
70
69
|
issues,
|
|
71
70
|
edge.relationPath,
|
|
@@ -74,9 +73,9 @@ function checkEdge(issues: ValidationIssue[], edge: EdgeCheck, tables: Reference
|
|
|
74
73
|
)
|
|
75
74
|
}
|
|
76
75
|
|
|
77
|
-
const object = edge.objectID
|
|
76
|
+
const object = edge.objectID ? tables.concepts.get(edge.objectID) : null
|
|
78
77
|
|
|
79
|
-
if (edge.objectID
|
|
78
|
+
if (edge.objectID && !object) {
|
|
80
79
|
add(
|
|
81
80
|
issues,
|
|
82
81
|
edge.objectPath,
|
|
@@ -133,7 +132,7 @@ function checkRelation(
|
|
|
133
132
|
}
|
|
134
133
|
}
|
|
135
134
|
|
|
136
|
-
if (relation.inverse
|
|
135
|
+
if (!relation.inverse) return
|
|
137
136
|
|
|
138
137
|
const inverse = index.get(relation.inverse)
|
|
139
138
|
|
|
@@ -186,13 +185,11 @@ function checkRelation(
|
|
|
186
185
|
}
|
|
187
186
|
|
|
188
187
|
/**
|
|
189
|
-
* Follow `isA` upward from one concept and report the trail if it returns to where it started
|
|
190
|
-
*
|
|
191
|
-
* The direct self-edge is left out of the walk: `checkIsA` already reports that as a self-reference, at the entry that
|
|
192
|
-
* carries it, and a second report saying the same concept cycles through itself tells its author nothing new.
|
|
188
|
+
* Follow `isA` upward from one concept and report the trail if it returns to where it started,
|
|
189
|
+
* leaving the direct self-edge to `checkIsA`'s self-reference report.
|
|
193
190
|
*/
|
|
194
|
-
function findIsACycle(start: ConceptView, concepts: ReadonlyMap<string, ConceptView>): string[] |
|
|
195
|
-
if (start.id
|
|
191
|
+
function findIsACycle(start: ConceptView, concepts: ReadonlyMap<string, ConceptView>): string[] | null {
|
|
192
|
+
if (!start.id) return null
|
|
196
193
|
|
|
197
194
|
const startID = start.id
|
|
198
195
|
const visited = new Set<string>()
|
|
@@ -217,7 +214,7 @@ function findIsACycle(start: ConceptView, concepts: ReadonlyMap<string, ConceptV
|
|
|
217
214
|
}
|
|
218
215
|
}
|
|
219
216
|
|
|
220
|
-
return
|
|
217
|
+
return null
|
|
221
218
|
}
|
|
222
219
|
|
|
223
220
|
function checkIsA(issues: ValidationIssue[], concept: ConceptView, concepts: ReadonlyMap<string, ConceptView>): void {
|
|
@@ -260,7 +257,7 @@ function checkDerivationInputs(issues: ValidationIssue[], fact: DerivedFactView,
|
|
|
260
257
|
}
|
|
261
258
|
|
|
262
259
|
for (const input of fact.inputs) {
|
|
263
|
-
if (input.kind
|
|
260
|
+
if (!input.kind || !input.id) continue
|
|
264
261
|
|
|
265
262
|
if (input.kind === DerivationInputKind.DerivedFact && input.id === fact.id) {
|
|
266
263
|
add(issues, `${input.path}.id`, ValidationIssueCode.SelfReference, "a derived fact is not one of its own inputs")
|
|
@@ -322,7 +319,7 @@ export function checkReferences(issues: ValidationIssue[], view: DocumentView):
|
|
|
322
319
|
}
|
|
323
320
|
|
|
324
321
|
for (const mapping of view.mappings) {
|
|
325
|
-
if (mapping.concept
|
|
322
|
+
if (mapping.concept && !concepts.has(mapping.concept)) {
|
|
326
323
|
add(
|
|
327
324
|
issues,
|
|
328
325
|
`${mapping.path}.concept`,
|
|
@@ -333,9 +330,9 @@ export function checkReferences(issues: ValidationIssue[], view: DocumentView):
|
|
|
333
330
|
}
|
|
334
331
|
|
|
335
332
|
for (const triple of [...view.observations, ...view.derivedFacts]) {
|
|
336
|
-
const subject = triple.subject
|
|
333
|
+
const subject = triple.subject ? concepts.get(triple.subject) : null
|
|
337
334
|
|
|
338
|
-
if (triple.subject
|
|
335
|
+
if (triple.subject && !subject) {
|
|
339
336
|
add(
|
|
340
337
|
issues,
|
|
341
338
|
`${triple.path}.subject`,
|
|
@@ -347,7 +344,7 @@ export function checkReferences(issues: ValidationIssue[], view: DocumentView):
|
|
|
347
344
|
checkEdge(
|
|
348
345
|
issues,
|
|
349
346
|
{
|
|
350
|
-
subjectKind: subject?.kind,
|
|
347
|
+
subjectKind: subject?.kind ?? null,
|
|
351
348
|
subjectPath: `${triple.path}.subject`,
|
|
352
349
|
relationID: triple.relation,
|
|
353
350
|
relationPath: `${triple.path}.relation`,
|
package/out/artifact.d.ts
CHANGED
|
@@ -3,61 +3,46 @@
|
|
|
3
3
|
* @license AGPL-3.0
|
|
4
4
|
* @author Teffen Ellis, et al.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
* bytes back into a typed value.
|
|
6
|
+
* Defines the compiled geographic model artifact, its canonical serialization and its reader.
|
|
8
7
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* consumer that had to walk `concepts[].isA` to answer a question about one concept would be doing
|
|
12
|
-
* query-time traversal, which the boundary record excludes.
|
|
8
|
+
* Runtime consumers read the artifact instead of the authored records. The artifact includes the
|
|
9
|
+
* materialized `isA` closure so that consumers never traverse `concepts[].isA` at query time.
|
|
13
10
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* canonicalizes by itself beats a hand-kept field order, which drifts the first time the schema
|
|
18
|
-
* gains a field.
|
|
19
|
-
* 2. **Every table is ordered by identifier**, under {@link compareIdentifiers} — code point, never
|
|
20
|
-
* `localeCompare`, whose answer depends on the machine's collation. Arrays INSIDE a record keep
|
|
21
|
-
* the order they were authored in; the compiler writes the tables, so it orders those.
|
|
22
|
-
*
|
|
23
|
-
* Nothing here records when compilation ran. `modelVersion` is the authored document's own version,
|
|
24
|
-
* so two builds of one document are byte-identical, and a clock in the artifact would make every
|
|
25
|
-
* regenerate a diff.
|
|
26
|
-
*
|
|
27
|
-
* Boundary record: `docs/superpowers/specs/2026-08-26-geographic-model-boundaries.md` (#1917).
|
|
11
|
+
* Serialization is deterministic. Object keys use code-point order at every depth.
|
|
12
|
+
* The compiler sorts each table by identifier in code-point order. Arrays inside a record keep authored order.
|
|
13
|
+
* The artifact records no build time. Two builds of one document are byte-identical.
|
|
28
14
|
*/
|
|
29
15
|
import type { ConceptID, ConceptRecord, DerivedFactRecord, ExternalMappingRecord, RelationRecord, SourceObservationRecord } from "#schema";
|
|
30
16
|
/**
|
|
31
|
-
* The artifact
|
|
32
|
-
* was not written for says so instead of quietly reading fields that moved.
|
|
17
|
+
* The artifact format version.
|
|
33
18
|
*
|
|
34
|
-
*
|
|
19
|
+
* Increment it whenever the compiled shape changes, so that a reader rejects
|
|
20
|
+
* an artifact it cannot interpret.
|
|
35
21
|
*/
|
|
36
22
|
export declare const ARTIFACT_SCHEMA_VERSION = 1;
|
|
37
23
|
/**
|
|
38
|
-
* One concept's transitive `isA` ancestors
|
|
24
|
+
* One concept's transitive `isA` ancestors.
|
|
39
25
|
*
|
|
40
|
-
* Every concept
|
|
41
|
-
*
|
|
42
|
-
* concept looks like.
|
|
26
|
+
* Every compiled concept has an entry, even with an empty ancestor list,
|
|
27
|
+
* so a missing entry always means an unknown concept.
|
|
43
28
|
*/
|
|
44
29
|
export interface InheritanceClosureEntry {
|
|
45
30
|
concept: ConceptID;
|
|
46
31
|
ancestors: readonly ConceptID[];
|
|
47
32
|
}
|
|
48
33
|
/**
|
|
49
|
-
* The
|
|
34
|
+
* The compiled artifact.
|
|
50
35
|
*
|
|
51
|
-
* The
|
|
52
|
-
*
|
|
36
|
+
* The compiler sorts the authored tables without changing any record.
|
|
37
|
+
* It adds the inheritance closure and the derived facts that it computes.
|
|
53
38
|
*/
|
|
54
39
|
export interface CompiledGeographicModel {
|
|
55
40
|
/**
|
|
56
|
-
* The format version
|
|
41
|
+
* The format version, equal to {@link ARTIFACT_SCHEMA_VERSION} when the artifact was written.
|
|
57
42
|
*/
|
|
58
43
|
schemaVersion: number;
|
|
59
44
|
/**
|
|
60
|
-
* The authored document's
|
|
45
|
+
* The authored document's `version`.
|
|
61
46
|
*/
|
|
62
47
|
modelVersion: string;
|
|
63
48
|
relations: readonly RelationRecord[];
|
|
@@ -66,18 +51,18 @@ export interface CompiledGeographicModel {
|
|
|
66
51
|
observations: readonly SourceObservationRecord[];
|
|
67
52
|
inheritanceClosure: readonly InheritanceClosureEntry[];
|
|
68
53
|
/**
|
|
69
|
-
* The authored derived facts
|
|
70
|
-
*
|
|
54
|
+
* The authored derived facts plus the facts that the compiler derived.
|
|
55
|
+
*
|
|
56
|
+
* Each fact records its procedure and every record that the procedure read.
|
|
71
57
|
*/
|
|
72
58
|
derivedFacts: readonly DerivedFactRecord[];
|
|
73
59
|
}
|
|
74
60
|
/**
|
|
75
|
-
*
|
|
61
|
+
* Serializes the artifact as tab-indented JSON with a trailing newline and sorted keys.
|
|
76
62
|
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
* two compiles.
|
|
63
|
+
* The committed copy also passes through `oxfmt`, which inlines short arrays.
|
|
64
|
+
* A freshness check compares parsed values.
|
|
65
|
+
* A byte comparison applies only to two compiler outputs.
|
|
81
66
|
*/
|
|
82
67
|
export declare function serializeCompiledModel(model: CompiledGeographicModel): string;
|
|
83
68
|
/**
|
|
@@ -87,13 +72,11 @@ export declare class GeographicModelArtifactError extends Error {
|
|
|
87
72
|
constructor(message: string);
|
|
88
73
|
}
|
|
89
74
|
/**
|
|
90
|
-
*
|
|
91
|
-
* {@link CompiledGeographicModel}.
|
|
75
|
+
* Reads a value from `JSON.parse` as a {@link CompiledGeographicModel}.
|
|
92
76
|
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
* written by a different compiler.
|
|
77
|
+
* The reader checks the format version, the model version and the presence of every table.
|
|
78
|
+
* It does not revalidate records, because the compiler accepts only documents
|
|
79
|
+
* that already passed validation.
|
|
97
80
|
*/
|
|
98
81
|
export declare function parseCompiledGeographicModel(input: unknown): CompiledGeographicModel;
|
|
99
82
|
//# sourceMappingURL=artifact.d.ts.map
|
package/out/artifact.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"artifact.d.ts","sourceRoot":"","sources":["../lib/artifact.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"artifact.d.ts","sourceRoot":"","sources":["../lib/artifact.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAMH,OAAO,KAAK,EACX,SAAS,EACT,aAAa,EACb,iBAAiB,EACjB,qBAAqB,EACrB,cAAc,EACd,uBAAuB,EACvB,MAAM,SAAS,CAAA;AAEhB;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,IAAI,CAAA;AAExC;;;;;GAKG;AACH,MAAM,WAAW,uBAAuB;IACvC,OAAO,EAAE,SAAS,CAAA;IAClB,SAAS,EAAE,SAAS,SAAS,EAAE,CAAA;CAC/B;AAED;;;;;GAKG;AACH,MAAM,WAAW,uBAAuB;IACvC;;OAEG;IACH,aAAa,EAAE,MAAM,CAAA;IACrB;;OAEG;IACH,YAAY,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,SAAS,cAAc,EAAE,CAAA;IACpC,QAAQ,EAAE,SAAS,aAAa,EAAE,CAAA;IAClC,QAAQ,EAAE,SAAS,qBAAqB,EAAE,CAAA;IAC1C,YAAY,EAAE,SAAS,uBAAuB,EAAE,CAAA;IAChD,kBAAkB,EAAE,SAAS,uBAAuB,EAAE,CAAA;IACtD;;;;OAIG;IACH,YAAY,EAAE,SAAS,iBAAiB,EAAE,CAAA;CAC1C;AAwBD;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,uBAAuB,GAAG,MAAM,CAE7E;AAED;;GAEG;AACH,qBAAa,4BAA6B,SAAQ,KAAK;IACtD,YAAY,OAAO,EAAE,MAAM,EAI1B;CACD;AAmCD;;;;;;GAMG;AACH,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,OAAO,GAAG,uBAAuB,CAMpF"}
|
package/out/artifact.js
CHANGED
|
@@ -3,48 +3,28 @@
|
|
|
3
3
|
* @license AGPL-3.0
|
|
4
4
|
* @author Teffen Ellis, et al.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
* bytes back into a typed value.
|
|
6
|
+
* Defines the compiled geographic model artifact, its canonical serialization and its reader.
|
|
8
7
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* consumer that had to walk `concepts[].isA` to answer a question about one concept would be doing
|
|
12
|
-
* query-time traversal, which the boundary record excludes.
|
|
8
|
+
* Runtime consumers read the artifact instead of the authored records. The artifact includes the
|
|
9
|
+
* materialized `isA` closure so that consumers never traverse `concepts[].isA` at query time.
|
|
13
10
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* canonicalizes by itself beats a hand-kept field order, which drifts the first time the schema
|
|
18
|
-
* gains a field.
|
|
19
|
-
* 2. **Every table is ordered by identifier**, under {@link compareIdentifiers} — code point, never
|
|
20
|
-
* `localeCompare`, whose answer depends on the machine's collation. Arrays INSIDE a record keep
|
|
21
|
-
* the order they were authored in; the compiler writes the tables, so it orders those.
|
|
22
|
-
*
|
|
23
|
-
* Nothing here records when compilation ran. `modelVersion` is the authored document's own version,
|
|
24
|
-
* so two builds of one document are byte-identical, and a clock in the artifact would make every
|
|
25
|
-
* regenerate a diff.
|
|
26
|
-
*
|
|
27
|
-
* Boundary record: `docs/superpowers/specs/2026-08-26-geographic-model-boundaries.md` (#1917).
|
|
11
|
+
* Serialization is deterministic. Object keys use code-point order at every depth.
|
|
12
|
+
* The compiler sorts each table by identifier in code-point order. Arrays inside a record keep authored order.
|
|
13
|
+
* The artifact records no build time. Two builds of one document are byte-identical.
|
|
28
14
|
*/
|
|
29
|
-
import { prettyJSON } from "@mailwoman/core/json";
|
|
15
|
+
import { prettyJSON, stringifyJSON } from "@mailwoman/core/json";
|
|
30
16
|
import { isPlainObject } from "@mailwoman/core/objects";
|
|
31
17
|
import { compareByCodePoint } from "@mailwoman/core/strings/compare";
|
|
32
18
|
/**
|
|
33
|
-
* The artifact
|
|
34
|
-
* was not written for says so instead of quietly reading fields that moved.
|
|
19
|
+
* The artifact format version.
|
|
35
20
|
*
|
|
36
|
-
*
|
|
21
|
+
* Increment it whenever the compiled shape changes, so that a reader rejects
|
|
22
|
+
* an artifact it cannot interpret.
|
|
37
23
|
*/
|
|
38
24
|
export const ARTIFACT_SCHEMA_VERSION = 1;
|
|
39
25
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* `String.prototype.localeCompare` is the trap this exists to avoid — its answer depends on the machine's collation, so
|
|
43
|
-
* an artifact ordered with it is reproducible only on the machine that built it.
|
|
44
|
-
*/
|
|
45
|
-
/**
|
|
46
|
-
* Rebuild `value` with every object's keys in code-point order, at every depth. Arrays keep their order — ordering
|
|
47
|
-
* those is the compiler's job, and doing it here would silently reorder authored data.
|
|
26
|
+
* Copies `value` with object keys in code-point order at every depth and drops `undefined` entries.
|
|
27
|
+
* Arrays keep their order.
|
|
48
28
|
*/
|
|
49
29
|
function canonicalize(value) {
|
|
50
30
|
if (Array.isArray(value))
|
|
@@ -61,12 +41,11 @@ function canonicalize(value) {
|
|
|
61
41
|
return canonical;
|
|
62
42
|
}
|
|
63
43
|
/**
|
|
64
|
-
*
|
|
44
|
+
* Serializes the artifact as tab-indented JSON with a trailing newline and sorted keys.
|
|
65
45
|
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* two compiles.
|
|
46
|
+
* The committed copy also passes through `oxfmt`, which inlines short arrays.
|
|
47
|
+
* A freshness check compares parsed values.
|
|
48
|
+
* A byte comparison applies only to two compiler outputs.
|
|
70
49
|
*/
|
|
71
50
|
export function serializeCompiledModel(model) {
|
|
72
51
|
return prettyJSON(canonicalize(model));
|
|
@@ -81,8 +60,7 @@ export class GeographicModelArtifactError extends Error {
|
|
|
81
60
|
}
|
|
82
61
|
}
|
|
83
62
|
/**
|
|
84
|
-
* The tables every artifact
|
|
85
|
-
* that table with an empty list.
|
|
63
|
+
* The tables that every artifact must contain.
|
|
86
64
|
*/
|
|
87
65
|
const ARTIFACT_TABLES = [
|
|
88
66
|
"concepts",
|
|
@@ -93,33 +71,28 @@ const ARTIFACT_TABLES = [
|
|
|
93
71
|
"relations",
|
|
94
72
|
];
|
|
95
73
|
/**
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
* Kept separate from {@link parseCompiledGeographicModel} so the reader casts the value it was HANDED, still typed
|
|
99
|
-
* `unknown`, rather than a narrowed shape it would have to launder through a second cast to widen again.
|
|
74
|
+
* Returns the reason `value` is not a compiled artifact, or `null` when it is one.
|
|
100
75
|
*/
|
|
101
76
|
function artifactProblem(value) {
|
|
102
77
|
if (!isPlainObject(value))
|
|
103
78
|
return "a compiled geographic model must be an object";
|
|
104
79
|
if (value.schemaVersion !== ARTIFACT_SCHEMA_VERSION) {
|
|
105
|
-
return `this reader understands artifact schema version ${ARTIFACT_SCHEMA_VERSION}; the artifact declares ${
|
|
80
|
+
return `this reader understands artifact schema version ${ARTIFACT_SCHEMA_VERSION}; the artifact declares ${stringifyJSON(value.schemaVersion)}`;
|
|
106
81
|
}
|
|
107
|
-
if (typeof value.modelVersion !== "string" || !value.modelVersion.trim()
|
|
82
|
+
if (typeof value.modelVersion !== "string" || !value.modelVersion.trim()) {
|
|
108
83
|
return "`modelVersion` must be a non-blank string";
|
|
109
84
|
}
|
|
110
85
|
const missing = ARTIFACT_TABLES.filter((table) => !Array.isArray(value[table]));
|
|
111
86
|
if (!missing.length)
|
|
112
|
-
return
|
|
87
|
+
return null;
|
|
113
88
|
return `the artifact is missing ${missing.map((table) => `\`${table}\``).join(", ")}`;
|
|
114
89
|
}
|
|
115
90
|
/**
|
|
116
|
-
*
|
|
117
|
-
* {@link CompiledGeographicModel}.
|
|
91
|
+
* Reads a value from `JSON.parse` as a {@link CompiledGeographicModel}.
|
|
118
92
|
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
* written by a different compiler.
|
|
93
|
+
* The reader checks the format version, the model version and the presence of every table.
|
|
94
|
+
* It does not revalidate records, because the compiler accepts only documents
|
|
95
|
+
* that already passed validation.
|
|
123
96
|
*/
|
|
124
97
|
export function parseCompiledGeographicModel(input) {
|
|
125
98
|
const problem = artifactProblem(input);
|
package/out/artifact.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"artifact.js","sourceRoot":"","sources":["../lib/artifact.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"artifact.js","sourceRoot":"","sources":["../lib/artifact.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AAChE,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAA;AACvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAA;AAWpE;;;;;GAKG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,CAAA;AAyCxC;;;GAGG;AACH,SAAS,YAAY,CAAC,KAAc;IACnC,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC,GAAG,CAAC,YAAY,CAAC,CAAA;IAExD,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAA;IAEvC,MAAM,SAAS,GAA4B,EAAE,CAAA;IAE7C,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,CAAC;QACnE,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,CAAA;QAExB,IAAI,KAAK,KAAK,SAAS;YAAE,SAAQ;QAEjC,SAAS,CAAC,GAAG,CAAC,GAAG,YAAY,CAAC,KAAK,CAAC,CAAA;IACrC,CAAC;IAED,OAAO,SAAS,CAAA;AACjB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,sBAAsB,CAAC,KAA8B;IACpE,OAAO,UAAU,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAA;AACvC,CAAC;AAED;;GAEG;AACH,MAAM,OAAO,4BAA6B,SAAQ,KAAK;IACtD,YAAY,OAAe;QAC1B,KAAK,CAAC,OAAO,CAAC,CAAA;QAEd,IAAI,CAAC,IAAI,GAAG,8BAA8B,CAAA;IAC3C,CAAC;CACD;AAED;;GAEG;AACH,MAAM,eAAe,GAAG;IACvB,UAAU;IACV,cAAc;IACd,oBAAoB;IACpB,UAAU;IACV,cAAc;IACd,WAAW;CACF,CAAA;AAEV;;GAEG;AACH,SAAS,eAAe,CAAC,KAAc;IACtC,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC;QAAE,OAAO,+CAA+C,CAAA;IAEjF,IAAI,KAAK,CAAC,aAAa,KAAK,uBAAuB,EAAE,CAAC;QACrD,OAAO,mDAAmD,uBAAuB,2BAA2B,aAAa,CAAC,KAAK,CAAC,aAAa,CAAC,EAAE,CAAA;IACjJ,CAAC;IAED,IAAI,OAAO,KAAK,CAAC,YAAY,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,EAAE,CAAC;QAC1E,OAAO,2CAA2C,CAAA;IACnD,CAAC;IAED,MAAM,OAAO,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;IAE/E,IAAI,CAAC,OAAO,CAAC,MAAM;QAAE,OAAO,IAAI,CAAA;IAEhC,OAAO,2BAA2B,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,KAAK,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAA;AACtF,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,4BAA4B,CAAC,KAAc;IAC1D,MAAM,OAAO,GAAG,eAAe,CAAC,KAAK,CAAC,CAAA;IAEtC,IAAI,OAAO;QAAE,MAAM,IAAI,4BAA4B,CAAC,OAAO,CAAC,CAAA;IAE5D,OAAO,KAAgC,CAAA;AACxC,CAAC"}
|