@glossarist/concept-browser 0.7.135 → 0.7.136

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.
@@ -1,6 +1,5 @@
1
1
  {
2
2
  "repo": "glossarist/concept-model",
3
- "ref": "v3.1.0",
4
- "commit": "49e4dc40dca52ebd783c92a9ff534a62439b236e",
5
- "syncedAt": "2026-07-05T15:10:00Z"
3
+ "ref": "v3.1.1",
4
+ "syncedAt": "2026-09-17T11:21:06.256Z"
6
5
  }
@@ -34,6 +34,9 @@
34
34
  "ConceptRef": "gloss:ConceptRef",
35
35
  "Reference": "gloss:Reference",
36
36
  "RelatedConcept": "gloss:RelatedConcept",
37
+ "PartitiveHyperedge": "gloss:PartitiveHyperedge",
38
+ "PartitiveEnumeration": "gloss:PartitiveEnumeration",
39
+ "PluralityMarker": "gloss:PluralityMarker",
37
40
  "DesignationRelationship": "gloss:DesignationRelationship",
38
41
  "ConceptDate": "gloss:ConceptDate",
39
42
  "NonVerbalRepresentation": "gloss:NonVerbalRepresentation",
@@ -54,6 +57,7 @@
54
57
  "isLocalizationOf": { "@id": "gloss:isLocalizationOf", "@type": "@id" },
55
58
  "hasStatus": { "@id": "gloss:hasStatus", "@type": "@id" },
56
59
  "hasDomain": { "@id": "gloss:hasDomain", "@type": "@id" },
60
+ "tag": { "@id": "gloss:tag", "@type": "xsd:string" },
57
61
  "hasRelatedConcept": { "@id": "gloss:hasRelatedConcept", "@type": "@id" },
58
62
  "hasDate": { "@id": "gloss:hasDate", "@type": "@id" },
59
63
  "hasSource": { "@id": "gloss:hasSource", "@type": "@id" },
@@ -112,8 +116,18 @@
112
116
  "relationshipType": { "@id": "gloss:relationshipType", "@type": "@id" },
113
117
  "relationshipContent": { "@id": "gloss:relationshipContent", "@type": "xsd:string" },
114
118
  "relationshipRef": { "@id": "gloss:relationshipRef", "@type": "@id" },
119
+ "relatedConceptBroader": { "@id": "gloss:relatedConceptBroader", "@type": "@id" },
120
+ "relatedConceptNarrower": { "@id": "gloss:relatedConceptNarrower", "@type": "@id" },
121
+
122
+ "hasPartitiveHyperedge": { "@id": "gloss:hasPartitiveHyperedge", "@type": "@id" },
123
+ "comprehensive": { "@id": "gloss:comprehensive", "@type": "@id" },
124
+ "hasPart": { "@id": "gloss:hasPart", "@type": "@id" },
125
+ "enumeration": { "@id": "gloss:enumeration", "@type": "@id" },
126
+ "hasPluralityMarker": { "@id": "gloss:hasPluralityMarker", "@type": "@id" },
127
+ "hyperedgeContent": { "@id": "gloss:hyperedgeContent", "@type": "xsd:string" },
115
128
 
116
129
  "refType": { "@id": "gloss:refType", "@type": "xsd:string" },
130
+ "source": { "@id": "gloss:source", "@type": "xsd:string" },
117
131
  "urn": { "@id": "gloss:urn", "@type": "xsd:anyURI" },
118
132
  "term": { "@id": "gloss:term", "@type": "xsd:string" },
119
133
  "refId": { "@id": "gloss:refId", "@type": "xsd:string" },
@@ -125,6 +139,7 @@
125
139
  "sourceId": { "@id": "gloss:sourceId", "@type": "xsd:string" },
126
140
  "sourceOrigin": { "@id": "gloss:sourceOrigin", "@type": "@id" },
127
141
  "modification": { "@id": "gloss:modification", "@type": "xsd:string" },
142
+ "sourcedFrom": { "@id": "gloss:sourcedFrom", "@type": "@id" },
128
143
 
129
144
  "hasCitationRef": { "@id": "gloss:hasCitationRef", "@type": "@id" },
130
145
  "hasCitationLocality": { "@id": "gloss:hasCitationLocality", "@type": "@id" },
