@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.
Files changed (62) hide show
  1. package/README.md +32 -32
  2. package/data/geographic-model.json +5 -5
  3. package/data/model/concepts.json +5 -5
  4. package/lib/artifact.ts +37 -65
  5. package/lib/compile.ts +55 -70
  6. package/lib/index.ts +22 -35
  7. package/lib/load.ts +60 -72
  8. package/lib/lookup.ts +12 -28
  9. package/lib/packaged.ts +50 -0
  10. package/lib/schema.ts +104 -97
  11. package/lib/validate.ts +32 -56
  12. package/lib/validation/issues.ts +32 -36
  13. package/lib/validation/references.ts +24 -27
  14. package/out/artifact.d.ts +28 -45
  15. package/out/artifact.d.ts.map +1 -1
  16. package/out/artifact.js +25 -52
  17. package/out/artifact.js.map +1 -1
  18. package/out/compile.d.ts +27 -41
  19. package/out/compile.d.ts.map +1 -1
  20. package/out/compile.js +42 -64
  21. package/out/compile.js.map +1 -1
  22. package/out/index.d.ts +22 -35
  23. package/out/index.d.ts.map +1 -1
  24. package/out/index.js +22 -35
  25. package/out/index.js.map +1 -1
  26. package/out/load.d.ts +30 -39
  27. package/out/load.d.ts.map +1 -1
  28. package/out/load.js +42 -56
  29. package/out/load.js.map +1 -1
  30. package/out/lookup.d.ts +12 -28
  31. package/out/lookup.d.ts.map +1 -1
  32. package/out/lookup.js +4 -16
  33. package/out/lookup.js.map +1 -1
  34. package/out/packaged.d.ts +24 -0
  35. package/out/packaged.d.ts.map +1 -0
  36. package/out/packaged.js +42 -0
  37. package/out/packaged.js.map +1 -0
  38. package/out/schema.d.ts +104 -97
  39. package/out/schema.d.ts.map +1 -1
  40. package/out/schema.js +37 -52
  41. package/out/schema.js.map +1 -1
  42. package/out/tools/build-artifact.d.ts +27 -0
  43. package/out/tools/build-artifact.d.ts.map +1 -0
  44. package/out/tools/build-artifact.js +44 -0
  45. package/out/tools/build-artifact.js.map +1 -0
  46. package/out/validate.d.ts +29 -50
  47. package/out/validate.d.ts.map +1 -1
  48. package/out/validate.js +12 -35
  49. package/out/validate.js.map +1 -1
  50. package/out/validation/issues.d.ts +17 -20
  51. package/out/validation/issues.d.ts.map +1 -1
  52. package/out/validation/issues.js +25 -29
  53. package/out/validation/issues.js.map +1 -1
  54. package/out/validation/references.d.ts.map +1 -1
  55. package/out/validation/references.js +20 -23
  56. package/out/validation/references.js.map +1 -1
  57. package/package.json +35 -76
  58. package/tools/build-artifact.ts +56 -0
  59. package/out/scripts/build-artifact.d.ts +0 -51
  60. package/out/scripts/build-artifact.d.ts.map +0 -1
  61. package/out/scripts/build-artifact.js +0 -85
  62. package/out/scripts/build-artifact.js.map +0 -1
@@ -3,20 +3,17 @@
3
3
  * @license AGPL-3.0
4
4
  * @author Teffen Ellis, et al.
5
5
  *
6
- * The issue vocabulary and the primitive field readers `./validate.ts` is built from.
6
+ * The issue vocabulary and the primitive field readers `./validate.ts` is built from.
7
7
  *
8
- * Every reader takes the issue list it appends to and returns `undefined` when it could not read the
9
- * field. That shape is deliberate: a reader that threw would end the pass at the first defect, and a
10
- * reader that substituted a default would convert "I could not read this" into a value, which at a
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. Closed, so a consumer branches on the code rather than on message prose.
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 carry entries was empty.
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. Refused wherever it appears, at any depth.
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, e.g. `$.concepts[0].assertions[1].modality`. The document root
68
- * is `$`.
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. Matched case-insensitively against every field name at every depth, so
77
- * `score`, `boost`, `penalty`, `rankWeight`, `relevanceWeight`, and `affinityWeight` are refused by one rule rather
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 | undefined {
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 undefined
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 undefined
152
+ return null
157
153
  }
158
154
 
