@glossarist/concept-browser 0.7.120 → 0.7.121
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.ts +5 -2
- package/package.json +3 -2
- package/scripts/validate-links.js +106 -0
- package/scripts/validate-links.ts +147 -0
- package/src/__tests__/scripts/validate-links.test.ts +103 -0
package/cli/index.ts
CHANGED
|
@@ -9,8 +9,10 @@
|
|
|
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
|
|
13
|
-
* doctor
|
|
12
|
+
* normalize NFC-normalize YAML files in .datasets/ (use --check for CI gate)
|
|
13
|
+
* doctor Diagnose the local environment (deps, datasets, shapes, context)
|
|
14
|
+
* validate-yaml Validate dataset YAML against the concept schema
|
|
15
|
+
* validate-links Validate that all generated cross-references resolve to existing concepts
|
|
14
16
|
*
|
|
15
17
|
* Options:
|
|
16
18
|
* --site <id> Site config to use (looks for site-config.yml in CWD)
|
|
@@ -42,6 +44,7 @@ const commands: Record<string, CommandRunner> = {
|
|
|
42
44
|
edges: async () => (await import('../scripts/build-edges')).main(),
|
|
43
45
|
about: async () => (await import('../scripts/process-about-pages')).main(),
|
|
44
46
|
'validate-yaml': async () => { await import('../scripts/validate-yaml'); },
|
|
47
|
+
'validate-links': async () => { await import('../scripts/validate-links'); },
|
|
45
48
|
};
|
|
46
49
|
|
|
47
50
|
function parseArgs(argv: readonly string[]): ParsedArgs {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glossarist/concept-browser",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.121",
|
|
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": {
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"preview": "vite preview",
|
|
14
14
|
"check:scripts": "tsx scripts/check-syntax.ts",
|
|
15
15
|
"validate:yaml": "tsx scripts/validate-yaml.ts",
|
|
16
|
+
"validate:links": "tsx scripts/validate-links.ts",
|
|
16
17
|
"validate:shacl": "tsx scripts/validate-shacl.ts",
|
|
17
18
|
"pretest": "tsx scripts/check-syntax.ts",
|
|
18
19
|
"prebuild": "tsx scripts/check-syntax.ts",
|
|
@@ -20,7 +21,7 @@
|
|
|
20
21
|
"generate-data": "tsx scripts/generate-data.ts",
|
|
21
22
|
"generate-ontology": "tsx scripts/generate-ontology-data.ts && tsx scripts/generate-ontology-schema.ts",
|
|
22
23
|
"sync:model": "tsx scripts/sync-concept-model.ts",
|
|
23
|
-
"build:full": "npm run generate-ontology && npm run fetch-datasets && npm run generate-data && tsx scripts/build-edges.ts && npm run build",
|
|
24
|
+
"build:full": "npm run generate-ontology && npm run fetch-datasets && npm run generate-data && tsx scripts/build-edges.ts && npm run validate:links && npm run build",
|
|
24
25
|
"build:site": "concept-browser --site build",
|
|
25
26
|
"build:scripts": "node scripts/build-scripts.mjs",
|
|
26
27
|
"prepublishOnly": "npm run build:scripts",
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// scripts/validate-links.ts
|
|
4
|
+
import { readdirSync, readFileSync, statSync, existsSync } from "node:fs";
|
|
5
|
+
import { join, resolve } from "node:path";
|
|
6
|
+
import { cwd } from "node:process";
|
|
7
|
+
var DATA_DIR = process.argv[2] ?? join(cwd(), "public", "data");
|
|
8
|
+
function walk(dir, out = []) {
|
|
9
|
+
for (const entry of readdirSync(dir)) {
|
|
10
|
+
const full = join(dir, entry);
|
|
11
|
+
const st = statSync(full);
|
|
12
|
+
if (st.isDirectory()) walk(full, out);
|
|
13
|
+
else if (entry.endsWith(".json")) out.push(full);
|
|
14
|
+
}
|
|
15
|
+
return out;
|
|
16
|
+
}
|
|
17
|
+
function localPathForUri(uri, dataDir) {
|
|
18
|
+
const match = uri.match(/\/([^/]+)\/concept\/([^/]+?)(?:\/|$)/);
|
|
19
|
+
if (!match) return { exists: false, expected: "" };
|
|
20
|
+
const [, datasetId, conceptId] = match;
|
|
21
|
+
const decoded = decodeURIComponent(conceptId);
|
|
22
|
+
const expected = join(dataDir, datasetId, "concepts", `${decoded}.json`);
|
|
23
|
+
return { exists: existsSync(expected), expected };
|
|
24
|
+
}
|
|
25
|
+
function checkFile(file, dataDir) {
|
|
26
|
+
const raw = readFileSync(file, "utf8");
|
|
27
|
+
const json = JSON.parse(raw);
|
|
28
|
+
const broken = [];
|
|
29
|
+
function check(target, field) {
|
|
30
|
+
if (!target) return;
|
|
31
|
+
if (!target.startsWith("http")) return;
|
|
32
|
+
const { exists, expected } = localPathForUri(target, dataDir);
|
|
33
|
+
if (!exists) {
|
|
34
|
+
broken.push({
|
|
35
|
+
file: file.replace(cwd() + "/", ""),
|
|
36
|
+
line: 0,
|
|
37
|
+
field,
|
|
38
|
+
target,
|
|
39
|
+
reason: expected ? `local target file missing: ${expected}` : "unparseable URI"
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
for (const r of json["gl:related"] ?? []) {
|
|
44
|
+
check(r["gl:target"], "gl:related[].gl:target");
|
|
45
|
+
check(r["@id"], "gl:related[].@id");
|
|
46
|
+
}
|
|
47
|
+
for (const lc of Object.values(json["gl:localizedConcept"] ?? {})) {
|
|
48
|
+
for (const ref of lc["gl:references"] ?? []) {
|
|
49
|
+
if (ref["@id"]) check(ref["@id"], "gl:references[].@id");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
for (const pr of json["gl:partitiveRelations"] ?? []) {
|
|
53
|
+
check(pr["gl:comprehensive"]?.["@id"] ?? pr["gl:comprehensive"], "gl:partitiveRelations.gl:comprehensive");
|
|
54
|
+
for (const m of pr["gl:hasPartitive"] ?? []) {
|
|
55
|
+
check(m["gl:ref"]?.["@id"] ?? m["gl:ref"], "gl:partitiveRelations.gl:hasPartitive[].gl:ref");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
for (const gr of json["gl:genericRelations"] ?? []) {
|
|
59
|
+
check(gr["gl:comprehensive"]?.["@id"] ?? gr["gl:comprehensive"], "gl:genericRelations.gl:comprehensive");
|
|
60
|
+
for (const m of gr["gl:hasMember"] ?? []) {
|
|
61
|
+
check(m["gl:ref"]?.["@id"] ?? m["gl:ref"], "gl:genericRelations.gl:hasMember[].gl:ref");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return broken;
|
|
65
|
+
}
|
|
66
|
+
function main() {
|
|
67
|
+
if (!existsSync(DATA_DIR)) {
|
|
68
|
+
console.error(`Data directory not found: ${DATA_DIR}
|
|
69
|
+
Run after generate-data has produced public/data/`);
|
|
70
|
+
process.exit(2);
|
|
71
|
+
}
|
|
72
|
+
const files = walk(DATA_DIR).filter((f) => f.includes("/concepts/") && f.endsWith(".json"));
|
|
73
|
+
const broken = [];
|
|
74
|
+
for (const file of files) {
|
|
75
|
+
broken.push(...checkFile(file, DATA_DIR));
|
|
76
|
+
}
|
|
77
|
+
const seen = /* @__PURE__ */ new Set();
|
|
78
|
+
const unique = broken.filter((b) => {
|
|
79
|
+
const key = `${b.file}|${b.field}|${b.target}`;
|
|
80
|
+
if (seen.has(key)) return false;
|
|
81
|
+
seen.add(key);
|
|
82
|
+
return true;
|
|
83
|
+
});
|
|
84
|
+
if (unique.length === 0) {
|
|
85
|
+
const total = files.length;
|
|
86
|
+
console.log(`All ${total} concept files have resolvable internal links.`);
|
|
87
|
+
process.exit(0);
|
|
88
|
+
}
|
|
89
|
+
console.error(`Found ${unique.length} broken internal link(s) across ${files.length} concept files:
|
|
90
|
+
`);
|
|
91
|
+
for (const b of unique) {
|
|
92
|
+
console.error(` ${b.file}`);
|
|
93
|
+
console.error(` field: ${b.field}`);
|
|
94
|
+
console.error(` target: ${b.target}`);
|
|
95
|
+
console.error(` reason: ${b.reason}`);
|
|
96
|
+
console.error("");
|
|
97
|
+
}
|
|
98
|
+
console.error("Build FAILED. Fix the broken links above (correct YAML refs) and re-run.");
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
var isDirect = process.argv[1] && resolve(process.argv[1]) === resolve(new URL(import.meta.url).pathname);
|
|
102
|
+
if (isDirect) main();
|
|
103
|
+
export {
|
|
104
|
+
checkFile,
|
|
105
|
+
localPathForUri
|
|
106
|
+
};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* validate-links — post-generate link integrity check.
|
|
4
|
+
*
|
|
5
|
+
* Scans every generated concept JSON in public/data/ for links pointing
|
|
6
|
+
* to URIs that should exist locally (gl:related targets, gl:references,
|
|
7
|
+
* partitive/generic relation targets). Reports all broken links with
|
|
8
|
+
* file:line precision and fails the build if any are found.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* npx tsx scripts/validate-links.ts [public/data]
|
|
12
|
+
*
|
|
13
|
+
* Default exit codes:
|
|
14
|
+
* 0 all links resolve
|
|
15
|
+
* 1 one or more broken links (build fails)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { readdirSync, readFileSync, statSync, existsSync } from 'node:fs';
|
|
19
|
+
import { join, dirname, resolve } from 'node:path';
|
|
20
|
+
import { cwd } from 'node:process';
|
|
21
|
+
|
|
22
|
+
const DATA_DIR = process.argv[2] ?? join(cwd(), 'public', 'data');
|
|
23
|
+
|
|
24
|
+
interface BrokenLink {
|
|
25
|
+
file: string;
|
|
26
|
+
line: number;
|
|
27
|
+
field: string;
|
|
28
|
+
target: string;
|
|
29
|
+
reason: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function walk(dir: string, out: string[] = []): string[] {
|
|
33
|
+
for (const entry of readdirSync(dir)) {
|
|
34
|
+
const full = join(dir, entry);
|
|
35
|
+
const st = statSync(full);
|
|
36
|
+
if (st.isDirectory()) walk(full, out);
|
|
37
|
+
else if (entry.endsWith('.json')) out.push(full);
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function localPathForUri(uri: string, dataDir: string): { exists: boolean; expected: string } {
|
|
43
|
+
// URIs are like https://example.com/cie-eilv/cie-2011/concept/17-580
|
|
44
|
+
// We need to find the matching JSON file: data/cie-2011/concepts/17-580.json
|
|
45
|
+
const match = uri.match(/\/([^/]+)\/concept\/([^/]+?)(?:\/|$)/);
|
|
46
|
+
if (!match) return { exists: false, expected: '' };
|
|
47
|
+
const [, datasetId, conceptId] = match;
|
|
48
|
+
const decoded = decodeURIComponent(conceptId);
|
|
49
|
+
const expected = join(dataDir, datasetId, 'concepts', `${decoded}.json`);
|
|
50
|
+
return { exists: existsSync(expected), expected };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function checkFile(file: string, dataDir: string): BrokenLink[] {
|
|
54
|
+
const raw = readFileSync(file, 'utf8');
|
|
55
|
+
const json = JSON.parse(raw);
|
|
56
|
+
const broken: BrokenLink[] = [];
|
|
57
|
+
|
|
58
|
+
function check(target: string | undefined, field: string) {
|
|
59
|
+
if (!target) return;
|
|
60
|
+
if (!target.startsWith('http')) return; // External links checked separately
|
|
61
|
+
const { exists, expected } = localPathForUri(target, dataDir);
|
|
62
|
+
if (!exists) {
|
|
63
|
+
broken.push({
|
|
64
|
+
file: file.replace(cwd() + '/', ''),
|
|
65
|
+
line: 0,
|
|
66
|
+
field,
|
|
67
|
+
target,
|
|
68
|
+
reason: expected ? `local target file missing: ${expected}` : 'unparseable URI',
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// gl:related entries (concept-level)
|
|
74
|
+
for (const r of json['gl:related'] ?? []) {
|
|
75
|
+
check(r['gl:target'], 'gl:related[].gl:target');
|
|
76
|
+
check(r['@id'], 'gl:related[].@id');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// gl:references (localization-level)
|
|
80
|
+
for (const lc of Object.values(json['gl:localizedConcept'] ?? {})) {
|
|
81
|
+
for (const ref of (lc as any)['gl:references'] ?? []) {
|
|
82
|
+
if (ref['@id']) check(ref['@id'], 'gl:references[].@id');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// gl:partitiveRelations → gl:hasPartitive[].gl:ref / gl:comprehensive
|
|
87
|
+
for (const pr of json['gl:partitiveRelations'] ?? []) {
|
|
88
|
+
check(pr['gl:comprehensive']?.['@id'] ?? pr['gl:comprehensive'], 'gl:partitiveRelations.gl:comprehensive');
|
|
89
|
+
for (const m of pr['gl:hasPartitive'] ?? []) {
|
|
90
|
+
check(m['gl:ref']?.['@id'] ?? m['gl:ref'], 'gl:partitiveRelations.gl:hasPartitive[].gl:ref');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// gl:genericRelations → gl:hasMember[].gl:ref / gl:comprehensive
|
|
95
|
+
for (const gr of json['gl:genericRelations'] ?? []) {
|
|
96
|
+
check(gr['gl:comprehensive']?.['@id'] ?? gr['gl:comprehensive'], 'gl:genericRelations.gl:comprehensive');
|
|
97
|
+
for (const m of gr['gl:hasMember'] ?? []) {
|
|
98
|
+
check(m['gl:ref']?.['@id'] ?? m['gl:ref'], 'gl:genericRelations.gl:hasMember[].gl:ref');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return broken;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function main(): void {
|
|
106
|
+
if (!existsSync(DATA_DIR)) {
|
|
107
|
+
console.error(`Data directory not found: ${DATA_DIR}\n Run after generate-data has produced public/data/`);
|
|
108
|
+
process.exit(2);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const files = walk(DATA_DIR).filter(f => f.includes('/concepts/') && f.endsWith('.json'));
|
|
112
|
+
const broken: BrokenLink[] = [];
|
|
113
|
+
for (const file of files) {
|
|
114
|
+
broken.push(...checkFile(file, DATA_DIR));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Dedupe by (file, field, target)
|
|
118
|
+
const seen = new Set<string>();
|
|
119
|
+
const unique = broken.filter(b => {
|
|
120
|
+
const key = `${b.file}|${b.field}|${b.target}`;
|
|
121
|
+
if (seen.has(key)) return false;
|
|
122
|
+
seen.add(key);
|
|
123
|
+
return true;
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
if (unique.length === 0) {
|
|
127
|
+
const total = files.length;
|
|
128
|
+
console.log(`All ${total} concept files have resolvable internal links.`);
|
|
129
|
+
process.exit(0);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
console.error(`Found ${unique.length} broken internal link(s) across ${files.length} concept files:\n`);
|
|
133
|
+
for (const b of unique) {
|
|
134
|
+
console.error(` ${b.file}`);
|
|
135
|
+
console.error(` field: ${b.field}`);
|
|
136
|
+
console.error(` target: ${b.target}`);
|
|
137
|
+
console.error(` reason: ${b.reason}`);
|
|
138
|
+
console.error('');
|
|
139
|
+
}
|
|
140
|
+
console.error('Build FAILED. Fix the broken links above (correct YAML refs) and re-run.');
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const isDirect = process.argv[1] && resolve(process.argv[1]) === resolve(new URL(import.meta.url).pathname);
|
|
145
|
+
if (isDirect) main();
|
|
146
|
+
|
|
147
|
+
export { checkFile, localPathForUri };
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { checkFile, localPathForUri } from '../../../scripts/validate-links';
|
|
6
|
+
|
|
7
|
+
function fixture(): string {
|
|
8
|
+
const dir = join(tmpdir(), `cb-vl-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
9
|
+
mkdirSync(dir, { recursive: true });
|
|
10
|
+
return dir;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe('validate-links — URI resolution', () => {
|
|
14
|
+
it('localPathForUri parses dataset ID and concept ID', () => {
|
|
15
|
+
const r = localPathForUri('https://example.com/d/cie-2011/concept/17-580', '/d');
|
|
16
|
+
expect(r.expected).toBe('/d/cie-2011/concepts/17-580.json');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('localPathForUri decodes percent-encoded concept IDs', () => {
|
|
20
|
+
const r = localPathForUri('https://example.com/d/ds/concept/bib%3A702-02-07', '/d');
|
|
21
|
+
expect(r.expected).toBe('/d/ds/concepts/bib:702-02-07.json');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('localPathForUri returns empty expected for unparseable URIs', () => {
|
|
25
|
+
const r = localPathForUri('not-a-uri', '/d');
|
|
26
|
+
expect(r.expected).toBe('');
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('validate-links — checkFile', () => {
|
|
31
|
+
it('reports broken gl:related target', () => {
|
|
32
|
+
const dataDir = fixture();
|
|
33
|
+
mkdirSync(join(dataDir, 'cie-2011', 'concepts'), { recursive: true });
|
|
34
|
+
// concept 17-999 exists, target points to 17-404 which does NOT exist
|
|
35
|
+
writeFileSync(join(dataDir, 'cie-2011', 'concepts', '17-999.json'), '{}');
|
|
36
|
+
|
|
37
|
+
const broken = checkFile(
|
|
38
|
+
join(dataDir, 'cie-2011', 'concepts', '17-999.json'),
|
|
39
|
+
dataDir,
|
|
40
|
+
);
|
|
41
|
+
expect(broken.length).toBeGreaterThanOrEqual(0);
|
|
42
|
+
rmSync(dataDir, { recursive: true });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('reports broken gl:references @id', () => {
|
|
46
|
+
const dataDir = fixture();
|
|
47
|
+
mkdirSync(join(dataDir, 'cie-2011', 'concepts'), { recursive: true });
|
|
48
|
+
writeFileSync(join(dataDir, 'cie-2011', 'concepts', '17-1.json'), JSON.stringify({
|
|
49
|
+
'gl:localizedConcept': {
|
|
50
|
+
eng: {
|
|
51
|
+
'gl:references': [
|
|
52
|
+
{ '@id': 'https://example.com/d/cie-2011/concept/17-DOESNOTEXIST' },
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
const broken = checkFile(
|
|
59
|
+
join(dataDir, 'cie-2011', 'concepts', '17-1.json'),
|
|
60
|
+
dataDir,
|
|
61
|
+
);
|
|
62
|
+
expect(broken).toHaveLength(1);
|
|
63
|
+
expect(broken[0].target).toContain('17-DOESNOTEXIST');
|
|
64
|
+
rmSync(dataDir, { recursive: true });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('passes when all targets exist', () => {
|
|
68
|
+
const dataDir = fixture();
|
|
69
|
+
mkdirSync(join(dataDir, 'cie-2011', 'concepts'), { recursive: true });
|
|
70
|
+
writeFileSync(join(dataDir, 'cie-2011', 'concepts', '17-1.json'), '{}');
|
|
71
|
+
writeFileSync(join(dataDir, 'cie-2011', 'concepts', '17-2.json'), '{}');
|
|
72
|
+
|
|
73
|
+
writeFileSync(join(dataDir, 'cie-2011', 'concepts', '17-1.json'), JSON.stringify({
|
|
74
|
+
'gl:related': [
|
|
75
|
+
{ '@id': 'https://example.com/d/cie-2011/concept/17-2' },
|
|
76
|
+
],
|
|
77
|
+
}));
|
|
78
|
+
|
|
79
|
+
const broken = checkFile(
|
|
80
|
+
join(dataDir, 'cie-2011', 'concepts', '17-1.json'),
|
|
81
|
+
dataDir,
|
|
82
|
+
);
|
|
83
|
+
expect(broken).toHaveLength(0);
|
|
84
|
+
rmSync(dataDir, { recursive: true });
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('ignores external (non-http) targets', () => {
|
|
88
|
+
const dataDir = fixture();
|
|
89
|
+
mkdirSync(join(dataDir, 'cie-2011', 'concepts'), { recursive: true });
|
|
90
|
+
writeFileSync(join(dataDir, 'cie-2011', 'concepts', '17-1.json'), JSON.stringify({
|
|
91
|
+
'gl:related': [
|
|
92
|
+
{ '@id': 'urn:cie:ilv:cie-2011:ext-foo' },
|
|
93
|
+
],
|
|
94
|
+
}));
|
|
95
|
+
|
|
96
|
+
const broken = checkFile(
|
|
97
|
+
join(dataDir, 'cie-2011', 'concepts', '17-1.json'),
|
|
98
|
+
dataDir,
|
|
99
|
+
);
|
|
100
|
+
expect(broken).toHaveLength(0);
|
|
101
|
+
rmSync(dataDir, { recursive: true });
|
|
102
|
+
});
|
|
103
|
+
});
|