@glossarist/concept-browser 0.7.51 → 0.7.53
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/cli/index.mjs +32 -0
- package/env.d.ts +15 -0
- package/package.json +12 -2
- package/scripts/__tests__/doctor.test.mjs +147 -0
- package/scripts/doctor.mjs +327 -0
- package/scripts/generate-data.mjs +136 -0
- package/scripts/generate-ontology-data.mjs +3 -3
- package/scripts/generate-ontology-schema.mjs +3 -3
- package/scripts/lib/agents-turtle.mjs +64 -0
- package/scripts/lib/bibliography-turtle.mjs +54 -0
- package/scripts/lib/build-activity-turtle.mjs +92 -0
- package/scripts/lib/build-cache.mjs +70 -0
- package/scripts/lib/dataset-turtle.mjs +79 -0
- package/scripts/lib/turtle-escape.mjs +0 -0
- package/scripts/lib/version-turtle.mjs +56 -0
- package/scripts/lib/vocab-turtle.mjs +64 -0
- package/scripts/normalize-yaml.mjs +99 -0
- package/scripts/sync-concept-model.mjs +86 -0
- package/scripts/validate-shacl.mjs +194 -0
- package/src/App.vue +2 -0
- package/src/__fixtures__/concept-shape.ttl +20 -0
- package/src/__fixtures__/shacl/bad/concept.ttl +7 -0
- package/src/__fixtures__/shacl/empty/.gitkeep +0 -0
- package/src/__fixtures__/shacl/good/concept.ttl +8 -0
- package/src/__tests__/__fixtures__/concepts.ts +221 -0
- package/src/__tests__/adapters/concept-identity.test.ts +76 -0
- package/src/__tests__/components/error-boundary.test.ts +109 -0
- package/src/__tests__/composables/use-dataset-series.test.ts +262 -0
- package/src/__tests__/concept-rdf/agents-emitter.test.ts +110 -0
- package/src/__tests__/concept-rdf/bibliography-emitter.test.ts +159 -0
- package/src/__tests__/concept-rdf/build-activity-emitter.test.ts +119 -0
- package/src/__tests__/concept-rdf/coerce-date.test.ts +97 -0
- package/src/__tests__/concept-rdf/concept-emitter.test.ts +258 -0
- package/src/__tests__/concept-rdf/dataset-emitter.test.ts +224 -0
- package/src/__tests__/concept-rdf/differential.test.ts +96 -0
- package/src/__tests__/concept-rdf/group-emitter.test.ts +109 -0
- package/src/__tests__/concept-rdf/image-variant-emitter.test.ts +135 -0
- package/src/__tests__/concept-rdf/jsonld-writer.test.ts +109 -0
- package/src/__tests__/concept-rdf/nonverbal-rep.test.ts +177 -0
- package/src/__tests__/concept-rdf/property-based.test.ts +179 -0
- package/src/__tests__/concept-rdf/provenance.test.ts +110 -0
- package/src/__tests__/concept-rdf/quad-isomorphism.test.ts +43 -0
- package/src/__tests__/concept-rdf/quad-isomorphism.ts +47 -0
- package/src/__tests__/concept-rdf/rdf-components.test.ts +145 -0
- package/src/__tests__/concept-rdf/rdf-graph.test.ts +115 -0
- package/src/__tests__/concept-rdf/round-trip.test.ts +243 -0
- package/src/__tests__/concept-rdf/scoped-examples.test.ts +126 -0
- package/src/__tests__/concept-rdf/sections-builder.test.ts +94 -0
- package/src/__tests__/concept-rdf/shacl-conformance.test.ts +110 -0
- package/src/__tests__/concept-rdf/shape-consistency.test.ts +138 -0
- package/src/__tests__/concept-rdf/snapshot-generation.test.ts +75 -0
- package/src/__tests__/concept-rdf/table-formula-emitter.test.ts +142 -0
- package/src/__tests__/concept-rdf/turtle-writer.test.ts +114 -0
- package/src/__tests__/concept-rdf/use-rdf-document.test.ts +246 -0
- package/src/__tests__/concept-rdf/version-emitter.test.ts +120 -0
- package/src/__tests__/concept-rdf/vocabulary-ssot.test.ts +100 -0
- package/src/__tests__/concept-rdf-view.test.ts +136 -0
- package/src/__tests__/config/group-renderers.test.ts +35 -0
- package/src/__tests__/config/group-types.test.ts +76 -0
- package/src/__tests__/dataset-style.test.ts +12 -7
- package/src/__tests__/errors/errors.test.ts +142 -0
- package/src/__tests__/format-downloads.test.ts +47 -65
- package/src/__tests__/markdown-lite.test.ts +19 -0
- package/src/__tests__/perf/bundle-layout.test.ts +50 -0
- package/src/__tests__/perf/serialization-perf.test.ts +121 -0
- package/src/__tests__/scripts/agents-turtle.test.ts +61 -0
- package/src/__tests__/scripts/bibliography-turtle.test.ts +59 -0
- package/src/__tests__/scripts/build-activity-turtle.test.ts +75 -0
- package/src/__tests__/scripts/build-cache.test.ts +78 -0
- package/src/__tests__/scripts/build-integration.test.ts +134 -0
- package/src/__tests__/scripts/dataset-turtle.test.ts +94 -0
- package/src/__tests__/scripts/normalize-yaml.test.ts +98 -0
- package/src/__tests__/scripts/stryker-config.test.ts +33 -0
- package/src/__tests__/scripts/turtle-escape.test.ts +63 -0
- package/src/__tests__/scripts/version-turtle.test.ts +72 -0
- package/src/__tests__/scripts/vocab-turtle.test.ts +63 -0
- package/src/__tests__/use-format-registry.test.ts +125 -0
- package/src/__tests__/utils/bcp47.test.ts +166 -0
- package/src/__tests__/utils/color-theme.test.ts +143 -0
- package/src/__tests__/utils/url-safety.test.ts +65 -0
- package/src/__tests__/validate-shacl.test.ts +100 -0
- package/src/adapters/DatasetAdapter.ts +11 -5
- package/src/adapters/GraphDataSource.ts +2 -1
- package/src/adapters/UriRouter.ts +2 -1
- package/src/adapters/concept-identity.ts +69 -0
- package/src/adapters/factory.ts +3 -2
- package/src/adapters/model-bridge.ts +2 -1
- package/src/adapters/non-verbal/glossarist-augment.d.ts +7 -0
- package/src/adapters/non-verbal-resolver.ts +2 -1
- package/src/components/AppSidebar.vue +189 -93
- package/src/components/ConceptDetail.vue +8 -0
- package/src/components/ConceptEditionRail.vue +222 -0
- package/src/components/ConceptRdfView.vue +37 -377
- package/src/components/DatasetSeriesCard.vue +270 -0
- package/src/components/ErrorBoundary.vue +95 -0
- package/src/components/FormatDownloads.vue +17 -13
- package/src/components/HomeSeriesSection.vue +277 -0
- package/src/components/RelationSphere.vue +1672 -0
- package/src/components/SidebarSeriesSection.vue +239 -0
- package/src/components/concept-rdf/RdfInstanceHeader.vue +47 -0
- package/src/components/concept-rdf/RdfInstanceSection.vue +54 -0
- package/src/components/concept-rdf/RdfPrefixLegend.vue +27 -0
- package/src/components/concept-rdf/RdfSourcePanel.vue +72 -0
- package/src/components/concept-rdf/agents-emitter.ts +82 -0
- package/src/components/concept-rdf/bibliography-emitter.ts +83 -0
- package/src/components/concept-rdf/build-activity-emitter.ts +89 -0
- package/src/components/concept-rdf/concept-emitter.ts +443 -0
- package/src/components/concept-rdf/dataset-emitter.ts +95 -0
- package/src/components/concept-rdf/group-emitter.ts +69 -0
- package/src/components/concept-rdf/image-variant-emitter.ts +46 -0
- package/src/components/concept-rdf/jsonld-writer.ts +82 -0
- package/src/components/concept-rdf/predicates.ts +261 -0
- package/src/components/concept-rdf/provenance.ts +80 -0
- package/src/components/concept-rdf/rdf-graph.ts +211 -0
- package/src/components/concept-rdf/rdf-prefixes.ts +23 -0
- package/src/components/concept-rdf/sections-builder.ts +62 -0
- package/src/components/concept-rdf/table-formula-emitter.ts +101 -0
- package/src/components/concept-rdf/turtle-writer.ts +116 -0
- package/src/components/concept-rdf/use-rdf-document.ts +72 -0
- package/src/components/concept-rdf/version-emitter.ts +65 -0
- package/src/components/concept-rdf/vocabulary-emitter.ts +62 -0
- package/src/components/groups/DatasetGroupRenderer.vue +32 -0
- package/src/components/groups/DefaultGroupSidebar.vue +50 -0
- package/src/components/groups/LineageGroupSidebar.vue +75 -0
- package/src/composables/use-color-theme.ts +82 -0
- package/src/composables/use-format-registry.ts +42 -0
- package/src/composables/useDatasetSeries.ts +258 -0
- package/src/composables/useSphereProjection.ts +125 -0
- package/src/config/group-renderers.ts +27 -0
- package/src/config/group-types.ts +92 -0
- package/src/config/types.ts +81 -2
- package/src/config/use-site-config.ts +2 -1
- package/src/errors.ts +136 -0
- package/src/i18n/locales/eng.yml +24 -0
- package/src/i18n/locales/fra.yml +24 -0
- package/src/stores/vocabulary.ts +3 -1
- package/src/style.css +17 -2
- package/src/types/agents-version-turtle.d.ts +27 -0
- package/src/types/bibliography-turtle.d.ts +12 -0
- package/src/types/build-activity-turtle.d.ts +16 -0
- package/src/types/build-cache.d.ts +20 -0
- package/src/types/dataset-turtle.d.ts +32 -0
- package/src/types/normalize-yaml.d.ts +16 -0
- package/src/types/turtle-escape.d.ts +6 -0
- package/src/types/vocab-turtle.d.ts +13 -0
- package/src/utils/asciidoc-lite.ts +11 -6
- package/src/utils/bcp47.ts +141 -0
- package/src/utils/color-theme-integration.ts +11 -0
- package/src/utils/color-theme.ts +129 -0
- package/src/utils/dataset-style.ts +31 -6
- package/src/utils/locale.ts +6 -14
- package/src/utils/markdown-lite.ts +6 -1
- package/src/utils/relation-sphere-styling.ts +63 -0
- package/src/utils/relationship-categories.ts +30 -0
- package/src/utils/url-safety.ts +30 -0
- package/src/views/ConceptView.vue +187 -9
- package/src/views/DatasetView.vue +6 -0
- package/src/views/HomeView.vue +5 -0
- package/vite.config.ts +7 -0
package/cli/index.mjs
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* edges Build cross-reference edges from generated concept data
|
|
10
10
|
* build Full pipeline: fetch + generate + edges + vite build
|
|
11
11
|
* site Same as build (alias)
|
|
12
|
+
* normalize NFC-normalize YAML files in .datasets/ (use --check for CI gate)
|
|
13
|
+
* doctor Diagnose the local environment (deps, datasets, shapes, context)
|
|
12
14
|
*
|
|
13
15
|
* Options:
|
|
14
16
|
* --site <id> Site config to use (looks for site-config.yml in CWD)
|
|
@@ -61,9 +63,12 @@ Commands:
|
|
|
61
63
|
edges Build cross-reference edges from generated concepts
|
|
62
64
|
build Full pipeline (fetch + generate + edges + vite build)
|
|
63
65
|
site Same as build
|
|
66
|
+
normalize NFC-normalize YAML files in .datasets/
|
|
67
|
+
doctor Diagnose the local environment (deps, datasets, shapes, context)
|
|
64
68
|
|
|
65
69
|
Options:
|
|
66
70
|
--site <id> Site config ID (looks for site-config.yml in CWD)
|
|
71
|
+
--check (normalize only) Non-mutating; exit 1 if any file is not NFC
|
|
67
72
|
|
|
68
73
|
Environment:
|
|
69
74
|
SITE_CONFIG Site config file path (highest priority)
|
|
@@ -187,6 +192,33 @@ Environment:
|
|
|
187
192
|
|
|
188
193
|
const runner = commands[cmd];
|
|
189
194
|
if (!runner) {
|
|
195
|
+
if (cmd === 'normalize') {
|
|
196
|
+
const { normalizeYaml } = await import('../scripts/normalize-yaml.mjs');
|
|
197
|
+
const check = process.argv.includes('--check');
|
|
198
|
+
const paths = process.argv.slice(2).filter(a => !a.startsWith('-') && a !== 'normalize');
|
|
199
|
+
const { checked, nonNfc, fixed } = normalizeYaml({ check, paths });
|
|
200
|
+
if (check) {
|
|
201
|
+
if (nonNfc === 0) {
|
|
202
|
+
console.log(`NFC OK: ${checked} file(s) checked, all normalized`);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
console.error(`NFC check failed: ${nonNfc} of ${checked} file(s) are not NFC-normalized\n`);
|
|
206
|
+
for (const f of fixed) console.error(` ${f}`);
|
|
207
|
+
process.exit(1);
|
|
208
|
+
}
|
|
209
|
+
if (nonNfc === 0) {
|
|
210
|
+
console.log(`NFC OK: ${checked} file(s) checked, all already normalized`);
|
|
211
|
+
} else {
|
|
212
|
+
console.log(`Normalized ${nonNfc} of ${checked} file(s)`);
|
|
213
|
+
for (const f of fixed) console.log(` ${f}`);
|
|
214
|
+
}
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (cmd === 'doctor') {
|
|
218
|
+
const { main: doctorMain } = await import('../scripts/doctor.mjs');
|
|
219
|
+
await doctorMain();
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
190
222
|
console.error(`Unknown command: ${cmd}`);
|
|
191
223
|
console.error('Run `concept-browser help` for usage.');
|
|
192
224
|
process.exit(1);
|
package/env.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/// <reference types="vite/client" />
|
|
2
2
|
|
|
3
|
+
declare const __CONCEPT_BROWSER_VERSION__: string;
|
|
4
|
+
|
|
3
5
|
declare module '*.json' {
|
|
4
6
|
const value: any;
|
|
5
7
|
export default value;
|
|
@@ -10,3 +12,16 @@ declare module '*.vue' {
|
|
|
10
12
|
const component: DefineComponent<{}, {}, any>
|
|
11
13
|
export default component
|
|
12
14
|
}
|
|
15
|
+
|
|
16
|
+
declare module '@rdfjs/dataset' {
|
|
17
|
+
export function dataset(): any;
|
|
18
|
+
const _default: { dataset: typeof dataset };
|
|
19
|
+
export default _default;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
declare module 'rdf-validate-shacl' {
|
|
23
|
+
export default class Validator {
|
|
24
|
+
constructor(shapes: any, options?: { factory?: any });
|
|
25
|
+
validate(data: any): { conforms: boolean; results: any[] };
|
|
26
|
+
}
|
|
27
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glossarist/concept-browser",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.53",
|
|
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": {
|
|
@@ -17,10 +17,13 @@
|
|
|
17
17
|
"fetch-datasets": "node scripts/fetch-datasets.mjs",
|
|
18
18
|
"generate-data": "node scripts/generate-data.mjs",
|
|
19
19
|
"generate-ontology": "node scripts/generate-ontology-data.mjs && node scripts/generate-ontology-schema.mjs",
|
|
20
|
+
"sync:model": "node scripts/sync-concept-model.mjs",
|
|
20
21
|
"build:full": "npm run generate-ontology && npm run fetch-datasets && npm run generate-data && node scripts/build-edges.js && npm run build",
|
|
21
22
|
"build:site": "concept-browser --site build",
|
|
22
23
|
"test": "vitest run",
|
|
23
|
-
"test:watch": "vitest"
|
|
24
|
+
"test:watch": "vitest",
|
|
25
|
+
"mutation:test": "stryker run stryker.config.mjs",
|
|
26
|
+
"validate:shacl": "node scripts/validate-shacl.mjs"
|
|
24
27
|
},
|
|
25
28
|
"dependencies": {
|
|
26
29
|
"@plurimath/plurimath": "^0.2.2",
|
|
@@ -40,9 +43,16 @@
|
|
|
40
43
|
"vue-router": "^4.5.1"
|
|
41
44
|
},
|
|
42
45
|
"devDependencies": {
|
|
46
|
+
"@rdfjs/dataset": "^2.0.2",
|
|
47
|
+
"@stryker-mutator/core": "^9.6.1",
|
|
48
|
+
"@stryker-mutator/vitest-runner": "^9.6.1",
|
|
43
49
|
"@types/d3": "^7.4.3",
|
|
50
|
+
"@types/n3": "^1.26.1",
|
|
44
51
|
"@vue/test-utils": "^2.4.8",
|
|
52
|
+
"fast-check": "^4.8.0",
|
|
45
53
|
"happy-dom": "^20.9.0",
|
|
54
|
+
"n3": "^1.26.0",
|
|
55
|
+
"rdf-validate-shacl": "^0.4.5",
|
|
46
56
|
"typescript": "~5.7.3",
|
|
47
57
|
"vitest": "^4.1.5",
|
|
48
58
|
"vue-tsc": "^2.2.8"
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { writeFileSync, mkdirSync, rmSync, existsSync } from 'node:fs';
|
|
3
|
+
import { resolve, join } from 'node:path';
|
|
4
|
+
import { mkdtempSync } from 'node:fs';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { runDoctor, formatResults, CHECKS, NODE_MIN_MAJOR } from '../doctor.mjs';
|
|
7
|
+
|
|
8
|
+
function makeTmpProject() {
|
|
9
|
+
const dir = mkdtempSync(join(tmpdir(), 'doctor-'));
|
|
10
|
+
return dir;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function writeDatasetsYml(root, ids) {
|
|
14
|
+
const entries = ids.map((id) => ` - id: ${id}\n title: "${id}"\n sourceRepo: "https://example.com/${id}"`).join('\n');
|
|
15
|
+
writeFileSync(join(root, 'datasets.yml'), `datasets:\n${entries}\n`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function stubGenerated(root, ids) {
|
|
19
|
+
for (const id of ids) {
|
|
20
|
+
mkdirSync(join(root, 'public', 'data', id), { recursive: true });
|
|
21
|
+
writeFileSync(join(root, 'public', 'data', id, 'manifest.json'), '{}');
|
|
22
|
+
}
|
|
23
|
+
writeFileSync(join(root, 'public', 'datasets.json'), JSON.stringify(ids.map((id) => ({ id }))));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function stubFetched(root, ids) {
|
|
27
|
+
for (const id of ids) {
|
|
28
|
+
mkdirSync(join(root, '.datasets', id, 'concepts'), { recursive: true });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('doctor — unit checks', () => {
|
|
33
|
+
it('exposes NODE_MIN_MAJOR as a positive integer', () => {
|
|
34
|
+
expect(NODE_MIN_MAJOR).toBeGreaterThanOrEqual(18);
|
|
35
|
+
expect(Number.isInteger(NODE_MIN_MAJOR)).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('registers exactly eight independent checks', () => {
|
|
39
|
+
expect(CHECKS).toHaveLength(8);
|
|
40
|
+
const ids = CHECKS.map((c) => c.name ?? c.toString());
|
|
41
|
+
expect(new Set(ids).size).toBe(8);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('doctor — runDoctor on synthetic projects', () => {
|
|
46
|
+
let root;
|
|
47
|
+
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
root = makeTmpProject();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
afterEach(() => {
|
|
53
|
+
rmSync(root, { recursive: true, force: true });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('fails on missing datasets.yml', async () => {
|
|
57
|
+
const { results } = await runDoctor(root);
|
|
58
|
+
const ymlCheck = results.find((r) => r.id === 'datasets-yml');
|
|
59
|
+
expect(ymlCheck.status).toBe('warn');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('fails on malformed datasets.yml', async () => {
|
|
63
|
+
writeFileSync(join(root, 'datasets.yml'), 'datasets: [this is broken');
|
|
64
|
+
const { results, exitCode } = await runDoctor(root);
|
|
65
|
+
const ymlCheck = results.find((r) => r.id === 'datasets-yml');
|
|
66
|
+
expect(ymlCheck.status).toBe('fail');
|
|
67
|
+
expect(exitCode).toBe(1);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('passes datasets-yml when the file parses and lists entries', async () => {
|
|
71
|
+
writeDatasetsYml(root, ['foo', 'bar']);
|
|
72
|
+
const { results } = await runDoctor(root);
|
|
73
|
+
const ymlCheck = results.find((r) => r.id === 'datasets-yml');
|
|
74
|
+
expect(ymlCheck.status).toBe('pass');
|
|
75
|
+
expect(ymlCheck.label).toContain('2 dataset(s)');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('fails when registered datasets are not fetched', async () => {
|
|
79
|
+
writeDatasetsYml(root, ['foo']);
|
|
80
|
+
const { results, exitCode } = await runDoctor(root);
|
|
81
|
+
const fetched = results.find((r) => r.id === 'datasets-fetched');
|
|
82
|
+
expect(fetched.status).toBe('fail');
|
|
83
|
+
expect(fetched.detail).toContain('foo');
|
|
84
|
+
expect(exitCode).toBe(1);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('passes when registered datasets exist under .datasets/{id}/concepts', async () => {
|
|
88
|
+
writeDatasetsYml(root, ['foo']);
|
|
89
|
+
stubFetched(root, ['foo']);
|
|
90
|
+
const { results } = await runDoctor(root);
|
|
91
|
+
const fetched = results.find((r) => r.id === 'datasets-fetched');
|
|
92
|
+
expect(fetched.status).toBe('pass');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('fails when public/data manifest is missing', async () => {
|
|
96
|
+
writeDatasetsYml(root, ['foo']);
|
|
97
|
+
stubFetched(root, ['foo']);
|
|
98
|
+
const { results, exitCode } = await runDoctor(root);
|
|
99
|
+
const gen = results.find((r) => r.id === 'datasets-generated');
|
|
100
|
+
expect(gen.status).toBe('fail');
|
|
101
|
+
expect(gen.detail).toContain('foo');
|
|
102
|
+
expect(exitCode).toBe(1);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('passes when manifests are present', async () => {
|
|
106
|
+
writeDatasetsYml(root, ['foo']);
|
|
107
|
+
stubFetched(root, ['foo']);
|
|
108
|
+
stubGenerated(root, ['foo']);
|
|
109
|
+
const { results } = await runDoctor(root);
|
|
110
|
+
const gen = results.find((r) => r.id === 'datasets-generated');
|
|
111
|
+
expect(gen.status).toBe('pass');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('fails when public/datasets.json is missing', async () => {
|
|
115
|
+
writeDatasetsYml(root, ['foo']);
|
|
116
|
+
stubFetched(root, ['foo']);
|
|
117
|
+
mkdirSync(join(root, 'public', 'data', 'foo'), { recursive: true });
|
|
118
|
+
writeFileSync(join(root, 'public', 'data', 'foo', 'manifest.json'), '{}');
|
|
119
|
+
const { results, exitCode } = await runDoctor(root);
|
|
120
|
+
const j = results.find((r) => r.id === 'public-datasets-json');
|
|
121
|
+
expect(j.status).toBe('fail');
|
|
122
|
+
expect(exitCode).toBe(1);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('does not crash when datasets.yml lists zero datasets', async () => {
|
|
126
|
+
writeFileSync(join(root, 'datasets.yml'), 'datasets: []\n');
|
|
127
|
+
const { results } = await runDoctor(root);
|
|
128
|
+
const ymlCheck = results.find((r) => r.id === 'datasets-yml');
|
|
129
|
+
expect(ymlCheck.status).toBe('warn');
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe('doctor — formatting', () => {
|
|
134
|
+
it('renders a pass/fail/warn glyph per result and a summary line', () => {
|
|
135
|
+
const out = formatResults([
|
|
136
|
+
{ id: 'a', label: 'A is good', status: 'pass' },
|
|
137
|
+
{ id: 'b', label: 'B is broken', status: 'fail', detail: 'because', hint: 'fix it' },
|
|
138
|
+
{ id: 'c', label: 'C is suspect', status: 'warn' },
|
|
139
|
+
]);
|
|
140
|
+
expect(out).toContain('✓ A is good');
|
|
141
|
+
expect(out).toContain('✗ B is broken');
|
|
142
|
+
expect(out).toContain(' because');
|
|
143
|
+
expect(out).toContain('→ fix it');
|
|
144
|
+
expect(out).toContain('! C is suspect');
|
|
145
|
+
expect(out).toMatch(/1 passed, 1 failed, 1 warnings \(3 total\)/);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `concept-browser doctor` — environment diagnostic.
|
|
3
|
+
//
|
|
4
|
+
// Runs a series of independent checks (Node version, dependencies,
|
|
5
|
+
// datasets registered / fetched / generated, SHACL shapes, JSON-LD
|
|
6
|
+
// context) and prints a pass/fail summary. Exits non-zero if any
|
|
7
|
+
// check fails.
|
|
8
|
+
//
|
|
9
|
+
// See docs/adr/0005-shacl-validation-gate.md and
|
|
10
|
+
// TODO.streamline/15-tooling-and-developer-experience.md §O1.
|
|
11
|
+
|
|
12
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
13
|
+
import { resolve, dirname, join } from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
import yaml from 'js-yaml';
|
|
16
|
+
|
|
17
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const PKG_ROOT = resolve(__dirname, '..');
|
|
19
|
+
|
|
20
|
+
export const NODE_MIN_MAJOR = 18;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {'pass'|'fail'|'warn'} CheckStatus
|
|
24
|
+
* @typedef {{ id: string, label: string, status: CheckStatus, detail?: string, hint?: string }} CheckResult
|
|
25
|
+
* @typedef {{ cwd: string, pkgRoot: string, datasetsYml?: any, datasetsYmlError?: string }} DoctorContext
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Wrap a check so an unexpected throw becomes a fail result, and
|
|
30
|
+
* preserve the check's `id` as the wrapper's `name` so callers can
|
|
31
|
+
* reason about the registry without anonymous arrows.
|
|
32
|
+
*
|
|
33
|
+
* @param {string} id
|
|
34
|
+
* @param {() => (Promise<CheckResult> | CheckResult)} check
|
|
35
|
+
* @returns {(ctx: DoctorContext) => Promise<CheckResult>}
|
|
36
|
+
*/
|
|
37
|
+
function safe(id, check) {
|
|
38
|
+
const wrapped = async (ctx) => {
|
|
39
|
+
try {
|
|
40
|
+
return await check(ctx);
|
|
41
|
+
} catch (err) {
|
|
42
|
+
return {
|
|
43
|
+
id: 'unexpected',
|
|
44
|
+
label: 'Unexpected error',
|
|
45
|
+
status: 'fail',
|
|
46
|
+
detail: err?.message ?? String(err),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
Object.defineProperty(wrapped, 'name', { value: id });
|
|
51
|
+
return wrapped;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function nodeVersionCheck() {
|
|
55
|
+
const major = Number.parseInt(process.versions.node.split('.')[0], 10);
|
|
56
|
+
return {
|
|
57
|
+
id: 'node-version',
|
|
58
|
+
label: `Node.js v${process.versions.node}`,
|
|
59
|
+
status: major >= NODE_MIN_MAJOR ? 'pass' : 'fail',
|
|
60
|
+
detail: major < NODE_MIN_MAJOR ? `requires >= v${NODE_MIN_MAJOR}` : undefined,
|
|
61
|
+
hint: major < NODE_MIN_MAJOR ? 'upgrade via nvm or your package manager' : undefined,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function packageDepsCheck(ctx) {
|
|
66
|
+
const nm = resolve(ctx.pkgRoot, 'node_modules');
|
|
67
|
+
if (!existsSync(nm)) {
|
|
68
|
+
return {
|
|
69
|
+
id: 'package-deps',
|
|
70
|
+
label: 'Dependencies installed',
|
|
71
|
+
status: 'fail',
|
|
72
|
+
detail: 'node_modules missing',
|
|
73
|
+
hint: 'run `npm install`',
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const required = ['vue', 'vue-router', 'pinia', 'vite', 'n3', 'glossarist', '@rdfjs/dataset'];
|
|
77
|
+
const missing = required.filter((p) => !existsSync(join(nm, p)));
|
|
78
|
+
if (missing.length) {
|
|
79
|
+
return {
|
|
80
|
+
id: 'package-deps',
|
|
81
|
+
label: 'Dependencies installed',
|
|
82
|
+
status: 'fail',
|
|
83
|
+
detail: `missing: ${missing.join(', ')}`,
|
|
84
|
+
hint: 'run `npm install`',
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return { id: 'package-deps', label: 'Dependencies installed', status: 'pass' };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function datasetsYmlCheck(ctx) {
|
|
91
|
+
const path = resolve(ctx.cwd, 'datasets.yml');
|
|
92
|
+
if (!existsSync(path)) {
|
|
93
|
+
return {
|
|
94
|
+
id: 'datasets-yml',
|
|
95
|
+
label: 'datasets.yml present',
|
|
96
|
+
status: 'warn',
|
|
97
|
+
detail: 'file not found at project root',
|
|
98
|
+
hint: 'datasets.yml registers every dataset the browser serves',
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
if (ctx.datasetsYmlError) {
|
|
102
|
+
return {
|
|
103
|
+
id: 'datasets-yml',
|
|
104
|
+
label: 'datasets.yml parses',
|
|
105
|
+
status: 'fail',
|
|
106
|
+
detail: ctx.datasetsYmlError,
|
|
107
|
+
hint: 'fix the YAML syntax error',
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
const datasets = ctx.datasetsYml?.datasets;
|
|
111
|
+
if (!Array.isArray(datasets) || datasets.length === 0) {
|
|
112
|
+
return {
|
|
113
|
+
id: 'datasets-yml',
|
|
114
|
+
label: 'datasets.yml lists datasets',
|
|
115
|
+
status: 'warn',
|
|
116
|
+
detail: '`datasets` key is empty or missing',
|
|
117
|
+
hint: 'add at least one dataset entry; see docs/adding-a-dataset.md',
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
id: 'datasets-yml',
|
|
122
|
+
label: `datasets.yml lists ${datasets.length} dataset(s)`,
|
|
123
|
+
status: 'pass',
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function datasetsFetchedCheck(ctx) {
|
|
128
|
+
const datasets = ctx.datasetsYml?.datasets ?? [];
|
|
129
|
+
if (!datasets.length) {
|
|
130
|
+
return { id: 'datasets-fetched', label: 'Source datasets fetched', status: 'warn', detail: 'no datasets registered' };
|
|
131
|
+
}
|
|
132
|
+
const dotDatasets = resolve(ctx.cwd, '.datasets');
|
|
133
|
+
const missing = datasets
|
|
134
|
+
.filter((d) => !existsSync(join(dotDatasets, d.id, 'concepts')))
|
|
135
|
+
.map((d) => d.id);
|
|
136
|
+
if (missing.length) {
|
|
137
|
+
return {
|
|
138
|
+
id: 'datasets-fetched',
|
|
139
|
+
label: 'Source datasets fetched',
|
|
140
|
+
status: 'fail',
|
|
141
|
+
detail: `.datasets/ missing: ${missing.join(', ')}`,
|
|
142
|
+
hint: 'run `npm run fetch-datasets`',
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
return { id: 'datasets-fetched', label: 'Source datasets fetched', status: 'pass' };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function datasetsGeneratedCheck(ctx) {
|
|
149
|
+
const datasets = ctx.datasetsYml?.datasets ?? [];
|
|
150
|
+
if (!datasets.length) {
|
|
151
|
+
return { id: 'datasets-generated', label: 'Generated data present', status: 'warn', detail: 'no datasets registered' };
|
|
152
|
+
}
|
|
153
|
+
const publicData = resolve(ctx.cwd, 'public', 'data');
|
|
154
|
+
const missing = datasets
|
|
155
|
+
.filter((d) => !existsSync(join(publicData, d.id, 'manifest.json')))
|
|
156
|
+
.map((d) => d.id);
|
|
157
|
+
if (missing.length) {
|
|
158
|
+
return {
|
|
159
|
+
id: 'datasets-generated',
|
|
160
|
+
label: 'Generated data present',
|
|
161
|
+
status: 'fail',
|
|
162
|
+
detail: `public/data/ missing manifest for: ${missing.join(', ')}`,
|
|
163
|
+
hint: 'run `npm run generate-data`',
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
return { id: 'datasets-generated', label: 'Generated data present', status: 'pass' };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function publicDatasetsJsonCheck(ctx) {
|
|
170
|
+
const path = resolve(ctx.cwd, 'public', 'datasets.json');
|
|
171
|
+
if (!existsSync(path)) {
|
|
172
|
+
return {
|
|
173
|
+
id: 'public-datasets-json',
|
|
174
|
+
label: 'public/datasets.json present',
|
|
175
|
+
status: 'fail',
|
|
176
|
+
hint: 'run `npm run generate-data`',
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
const doc = JSON.parse(readFileSync(path, 'utf8'));
|
|
181
|
+
if (!Array.isArray(doc) || doc.length === 0) {
|
|
182
|
+
return {
|
|
183
|
+
id: 'public-datasets-json',
|
|
184
|
+
label: 'public/datasets.json non-empty',
|
|
185
|
+
status: 'warn',
|
|
186
|
+
detail: 'datasets list is empty',
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
return { id: 'public-datasets-json', label: 'public/datasets.json parses', status: 'pass' };
|
|
190
|
+
} catch (err) {
|
|
191
|
+
return {
|
|
192
|
+
id: 'public-datasets-json',
|
|
193
|
+
label: 'public/datasets.json parses',
|
|
194
|
+
status: 'fail',
|
|
195
|
+
detail: err.message,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function shaclShapesCheck(ctx) {
|
|
201
|
+
const shapesPath = resolve(ctx.pkgRoot, 'data', 'concept-model', 'shapes', 'glossarist.shacl.ttl');
|
|
202
|
+
if (!existsSync(shapesPath)) {
|
|
203
|
+
return {
|
|
204
|
+
id: 'shacl-shapes',
|
|
205
|
+
label: 'SHACL shapes present',
|
|
206
|
+
status: 'fail',
|
|
207
|
+
hint: 'run `npm run sync:model` to vendor shapes from concept-model',
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
const ttl = readFileSync(shapesPath, 'utf8');
|
|
211
|
+
const looksLikeShapes = /sh:\s*(NodeShape|PropertyShape|targetClass)/.test(ttl) || /a\s+sh:NodeShape/.test(ttl);
|
|
212
|
+
if (!looksLikeShapes) {
|
|
213
|
+
return {
|
|
214
|
+
id: 'shacl-shapes',
|
|
215
|
+
label: 'SHACL shapes well-formed',
|
|
216
|
+
status: 'fail',
|
|
217
|
+
detail: 'file exists but does not declare any sh:NodeShape',
|
|
218
|
+
hint: 're-sync from glossarist/concept-model',
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
return { id: 'shacl-shapes', label: 'SHACL shapes present', status: 'pass' };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function jsonldContextCheck(ctx) {
|
|
225
|
+
const ctxPath = resolve(ctx.pkgRoot, 'data', 'concept-model', 'glossarist.context.jsonld');
|
|
226
|
+
if (!existsSync(ctxPath)) {
|
|
227
|
+
return {
|
|
228
|
+
id: 'jsonld-context',
|
|
229
|
+
label: 'JSON-LD context present',
|
|
230
|
+
status: 'fail',
|
|
231
|
+
hint: 'run `npm run sync:model` to vendor the context',
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
try {
|
|
235
|
+
const doc = JSON.parse(readFileSync(ctxPath, 'utf8'));
|
|
236
|
+
if (!doc['@context'] || typeof doc['@context'] !== 'object') {
|
|
237
|
+
return {
|
|
238
|
+
id: 'jsonld-context',
|
|
239
|
+
label: 'JSON-LD context well-formed',
|
|
240
|
+
status: 'fail',
|
|
241
|
+
detail: 'missing or invalid @context key',
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
return { id: 'jsonld-context', label: 'JSON-LD context present', status: 'pass' };
|
|
245
|
+
} catch (err) {
|
|
246
|
+
return {
|
|
247
|
+
id: 'jsonld-context',
|
|
248
|
+
label: 'JSON-LD context parses',
|
|
249
|
+
status: 'fail',
|
|
250
|
+
detail: err.message,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** @type {Array<(ctx: DoctorContext) => (Promise<CheckResult> | CheckResult)>} */
|
|
256
|
+
export const CHECKS = [
|
|
257
|
+
safe('node-version', nodeVersionCheck),
|
|
258
|
+
safe('package-deps', packageDepsCheck),
|
|
259
|
+
safe('datasets-yml', datasetsYmlCheck),
|
|
260
|
+
safe('datasets-fetched', datasetsFetchedCheck),
|
|
261
|
+
safe('datasets-generated', datasetsGeneratedCheck),
|
|
262
|
+
safe('public-datasets-json', publicDatasetsJsonCheck),
|
|
263
|
+
safe('shacl-shapes', shaclShapesCheck),
|
|
264
|
+
safe('jsonld-context', jsonldContextCheck),
|
|
265
|
+
];
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* @param {string} [cwd]
|
|
269
|
+
* @returns {Promise<{ results: CheckResult[], exitCode: number }>}
|
|
270
|
+
*/
|
|
271
|
+
export async function runDoctor(cwd = process.cwd()) {
|
|
272
|
+
const datasetsYmlPath = resolve(cwd, 'datasets.yml');
|
|
273
|
+
let datasetsYml;
|
|
274
|
+
let datasetsYmlError;
|
|
275
|
+
if (existsSync(datasetsYmlPath)) {
|
|
276
|
+
try {
|
|
277
|
+
datasetsYml = yaml.load(readFileSync(datasetsYmlPath, 'utf8'));
|
|
278
|
+
} catch (err) {
|
|
279
|
+
datasetsYmlError = err.message;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
/** @type {DoctorContext} */
|
|
283
|
+
const ctx = { cwd, pkgRoot: PKG_ROOT, datasetsYml, datasetsYmlError };
|
|
284
|
+
|
|
285
|
+
const results = [];
|
|
286
|
+
for (const check of CHECKS) {
|
|
287
|
+
results.push(await check(ctx));
|
|
288
|
+
}
|
|
289
|
+
const failed = results.filter((r) => r.status === 'fail').length;
|
|
290
|
+
const exitCode = failed > 0 ? 1 : 0;
|
|
291
|
+
return { results, exitCode };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const STATUS_GLYPH = { pass: '✓', fail: '✗', warn: '!' };
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* @param {CheckResult[]} results
|
|
298
|
+
* @returns {string}
|
|
299
|
+
*/
|
|
300
|
+
export function formatResults(results) {
|
|
301
|
+
const lines = [];
|
|
302
|
+
for (const r of results) {
|
|
303
|
+
lines.push(`${STATUS_GLYPH[r.status]} ${r.label}`);
|
|
304
|
+
if (r.detail) lines.push(` ${r.detail}`);
|
|
305
|
+
if (r.hint) lines.push(` → ${r.hint}`);
|
|
306
|
+
}
|
|
307
|
+
const pass = results.filter((r) => r.status === 'pass').length;
|
|
308
|
+
const fail = results.filter((r) => r.status === 'fail').length;
|
|
309
|
+
const warn = results.filter((r) => r.status === 'warn').length;
|
|
310
|
+
lines.push('');
|
|
311
|
+
lines.push(`${pass} passed, ${fail} failed, ${warn} warnings (${results.length} total)`);
|
|
312
|
+
return lines.join('\n');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export async function main() {
|
|
316
|
+
const { results, exitCode } = await runDoctor();
|
|
317
|
+
console.log(formatResults(results));
|
|
318
|
+
process.exit(exitCode);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Run when invoked directly as a script.
|
|
322
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
323
|
+
main().catch((err) => {
|
|
324
|
+
console.error(err);
|
|
325
|
+
process.exit(1);
|
|
326
|
+
});
|
|
327
|
+
}
|