@@ -193,6 +193,44 @@ gloss:RelatedConcept a owl:Class ;
193
193
  rdfs:label "Related Concept"@en ;
194
194
  rdfs:comment """
195
195
  A concept with a typed relationship (deprecates, broader, equivalent, etc.).
196
+ Binary: one source concept, one target concept via a ConceptRef.
197
+ For one-to-many partitive decompositions with plurality markers or
198
+ open/closed enumeration, use gloss:PartitiveHyperedge.
199
+ """@en .
200
+
201
+ gloss:PartitiveHyperedge a owl:Class ;
202
+ rdfs:label "Partitive Hyperedge"@en ;
203
+ rdfs:comment """
204
+ A one-to-many partitive decomposition: one comprehensive (whole)
205
+ concept related to one or more parts as a SINGLE relationship.
206
+
207
+ Used when the source diagram depicts a partitive rake whose
208
+ membership or notation is jointly significant — i.e. the
209
+ decomposition cannot be faithfully split into binary
210
+ RelatedConcept edges without losing invariants (which
211
+ comprehensive a part belongs to, the plurality of the rake,
212
+ the completeness of the enumeration).
213
+
214
+ Independent of gloss:hasRelatedConcept. A concept may have both
215
+ binary related entries and partitive hyperedges.
216
+ """@en .
217
+
218
+ gloss:PartitiveEnumeration a owl:Class ;
219
+ rdfs:label "Partitive Enumeration"@en ;
220
+ rdfs:comment """
221
+ Whether the parts array of a PartitiveHyperedge constitutes the
222
+ COMPLETE decomposition of the comprehensive (closed) or only a
223
+ PARTIAL one (open). Orthogonal to PluralityMarker.
224
+ """@en .
225
+
226
+ gloss:PluralityMarker a owl:Class ;
227
+ rdfs:label "Plurality Marker"@en ;
228
+ rdfs:comment """
229
+ Diagram notation flags that travel with a PartitiveHyperedge so
230
+ the rendering layer can reproduce the source diagram's notation
231
+ (close-set double line, dashed line). Originates from VIM
232
+ (International Vocabulary of Metrology) concept-relationship
233
+ diagrams. Orthogonal to PartitiveEnumeration.
196
234
  """@en .
197
235
 
198
236
  gloss:DesignationRelationship a owl:Class ;
@@ -312,7 +350,37 @@ gloss:hasRelatedConcept a owl:ObjectProperty ;
312
350
  ] ;
313
351
  rdfs:range gloss:RelatedConcept ;
314
352
  rdfs:label "has related concept"@en ;
315
- rdfs:comment "DATA: Typed semantic relationship — applicable at concept and localization levels."@en .
353
+ rdfs:comment "DATA: Typed binary semantic relationship — applicable at concept and localization levels."@en .
354
+
355
+ gloss:hasPartitiveHyperedge a owl:ObjectProperty ;
356
+ rdfs:domain gloss:Concept ;
357
+ rdfs:range gloss:PartitiveHyperedge ;
358
+ rdfs:label "has partitive hyperedge"@en ;
359
+ rdfs:comment "DATA: One-to-many partitive decomposition on a managed concept. Concept-level only — hyperedges are not localized."@en .
360
+
361
+ gloss:comprehensive a owl:ObjectProperty ;
362
+ rdfs:domain gloss:PartitiveHyperedge ;
363
+ rdfs:range gloss:ConceptRef ;
364
+ rdfs:label "comprehensive"@en ;
365
+ rdfs:comment "The whole concept that owns the parts in a partitive hyperedge. Exactly one per hyperedge."@en .
366
+
367
+ gloss:hasPart a owl:ObjectProperty ;
368
+ rdfs:domain gloss:PartitiveHyperedge ;
369
+ rdfs:range gloss:ConceptRef ;
370
+ rdfs:label "has part"@en ;
371
+ rdfs:comment "A partitive member of a hyperedge. A hyperedge has one or more parts."@en .
372
+
373
+ gloss:enumeration a owl:ObjectProperty ;
374
+ rdfs:domain gloss:PartitiveHyperedge ;
375
+ rdfs:range gloss:PartitiveEnumeration ;
376
+ rdfs:label "enumeration"@en ;
377
+ rdfs:comment "Whether the encoded parts constitute a complete (closed) or partial (open) decomposition."@en .
378
+
379
+ gloss:hasPluralityMarker a owl:ObjectProperty ;
380
+ rdfs:domain gloss:PartitiveHyperedge ;
381
+ rdfs:range gloss:PluralityMarker ;
382
+ rdfs:label "has plurality marker"@en ;
383
+ rdfs:comment "Diagram notation flag on a hyperedge (double for close-set double line, dashed for uncertainty)."@en .
316
384
 
