@eriknaslund/skiss 0.3.0 → 0.5.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/dist/index.js CHANGED
@@ -1,9 +1,89 @@
1
+ // src/inheritance.ts
2
+ function inheritance(classes) {
3
+ const declared = /* @__PURE__ */ new Map();
4
+ for (const cls of classes) {
5
+ if (!declared.has(cls.name.text)) declared.set(cls.name.text, cls);
6
+ }
7
+ const circles = /* @__PURE__ */ new Map();
8
+ const parentOf = (cls) => {
9
+ if (cls.parent === void 0 || circles.has(cls)) return void 0;
10
+ return declared.get(cls.parent.text);
11
+ };
12
+ for (const cls of declared.values()) {
13
+ const path = [cls];
14
+ const seen = new Set(path);
15
+ for (let next = parentOf(cls); next !== void 0; next = parentOf(next)) {
16
+ if (seen.has(next)) {
17
+ const circle = path.slice(path.indexOf(next));
18
+ let closer = circle[0];
19
+ for (const member2 of circle) {
20
+ if (member2.line > closer.line) closer = member2;
21
+ }
22
+ const at = circle.indexOf(closer);
23
+ circles.set(closer, [...circle.slice(at), ...circle.slice(0, at)]);
24
+ break;
25
+ }
26
+ path.push(next);
27
+ seen.add(next);
28
+ }
29
+ }
30
+ const ancestors = (cls) => {
31
+ const out = [];
32
+ const seen = /* @__PURE__ */ new Set([cls]);
33
+ for (let next = parentOf(cls); next !== void 0; next = parentOf(next)) {
34
+ if (seen.has(next)) break;
35
+ out.push(next);
36
+ seen.add(next);
37
+ }
38
+ return out;
39
+ };
40
+ const find = (cls, matches) => {
41
+ for (const from of ancestors(cls)) {
42
+ const field = from.fields.find(matches);
43
+ if (field !== void 0) return { field, from };
44
+ }
45
+ return void 0;
46
+ };
47
+ return {
48
+ ancestors,
49
+ inherited: (cls, fieldName) => find(cls, (field) => field.name.text === fieldName),
50
+ inheritedIdentifier: (cls) => find(cls, (field) => field.identifier),
51
+ circle: (cls) => circles.get(cls)
52
+ };
53
+ }
54
+ function sameField(child, inheritedField) {
55
+ return meaning(child) === meaning(inheritedField);
56
+ }
57
+ var meaning = (field) => JSON.stringify([
58
+ field.name.text,
59
+ field.identifier,
60
+ typeMeaning(field),
61
+ field.system?.text,
62
+ field.joinsTo === void 0 ? void 0 : [field.joinsTo.className.text, field.joinsTo.fieldName.text],
63
+ field.description,
64
+ field.note
65
+ ]);
66
+ function typeMeaning(field) {
67
+ const type = field.type;
68
+ if (type === void 0) return void 0;
69
+ switch (type.kind) {
70
+ case "primitive":
71
+ return [type.kind, type.name, type.many];
72
+ case "class":
73
+ case "unknown":
74
+ return [type.kind, type.name.text, type.many];
75
+ case "enum":
76
+ return [type.kind, type.values.map((v) => v.text), type.many];
77
+ }
78
+ }
79
+
1
80
  // src/parse.ts
2
81
  var isWs = (c) => c === " " || c === " ";
3
82
  var trimWs = (s) => s.replace(/^[ \t]+/, "").replace(/[ \t]+$/, "");
4
83
  var CLASS_NAME = /^[A-Z][A-Za-z0-9]*$/;
5
84
  var FIELD_NAME = /^[a-z][A-Za-z0-9]*$/;
