@cruba/crubal 0.0.0-stage → 0.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/LICENSE +202 -0
- package/README.md +97 -2
- package/dist/bin/crubal.d.ts +2 -0
- package/dist/bin/crubal.js +103 -0
- package/dist/bin/crubal.js.map +1 -0
- package/dist/src/access.d.ts +126 -0
- package/dist/src/access.js +517 -0
- package/dist/src/access.js.map +1 -0
- package/dist/src/arrival.d.ts +55 -0
- package/dist/src/arrival.js +124 -0
- package/dist/src/arrival.js.map +1 -0
- package/dist/src/browser.d.ts +10 -0
- package/dist/src/browser.js +13 -0
- package/dist/src/browser.js.map +1 -0
- package/dist/src/bundle.d.ts +66 -0
- package/dist/src/bundle.js +753 -0
- package/dist/src/bundle.js.map +1 -0
- package/dist/src/canonical.d.ts +6 -0
- package/dist/src/canonical.js +47 -0
- package/dist/src/canonical.js.map +1 -0
- package/dist/src/conditions.d.ts +28 -0
- package/dist/src/conditions.js +216 -0
- package/dist/src/conditions.js.map +1 -0
- package/dist/src/corpus.d.ts +8 -0
- package/dist/src/corpus.js +1207 -0
- package/dist/src/corpus.js.map +1 -0
- package/dist/src/defs.d.ts +151 -0
- package/dist/src/defs.js +611 -0
- package/dist/src/defs.js.map +1 -0
- package/dist/src/dictionary.d.ts +65 -0
- package/dist/src/dictionary.js +557 -0
- package/dist/src/dictionary.js.map +1 -0
- package/dist/src/exchange.d.ts +236 -0
- package/dist/src/exchange.js +1035 -0
- package/dist/src/exchange.js.map +1 -0
- package/dist/src/forms.d.ts +16 -0
- package/dist/src/forms.js +169 -0
- package/dist/src/forms.js.map +1 -0
- package/dist/src/index.d.ts +27 -0
- package/dist/src/index.js +29 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/lex.d.ts +21 -0
- package/dist/src/lex.js +142 -0
- package/dist/src/lex.js.map +1 -0
- package/dist/src/lifecycle.d.ts +84 -0
- package/dist/src/lifecycle.js +415 -0
- package/dist/src/lifecycle.js.map +1 -0
- package/dist/src/log.d.ts +78 -0
- package/dist/src/log.js +506 -0
- package/dist/src/log.js.map +1 -0
- package/dist/src/migrate.d.ts +111 -0
- package/dist/src/migrate.js +286 -0
- package/dist/src/migrate.js.map +1 -0
- package/dist/src/model.d.ts +68 -0
- package/dist/src/model.js +188 -0
- package/dist/src/model.js.map +1 -0
- package/dist/src/packages.d.ts +69 -0
- package/dist/src/packages.js +232 -0
- package/dist/src/packages.js.map +1 -0
- package/dist/src/print-text.d.ts +7 -0
- package/dist/src/print-text.js +216 -0
- package/dist/src/print-text.js.map +1 -0
- package/dist/src/publish.d.ts +27 -0
- package/dist/src/publish.js +275 -0
- package/dist/src/publish.js.map +1 -0
- package/dist/src/read-text.d.ts +10 -0
- package/dist/src/read-text.js +385 -0
- package/dist/src/read-text.js.map +1 -0
- package/dist/src/records.d.ts +31 -0
- package/dist/src/records.js +72 -0
- package/dist/src/records.js.map +1 -0
- package/dist/src/review.d.ts +22 -0
- package/dist/src/review.js +198 -0
- package/dist/src/review.js.map +1 -0
- package/dist/src/rules.d.ts +96 -0
- package/dist/src/rules.js +611 -0
- package/dist/src/rules.js.map +1 -0
- package/dist/src/schedule.d.ts +24 -0
- package/dist/src/schedule.js +126 -0
- package/dist/src/schedule.js.map +1 -0
- package/dist/src/std-packages.d.ts +9 -0
- package/dist/src/std-packages.js +268 -0
- package/dist/src/std-packages.js.map +1 -0
- package/dist/src/store.d.ts +164 -0
- package/dist/src/store.js +538 -0
- package/dist/src/store.js.map +1 -0
- package/dist/src/tree.d.ts +35 -0
- package/dist/src/tree.js +159 -0
- package/dist/src/tree.js.map +1 -0
- package/dist/src/units-table.d.ts +1 -0
- package/dist/src/units-table.js +200 -0
- package/dist/src/units-table.js.map +1 -0
- package/dist/src/units.d.ts +42 -0
- package/dist/src/units.js +98 -0
- package/dist/src/units.js.map +1 -0
- package/dist/src/views.d.ts +216 -0
- package/dist/src/views.js +761 -0
- package/dist/src/views.js.map +1 -0
- package/package.json +60 -4
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { ClassLine, Dictionary, Facet } from "./defs.ts";
|
|
2
|
+
import type { Json, JsonObject, Record } from "./model.ts";
|
|
3
|
+
/** A fold key (4.3): NFC, case-folded, trimmed, inner white space made one space. */
|
|
4
|
+
export declare function foldKey(s: string): string;
|
|
5
|
+
export declare function facetJson(f: Facet): JsonObject;
|
|
6
|
+
export type Warning = {
|
|
7
|
+
line: number;
|
|
8
|
+
message: string;
|
|
9
|
+
};
|
|
10
|
+
/** Run every publish check on a dictionary. Throws the first error; returns warnings. */
|
|
11
|
+
export declare function checkPackage(dict: Dictionary): Warning[];
|
|
12
|
+
export type ResolvedLine = {
|
|
13
|
+
facet: string;
|
|
14
|
+
required?: boolean;
|
|
15
|
+
values?: string[];
|
|
16
|
+
length?: number;
|
|
17
|
+
pick?: string;
|
|
18
|
+
range?: {
|
|
19
|
+
min: string;
|
|
20
|
+
max: string;
|
|
21
|
+
};
|
|
22
|
+
levels: string[];
|
|
23
|
+
when?: string;
|
|
24
|
+
section?: string;
|
|
25
|
+
/** Every line that contributed, as `Class: line`, in resolution order. */
|
|
26
|
+
from: {
|
|
27
|
+
cls: string;
|
|
28
|
+
line: ClassLine;
|
|
29
|
+
}[];
|
|
30
|
+
};
|
|
31
|
+
/** Resolve the meet of classes (a class with its uses, or everything a record wears). */
|
|
32
|
+
export declare function resolve(dict: Dictionary, names: string[], line?: number): ResolvedLine[];
|
|
33
|
+
/** A resolved class as JSON, as the corpus states it. */
|
|
34
|
+
export declare function resolvedJson(lines: ResolvedLine[]): JsonObject[];
|
|
35
|
+
export declare function foldValue(f: Facet, v: Json, dict: Dictionary): Json;
|
|
36
|
+
/** Fold a record: key aliases, value aliases and accepted units become real names (chapter 4.3). */
|
|
37
|
+
export declare function foldRecord(r: Record, dict: Dictionary): Record;
|
|
38
|
+
export type Outcome = {
|
|
39
|
+
[key: string]: Json;
|
|
40
|
+
};
|
|
41
|
+
declare const RANK: {
|
|
42
|
+
readonly year: 0;
|
|
43
|
+
readonly month: 1;
|
|
44
|
+
readonly day: 2;
|
|
45
|
+
readonly hour: 3;
|
|
46
|
+
readonly minute: 4;
|
|
47
|
+
readonly second: 5;
|
|
48
|
+
};
|
|
49
|
+
/** A date's precision and whether it is well formed (1.4.5): real calendar parts and clock parts. */
|
|
50
|
+
export declare function dateParts(d: string): {
|
|
51
|
+
precision: keyof typeof RANK;
|
|
52
|
+
offset: string | null;
|
|
53
|
+
} | null;
|
|
54
|
+
/** Check one scalar against its facet: an outcome and an optional flag. */
|
|
55
|
+
export declare function checkScalar(f: Facet, v: Json): {
|
|
56
|
+
outcome: string;
|
|
57
|
+
flag?: string;
|
|
58
|
+
cites?: JsonObject;
|
|
59
|
+
};
|
|
60
|
+
/** Check a record: every value against its facet and worn classes, required facets, and parts. */
|
|
61
|
+
export declare function checkRecord(r: Record, dict: Dictionary): {
|
|
62
|
+
outcomes: Outcome[];
|
|
63
|
+
summary: Outcome;
|
|
64
|
+
};
|
|
65
|
+
export {};
|
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
// dictionary.ts: the Dictionary level (spec chapters 4 and 5). Definitions as JSON (4.6), the publish
|
|
2
|
+
// checks on definitions and classes (4.7, 5.10), class resolution by meet (5.4), alias folding (4.3), and
|
|
3
|
+
// checking each value against its facet and the classes its record wears (4.9, 5.3, 5.5, 5.6).
|
|
4
|
+
import { evaluate, parseCondition } from "./conditions.js";
|
|
5
|
+
import { itemText } from "./print-text.js";
|
|
6
|
+
import { readUnder } from "./read-text.js";
|
|
7
|
+
import { CrubalError } from "./tree.js";
|
|
8
|
+
import { convert, knownUnit, unitOf } from "./units.js";
|
|
9
|
+
const isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
10
|
+
/** A fold key (4.3): NFC, case-folded, trimmed, inner white space made one space. */
|
|
11
|
+
export function foldKey(s) {
|
|
12
|
+
return s.normalize("NFC").toLowerCase().trim().replace(/\s+/g, " ");
|
|
13
|
+
}
|
|
14
|
+
// ─── Definitions as JSON (4.6) ────────────────────────────────────────────────
|
|
15
|
+
export function facetJson(f) {
|
|
16
|
+
const o = { facet: f.key, type: f.type };
|
|
17
|
+
const put = (k, v, skip) => {
|
|
18
|
+
if (v === undefined || v === skip)
|
|
19
|
+
return;
|
|
20
|
+
if (Array.isArray(v) && v.length === 0)
|
|
21
|
+
return;
|
|
22
|
+
o[k] = v;
|
|
23
|
+
};
|
|
24
|
+
put("label", f.label);
|
|
25
|
+
put("ask", f.ask);
|
|
26
|
+
put("describe", f.describe);
|
|
27
|
+
put("pick", f.pick, "1");
|
|
28
|
+
put("ordered", f.ordered || undefined);
|
|
29
|
+
put("state", f.state, "active");
|
|
30
|
+
put("qualifiers", f.qualifiers);
|
|
31
|
+
put("aliases", f.aliases);
|
|
32
|
+
put("same-as", f.sameAs);
|
|
33
|
+
put("agree-by", f.agreeBy);
|
|
34
|
+
put("length", f.length === undefined ? undefined : String(f.length));
|
|
35
|
+
put("unit", f.unit);
|
|
36
|
+
put("accepts", f.accepts);
|
|
37
|
+
if (f.range)
|
|
38
|
+
o.range = { ...f.range };
|
|
39
|
+
put("places", f.places === undefined ? undefined : String(f.places));
|
|
40
|
+
put("precision", f.precision, "any");
|
|
41
|
+
if (f.words)
|
|
42
|
+
o.words = [...f.words];
|
|
43
|
+
if (f.values)
|
|
44
|
+
o.values = f.values.map((v) => (v.aliases.length ? { value: v.value, aliases: [...v.aliases] } : { value: v.value }));
|
|
45
|
+
put("open", f.open || undefined);
|
|
46
|
+
for (const k of ["fixed", "ranked", "scored", "required", "na", "unique", "transitions"])
|
|
47
|
+
put(k, f[k] || undefined);
|
|
48
|
+
put("to", f.to);
|
|
49
|
+
if (f.children)
|
|
50
|
+
o.facets = [...f.children.values()].map(facetJson);
|
|
51
|
+
put("identity", f.identity);
|
|
52
|
+
put("when", f.when);
|
|
53
|
+
put("combine", f.combine);
|
|
54
|
+
put("version", f.version);
|
|
55
|
+
for (const [k, v] of Object.entries(f.extra))
|
|
56
|
+
o[k] = v;
|
|
57
|
+
return o;
|
|
58
|
+
}
|
|
59
|
+
function checkFacet(f) {
|
|
60
|
+
const line = f.line;
|
|
61
|
+
if (f.type === "choice" && !f.values?.length)
|
|
62
|
+
throw new CrubalError(`the choice facet ${f.key} has no values`, line);
|
|
63
|
+
if (f.type === "compound" && !f.children?.size)
|
|
64
|
+
throw new CrubalError(`the compound facet ${f.key} has no children`, line);
|
|
65
|
+
if (f.open && f.fixed)
|
|
66
|
+
throw new CrubalError(`the list of ${f.key} is both open and fixed`, line);
|
|
67
|
+
if ((f.ranked || f.scored || f.fixed || f.open) && f.type !== "choice") {
|
|
68
|
+
throw new CrubalError(`${f.key}: ranked, scored, fixed and open lists are for choice facets`, line);
|
|
69
|
+
}
|
|
70
|
+
if (f.ordered && !f.many)
|
|
71
|
+
throw new CrubalError(`${f.key} is ordered but holds one value`, line);
|
|
72
|
+
if (f.accepts.length && !f.unit)
|
|
73
|
+
throw new CrubalError(`${f.key} accepts other units but has no unit`, line);
|
|
74
|
+
// 4.7 (6): a unit is a spelling from the units table, `currency`, or an ISO 4217 code.
|
|
75
|
+
for (const u of [...(f.unit ? [f.unit] : []), ...f.accepts])
|
|
76
|
+
if (!knownUnit(u))
|
|
77
|
+
throw new CrubalError(`${f.key}: ${u} isn't a unit the units table knows`, line);
|
|
78
|
+
if (f.range && Number(f.range.min) > Number(f.range.max))
|
|
79
|
+
throw new CrubalError(`${f.key}'s range runs backwards`, line);
|
|
80
|
+
if (f.identity) {
|
|
81
|
+
const child = f.children?.get(f.identity);
|
|
82
|
+
if (!child)
|
|
83
|
+
throw new CrubalError(`the identity child ${f.identity} of ${f.key} isn't one of its children`, line);
|
|
84
|
+
if (child.type !== "link")
|
|
85
|
+
throw new CrubalError(`the identity child ${f.identity} of ${f.key} is not a link`, line);
|
|
86
|
+
}
|
|
87
|
+
const seen = new Map();
|
|
88
|
+
for (const v of f.values ?? []) {
|
|
89
|
+
for (const spelling of [v.value, ...v.aliases]) {
|
|
90
|
+
const k = foldKey(spelling);
|
|
91
|
+
const owner = seen.get(k);
|
|
92
|
+
if (owner !== undefined && owner !== v.value) {
|
|
93
|
+
throw new CrubalError(`${f.key}: the spelling ${spelling} folds to the same as ${owner}`, line);
|
|
94
|
+
}
|
|
95
|
+
seen.set(k, v.value);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
for (const c of f.children?.values() ?? []) {
|
|
99
|
+
if (["row", "origin", "via", "owner"].includes(c.key))
|
|
100
|
+
throw new CrubalError(`${f.key} has a child named ${c.key}, which is reserved`, line);
|
|
101
|
+
checkFacet(c);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/** Run every publish check on a dictionary. Throws the first error; returns warnings. */
|
|
105
|
+
export function checkPackage(dict) {
|
|
106
|
+
const warnings = [];
|
|
107
|
+
// An identify key names a record, so it must be a facet and unique (11.2).
|
|
108
|
+
for (const [level, ks] of dict.identify) {
|
|
109
|
+
for (const k of ks) {
|
|
110
|
+
const f = dict.facets.get(k);
|
|
111
|
+
if (!f)
|
|
112
|
+
throw new CrubalError(`identify names ${k} at ${level}, which isn't a facet`);
|
|
113
|
+
if (!f.unique)
|
|
114
|
+
throw new CrubalError(`${k} is an identify key at ${level}, so it must be unique`, f.line);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const keys = new Map();
|
|
118
|
+
for (const f of dict.facets.values()) {
|
|
119
|
+
if (f.key.startsWith("crubal:"))
|
|
120
|
+
continue;
|
|
121
|
+
checkFacet(f);
|
|
122
|
+
for (const spelling of [f.key, ...f.aliases]) {
|
|
123
|
+
const k = foldKey(spelling);
|
|
124
|
+
const owner = keys.get(k);
|
|
125
|
+
if (owner !== undefined && owner !== f.key) {
|
|
126
|
+
const what = spelling === f.key ? `the key ${f.key}` : `the alias ${spelling} of ${f.key}`;
|
|
127
|
+
throw new CrubalError(`${what} folds to ${owner === k ? "the key" : "an alias"} of ${owner}`, f.line);
|
|
128
|
+
}
|
|
129
|
+
keys.set(k, f.key);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
for (const c of dict.classes.values()) {
|
|
133
|
+
for (const l of c.lines)
|
|
134
|
+
checkClassLine(c, l, dict);
|
|
135
|
+
for (const slot of c.parts) {
|
|
136
|
+
if (!dict.classes.has(slot.wears))
|
|
137
|
+
throw new CrubalError(`the part slot ${slot.slot} wears ${slot.wears}, which isn't a class`, slot.line);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// 5.10 (4): a level prefix names a level of every kind in `for` (every kind, when `for` is absent).
|
|
141
|
+
const kindsAll = [...new Set(dict.levels.values())];
|
|
142
|
+
const levelsOf = (kind) => [...dict.levels].filter(([, k]) => k === kind).map(([l]) => l);
|
|
143
|
+
for (const c of dict.classes.values()) {
|
|
144
|
+
for (const l of c.lines) {
|
|
145
|
+
if (!l.level)
|
|
146
|
+
continue;
|
|
147
|
+
for (const kind of c.for.length ? c.for : kindsAll)
|
|
148
|
+
if (!levelsOf(kind).includes(l.level))
|
|
149
|
+
throw new CrubalError(`${c.name}: ${l.level}:: isn't a level of ${kind}`, l.line);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
for (const c of dict.classes.values()) {
|
|
153
|
+
findCycle(c.name, dict, []);
|
|
154
|
+
resolve(dict, [c.name], c.line);
|
|
155
|
+
warnings.push(...redundantLines(c, dict));
|
|
156
|
+
}
|
|
157
|
+
return warnings;
|
|
158
|
+
}
|
|
159
|
+
function checkClassLine(c, l, dict) {
|
|
160
|
+
const f = dict.facets.get(l.facet);
|
|
161
|
+
if (!f)
|
|
162
|
+
throw new CrubalError(`the class line ${l.text} names a facet that isn't defined`, l.line);
|
|
163
|
+
if (f.state === "candidate")
|
|
164
|
+
throw new CrubalError(`${f.key} is a candidate; a class line can only name an active facet`, l.line);
|
|
165
|
+
if (f.state === "retired")
|
|
166
|
+
throw new CrubalError(`${f.key} is retired; a class line can only name an active facet`, l.line);
|
|
167
|
+
const type = l.type;
|
|
168
|
+
if (type && type !== f.type)
|
|
169
|
+
throw new CrubalError(`the class line ${l.text} marks ${f.key} as ${type}, but it is ${f.type}`, l.line);
|
|
170
|
+
if (l.values) {
|
|
171
|
+
const real = new Set((f.values ?? []).map((v) => v.value));
|
|
172
|
+
const extra = l.values.filter((v) => !real.has(v));
|
|
173
|
+
if (extra.length)
|
|
174
|
+
throw new CrubalError(`the class line ${l.text} widens ${f.key}: ${extra.join(", ")} is not one of its values`, l.line);
|
|
175
|
+
}
|
|
176
|
+
if (l.length !== undefined && f.length !== undefined && l.length > f.length)
|
|
177
|
+
throw new CrubalError(`the class line ${l.text} widens ${f.key}'s length`, l.line);
|
|
178
|
+
if (l.pick && f.pick !== "any" && (l.pick === "any" || Number(l.pick) > Number(f.pick)))
|
|
179
|
+
throw new CrubalError(`the class line ${l.text} widens ${f.key}'s pick`, l.line);
|
|
180
|
+
if (l.range && f.range && (Number(l.range.min) < Number(f.range.min) || Number(l.range.max) > Number(f.range.max))) {
|
|
181
|
+
throw new CrubalError(`the class line ${l.text} widens ${f.key}'s range`, l.line);
|
|
182
|
+
}
|
|
183
|
+
if (l.when)
|
|
184
|
+
parseCondition(l.when, l.line);
|
|
185
|
+
if (l.frame && f.type !== "image")
|
|
186
|
+
throw new CrubalError(`the class line ${l.text} gives ${f.key} a frame, but it isn't an image`, l.line);
|
|
187
|
+
void c;
|
|
188
|
+
}
|
|
189
|
+
function findCycle(name, dict, path) {
|
|
190
|
+
if (path.includes(name)) {
|
|
191
|
+
const loop = path.slice(path.indexOf(name));
|
|
192
|
+
const first = dict.classes.get(loop[0]);
|
|
193
|
+
throw new CrubalError(`the classes ${loop.join(" and ")} use each other`, first?.line);
|
|
194
|
+
}
|
|
195
|
+
const c = dict.classes.get(name);
|
|
196
|
+
if (!c)
|
|
197
|
+
return;
|
|
198
|
+
for (const u of c.uses) {
|
|
199
|
+
if (!dict.classes.has(u))
|
|
200
|
+
throw new CrubalError(`${name} uses ${u}, which isn't a class`, c.line);
|
|
201
|
+
findCycle(u, dict, [...path, name]);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
function order(dict, names, seen = new Set()) {
|
|
205
|
+
const out = [];
|
|
206
|
+
for (const n of names) {
|
|
207
|
+
if (seen.has(n))
|
|
208
|
+
continue;
|
|
209
|
+
const c = dict.classes.get(n);
|
|
210
|
+
if (!c)
|
|
211
|
+
continue;
|
|
212
|
+
seen.add(n);
|
|
213
|
+
out.push(...order(dict, c.uses, seen), c);
|
|
214
|
+
}
|
|
215
|
+
return out;
|
|
216
|
+
}
|
|
217
|
+
/** Resolve the meet of classes (a class with its uses, or everything a record wears). */
|
|
218
|
+
export function resolve(dict, names, line) {
|
|
219
|
+
const lines = new Map();
|
|
220
|
+
for (const c of order(dict, names)) {
|
|
221
|
+
for (const l of c.lines) {
|
|
222
|
+
const f = dict.facets.get(l.facet);
|
|
223
|
+
let r = lines.get(l.facet);
|
|
224
|
+
if (!r) {
|
|
225
|
+
r = { facet: l.facet, levels: [], from: [] };
|
|
226
|
+
if (l.section)
|
|
227
|
+
r.section = l.section;
|
|
228
|
+
lines.set(l.facet, r);
|
|
229
|
+
}
|
|
230
|
+
r.from.push({ cls: c.name, line: l });
|
|
231
|
+
if (l.required)
|
|
232
|
+
r.required = true;
|
|
233
|
+
if (l.level && !r.levels.includes(l.level))
|
|
234
|
+
r.levels.push(l.level);
|
|
235
|
+
if (l.when)
|
|
236
|
+
r.when = r.when ? `(${r.when}) and (${l.when})` : l.when;
|
|
237
|
+
if (l.values) {
|
|
238
|
+
const base = r.values ?? (f?.values ?? []).map((v) => v.value);
|
|
239
|
+
const next = base.filter((v) => l.values?.includes(v));
|
|
240
|
+
if (!next.length) {
|
|
241
|
+
const others = r.from.filter((x) => x.line.values && x.cls !== c.name).map((x) => x.cls);
|
|
242
|
+
throw new CrubalError(`${names.join(", ")} allows no value of ${l.facet}: ${[...new Set([...others, c.name])].join(" and ")} have nothing in common`, line ?? l.line);
|
|
243
|
+
}
|
|
244
|
+
r.values = next;
|
|
245
|
+
}
|
|
246
|
+
if (l.length !== undefined)
|
|
247
|
+
r.length = Math.min(r.length ?? Number.POSITIVE_INFINITY, l.length);
|
|
248
|
+
if (l.pick)
|
|
249
|
+
r.pick = r.pick && r.pick !== "any" && (l.pick === "any" || Number(r.pick) < Number(l.pick)) ? r.pick : l.pick;
|
|
250
|
+
if (l.range) {
|
|
251
|
+
const min = r.range ? String(Math.max(Number(r.range.min), Number(l.range.min))) : l.range.min;
|
|
252
|
+
const max = r.range ? String(Math.min(Number(r.range.max), Number(l.range.max))) : l.range.max;
|
|
253
|
+
r.range = { min, max };
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return [...lines.values()];
|
|
258
|
+
}
|
|
259
|
+
/** A resolved class as JSON, as the corpus states it. */
|
|
260
|
+
export function resolvedJson(lines) {
|
|
261
|
+
return lines.map((r) => {
|
|
262
|
+
const o = { facet: r.facet };
|
|
263
|
+
if (r.required)
|
|
264
|
+
o.required = true;
|
|
265
|
+
if (r.values)
|
|
266
|
+
o.values = r.values;
|
|
267
|
+
if (r.length !== undefined)
|
|
268
|
+
o.length = String(r.length);
|
|
269
|
+
if (r.pick)
|
|
270
|
+
o.pick = r.pick;
|
|
271
|
+
if (r.range)
|
|
272
|
+
o.range = r.range;
|
|
273
|
+
if (r.levels.length === 1)
|
|
274
|
+
o.level = r.levels[0];
|
|
275
|
+
else if (r.levels.length > 1)
|
|
276
|
+
o.levels = r.levels;
|
|
277
|
+
if (r.when)
|
|
278
|
+
o.when = r.when;
|
|
279
|
+
return o;
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
function redundantLines(c, dict) {
|
|
283
|
+
const inherited = new Map(resolve(dict, c.uses).map((r) => [r.facet, r]));
|
|
284
|
+
const out = [];
|
|
285
|
+
for (const l of c.lines) {
|
|
286
|
+
if (!inherited.has(l.facet))
|
|
287
|
+
continue;
|
|
288
|
+
const narrows = l.required || l.values || l.length !== undefined || l.pick || l.range || l.level || l.when;
|
|
289
|
+
if (!narrows)
|
|
290
|
+
out.push({ line: l.line, message: `${c.name} restates ${l.facet}, which a used class already asks for` });
|
|
291
|
+
}
|
|
292
|
+
return out;
|
|
293
|
+
}
|
|
294
|
+
// ─── Folding (4.3) ───────────────────────────────────────────────────────────
|
|
295
|
+
function keyIndex(dict) {
|
|
296
|
+
const idx = new Map();
|
|
297
|
+
for (const f of dict.facets.values()) {
|
|
298
|
+
for (const s of [f.key, ...f.aliases])
|
|
299
|
+
idx.set(foldKey(s), f.key);
|
|
300
|
+
}
|
|
301
|
+
return idx;
|
|
302
|
+
}
|
|
303
|
+
function foldChoice(f, v) {
|
|
304
|
+
if (typeof v === "string" && f.values) {
|
|
305
|
+
const k = foldKey(v);
|
|
306
|
+
for (const cv of f.values)
|
|
307
|
+
if ([cv.value, ...cv.aliases].some((s) => foldKey(s) === k))
|
|
308
|
+
return cv.value;
|
|
309
|
+
return v;
|
|
310
|
+
}
|
|
311
|
+
if (isObj(v) && "$value" in v)
|
|
312
|
+
return { ...v, $value: foldChoice(f, v.$value) };
|
|
313
|
+
return v;
|
|
314
|
+
}
|
|
315
|
+
function foldQuantity(f, v, units) {
|
|
316
|
+
if (!isObj(v) || !("$qty" in v) || !f.unit)
|
|
317
|
+
return v;
|
|
318
|
+
const from = unitOf(String(v.unit), units) ?? String(v.unit);
|
|
319
|
+
if (from === f.unit)
|
|
320
|
+
return from === v.unit ? v : { $qty: v.$qty, unit: from };
|
|
321
|
+
if (!f.accepts.includes(from) && !f.accepts.includes(String(v.unit)))
|
|
322
|
+
return v;
|
|
323
|
+
const amount = convert(String(v.$qty), from, f.unit, f.places, units);
|
|
324
|
+
return amount === null ? v : { $qty: amount, unit: f.unit };
|
|
325
|
+
}
|
|
326
|
+
export function foldValue(f, v, dict) {
|
|
327
|
+
if (Array.isArray(v))
|
|
328
|
+
return v.map((x) => foldValue(f, x, dict));
|
|
329
|
+
if (f.type === "choice")
|
|
330
|
+
return foldChoice(f, v);
|
|
331
|
+
if (f.type === "number")
|
|
332
|
+
return foldQuantity(f, v, dict.units);
|
|
333
|
+
if (f.type === "compound" && isObj(v))
|
|
334
|
+
return foldFields(v, f.children ?? new Map(), dict);
|
|
335
|
+
return v;
|
|
336
|
+
}
|
|
337
|
+
function foldFields(obj, within, dict) {
|
|
338
|
+
const idx = within ? new Map([...within.keys()].map((k) => [foldKey(k), k])) : keyIndex(dict);
|
|
339
|
+
const out = {};
|
|
340
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
341
|
+
const lookup = within ?? dict.facets;
|
|
342
|
+
let key = k;
|
|
343
|
+
let value = v;
|
|
344
|
+
let f = lookup.get(k);
|
|
345
|
+
if (!f) {
|
|
346
|
+
const real = idx.get(foldKey(k));
|
|
347
|
+
const rf = real ? lookup.get(real) : undefined;
|
|
348
|
+
if (real && rf && typeof v === "string") {
|
|
349
|
+
key = real;
|
|
350
|
+
f = rf;
|
|
351
|
+
value = readUnder(rf, v, dict) ?? v;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
out[key] = f ? foldValue(f, value, dict) : value;
|
|
355
|
+
}
|
|
356
|
+
return out;
|
|
357
|
+
}
|
|
358
|
+
/** Fold a record: key aliases, value aliases and accepted units become real names (chapter 4.3). */
|
|
359
|
+
export function foldRecord(r, dict) {
|
|
360
|
+
const out = { ...r, values: foldFields(r.values, null, dict) };
|
|
361
|
+
if (r.parts) {
|
|
362
|
+
out.parts = r.parts.map((p) => (isObj(p) && isObj(p.values) ? { ...p, values: foldFields(p.values, null, dict) } : p));
|
|
363
|
+
}
|
|
364
|
+
return out;
|
|
365
|
+
}
|
|
366
|
+
const NUMBER = /^-?(0|[1-9]\d*)(\.\d+)?$/;
|
|
367
|
+
const DATE = /^(\d{4})(?:-(\d{2})(?:-(\d{2})(?:T(\d{2})(?::(\d{2})(?::(\d{2}))?)?(Z|[+-]\d{2}:\d{2})?)?)?)?$/;
|
|
368
|
+
const TIME = /^(\d{2}):(\d{2})(?::(\d{2}))?$/;
|
|
369
|
+
const RANK = { year: 0, month: 1, day: 2, hour: 3, minute: 4, second: 5 };
|
|
370
|
+
/** A date's precision and whether it is well formed (1.4.5): real calendar parts and clock parts. */
|
|
371
|
+
export function dateParts(d) {
|
|
372
|
+
const m = d.match(DATE);
|
|
373
|
+
if (!m)
|
|
374
|
+
return null;
|
|
375
|
+
const [, , mo, dd, hh, mi, ss, off] = m;
|
|
376
|
+
if (mo && (Number(mo) < 1 || Number(mo) > 12))
|
|
377
|
+
return null;
|
|
378
|
+
if (dd && (Number(dd) < 1 || Number(dd) > 31))
|
|
379
|
+
return null;
|
|
380
|
+
if (hh && Number(hh) > 23)
|
|
381
|
+
return null;
|
|
382
|
+
if (mi && Number(mi) > 59)
|
|
383
|
+
return null;
|
|
384
|
+
if (ss && Number(ss) > 59)
|
|
385
|
+
return null;
|
|
386
|
+
const precision = ss ? "second" : mi ? "minute" : hh ? "hour" : dd ? "day" : mo ? "month" : "year";
|
|
387
|
+
return { precision, offset: off ?? null };
|
|
388
|
+
}
|
|
389
|
+
function core(v) {
|
|
390
|
+
return isObj(v) && "$value" in v ? v.$value : v;
|
|
391
|
+
}
|
|
392
|
+
/** Check one scalar against its facet: an outcome and an optional flag. */
|
|
393
|
+
export function checkScalar(f, v) {
|
|
394
|
+
const c = core(v);
|
|
395
|
+
if (isObj(c) && "$empty" in c)
|
|
396
|
+
return { outcome: "absorbed" };
|
|
397
|
+
switch (f.type) {
|
|
398
|
+
case "number": {
|
|
399
|
+
let amount = null;
|
|
400
|
+
if (typeof c === "string" && NUMBER.test(c) && !f.unit)
|
|
401
|
+
amount = c;
|
|
402
|
+
if (isObj(c) && "$qty" in c && c.unit === f.unit)
|
|
403
|
+
amount = String(c.$qty);
|
|
404
|
+
if (isObj(c) && "$money" in c && (f.unit === "currency" || c.currency === f.unit))
|
|
405
|
+
amount = String(c.$money);
|
|
406
|
+
// A spelling the units table lacks is a unit question for review (11.9), not a contradiction.
|
|
407
|
+
if (amount === null && isObj(c) && "$qty" in c && f.unit && unitOf(String(c.unit)) === null)
|
|
408
|
+
return { outcome: "new", cites: { unit: String(c.unit) } };
|
|
409
|
+
if (amount === null)
|
|
410
|
+
return { outcome: "contradiction" };
|
|
411
|
+
if (f.range && (Number(amount) < Number(f.range.min) || Number(amount) > Number(f.range.max)))
|
|
412
|
+
return { outcome: "absorbed", flag: "range" };
|
|
413
|
+
return { outcome: "absorbed" };
|
|
414
|
+
}
|
|
415
|
+
case "date": {
|
|
416
|
+
const parts = typeof c === "string" ? dateParts(c) : null;
|
|
417
|
+
if (!parts)
|
|
418
|
+
return { outcome: "contradiction" };
|
|
419
|
+
const timed = RANK[parts.precision] >= RANK.hour;
|
|
420
|
+
// A moment carries its offset; a local wall-clock date never does (1.4.5).
|
|
421
|
+
if (timed && (f.zone === "local" ? parts.offset !== null : parts.offset === null))
|
|
422
|
+
return { outcome: "contradiction" };
|
|
423
|
+
const want = f.precision ?? "any";
|
|
424
|
+
if (want !== "any" && RANK[parts.precision] < RANK[want])
|
|
425
|
+
return { outcome: "absorbed", flag: "precision" };
|
|
426
|
+
return { outcome: "absorbed" };
|
|
427
|
+
}
|
|
428
|
+
case "time": {
|
|
429
|
+
const m = typeof c === "string" ? c.match(TIME) : null;
|
|
430
|
+
if (!m || Number(m[1]) > 23 || Number(m[2]) > 59 || (m[3] !== undefined && Number(m[3]) > 59))
|
|
431
|
+
return { outcome: "contradiction" };
|
|
432
|
+
return { outcome: "absorbed" };
|
|
433
|
+
}
|
|
434
|
+
case "yes-no":
|
|
435
|
+
return typeof c === "boolean" ? { outcome: "absorbed" } : { outcome: "contradiction" };
|
|
436
|
+
case "link":
|
|
437
|
+
return isObj(c) && "$link" in c ? { outcome: "absorbed" } : { outcome: "contradiction" };
|
|
438
|
+
case "image":
|
|
439
|
+
return isObj(c) && "$image" in c ? { outcome: "absorbed" } : { outcome: "contradiction" };
|
|
440
|
+
case "choice": {
|
|
441
|
+
if (typeof c !== "string")
|
|
442
|
+
return { outcome: "contradiction" };
|
|
443
|
+
if ((f.values ?? []).some((x) => x.value === c))
|
|
444
|
+
return { outcome: "absorbed" };
|
|
445
|
+
return f.fixed ? { outcome: "contradiction" } : { outcome: "new" };
|
|
446
|
+
}
|
|
447
|
+
case "compound":
|
|
448
|
+
return isObj(c) ? { outcome: "absorbed" } : { outcome: "contradiction" };
|
|
449
|
+
default: {
|
|
450
|
+
if (typeof c !== "string")
|
|
451
|
+
return { outcome: "contradiction" };
|
|
452
|
+
if (f.length !== undefined && [...c].length > f.length)
|
|
453
|
+
return { outcome: "absorbed", flag: "length" };
|
|
454
|
+
return { outcome: "absorbed" };
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
function outcome(record, address, r, cites) {
|
|
459
|
+
const o = { record, address, outcome: r.outcome };
|
|
460
|
+
if (r.flag)
|
|
461
|
+
o.flag = r.flag;
|
|
462
|
+
if (cites)
|
|
463
|
+
o.cites = cites;
|
|
464
|
+
else if (r.outcome === "contradiction")
|
|
465
|
+
o.cites = { type: address.replace(/\[.*$/, "").split(".").at(-1) };
|
|
466
|
+
return o;
|
|
467
|
+
}
|
|
468
|
+
function checkValue(record, f, key, v, address, narrow) {
|
|
469
|
+
const items = Array.isArray(v) ? v : null;
|
|
470
|
+
const pickLimit = narrow?.pick ?? f.pick;
|
|
471
|
+
if (items) {
|
|
472
|
+
if (pickLimit !== "any" && items.length > Number(pickLimit)) {
|
|
473
|
+
return [{ record, address, outcome: "contradiction", cites: { type: f.key } }];
|
|
474
|
+
}
|
|
475
|
+
return items.map((item) => itemOutcome(record, f, key, item, `${address}[${JSON.stringify(itemText(f, item))}]`, narrow));
|
|
476
|
+
}
|
|
477
|
+
return [itemOutcome(record, f, key, v, address, narrow)];
|
|
478
|
+
}
|
|
479
|
+
function citeLine(cls, l) {
|
|
480
|
+
return `${cls}: ${l.text}`;
|
|
481
|
+
}
|
|
482
|
+
function itemOutcome(record, f, _key, v, address, narrow) {
|
|
483
|
+
const base = checkScalar(f, v);
|
|
484
|
+
const c = core(v);
|
|
485
|
+
if (narrow?.values && typeof c === "string" && !(isObj(c) && "$empty" in c) && !narrow.values.includes(c)) {
|
|
486
|
+
const by = narrow.from.find((x) => x.line.values && !x.line.values.includes(c));
|
|
487
|
+
return outcome(record, address, { outcome: "contradiction" }, { rule: by ? citeLine(by.cls, by.line) : f.key });
|
|
488
|
+
}
|
|
489
|
+
return outcome(record, address, base);
|
|
490
|
+
}
|
|
491
|
+
function isEmptyValue(v) {
|
|
492
|
+
return v === undefined || (isObj(v) && v.$empty === "unknown");
|
|
493
|
+
}
|
|
494
|
+
/** Check a record: every value against its facet and worn classes, required facets, and parts. */
|
|
495
|
+
export function checkRecord(r, dict) {
|
|
496
|
+
const outcomes = [];
|
|
497
|
+
const worn = (r.wears ?? []).filter((w) => dict.classes.has(w));
|
|
498
|
+
const resolved = resolve(dict, worn);
|
|
499
|
+
const byFacet = new Map(resolved.map((l) => [l.facet, l]));
|
|
500
|
+
const asked = [];
|
|
501
|
+
const missing = [];
|
|
502
|
+
for (const l of resolved) {
|
|
503
|
+
let holds = true;
|
|
504
|
+
if (l.when)
|
|
505
|
+
holds = evaluate(parseCondition(l.when), r.values);
|
|
506
|
+
if (holds !== true)
|
|
507
|
+
continue;
|
|
508
|
+
asked.push(l.facet);
|
|
509
|
+
if (l.required && isEmptyValue(r.values[l.facet])) {
|
|
510
|
+
missing.push(l.facet);
|
|
511
|
+
const by = l.from.find((x) => x.line.required);
|
|
512
|
+
outcomes.push({ record: r.id, address: l.facet, outcome: "incomplete", cites: { rule: by ? citeLine(by.cls, by.line) : l.facet } });
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
for (const [key, v] of Object.entries(r.values)) {
|
|
516
|
+
const f = dict.facets.get(key);
|
|
517
|
+
if (!f) {
|
|
518
|
+
outcomes.push({ record: r.id, address: key, outcome: "unknown" });
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
outcomes.push(...checkValue(r.id, f, key, v, key, byFacet.get(key)));
|
|
522
|
+
}
|
|
523
|
+
const slots = worn.flatMap((w) => (dict.classes.get(w)?.parts ?? []).map((s) => ({ cls: w, s })));
|
|
524
|
+
for (const p of r.parts ?? []) {
|
|
525
|
+
if (!isObj(p))
|
|
526
|
+
continue;
|
|
527
|
+
const row = String(p.$row);
|
|
528
|
+
const wears = Array.isArray(p.wears) ? p.wears.map(String) : [];
|
|
529
|
+
const slot = slots.find((x) => wears.includes(x.s.wears));
|
|
530
|
+
const at = `parts[${row}]`;
|
|
531
|
+
const cite = slot ? { type: `${slot.cls}: ${slot.s.slot}` } : undefined;
|
|
532
|
+
if (slot && !slot.s.fungible && (row === `${slot.s.slot}s` || row === `${slot.s.slot}es`)) {
|
|
533
|
+
outcomes.push({ record: r.id, address: at, outcome: "contradiction", cites: cite, reason: "plural of a non-fungible slot" });
|
|
534
|
+
}
|
|
535
|
+
const values = isObj(p.values) ? p.values : {};
|
|
536
|
+
for (const [key, v] of Object.entries(values)) {
|
|
537
|
+
const address = `${at}.${key}`;
|
|
538
|
+
if (key === "crubal:count") {
|
|
539
|
+
if (slot && !slot.s.fungible) {
|
|
540
|
+
outcomes.push({ record: r.id, address, outcome: "contradiction", cites: cite, reason: "count on a non-fungible slot" });
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
const f = dict.facets.get(key);
|
|
545
|
+
if (!f) {
|
|
546
|
+
outcomes.push({ record: r.id, address, outcome: "unknown" });
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
outcomes.push(...checkValue(r.id, f, key, v, address, undefined));
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
const summary = { record: r.id, asked, outcome: missing.length ? "incomplete" : "complete" };
|
|
553
|
+
if (missing.length)
|
|
554
|
+
summary.missing = missing;
|
|
555
|
+
return { outcomes, summary };
|
|
556
|
+
}
|
|
557
|
+
//# sourceMappingURL=dictionary.js.map
|