317
385
  gloss:hasDate a owl:ObjectProperty ;
318
386
  rdfs:domain [
@@ -632,6 +700,12 @@ gloss:relationshipContent a owl:DatatypeProperty ;
632
700
  rdfs:label "relationship content"@en ;
633
701
  rdfs:comment "Free-text description of the relationship."@en .
634
702
 
703
+ gloss:hyperedgeContent a owl:DatatypeProperty ;
704
+ rdfs:domain gloss:PartitiveHyperedge ;
705
+ rdfs:range xsd:string ;
706
+ rdfs:label "hyperedge content"@en ;
707
+ rdfs:comment "Free-form text associated with a PartitiveHyperedge, e.g. a diagram caption or label."@en .
708
+
635
709
  gloss:relationshipRef a owl:ObjectProperty ;
636
710
  rdfs:domain gloss:RelatedConcept ;
637
711
  rdfs:range gloss:ConceptRef ;
@@ -720,6 +794,17 @@ gloss:modification a owl:DatatypeProperty ;
720
794
  rdfs:label "modification"@en ;
721
795
  rdfs:comment "How the concept was modified from the source."@en .
722
796
 
797
+ gloss:sourcedFrom a owl:ObjectProperty ;
798
+ rdfs:domain gloss:ConceptSource ;
799
+ rdfs:range gloss:Citation ;
800
+ rdfs:label "sourced from"@en ;
801
+ rdfs:comment """
802
+ Citations to the documents that this source was itself sourced
803
+ from. Only valid on lineage sources; authoritative sources are
804
+ terminal. When present, status and modification describe how
805
+ origin relates to these upstream sources.
806
+ """@en .
807
+
723
808
  # ── Citation resolution ────────────────────────────────────────────────────
724
809
  #
725
810
  # An inline `{{cite:id}}` mention in a concept's text
@@ -43,13 +43,17 @@ gloss:ConceptShape a sh:NodeShape ;
43
43
  sh:property [
44
44
  sh:path gloss:hasStatus ;
45
45
  sh:class skos:Concept ;
46
- sh:valuesFrom <https://www.glossarist.org/ontologies/conceptstatus> ;
46
+ sh:valuesFrom <https://www.glossarist.org/ontologies/status> ;
47
47
  sh:maxCount 1 ;
48
48
  ] ;
49
49
  sh:property [
50
50
  sh:path gloss:hasRelatedConcept ;
51
51
  sh:class gloss:RelatedConcept ;
52
52
  ] ;
53
+ sh:property [
54
+ sh:path gloss:hasPartitiveHyperedge ;
55
+ sh:class gloss:PartitiveHyperedge ;
56
+ ] ;
53
57
  sh:property [
54
58
  sh:path gloss:hasDate ;
55
59
  sh:class gloss:ConceptDate ;
@@ -391,6 +395,24 @@ gloss:ConceptSourceShape a sh:NodeShape ;
391
395
  sh:path gloss:modification ;
392
396
  sh:datatype xsd:string ;
393
397
  sh:maxCount 1 ;
398
+ ] ;
399
+ sh:property [
400
+ sh:path gloss:sourcedFrom ;
401
+ sh:class gloss:Citation ;
402
+ ] ;
403
+ sh:sparql [
404
+ sh:message "sourced_from is only allowed on sources of type lineage"@en ;
405
+ sh:select """
406
+ PREFIX gloss: <https://www.glossarist.org/ontologies/>
407
+ PREFIX srctype: <https://www.glossarist.org/ontologies/srctype/>
408
+ SELECT $this
409
+ WHERE {
410
+ $this gloss:sourcedFrom ?target .
411
+ FILTER NOT EXISTS {
412
+ $this gloss:sourceType srctype:lineage .
413
+ }
414
+ }
415
+ """ ;
394
416
  ] .
395
417
 
396
418
  gloss:ConceptDateShape a sh:NodeShape ;
@@ -699,6 +721,152 @@ gloss:RelatedConceptShape a sh:NodeShape ;
699
721
  sh:maxCount 1 ;
700
722
  ] .