159
- if (!value.trim().length) {
155
+ if (!value.trim()) {
160
156
  add(issues, fieldPath, ValidationIssueCode.EmptyValue, `\`${key}\` must not be blank`)
161
157
 
162
- return undefined
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 | undefined {
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 undefined
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 undefined
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[] | undefined {
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 undefined
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 undefined
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[] | undefined {
221
+ ): string[] | null {
226
222
  const entries = readArray(issues, path, container, key, required)
227
223
 
228
- if (!entries) return undefined
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().length) {
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 | undefined {
256
+ ): T | null {
261
257
  const value = readString(issues, path, container, key, true)
262
258
 
263
- if (value === undefined) return undefined
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[] | undefined {
277
+ ): T[] | null {
282
278
  const values = readStringArray(issues, path, container, key, true)
283
279
 
284
- if (!values) return undefined
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 claims an identifier already taken.
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?: string }>(
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 === undefined) continue
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?: ConceptKind
52
+ subjectKind: ConceptKind | null
52
53
  subjectPath: string
53
- relationID?: string
54
+ relationID: string | null
54
55
  relationPath: string
55
- objectID?: string
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 and range kinds.
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 === undefined ? undefined : tables.relations.get(edge.relationID)
65
+ const relation = edge.relationID ? tables.relations.get(edge.relationID) : null
67
66
 
68
- if (edge.relationID !== undefined && !relation) {
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 === undefined ? undefined : tables.concepts.get(edge.objectID)
76
+ const object = edge.objectID ? tables.concepts.get(edge.objectID) : null
78
77
 
79
- if (edge.objectID !== undefined && !object) {
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 === undefined) return
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[] | undefined {
195
- if (start.id === undefined) return undefined
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 undefined
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 === undefined || input.id === undefined) continue
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 !== undefined && !concepts.has(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 === undefined ? undefined : concepts.get(triple.subject)
333
+ const subject = triple.subject ? concepts.get(triple.subject) : null
337
334
 
338
- if (triple.subject !== undefined && !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
- * The compiled runtime artifact: its shape, its canonical bytes, and the reader that turns those
7
- * bytes back into a typed value.
6
+ * Defines the compiled geographic model artifact, its canonical serialization and its reader.
8
7
  *
9
- * A runtime consumer reads THIS and never the authored records. That is the whole point of the
10
- * artifact, and it is why the shape carries every table plus the materialized `isA` closure: a
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
- * Two determinism rules define the bytes, and both are stated so a reader can check them:
15
- *
16
- * 1. **Every object's keys are emitted in code-point order**, at every depth. A rule that
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 FORMAT version — incremented by hand when the compiled shape changes, so a reader meeting an artifact it
32
- * was not written for says so instead of quietly reading fields that moved.
17
+ * The artifact format version.
33
18
  *
34
- * It is not data about the world, which is why a number is allowed here and nowhere in `./schema.ts`.
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, materialized so a consumer answers "what is this a kind of" with a lookup.
24
+ * One concept's transitive `isA` ancestors.
39
25
  *
40
- * Every concept gets an entry, including one with no ancestors at all: an entry with an empty list says the concept was
41
- * compiled and is a kind of nothing, and a missing entry would say the same thing while also being what an absent
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 whole compiled artifact.
34
+ * The compiled artifact.
50
35
  *
51
- * The five authored tables travel unchanged — the compiler orders them and rewrites no record — beside the two tables
52
- * it computes: {@link InheritanceClosureEntry} per concept, and the derived facts the closure materializes.
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 of this artifact — {@link ARTIFACT_SCHEMA_VERSION} at the time it was written.
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 own `version`. Never a build date.
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, plus every fact the compiler's derivations produced. Each names the procedure that
70
- * wrote it and every record that procedure read.
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
- * The artifact's canonical bytes: tab-indented, one trailing newline, keys in code-point order.
61
+ * Serializes the artifact as tab-indented JSON with a trailing newline and sorted keys.
76
62
  *
77
- * Tab indentation and the trailing newline match the repository's other committed JSON tables (`taxonomy.json`,
78
- * `brands.json`). A committed copy of these bytes is the generator's output run through `oxfmt`, which inlines short
79
- * arrays — so a freshness check compares the PARSED artifact against a fresh compile, and a byte comparison compares
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
- * Read a parsed artifact — the value `JSON.parse` produced from the compiled bytes — as a
91
- * {@link CompiledGeographicModel}.
75
+ * Reads a value from `JSON.parse` as a {@link CompiledGeographicModel}.
92
76
  *
93
- * It checks the format version and the presence of every table, and does NOT re-validate the records. An artifact is
94
- * generated from a document that `parseGeographicModelDocument` already accepted; re-checking every record here would
95
- * be a second validator, and the version check is what catches the failure this reader can actually meet — an artifact
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
@@ -1 +1 @@
1
- {"version":3,"file":"artifact.d.ts","sourceRoot":"","sources":["../lib/artifact.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;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;;;;;;GAMG;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;;;OAGG;IACH,YAAY,EAAE,SAAS,iBAAiB,EAAE,CAAA;CAC1C;AA+BD;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,uBAAuB,GAAG,MAAM,CAE7E;AAED;;GAEG;AACH,qBAAa,4BAA6B,SAAQ,KAAK;IACtD,YAAY,OAAO,EAAE,MAAM,EAI1B;CACD;AAuCD;;;;;;;;GAQG;AACH,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,OAAO,GAAG,uBAAuB,CAMpF"}
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
- * The compiled runtime artifact: its shape, its canonical bytes, and the reader that turns those
7
- * bytes back into a typed value.
6
+ * Defines the compiled geographic model artifact, its canonical serialization and its reader.
8
7
  *
9
- * A runtime consumer reads THIS and never the authored records. That is the whole point of the
10
- * artifact, and it is why the shape carries every table plus the materialized `isA` closure: a
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
- * Two determinism rules define the bytes, and both are stated so a reader can check them:
15
- *
16
- * 1. **Every object's keys are emitted in code-point order**, at every depth. A rule that
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 FORMAT version — incremented by hand when the compiled shape changes, so a reader meeting an artifact it
34
- * was not written for says so instead of quietly reading fields that moved.
19
+ * The artifact format version.
35
20
  *
36
- * It is not data about the world, which is why a number is allowed here and nowhere in `./schema.ts`.
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
- * The artifact's stated order: UTF-16 code point, ascending.
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
- * The artifact's canonical bytes: tab-indented, one trailing newline, keys in code-point order.
44
+ * Serializes the artifact as tab-indented JSON with a trailing newline and sorted keys.
65
45
  *
66
- * Tab indentation and the trailing newline match the repository's other committed JSON tables (`taxonomy.json`,
67
- * `brands.json`). A committed copy of these bytes is the generator's output run through `oxfmt`, which inlines short
68
- * arrays — so a freshness check compares the PARSED artifact against a fresh compile, and a byte comparison compares
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 carries. A reader that finds one missing refuses rather than answering every question about
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
- * Why `value` is not a compiled artifact, or nothing when it is one.
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 ${JSON.stringify(value.schemaVersion)}`;
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().length) {
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 undefined;
87
+ return null;
113
88
  return `the artifact is missing ${missing.map((table) => `\`${table}\``).join(", ")}`;
114
89
  }
115
90
  /**
116
- * Read a parsed artifact — the value `JSON.parse` produced from the compiled bytes — as a
117
- * {@link CompiledGeographicModel}.
91
+ * Reads a value from `JSON.parse` as a {@link CompiledGeographicModel}.
118
92
  *
119
- * It checks the format version and the presence of every table, and does NOT re-validate the records. An artifact is
120
- * generated from a document that `parseGeographicModelDocument` already accepted; re-checking every record here would
121
- * be a second validator, and the version check is what catches the failure this reader can actually meet — an artifact
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);
@@ -1 +1 @@
1
- {"version":3,"file":"artifact.js","sourceRoot":"","sources":["../lib/artifact.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AACjD,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;;;;;GAKG;AAEH;;;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;;;;;;;GAOG;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;;;GAGG;AACH,MAAM,eAAe,GAAG;IACvB,UAAU;IACV,cAAc;IACd,oBAAoB;IACpB,UAAU;IACV,cAAc;IACd,WAAW;CACF,CAAA;AAEV;;;;;GAKG;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,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,aAAa,CAAC,EAAE,CAAA;IAClJ,CAAC;IAED,IAAI,OAAO,KAAK,CAAC,YAAY,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC,MAAM,EAAE,CAAC;QACjF,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,SAAS,CAAA;IAErC,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;;;;;;;;GAQG;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"}
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"}