6
- var SYSTEM_OR_VALUE = /^[A-Za-z][A-Za-z0-9_-]*$/;
85
+ var SYSTEM_NAME = /^[A-Za-z][A-Za-z0-9_-]*$/;
86
+ var ENUM_VALUE = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
7
87
  var PRIMITIVES = {
8
88
  string: "string",
9
89
  int: "int",
@@ -20,7 +100,7 @@ function primitiveFor(word) {
20
100
  return Object.hasOwn(PRIMITIVES, word) ? PRIMITIVES[word] : void 0;
21
101
  }
22
102
  var FIELD_ORDER = "name, `*`, `: type`, `@System`, `= Class.field`, then `#` and `?`";
23
- var CLASS_ORDER = "name, `@System`, `~ Class`, then `#` and `?`";
103
+ var CLASS_ORDER = "name, `< Parent`, `@System`, `~ Class`, then `#` and `?`";
24
104
  function parse(source) {
25
105
  const classes = [];
26
106
  const diagnostics = [];
@@ -107,7 +187,7 @@ function splitTrailer(text) {
107
187
  return trailer;
108
188
  }
109
189
  var WORD = /[\p{L}\p{N}_-]+/uy;
110
- var OPERATORS = ["*", ":", "@", "~", "=", ".", "|", "["];
190
+ var OPERATORS = ["*", ":", "@", "~", "=", ".", "|", "<", ",", "["];
111
191
  function tokenize(head) {
112
192
  const tokens = [];
113
193
  let i = 0;
@@ -169,6 +249,13 @@ function parseClassLine(text, line) {
169
249
  );
170
250
  }
171
251
  const node = { name: name(head, line), line };
252
+ const lt = peek(c);
253
+ if (lt?.kind === "<") {
254
+ take(c);
255
+ const parent = parseParent(c, lt);
256
+ if (!parent.ok) return parent;
257
+ node.parent = parent.value;
258
+ }
172
259
  const at = peek(c);
173
260
  if (at?.kind === "@") {
174
261
  take(c);
@@ -298,6 +385,31 @@ function parseFieldLine(text, line) {
298
385
  if (trailer.note !== void 0) node.note = trailer.note;
299
386
  return { ok: true, value: node };
300
387
  }
388
+ function parseParent(c, lt) {
389
+ const parent = take(c);
390
+ if (parent === void 0 || parent.kind !== "word") {
391
+ return fail(
392
+ "E_UNPARSABLE",
393
+ "Expected a class name after `<`, e.g. `Jedi < Character`",
394
+ c.line,
395
+ parent ?? afterSpan(lt)
396
+ );
397
+ }
398
+ if (!CLASS_NAME.test(parent.text)) {
399
+ const message = Object.hasOwn(PRIMITIVES, parent.text) ? `\`${parent.text}\` is a primitive, not a class; a parent is a class` : `The class after \`<\` must be UpperCamelCase: \`${parent.text}\``;
400
+ return fail("E_BAD_NAME", message, c.line, parent);
401
+ }
402
+ const next = peek(c);
403
+ if (next?.kind === "," || next?.kind === "<") {
404
+ return fail(
405
+ "E_UNPARSABLE",
406
+ "A class has one parent; `<` takes a single class name",
407
+ c.line,
408
+ next
409
+ );
410
+ }
411
+ return { ok: true, value: name(parent, c.line) };
412
+ }
301
413
  function parseSystem(c, at) {
302
414
  const system = take(c);
303
415
  if (system === void 0 || system.kind !== "word") {
@@ -308,7 +420,7 @@ function parseSystem(c, at) {
308
420
  system ?? afterSpan(at)
309
421
  );
310
422
  }
311
- if (!SYSTEM_OR_VALUE.test(system.text)) {
423
+ if (!SYSTEM_NAME.test(system.text)) {
312
424
  return fail(
313
425
  "E_UNPARSABLE",
314
426
  `A system name is letters, digits, \`-\` and \`_\`, starting with a letter: \`${system.text}\``,
@@ -366,10 +478,10 @@ function parseType(c, colon) {
366
478
  }
367
479
  if (words.length > 1) {
368
480
  for (const w of words) {
369
- if (!SYSTEM_OR_VALUE.test(w.text)) {
481
+ if (!ENUM_VALUE.test(w.text)) {
370
482
  return fail(
371
483
  "E_UNPARSABLE",
372
- `An enum value is letters, digits, \`-\` and \`_\`, starting with a letter: \`${w.text}\``,
484
+ `An enum value is letters, digits, \`-\` and \`_\`, and does not start with \`-\` or \`_\`: \`${w.text}\``,
373
485
  c.line,
374
486
  w
375
487
  );
@@ -407,7 +519,7 @@ function parseType(c, colon) {
407
519
  );
408
520
  }
409
521
  function unexpected(token, line, kind, order) {
410
- const modifiers = kind === "field" ? ["*", ":", "@", "="] : ["@", "~"];
522
+ const modifiers = kind === "field" ? ["*", ":", "@", "="] : ["<", "@", "~"];
411
523
  if (modifiers.includes(token.kind)) {
412
524
  return fail(
413
525
  "E_UNPARSABLE",
@@ -416,10 +528,10 @@ function unexpected(token, line, kind, order) {
416
528
  token
417
529
  );
418
530
  }
419
- if (token.text === "<") {
531
+ if (token.kind === "<") {
420
532
  return fail(
421
533
  "E_UNPARSABLE",
422
- "`<` is reserved for inheritance and not yet part of the language",
534
+ "`<` marks inheritance and belongs on the class line: `Child < Parent`",
423
535
  line,
424
536
  token
425
537
  );
@@ -448,10 +560,14 @@ function resolve(doc) {
448
560
  };
449
561
  const classes = out.classes;
450
562
  const declared = new Set(classes.map((c) => c.name.text));
451
- const warnings = [];
563
+ const found = [];
452
564
  const undeclared = /* @__PURE__ */ new Map();
565
+ const tree = inheritance(classes);
453
566
  const warn = (code, message, at) => {
454
- warnings.push({ severity: "warning", code, message, line: at.line, col: at.col, end: at.end });
567
+ found.push({ severity: "warning", code, message, line: at.line, col: at.col, end: at.end });
568
+ };
569
+ const error = (code, message, at) => {
570
+ found.push({ severity: "error", code, message, line: at.line, col: at.col, end: at.end });
455
571
  };
456
572
  const reference = (name2) => {
457
573
  if (declared.has(name2.text)) return true;
@@ -472,9 +588,25 @@ function resolve(doc) {
472
588
  cls.name
473
589
  );
474
590
  }
591
+ const parent = cls.parent;
592
+ if (parent !== void 0) {
593
+ reference(parent);
594
+ const circle = tree.circle(cls);
595
+ if (circle !== void 0) {
596
+ const through = circle.slice(1).map((member2) => `\`${member2.name.text}\``);
597
+ const path = through.length === 0 ? "" : ` through ${through.join(", ")}`;
598
+ error(
599
+ "E_INHERITANCE_CYCLE",
600
+ `class \`${cls.name.text}\` inherits from itself${path}; this \`<\` is not carried`,
601
+ parent
602
+ );
603
+ cls.parent = void 0;
604
+ }
605
+ }
475
606
  if (cls.similarTo !== void 0) reference(cls.similarTo);
476
607
  const firstField = /* @__PURE__ */ new Map();
477
- let identifier;
608
+ const inheritedIdentifier = tree.inheritedIdentifier(cls);
609
+ let identifier = inheritedIdentifier?.field;
478
610
  for (const field of cls.fields) {
479
611
  const earlierField = firstField.get(field.name.text);
480
612
  if (earlierField === void 0) firstField.set(field.name.text, field);
@@ -488,14 +620,23 @@ function resolve(doc) {
488
620
  if (field.identifier) {
489
621
  if (identifier === void 0) identifier = field;
490
622
  else {
623
+ const from = identifier === inheritedIdentifier?.field ? ` inherits the identifier \`${identifier.name.text}\` from class \`${inheritedIdentifier.from.name.text}\`, line` : ` already has the identifier \`${identifier.name.text}\` on line`;
491
624
  warn(
492
625
  "W_MULTIPLE_IDENTIFIERS",
493
- `\`*\` on \`${field.name.text}\` is ignored: class \`${cls.name.text}\` already has the identifier \`${identifier.name.text}\` on line ${identifier.line}`,
626
+ `\`*\` on \`${field.name.text}\` is ignored: class \`${cls.name.text}\`${from} ${identifier.line}`,
494
627
  field.identifierAt ?? field.name
495
628
  );
496
629
  field.identifier = false;
497
630
  }
498
631
  }
632
+ const replaced = tree.inherited(cls, field.name.text);
633
+ if (replaced !== void 0 && sameField(field, replaced.field)) {
634
+ warn(
635
+ "W_REDUNDANT_OVERRIDE",
636
+ `field \`${field.name.text}\` is identical to the one inherited from class \`${replaced.from.name.text}\` on line ${replaced.field.line}`,
637
+ field.name
638
+ );
639
+ }
499
640
  if (field.type?.kind === "unknown") {
500
641
  const word = field.type.name.text;
501
642
  const suggestion = closestTypeWord(word);
@@ -506,6 +647,18 @@ function resolve(doc) {
506
647
  );
507
648
  } else if (field.type?.kind === "class") {
508
649
  reference(field.type.name);
650
+ } else if (field.type?.kind === "enum") {
651
+ const seen = /* @__PURE__ */ new Set();
652
+ for (const value of field.type.values) {
653
+ if (seen.has(value.text)) {
654
+ warn(
655
+ "W_DUPLICATE_ENUM_VALUE",
656
+ `value \`${value.text}\` is already in this enum; it is carried once`,
657
+ value
658
+ );
659
+ }
660
+ seen.add(value.text);
661
+ }
509
662
  }
510
663
  if (field.joinsTo !== void 0) {
511
664
  const { className, fieldName } = field.joinsTo;
@@ -521,8 +674,8 @@ function resolve(doc) {
521
674
  }
522
675
  }
523
676
  const present2 = new Set(out.diagnostics.map(key));
524
- for (const w of warnings) {
525
- if (!present2.has(key(w))) out.diagnostics.push(w);
677
+ for (const d of found) {
678
+ if (!present2.has(key(d))) out.diagnostics.push(d);
526
679
  }
527
680
  out.diagnostics.sort((a, b) => a.line - b.line);
528
681
  out.undeclared = [...undeclared.values()].map((n) => ({ ...n }));
@@ -553,6 +706,7 @@ function copyField(f) {
553
706
  }
554
707
  function copyClass(c) {
555
708
  const out = { ...c, name: copyName(c.name), fields: c.fields.map(copyField) };
709
+ if (c.parent !== void 0) out.parent = copyName(c.parent);
556
710
  if (c.system !== void 0) out.system = copyName(c.system);
557
711
  if (c.similarTo !== void 0) out.similarTo = copyName(c.similarTo);
558
712
  return out;
@@ -619,7 +773,8 @@ function toLinkML(doc, opts) {
619
773
  const classNames = new Set(classes.map((cls) => cls.node.name.text));
620
774
  for (const name2 of resolved.undeclared) classNames.add(name2.text);
621
775
  const enums = nameEnums(classes, classNames);
622
- const prefixes = { linkml: "https://w3id.org/linkml/" };
776
+ const prefixes = nameKeyed();
777
+ prefixes.linkml = "https://w3id.org/linkml/";
623
778
  if (!Object.hasOwn(prefixes, schemaName)) prefixes[schemaName] = `${BASE_URI}/${schemaName}/`;
624
779
  const systemPrefix = /* @__PURE__ */ new Map();
625
780
  const addSystem = (system) => {
@@ -650,12 +805,12 @@ function toLinkML(doc, opts) {
650
805
  prefixes,
651
806
  imports: ["linkml:types"]
652
807
  };
653
- const out = {};
808
+ const out = nameKeyed();
654
809
  for (const cls of classes) {
655
810
  out[cls.node.name.text] = buildClass(cls, mappingPrefix, enums);
656
811
  }
657
812
  for (const name2 of resolved.undeclared) {
658
- if (!(name2.text in out)) out[name2.text] = { annotations: { undeclared: true } };
813
+ if (!Object.hasOwn(out, name2.text)) out[name2.text] = { annotations: { undeclared: true } };
659
814
  }
660
815
  if (Object.keys(out).length > 0) schema.classes = out;
661
816
  const enumBlock = buildEnums(classes, enums);
@@ -713,13 +868,13 @@ function nameEnums(classes, classNames) {
713
868
  return names;
714
869
  }
715
870
  function buildEnums(classes, enums) {
716
- const out = {};
871
+ const out = nameKeyed();
717
872
  for (const { node, fields } of classes) {
718
873
  for (const field of fields) {
719
874
  if (field.type?.kind !== "enum") continue;
720
875
  const name2 = enums.get(useKey(node.name.text, field.name.text));
721
- if (name2 === void 0 || name2 in out) continue;
722
- const values = {};
876
+ if (name2 === void 0 || Object.hasOwn(out, name2)) continue;
877
+ const values = nameKeyed();
723
878
  for (const value of field.type.values) values[value.text] = null;
724
879
  out[name2] = { permissible_values: values };
725
880
  }
@@ -730,6 +885,7 @@ function buildClass(cls, mappingPrefix, enums) {
730
885
  const { node, fields } = cls;
731
886
  const out = {};
732
887
  if (node.description !== void 0) out.description = node.description;
888
+ if (node.parent !== void 0) out.is_a = node.parent.text;
733
889
  const annotations = {};
734
890
  if (node.system !== void 0) annotations.system = node.system.text;
735
891
  if (node.note !== void 0) annotations.note = node.note;
@@ -738,7 +894,7 @@ function buildClass(cls, mappingPrefix, enums) {
738
894
  const target = node.similarTo.text;
739
895
  out.close_mappings = [`${mappingPrefix(target)}:${target}`];
740
896
  }
741
- const attributes = {};
897
+ const attributes = nameKeyed();
742
898
  for (const field of fields) {
743
899
  attributes[field.name.text] = buildAttribute(field, node.name.text, enums);
744
900
  }
@@ -776,10 +932,12 @@ function rangeOf(field, className, enums) {
776
932
  }
777
933
  }
778
934
  var RESERVED_PREFIXES = /* @__PURE__ */ new Set(["linkml", "xsd", "shex", "schema"]);
935
+ var PROTOTYPE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
936
+ var nameKeyed = () => /* @__PURE__ */ Object.create(null);
779
937
  var linkmlName = (text) => {
780
938
  const name2 = text.toLowerCase().replace(/[^a-z0-9]/g, "_");
781
939
  if (!/[a-z0-9]/.test(name2)) return "sketch";
782
- if (RESERVED_PREFIXES.has(name2)) return `${name2}_`;
940
+ if (RESERVED_PREFIXES.has(name2) || PROTOTYPE_KEYS.has(name2)) return `${name2}_`;
783
941
  return /^[0-9]/.test(name2) ? `_${name2}` : name2;
784
942
  };
785
943
  var capitalise = (text) => text.charAt(0).toUpperCase() + text.slice(1);
@@ -810,6 +968,7 @@ function toMermaid(doc, opts) {
810
968
  }
811
969
  for (const cls of resolved.classes) {
812
970
  const from = cls.name.text;
971
+ if (cls.parent !== void 0) lines.push(` ${cls.parent.text} <|-- ${from}`);
813
972
  if (cls.similarTo !== void 0) lines.push(` ${from} ..> ${cls.similarTo.text} : similar`);
814
973
  for (const field of cls.fields) {
815
974
  const type = field.type;
@@ -916,7 +1075,7 @@ function toSkiss(doc) {
916
1075
  `;
917
1076
  }
918
1077
  function classBlock(cls) {
919
- const head = cls.name.text + (cls.system === void 0 ? "" : ` @${cls.system.text}`) + (cls.similarTo === void 0 ? "" : ` ~ ${cls.similarTo.text}`);
1078
+ const head = cls.name.text + (cls.parent === void 0 ? "" : ` < ${cls.parent.text}`) + (cls.system === void 0 ? "" : ` @${cls.system.text}`) + (cls.similarTo === void 0 ? "" : ` ~ ${cls.similarTo.text}`);
920
1079
  return [withTrailer(head, cls), ...cls.fields.map(fieldLine)].join("\n");
921
1080
  }
922
1081
  function fieldLine(field) {
@@ -942,10 +1101,15 @@ function typeText2(type) {
942
1101
  }
943
1102
  return type.many ? `${text}[]` : text;
944
1103
  }
1104
+ function writeDescription(text) {
1105
+ const attached = text.replace(/[ \t]+\?(?=[ \t]|$)/g, "?");
1106
+ return /^\?([ \t]|$)/.test(attached) ? `#${attached}` : `# ${attached}`;
1107
+ }
1108
+ var rewordsDescription = (text) => writeDescription(text) !== `# ${text}`;
945
1109
  function withTrailer(head, node) {
946
1110
  const parts2 = [];
947
1111
  if (node.description !== void 0 && node.description !== "")
948
- parts2.push(`# ${node.description}`);
1112
+ parts2.push(writeDescription(node.description));
949
1113
  if (node.note !== void 0 && node.note !== "") parts2.push(`? ${node.note}`);
950
1114
  if (parts2.length === 0) return head;
951
1115
  const gap = head.length < TRAILER_COLUMN - 1 ? " ".repeat(TRAILER_COLUMN - head.length) : " ";
@@ -965,7 +1129,14 @@ var PRIMITIVE_RANGE = {
965
1129
  uri: "uri",
966
1130
  uriorcurie: "uri"
967
1131
  };
968
- var CLASS_KEYS = /* @__PURE__ */ new Set(["description", "annotations", "close_mappings", "attributes", "slots"]);
1132
+ var CLASS_KEYS = /* @__PURE__ */ new Set([
1133
+ "description",
1134
+ "is_a",
1135
+ "annotations",
1136
+ "close_mappings",
1137
+ "attributes",
1138
+ "slots"
1139
+ ]);
969
1140
  var SLOT_KEYS = /* @__PURE__ */ new Set(["description", "annotations", "range", "multivalued", "identifier"]);
970
1141
  var CLASS_TAGS = /* @__PURE__ */ new Set(["system", "note", "undeclared"]);
971
1142
  var SLOT_TAGS = /* @__PURE__ */ new Set(["system", "note", "joins_to"]);
@@ -983,6 +1154,7 @@ var QUIET_SCHEMA_KEYS = /* @__PURE__ */ new Set([
983
1154
  "slots",
984
1155
  "enums"
985
1156
  ]);
1157
+ var REWORDED = "a standalone `?` is attached to the word before it";
986
1158
  var UNKNOWN_TYPE = /^[a-z][a-z0-9_-]*$/;
987
1159
  function fromLinkML(schema) {
988
1160
  const root = asRecord(schema);
@@ -1007,7 +1179,7 @@ function fromLinkML(schema) {
1007
1179
  for (const key2 of Object.keys(classDefs)) {
1008
1180
  const names = /* @__PURE__ */ new Map();
1009
1181
  const taken = /* @__PURE__ */ new Set();
1010
- for (const slot of slotKeysOf(asRecord(get(classDefs, key2)) ?? {})) {
1182
+ for (const slot of fieldKeysOf(key2)) {
1011
1183
  names.set(slot, unique(toFieldName(slot), taken));
1012
1184
  }
1013
1185
  fieldNames.set(key2, names);
@@ -1026,7 +1198,7 @@ function fromLinkML(schema) {
1026
1198
  const rawValues = get(asRecord(get(enumDefs, key2)) ?? {}, "permissible_values");
1027
1199
  const values = asRecord(rawValues);
1028
1200
  const keys = Object.keys(values ?? {});
1029
- const bad = keys.find((value) => !SYSTEM_OR_VALUE.test(value));
1201
+ const bad = keys.find((value) => !ENUM_VALUE.test(value));
1030
1202
  if (values === void 0 && present(rawValues)) {
1031
1203
  unwritable.set(key2, `${shown(rawValues)} is not a map of permissible values`);
1032
1204
  } else if (keys.length < 2) {
@@ -1057,14 +1229,32 @@ function fromLinkML(schema) {
1057
1229
  }
1058
1230
  continue;
1059
1231
  }
1232
+ const undeclared = annotations.get("undeclared");
1233
+ if (undeclared !== void 0 && typeof undeclared !== "boolean") {
1234
+ report("annotation", name2, notText("undeclared", undeclared));
1235
+ }
1060
1236
  const node = { name: at0(name2), fields: [], line: 0 };
1061
1237
  const description = oneLine(stringOf(get(def, "description")));
1062
- if (description !== void 0) node.description = description;
1238
+ if (description !== void 0) {
1239
+ node.description = description;
1240
+ if (rewordsDescription(description)) report("reworded", name2, REWORDED);
1241
+ }
1063
1242
  const system = annotations.get("system");
1064
- if (typeof system === "string" && SYSTEM_OR_VALUE.test(system)) node.system = at0(system);
1065
- else if (system !== void 0) report("annotation", name2, "`system` is not a system name");
1066
- const note = oneLine(stringOf(annotations.get("note")));
1067
- if (note !== void 0) node.note = note;
1243
+ if (typeof system === "string") {
1244
+ if (SYSTEM_NAME.test(system)) node.system = at0(system);
1245
+ else report("annotation", name2, "`system` is not a system name");
1246
+ } else if (system !== void 0) report("annotation", name2, notText("system", system));
1247
+ const rawNote = annotations.get("note");
1248
+ if (typeof rawNote === "string") {
1249
+ const note = oneLine(rawNote);
1250
+ if (note !== void 0) node.note = note;
1251
+ } else if (rawNote !== void 0) report("annotation", name2, notText("note", rawNote));
1252
+ const parent = stringOf(get(def, "is_a"));
1253
+ if (parent !== void 0) {
1254
+ const parentName = Object.hasOwn(classDefs, parent) ? classNameOf(parent) : toClassName(parent);
1255
+ node.parent = at0(parentName);
1256
+ reference(name2, parent, parentName);
1257
+ }
1068
1258
  const mappings = stringList(get(def, "close_mappings"));
1069
1259
  const first = mappings[0];
1070
1260
  const target = first === void 0 ? "" : localName(first);
@@ -1087,6 +1277,9 @@ function fromLinkML(schema) {
1087
1277
  }
1088
1278
  }
1089
1279
  const block = get(def, other);
1280
+ if (other === "is_a" && present(block) && stringOf(block) === void 0) {
1281
+ report("is_a", name2, `${shown(block)} is not a class name`);
1282
+ }
1090
1283
  if (other === "attributes" && present(block) && asRecord(block) === void 0) {
1091
1284
  report("attributes", name2, `${shown(block)} is not a map`);
1092
1285
  }
@@ -1101,10 +1294,12 @@ function fromLinkML(schema) {
1101
1294
  report("slot_usage", name2);
1102
1295
  continue;
1103
1296
  }
1297
+ const written = slotKeysOf(def);
1298
+ const inherited = inheritedSlots(key2);
1104
1299
  for (const slot of Object.keys(usage)) {
1105
- if (fieldNames.get(key2)?.has(slot) === true) {
1300
+ if (written.includes(slot)) {
1106
1301
  report("slot_usage", `${name2}.${fieldNameOf(key2, slot)}`);
1107
- } else {
1302
+ } else if (!inherited.has(slot)) {
1108
1303
  report("slot_usage", name2, `\`${slot}\` is inherited and not in the sketch`);
1109
1304
  }
1110
1305
  }
@@ -1112,9 +1307,10 @@ function fromLinkML(schema) {
1112
1307
  }
1113
1308
  report(other, name2);
1114
1309
  }
1115
- for (const slot of slotKeysOf(def)) {
1310
+ const own = slotKeysOf(def);
1311
+ for (const slot of fieldKeysOf(key2)) {
1116
1312
  const attributes = asRecord(get(def, "attributes")) ?? {};
1117
- const rawSlot = Object.hasOwn(attributes, slot) ? get(attributes, slot) : get(slotDefs, slot);
1313
+ const rawSlot = !own.includes(slot) ? get(asRecord(get(def, "slot_usage")) ?? {}, slot) : Object.hasOwn(attributes, slot) ? get(attributes, slot) : get(slotDefs, slot);
1118
1314
  const slotDef = asRecord(rawSlot);
1119
1315
  if (slotDef === void 0 && present(rawSlot)) {
1120
1316
  report("slot", `${name2}.${fieldNameOf(key2, slot)}`, "is not a slot definition");
@@ -1169,13 +1365,18 @@ function fromLinkML(schema) {
1169
1365
  line: 0
1170
1366
  };
1171
1367
  const description = oneLine(stringOf(get(def, "description")));
1172
- if (description !== void 0) field.description = description;
1368
+ if (description !== void 0) {
1369
+ field.description = description;
1370
+ if (rewordsDescription(description)) report("reworded", element, REWORDED);
1371
+ }
1173
1372
  const type = typeFor(element, name2, def);
1174
1373
  if (type !== void 0) field.type = type;
1175
1374
  const annotations = annotationsOf(def);
1176
1375
  const system = annotations.get("system");
1177
- if (typeof system === "string" && SYSTEM_OR_VALUE.test(system)) field.system = at0(system);
1178
- else if (system !== void 0) report("annotation", element, "`system` is not a system name");
1376
+ if (typeof system === "string") {
1377
+ if (SYSTEM_NAME.test(system)) field.system = at0(system);
1378
+ else report("annotation", element, "`system` is not a system name");
1379
+ } else if (system !== void 0) report("annotation", element, notText("system", system));
1179
1380
  const joins = annotations.get("joins_to");
1180
1381
  if (typeof joins === "string") {
1181
1382
  const dot = joins.indexOf(".");
@@ -1188,9 +1389,12 @@ function fromLinkML(schema) {
1188
1389
  reference(element, target, targetClass);
1189
1390
  reference(element, targetField, targetName);
1190
1391
  } else report("annotation", element, "`joins_to` is not `Class.field`");
1191
- } else if (joins !== void 0) report("annotation", element, "`joins_to` is not text");
1192
- const note = oneLine(stringOf(annotations.get("note")));
1193
- if (note !== void 0) field.note = note;
1392
+ } else if (joins !== void 0) report("annotation", element, notText("joins_to", joins));
1393
+ const rawNote = annotations.get("note");
1394
+ if (typeof rawNote === "string") {
1395
+ const note = oneLine(rawNote);
1396
+ if (note !== void 0) field.note = note;
1397
+ } else if (rawNote !== void 0) report("annotation", element, notText("note", rawNote));
1194
1398
  for (const other of Object.keys(def)) {
1195
1399
  if (!SLOT_KEYS.has(other)) {
1196
1400
  report(other, element);
@@ -1243,6 +1447,34 @@ function fromLinkML(schema) {
1243
1447
  report("narrowed", element, `\`${range}\` is not a Skiss type; it falls back to string`);
1244
1448
  return { kind: "unknown", name: at0(word), many };
1245
1449
  }
1450
+ function parentKeyOf(classKey) {
1451
+ const def = asRecord(get(classDefs, classKey)) ?? {};
1452
+ const parent = stringOf(get(def, "is_a"));
1453
+ if (parent === void 0 || !Object.hasOwn(classDefs, parent)) return void 0;
1454
+ return parent;
1455
+ }
1456
+ function inheritedSlots(classKey) {
1457
+ const out = /* @__PURE__ */ new Set();
1458
+ const seen = /* @__PURE__ */ new Set([classKey]);
1459
+ let key2 = parentKeyOf(classKey);
1460
+ while (key2 !== void 0 && !seen.has(key2)) {
1461
+ seen.add(key2);
1462
+ for (const slot of slotKeysOf(asRecord(get(classDefs, key2)) ?? {})) out.add(slot);
1463
+ key2 = parentKeyOf(key2);
1464
+ }
1465
+ return out;
1466
+ }
1467
+ function fieldKeysOf(classKey) {
1468
+ const def = asRecord(get(classDefs, classKey)) ?? {};
1469
+ const keys = slotKeysOf(def);
1470
+ const usage = asRecord(get(def, "slot_usage"));
1471
+ if (usage === void 0) return keys;
1472
+ const inherited = inheritedSlots(classKey);
1473
+ for (const slot of Object.keys(usage)) {
1474
+ if (inherited.has(slot) && !keys.includes(slot)) keys.push(slot);
1475
+ }
1476
+ return keys;
1477
+ }
1246
1478
  function listedSomewhere(slot) {
1247
1479
  for (const key2 of Object.keys(classDefs)) {
1248
1480
  const def = asRecord(get(classDefs, key2)) ?? {};
@@ -1265,13 +1497,17 @@ var COUNTED = {
1265
1497
  class: ["class", "classes"],
1266
1498
  slot: ["slot", "slots"],
1267
1499
  schema: ["schema key", "schema keys"],
1500
+ narrowed: ["narrowed range", "narrowed ranges"],
1501
+ enum_detail: ["enum detail", "enum details"],
1502
+ annotation: ["unreadable annotation", "unreadable annotations"],
1503
+ reworded: ["description reworded", "descriptions reworded"],
1268
1504
  // `mixin: true` marks the class; `mixins: [M]` points at another one.
1269
1505
  mixin: ["mixin class", "mixin classes"],
1270
1506
  mixins: ["mixin", "mixins"],
1271
1507
  pattern: ["pattern", "patterns"],
1272
1508
  unit: ["unit", "units"]
1273
1509
  };
1274
- var ENUM_KINDS = /* @__PURE__ */ new Set(["enum_detail"]);
1510
+ var UNREADABLE = "unreadable";
1275
1511
  function formatDropped(dropped) {
1276
1512
  const groups = /* @__PURE__ */ new Map();
1277
1513
  for (const entry of dropped) {
@@ -1280,15 +1516,21 @@ function formatDropped(dropped) {
1280
1516
  else group.push(entry);
1281
1517
  }
1282
1518
  const drops = [];
1519
+ const notRead = [];
1283
1520
  let renamed = "";
1284
1521
  let inlined = "";
1285
1522
  for (const [kind, entries] of groups) {
1523
+ if (kind === UNREADABLE) {
1524
+ for (const entry of entries) notRead.push(entry.detail ?? "the schema could not be read");
1525
+ continue;
1526
+ }
1286
1527
  const phrase = phraseFor(kind, entries);
1287
1528
  if (kind === "renamed") renamed = phrase;
1288
1529
  else if (kind === "inlined") inlined = phrase;
1289
1530
  else drops.push(phrase);
1290
1531
  }
1291
1532
  const sentences = [];
1533
+ if (notRead.length > 0) sentences.push(`Not read: ${notRead.join("; ")}.`);
1292
1534
  if (drops.length > 0) sentences.push(`Dropped: ${drops.join(", ")}.`);
1293
1535
  if (renamed !== "") sentences.push(`Renamed: ${renamed}.`);
1294
1536
  if (inlined !== "") sentences.push(`Inlined: ${inlined}.`);
@@ -1298,11 +1540,10 @@ function phraseFor(kind, entries) {
1298
1540
  const n = entries.length;
1299
1541
  const counted = Object.hasOwn(COUNTED, kind) ? COUNTED[kind] : void 0;
1300
1542
  if (counted !== void 0) return `${n} ${n === 1 ? counted[0] : counted[1]}`;
1301
- return `${kind} on ${n} ${nounFor(kind, entries)}`;
1543
+ return `${kind} on ${n} ${nounFor(entries)}`;
1302
1544
  }
1303
- function nounFor(kind, entries) {
1545
+ function nounFor(entries) {
1304
1546
  const one = entries.length === 1;
1305
- if (ENUM_KINDS.has(kind)) return one ? "enum" : "enums";
1306
1547
  const slots = entries.filter((entry) => entry.element.includes(".")).length;
1307
1548
  if (slots === entries.length) return one ? "slot" : "slots";
1308
1549
  if (slots === 0) return one ? "class" : "classes";
@@ -1353,12 +1594,18 @@ function annotationsOf(def) {
1353
1594
  if (annotations === void 0) return out;
1354
1595
  for (const tag of Object.keys(annotations)) {
1355
1596
  const raw = get(annotations, tag);
1356
- const value = asRecord(raw) === void 0 ? raw : get(asRecord(raw) ?? {}, "value");
1357
- if (typeof value === "string" || typeof value === "boolean") out.set(tag, value);
1358
- else out.set(tag, String(value));
1597
+ const long = asRecord(raw);
1598
+ const value = long === void 0 ? raw : get(long, "value");
1599
+ out.set(tag, value ?? null);
1359
1600
  }
1360
1601
  return out;
1361
1602
  }
1603
+ function notText(tag, value) {
1604
+ if (value === null) return `\`${tag}\` was written with no value`;
1605
+ if (Array.isArray(value)) return `\`${tag}\` is a list, not text`;
1606
+ if (typeof value === "object") return `\`${tag}\` is a map, not text`;
1607
+ return `\`${tag}\` is a ${typeof value}, not text`;
1608
+ }
1362
1609
  function slotKeysOf(def) {
1363
1610
  const keys = Object.keys(asRecord(get(def, "attributes")) ?? {});
1364
1611
  for (const slot of stringList(get(def, "slots"))) {
@@ -1383,7 +1630,7 @@ var schemaElement = (root) => stringOf(get(root, "name")) ?? "schema";
1383
1630
  var nothing = (element, detail) => ({
1384
1631
  document: parse(""),
1385
1632
  source: "",
1386
- dropped: [{ kind: "schema", element, detail }]
1633
+ dropped: [{ kind: UNREADABLE, element, detail }]
1387
1634
  });
1388
1635
 
1389
1636
  // src/import.ts
@@ -1410,7 +1657,7 @@ function notYAML(error) {
1410
1657
  }
1411
1658
 
1412
1659
  // src/index.ts
1413
- var VERSION = "0.3.0";
1660
+ var VERSION = "0.5.0";
1414
1661
  export {
1415
1662
  VERSION,
1416
1663
  compile,