701
723
 
724
+ # ══════════════════════════════════════════════════════════════════════════
725
+ # Partitive Hyperedge shapes
726
+ # ══════════════════════════════════════════════════════════════════════════
727
+
728
+ gloss:PartitiveHyperedgeShape a sh:NodeShape ;
729
+ sh:targetClass gloss:PartitiveHyperedge ;
730
+ sh:property [
731
+ sh:path gloss:comprehensive ;
732
+ sh:class gloss:ConceptRef ;
733
+ sh:minCount 1 ;
734
+ sh:maxCount 1 ;
735
+ ] ;
736
+ sh:property [
737
+ sh:path gloss:hasPart ;
738
+ sh:class gloss:ConceptRef ;
739
+ sh:minCount 1 ;
740
+ ] ;
741
+ sh:property [
742
+ sh:path gloss:enumeration ;
743
+ sh:class skos:Concept ;
744
+ sh:valuesFrom <https://www.glossarist.org/ontologies/partitiveEnumeration> ;
745
+ sh:maxCount 1 ;
746
+ ] ;
747
+ sh:property [
748
+ sh:path gloss:hasPluralityMarker ;
749
+ sh:class skos:Concept ;
750
+ sh:valuesFrom <https://www.glossarist.org/ontologies/pluralityMarker> ;
751
+ ] ;
752
+ sh:property [
753
+ sh:path gloss:hyperedgeContent ;
754
+ sh:datatype xsd:string ;
755
+ sh:maxCount 1 ;
756
+ ] .
757
+
758
+ # Ownership invariant — soft (warning severity). A PartitiveHyperedge
759
+ # is *conventionally* carried by its comprehensive's own concept entry:
760
+ # <concept/X> gloss:hasPartitiveHyperedge [
761
+ # gloss:comprehensive [ a gloss:ConceptRef ; gloss:ref-id "X" ]
762
+ # ] .
763
+ #
764
+ # Datasets in the wild may legitimately cross-reference (a concept
765
+ # carries a hyperedge whose comprehensive points elsewhere — e.g. for
766
+ # indexing). We do not forbid this; we warn so reviewers can confirm
767
+ # the cross-reference is intentional.
768
+ #
769
+ # The SPARQL constraint matches when the carrying concept's URI-ending
770
+ # identifier differs from the comprehensive ConceptRef's `gloss:id`
771
+ # literal.
772
+ gloss:PartitiveHyperedgeOwnershipShape a sh:NodeShape ;
773
+ sh:targetClass gloss:PartitiveHyperedge ;
774
+ sh:severity sh:Warning ;
775
+ sh:message "partitive_hyperedge is carried by a concept that is not its comprehensive — confirm the cross-reference is intentional."@en ;
776
+ sh:sparql [
777
+ a sh:SPARQLConstraint ;
778
+ sh:prefixes [
779
+ sh:declare [
780
+ sh:prefix "gloss" ;
781
+ sh:namespace "https://www.glossarist.org/ontologies/"^^xsd:anyURI ;
782
+ ] ;
783
+ sh:declare [
784
+ sh:prefix "xsd" ;
785
+ sh:namespace "http://www.w3.org/2001/XMLSchema#"^^xsd:anyURI ;
786
+ ] ;
787
+ ] ;
788
+ sh:select """
789
+ SELECT $this ?carryingConcept ?compRef
790
+ WHERE {
791
+ ?carryingConcept gloss:hasPartitiveHyperedge $this .
792
+ $this gloss:comprehensive ?compRef .
793
+ # Two encoding shapes exist:
794
+ # (a) ?compRef is a gloss:ConceptRef node with gloss:conceptRefId
795
+ # (b) ?compRef is a direct URI ending in /<concept_id>
796
+ # We bind ?compKey to whichever form is present, then compare
797
+ # against the URI suffix of the carrying concept.
798
+ OPTIONAL { ?compRef gloss:conceptRefId ?literalId . }
799
+ BIND(COALESCE(?literalId, STRAFTER(STR(?compRef), CONCAT(STR(?carryingConcept), "/"))) AS ?compKey)
800
+ FILTER (BOUND(?compKey) && !STRENDS(STR(?carryingConcept), CONCAT("/", ?compKey)))
801
+ }
802
+ """ ;
803
+ ] .
804
+
805
+ # ── Enum scheme shapes ───────────────────────────────────────────────────
806
+ # PartitiveEnumeration and PluralityMarker are modelled as owl:Class
807
+ # but functionally are SKOS ConceptSchemes. The shape asserts the
808
+ # scheme contract: every instance must be a skos:ConceptScheme.
809
+
810
+ gloss:PartitiveEnumerationShape a sh:NodeShape ;
811
+ sh:targetClass gloss:PartitiveEnumeration ;
812
+ sh:class skos:ConceptScheme .
813
+
814
+ gloss:PluralityMarkerShape a sh:NodeShape ;
815
+ sh:targetClass gloss:PluralityMarker ;
816
+ sh:class skos:ConceptScheme .
817
+
818
+ # ── Non-verbal entity hierarchy ──────────────────────────────────────────
819
+ # Abstract base + shared dataset-wide entity. Concrete shapes
820
+ # (Figure, Table, Formula) live elsewhere in this file.
821
+
822
+ gloss:NonVerbalEntityShape a sh:NodeShape ;
823
+ sh:targetClass gloss:NonVerbalEntity ;
824
+ sh:property [
825
+ sh:path gloss:caption ;
826
+ sh:datatype xsd:string ;
827
+ sh:maxCount 1 ;
828
+ ] ;
829
+ sh:property [
830
+ sh:path gloss:altText ;
831
+ sh:datatype xsd:string ;
832
+ sh:maxCount 1 ;
833
+ ] ;
834
+ sh:property [
835
+ sh:path gloss:description ;
836
+ sh:datatype xsd:string ;
837
+ ] ;
838
+ sh:property [
839
+ sh:path gloss:hasSource ;
840
+ sh:class gloss:ConceptSource ;
841
+ ] .
842
+
843
+ gloss:SharedNonVerbalEntityShape a sh:NodeShape ;
844
+ sh:targetClass gloss:SharedNonVerbalEntity ;
845
+ sh:class gloss:NonVerbalEntity ;
846
+ sh:property [
847
+ sh:path gloss:role ;
848
+ sh:datatype xsd:string ;
849
+ sh:maxCount 1 ;
850
+ ] .
851
+
852
+ gloss:FigureImageShape a sh:NodeShape ;
853
+ sh:targetClass gloss:FigureImage ;
854
+ sh:property [
855
+ sh:path gloss:src ;
856
+ sh:datatype xsd:string ;
857
+ sh:minCount 1 ;
858
+ sh:maxCount 1 ;
859
+ ] ;
860
+ sh:property [
861
+ sh:path gloss:format ;
862
+ sh:datatype xsd:string ;
863
+ sh:maxCount 1 ;
864
+ ] ;
865
+ sh:property [
866
+ sh:path gloss:hasSubfigure ;
867
+ sh:class gloss:FigureImage ;
868
+ ] .
869
+
702
870
  gloss:GrammarInfoShape a sh:NodeShape ;
703
871
  sh:targetClass gloss:GrammarInfo ;
704
872
  sh:property [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glossarist/concept-browser",
3
- "version": "0.7.135",
3
+ "version": "0.7.136",
4
4
  "description": "Vue SPA for browsing Glossarist terminology datasets with cross-reference resolution, graph visualization, and multi-language support",
5
5
  "type": "module",
6
6
  "bin": {
@@ -43,7 +43,7 @@
43
43
  "astro": "^7.0.7",
44
44
  "d3": "^7.9.0",
45
45
  "favicons": "^7.2.0",
46
- "glossarist": "^0.4.56",
46
+ "glossarist": "^0.4.58",
47
47
  "js-yaml": "^4.1.0",
48
48
  "jszip": "^3.10.1",
49
49
  "n3": "^1.26.0",
@@ -55,24 +55,64 @@ describe('doctor — runDoctor on synthetic projects', () => {
55
55
 
56
56
  it('fails on missing datasets.yml', async () => {
57
57
  const { results } = await runDoctor(root);
58
- const ymlCheck = results.find((r) => r.id === 'datasets-yml');
58
+ const ymlCheck = results.find((r) => r.id === 'datasets-registered');
59
59
  expect(ymlCheck.status).toBe('warn');
60
60
  });
61
61
 
62
62
  it('fails on malformed datasets.yml', async () => {
63
63
  writeFileSync(join(root, 'datasets.yml'), 'datasets: [this is broken');
64
64
  const { results, exitCode } = await runDoctor(root);
65
- const ymlCheck = results.find((r) => r.id === 'datasets-yml');
65
+ const ymlCheck = results.find((r) => r.id === 'datasets-registered');
66
66
  expect(ymlCheck.status).toBe('fail');
67
67
  expect(exitCode).toBe(1);
68
68
  });
69
69
 
70
- it('passes datasets-yml when the file parses and lists entries', async () => {
70
+ it('passes datasets-registered when the file parses and lists entries', async () => {
71
71
  writeDatasetsYml(root, ['foo', 'bar']);
72
72
  const { results } = await runDoctor(root);
73
- const ymlCheck = results.find((r) => r.id === 'datasets-yml');
73
+ const ymlCheck = results.find((r) => r.id === 'datasets-registered');
74
74
  expect(ymlCheck.status).toBe('pass');
75
75
  expect(ymlCheck.label).toContain('2 dataset(s)');
76
+ expect(ymlCheck.label).toContain('datasets.yml');
77
+ });
78
+
79
+ it('registers datasets from site-config.yml when datasets.yml is absent', async () => {
80
+ writeFileSync(join(root, 'site-config.yml'), 'datasets:\n - id: viml-2022\n local_path: datasets/viml-2022\n');
81
+ mkdirSync(join(root, 'datasets', 'viml-2022'), { recursive: true });
82
+ stubGenerated(root, ['viml-2022']);
83
+ const { results, exitCode } = await runDoctor(root);
84
+ const reg = results.find((r) => r.id === 'datasets-registered');
85
+ expect(reg.status).toBe('pass');
86
+ expect(reg.label).toContain('site-config.yml');
87
+ const fetched = results.find((r) => r.id === 'datasets-fetched');
88
+ expect(fetched.status).toBe('pass');
89
+ expect(exitCode).toBe(0);
90
+ });
91
+
92
+ it('fails datasets-fetched when a site-config local_path is missing', async () => {
93
+ writeFileSync(join(root, 'site-config.yml'), 'datasets:\n - id: viml-2022\n local_path: datasets/viml-2022\n');
94
+ const { results, exitCode } = await runDoctor(root);
95
+ const fetched = results.find((r) => r.id === 'datasets-fetched');
96
+ expect(fetched.status).toBe('fail');
97
+ expect(fetched.detail).toContain('viml-2022');
98
+ expect(exitCode).toBe(1);
99
+ });
100
+
101
+ it('reports generated manifests for site-config datasets', async () => {
102
+ writeFileSync(join(root, 'site-config.yml'), 'datasets:\n - id: viml-2022\n local_path: datasets/viml-2022\n');
103
+ mkdirSync(join(root, 'datasets', 'viml-2022'), { recursive: true });
104
+ stubGenerated(root, ['viml-2022']);
105
+ const { results } = await runDoctor(root);
106
+ const gen = results.find((r) => r.id === 'datasets-generated');
107
+ expect(gen.status).toBe('pass');
108
+ });
109
+
110
+ it('fails on malformed site-config.yml', async () => {
111
+ writeFileSync(join(root, 'site-config.yml'), 'datasets: [broken');
112
+ const { results, exitCode } = await runDoctor(root);
113
+ const reg = results.find((r) => r.id === 'datasets-registered');
114
+ expect(reg.status).toBe('fail');
115
+ expect(exitCode).toBe(1);
76
116
  });
77
117
 
78
118
  it('fails when registered datasets are not fetched', async () => {
@@ -125,7 +165,7 @@ describe('doctor — runDoctor on synthetic projects', () => {
125
165
  it('does not crash when datasets.yml lists zero datasets', async () => {
126
166
  writeFileSync(join(root, 'datasets.yml'), 'datasets: []\n');
127
167
  const { results } = await runDoctor(root);
128
- const ymlCheck = results.find((r) => r.id === 'datasets-yml');
168
+ const ymlCheck = results.find((r) => r.id === 'datasets-registered');
129
169
  expect(ymlCheck.status).toBe('warn');
130
170
  });
131
171
  });
package/scripts/doctor.js CHANGED
@@ -35,8 +35,8 @@ function nodeVersionCheck() {
35
35
  };
36
36
  }
37
37
  function packageDepsCheck(ctx) {
38
- const nm = resolve(ctx.pkgRoot, "node_modules");
39
- if (!existsSync(nm)) {
38
+ const roots = [resolve(ctx.cwd, "node_modules"), resolve(ctx.pkgRoot, "node_modules")];
39
+ if (!roots.some((nm) => existsSync(nm))) {
40
40
  return {
41
41
  id: "package-deps",
42
42
  label: "Dependencies installed",
@@ -46,7 +46,7 @@ function packageDepsCheck(ctx) {
46
46
  };
47
47
  }
48
48
  const required = ["vue", "vue-router", "pinia", "vite", "n3", "glossarist", "@rdfjs/dataset"];
49
- const missing = required.filter((p) => !existsSync(join(nm, p)));
49
+ const missing = required.filter((p) => !roots.some((nm) => existsSync(join(nm, p))));
50
50
  if (missing.length) {
51
51
  return {
52
52
  id: "package-deps",
@@ -58,63 +58,86 @@ function packageDepsCheck(ctx) {
58
58
  }
59
59
  return { id: "package-deps", label: "Dependencies installed", status: "pass" };
60
60
  }
61
- function datasetsYmlCheck(ctx) {
62
- const path = resolve(ctx.cwd, "datasets.yml");
63
- if (!existsSync(path)) {
64
- return {
65
- id: "datasets-yml",
66
- label: "datasets.yml present",
67
- status: "warn",
68
- detail: "file not found at project root",
69
- hint: "datasets.yml registers every dataset the browser serves"
70
- };
61
+ function resolveDatasetRegistry(cwd, fileError = void 0) {
62
+ if (fileError) return { registered: [], source: "datasets.yml", error: fileError };
63
+ const ymlPath = resolve(cwd, "datasets.yml");
64
+ if (existsSync(ymlPath)) {
65
+ const list = normalizeRegistry(yaml.load(readFileSync(ymlPath, "utf8")));
66
+ return { registered: list, source: "datasets.yml" };
67
+ }
68
+ const sitePath = resolve(cwd, "site-config.yml");
69
+ if (existsSync(sitePath)) {
70
+ try {
71
+ const list = normalizeRegistry(yaml.load(readFileSync(sitePath, "utf8")));
72
+ return { registered: list, source: "site-config.yml" };
73
+ } catch (err) {
74
+ return { registered: [], source: "site-config.yml", error: err.message };
75
+ }
71
76
  }
72
- if (ctx.datasetsYmlError) {
77
+ return { registered: [], source: null };
78
+ }
79
+ function normalizeRegistry(doc) {
80
+ const datasets = doc?.datasets;
81
+ if (!Array.isArray(datasets)) return [];
82
+ return datasets.filter((d) => d && typeof d === "object" && d.id).map((d) => ({ id: String(d.id), localPath: d.local_path ?? d.localPath }));
83
+ }
84
+ function datasetsRegisteredCheck(ctx) {
85
+ if (ctx.registry.error) {
73
86
  return {
74
- id: "datasets-yml",
75
- label: "datasets.yml parses",
87
+ id: "datasets-registered",
88
+ label: `${ctx.registry.source} parses`,
76
89
  status: "fail",
77
- detail: ctx.datasetsYmlError,
90
+ detail: ctx.registry.error,
78
91
  hint: "fix the YAML syntax error"
79
92
  };
80
93
  }
81
- const datasets = ctx.datasetsYml?.datasets;
82
- if (!Array.isArray(datasets) || datasets.length === 0) {
94
+ if (!ctx.registry.source) {
83
95
  return {
84
- id: "datasets-yml",
85
- label: "datasets.yml lists datasets",
96
+ id: "datasets-registered",
97
+ label: "Dataset registry present",
86
98
  status: "warn",
87
- detail: "`datasets` key is empty or missing",
99
+ detail: "neither datasets.yml nor site-config.yml found at project root",
100
+ hint: "datasets.yml / site-config.yml register every dataset the browser serves"
101
+ };
102
+ }
103
+ if (ctx.registry.registered.length === 0) {
104
+ return {
105
+ id: "datasets-registered",
106
+ label: "Dataset registry lists datasets",
107
+ status: "warn",
108
+ detail: `\`datasets\` key is empty or missing in ${ctx.registry.source}`,
88
109
  hint: "add at least one dataset entry; see docs/adding-a-dataset.md"
89
110
  };
90
111
  }
91
112
  return {
92
- id: "datasets-yml",
93
- label: `datasets.yml lists ${datasets.length} dataset(s)`,
113
+ id: "datasets-registered",
114
+ label: `${ctx.registry.source} lists ${ctx.registry.registered.length} dataset(s)`,
94
115
  status: "pass"
95
116
  };
96
117
  }
97
118
  function datasetsFetchedCheck(ctx) {
98
- const datasets = ctx.datasetsYml?.datasets ?? [];
99
- if (!datasets.length) {
119
+ const datasets = ctx.registry.registered;
120
+ if (!ctx.registry.source || !datasets.length) {
100
121
  return { id: "datasets-fetched", label: "Source datasets fetched", status: "warn", detail: "no datasets registered" };
101
122
  }
102
- const dotDatasets = resolve(ctx.cwd, ".datasets");
103
- const missing = datasets.filter((d) => !existsSync(join(dotDatasets, d.id, "concepts"))).map((d) => d.id);
123
+ const missing = datasets.filter((d) => {
124
+ if (d.localPath) return !existsSync(resolve(ctx.cwd, d.localPath));
125
+ return !existsSync(join(resolve(ctx.cwd, ".datasets"), d.id, "concepts"));
126
+ }).map((d) => d.id);
104
127
  if (missing.length) {
105
128
  return {
106
129
  id: "datasets-fetched",
107
130
  label: "Source datasets fetched",
108
131
  status: "fail",
109
- detail: `.datasets/ missing: ${missing.join(", ")}`,
110
- hint: "run `npm run fetch-datasets`"
132
+ detail: `dataset sources missing: ${missing.join(", ")}`,
133
+ hint: "run `npm run fetch-datasets` (or check local_path entries in site-config.yml)"
111
134
  };
112
135
  }
113
136
  return { id: "datasets-fetched", label: "Source datasets fetched", status: "pass" };
114
137
  }
115
138
  function datasetsGeneratedCheck(ctx) {
116
- const datasets = ctx.datasetsYml?.datasets ?? [];
117
- if (!datasets.length) {
139
+ const datasets = ctx.registry.registered;
140
+ if (!ctx.registry.source || !datasets.length) {
118
141
  return { id: "datasets-generated", label: "Generated data present", status: "warn", detail: "no datasets registered" };
119
142
  }
120
143
  const publicData = resolve(ctx.cwd, "public", "data");
@@ -216,7 +239,7 @@ function jsonldContextCheck(ctx) {
216
239
  var CHECKS = [
217
240
  safe("node-version", nodeVersionCheck),
218
241
  safe("package-deps", packageDepsCheck),
219
- safe("datasets-yml", datasetsYmlCheck),
242
+ safe("datasets-registered", datasetsRegisteredCheck),
220
243
  safe("datasets-fetched", datasetsFetchedCheck),
221
244
  safe("datasets-generated", datasetsGeneratedCheck),
222
245
  safe("public-datasets-json", publicDatasetsJsonCheck),
@@ -234,7 +257,13 @@ async function runDoctor(cwd = process.cwd()) {
234
257
  datasetsYmlError = err.message;
235
258
  }
236
259
  }
237
- const ctx = { cwd, pkgRoot: PKG_ROOT, datasetsYml, datasetsYmlError };
260
+ const ctx = {
261
+ cwd,
262
+ pkgRoot: PKG_ROOT,
263
+ datasetsYml,
264
+ datasetsYmlError,
265
+ registry: resolveDatasetRegistry(cwd, datasetsYmlError)
266
+ };
238
267
  const results = [];
239
268
  for (const check of CHECKS) {
240
269
  results.push(await check(ctx));
@@ -274,5 +303,6 @@ export {
274
303
  NODE_MIN_MAJOR,
275
304
  formatResults,
276
305
  main,
306
+ resolveDatasetRegistry,
277
307
  runDoctor
278
308
  };