@glossarist/concept-browser 0.7.114 → 0.7.115

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.
Files changed (82) hide show
  1. package/README.md +45 -2
  2. package/cli/index.mjs +15 -15
  3. package/cli/index.ts +9 -11
  4. package/data/concept-model/shapes/glossarist.concept.yaml.schema.json +255 -0
  5. package/package.json +14 -12
  6. package/scripts/__tests__/{bibliography.test.mjs → bibliography.test.ts} +1 -1
  7. package/scripts/__tests__/{cli-script-contract.test.mjs → cli-script-contract.test.ts} +3 -3
  8. package/scripts/__tests__/{concept-groups.test.mjs → concept-groups.test.ts} +1 -1
  9. package/scripts/__tests__/{doctor.test.mjs → doctor.test.ts} +1 -1
  10. package/scripts/__tests__/{fetch-datasets.test.mjs → fetch-datasets.test.ts} +1 -1
  11. package/scripts/__tests__/{first-non-empty.test.mjs → first-non-empty.test.ts} +1 -1
  12. package/scripts/__tests__/{process-about-pages.test.mjs → process-about-pages.test.ts} +1 -1
  13. package/scripts/{bridge-to-astro.mjs → bridge-to-astro.ts} +2 -4
  14. package/scripts/{build-edges.js → build-edges.ts} +11 -13
  15. package/scripts/{check-syntax.mjs → check-syntax.ts} +20 -13
  16. package/scripts/{fetch-datasets.mjs → fetch-datasets.ts} +2 -2
  17. package/scripts/{generate-data.mjs → generate-data.ts} +73 -460
  18. package/scripts/{generate-ontology-schema.mjs → generate-ontology-schema.ts} +2 -2
  19. package/scripts/lib/{build-cache.mjs → build-cache.ts} +3 -2
  20. package/scripts/lib/concept-formats.ts +206 -0
  21. package/scripts/lib/concept-uri.ts +14 -0
  22. package/scripts/lib/{dataset-turtle.mjs → dataset-turtle.ts} +4 -3
  23. package/scripts/lib/harmonize-concept.ts +86 -0
  24. package/scripts/lib/jsonld-emitter.ts +169 -0
  25. package/scripts/lib/{local-path-safety.mjs → local-path-safety.ts} +6 -2
  26. package/scripts/lib/yaml-types.ts +262 -0
  27. package/scripts/{process-about-pages.mjs → process-about-pages.ts} +1 -1
  28. package/scripts/{smoke.mjs → smoke.ts} +4 -3
  29. package/scripts/{validate-shacl.mjs → validate-shacl.ts} +5 -5
  30. package/scripts/validate-yaml.ts +144 -0
  31. package/src/__tests__/extract-source-refs.test.ts +2 -3
  32. package/src/__tests__/perf/serialization-perf.test.ts +5 -5
  33. package/src/__tests__/scripts/agents-turtle.test.ts +1 -1
  34. package/src/__tests__/scripts/bibliography-turtle.test.ts +1 -1
  35. package/src/__tests__/scripts/build-activity-turtle.test.ts +1 -1
  36. package/src/__tests__/scripts/build-cache.test.ts +1 -1
  37. package/src/__tests__/scripts/build-integration.test.ts +19 -19
  38. package/src/__tests__/scripts/dataset-turtle.test.ts +1 -1
  39. package/src/__tests__/scripts/migrate-v1-to-v3.test.ts +1 -1
  40. package/src/__tests__/scripts/normalize-yaml.test.ts +1 -1
  41. package/src/__tests__/scripts/turtle-escape.test.ts +1 -1
  42. package/src/__tests__/scripts/validate-yaml.test.ts +76 -0
  43. package/src/__tests__/scripts/version-turtle.test.ts +1 -1
  44. package/src/__tests__/scripts/vocab-turtle.test.ts +1 -1
  45. package/src/__tests__/scripts-syntax.test.ts +2 -2
  46. package/src/__tests__/use-rdf-document.test.ts +2 -3
  47. package/src/__tests__/utils/content-renderer-xss.test.ts +57 -0
  48. package/src/__tests__/validate-shacl.test.ts +3 -3
  49. package/src/adapters/wire/index.ts +69 -0
  50. package/src/utils/content-renderer.ts +7 -3
  51. package/src/utils/url-safety.ts +1 -5
  52. package/tsconfig.json +2 -2
  53. package/vite.config.ts +1 -1
  54. package/src/__tests__/composables/use-concept-edges-v2.test.ts +0 -167
  55. package/src/__tests__/composables/use-concept-edges-v2.test.ts.bak +0 -166
  56. package/src/types/agents-version-turtle.d.ts +0 -27
  57. package/src/types/bibliography-turtle.d.ts +0 -12
  58. package/src/types/build-activity-turtle.d.ts +0 -16
  59. package/src/types/build-cache.d.ts +0 -20
  60. package/src/types/dataset-turtle.d.ts +0 -32
  61. package/src/types/normalize-yaml.d.ts +0 -16
  62. package/src/types/turtle-escape.d.ts +0 -6
  63. package/src/types/vocab-turtle.d.ts +0 -13
  64. /package/scripts/{doctor.mjs → doctor.ts} +0 -0
  65. /package/scripts/{extract-source-refs.js → extract-source-refs.ts} +0 -0
  66. /package/scripts/{generate-404.js → generate-404.ts} +0 -0
  67. /package/scripts/{generate-ontology-data.mjs → generate-ontology-data.ts} +0 -0
  68. /package/scripts/lib/{agents-turtle.mjs → agents-turtle.ts} +0 -0
  69. /package/scripts/lib/{bibliography-turtle.mjs → bibliography-turtle.ts} +0 -0
  70. /package/scripts/lib/{bibliography.mjs → bibliography.ts} +0 -0
  71. /package/scripts/lib/build/{image-assets.mjs → image-assets.ts} +0 -0
  72. /package/scripts/lib/build/{non-verbal-consumer.mjs → non-verbal-consumer.ts} +0 -0
  73. /package/scripts/lib/{build-activity-turtle.mjs → build-activity-turtle.ts} +0 -0
  74. /package/scripts/lib/{concept-groups.mjs → concept-groups.ts} +0 -0
  75. /package/scripts/lib/{first-non-empty.mjs → first-non-empty.ts} +0 -0
  76. /package/scripts/lib/{turtle-escape.mjs → turtle-escape.ts} +0 -0
  77. /package/scripts/lib/{version-turtle.mjs → version-turtle.ts} +0 -0
  78. /package/scripts/lib/{vocab-turtle.mjs → vocab-turtle.ts} +0 -0
  79. /package/scripts/{load-site-config.mjs → load-site-config.ts} +0 -0
  80. /package/scripts/{migrate-v1-to-v3.mjs → migrate-v1-to-v3.ts} +0 -0
  81. /package/scripts/{normalize-yaml.mjs → normalize-yaml.ts} +0 -0
  82. /package/scripts/{sync-concept-model.mjs → sync-concept-model.ts} +0 -0
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect } from 'vitest';
2
2
  import { Parser, Store } from 'n3';
3
- import { buildDatasetTurtle } from '../../../scripts/lib/dataset-turtle.mjs';
3
+ import { buildDatasetTurtle } from '../../../scripts/lib/dataset-turtle';
4
4
 
5
5
  const BASE = 'https://glossarist.org/dataset';
6
6
 
@@ -5,7 +5,7 @@ import {
5
5
  migrateRelation,
6
6
  migrateConceptData,
7
7
  isAlreadyMigrated,
8
- } from '../../../scripts/migrate-v1-to-v3.mjs';
8
+ } from '../../../scripts/migrate-v1-to-v3';
9
9
 
10
10
  describe('migrate-v1-to-v3 pure conversion', () => {
11
11
  describe('isV1PartitiveRelation', () => {
@@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
2
  import { mkdtempSync, writeFileSync, readFileSync, rmSync, mkdirSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import { tmpdir } from 'node:os';
5
- import { normalizeYaml } from '../../../scripts/normalize-yaml.mjs';
5
+ import { normalizeYaml } from '../../../scripts/normalize-yaml';
6
6
 
7
7
  const NFC_STRING = 'café';
8
8
  const NON_NFC_STRING = 'café';
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from 'vitest';
2
- import { ttlLit, ttlPrefixed, ttlIri, assertValidIri } from '../../../scripts/lib/turtle-escape.mjs';
2
+ import { ttlLit, ttlPrefixed, ttlIri, assertValidIri } from '../../../scripts/lib/turtle-escape';
3
3
 
4
4
  describe('D1/D2 — Turtle escaping hardening', () => {
5
5
  describe('ttlLit', () => {
@@ -0,0 +1,76 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { readFileSync } from 'node:fs';
3
+ import { resolve, dirname } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { validateYaml } from '../../../scripts/validate-yaml';
6
+
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ const SCHEMA_PATH = resolve(__dirname, '..', '..', '..', 'data', 'concept-model', 'shapes', 'glossarist.concept.yaml.schema.json');
9
+ const schema = JSON.parse(readFileSync(SCHEMA_PATH, 'utf8'));
10
+
11
+ describe('validate-yaml', () => {
12
+ it('accepts a valid concept with termid and localization', () => {
13
+ const yaml = `
14
+ termid: '1'
15
+ eng:
16
+ terms:
17
+ - designation: measurement unit
18
+ normative_status: preferred
19
+ definition:
20
+ - content: A concept used as a basis for comparison.
21
+ `;
22
+ const result = validateYaml(yaml, schema);
23
+ if (!result.valid) console.log('Errors:', result.errors);
24
+ expect(result.valid).toBe(true);
25
+ });
26
+
27
+ it('rejects a concept without termid', () => {
28
+ const yaml = `
29
+ eng:
30
+ terms:
31
+ - designation: missing id
32
+ `;
33
+ const result = validateYaml(yaml, schema);
34
+ expect(result.valid).toBe(false);
35
+ expect(result.errors.some(e => e.message.includes('termid'))).toBe(true);
36
+ });
37
+
38
+ it('accepts managed concept format with underscore fields', () => {
39
+ const yaml = `
40
+ termid: '5'
41
+ _domains:
42
+ - physics
43
+ _status: valid
44
+ `;
45
+ const result = validateYaml(yaml, schema);
46
+ if (!result.valid) console.log('Errors:', result.errors);
47
+ expect(result.valid).toBe(true);
48
+ });
49
+
50
+ it('accepts partitive relations with MECE axes', () => {
51
+ const yaml = `
52
+ termid: '3'
53
+ _partitiveRelations:
54
+ - type: hasPart
55
+ members:
56
+ - ref: part-a
57
+ presence: required
58
+ count: exactly_one
59
+ `;
60
+ const result = validateYaml(yaml, schema);
61
+ if (!result.valid) console.log('Errors:', result.errors);
62
+ expect(result.valid).toBe(true);
63
+ });
64
+
65
+ it('rejects non-string termid', () => {
66
+ const yaml = `
67
+ termid: 12345
68
+ eng:
69
+ terms:
70
+ - designation: test
71
+ `;
72
+ const result = validateYaml(yaml, schema);
73
+ expect(result.valid).toBe(false);
74
+ });
75
+ });
76
+
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect } from 'vitest';
2
2
  import { Parser, Store } from 'n3';
3
- import { buildVersionTurtle, buildVersionHistoryTurtle } from '../../../scripts/lib/version-turtle.mjs';
3
+ import { buildVersionTurtle, buildVersionHistoryTurtle } from '../../../scripts/lib/version-turtle';
4
4
 
5
5
  function parse(turtle: string): Store {
6
6
  const parser = new Parser({ format: 'Turtle' });
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect } from 'vitest';
2
2
  import { Parser, Store } from 'n3';
3
- import { buildVocabularyTurtle, listVocabSchemes } from '../../../scripts/lib/vocab-turtle.mjs';
3
+ import { buildVocabularyTurtle, listVocabSchemes } from '../../../scripts/lib/vocab-turtle';
4
4
 
5
5
  function parse(turtle: string): Store {
6
6
  const parser = new Parser({ format: 'Turtle' });
@@ -9,10 +9,10 @@ const repoRoot = path.resolve(
9
9
  );
10
10
 
11
11
  describe('scripts syntax gate', () => {
12
- it('`npm run check:scripts` exits 0 (every .mjs/.js/.cjs in scripts/ and cli/ parses)', () => {
12
+ it('`npm run check:scripts` exits 0 (every .ts/.mjs/.js/.cjs in scripts/ and cli/ parses)', () => {
13
13
  const result = spawnSync(
14
14
  process.execPath,
15
- [path.join(repoRoot, 'scripts', 'check-syntax.mjs')],
15
+ ['--import', 'tsx', path.join(repoRoot, 'scripts', 'check-syntax.ts')],
16
16
  { encoding: 'utf8' },
17
17
  );
18
18
 
@@ -1,10 +1,9 @@
1
- // @ts-nocheck — TODO.typescript/12: remove after glossarist TS migration
2
1
  import { describe, it, expect } from 'vitest';
3
2
  import { ref } from 'vue';
4
3
  import { Concept } from 'glossarist';
5
4
  import { useRdfDocument } from '../components/use-rdf-document';
6
5
 
7
- function makeConcept(): Concept {
6
+ function makeConcept(): any {
8
7
  return Concept.fromJSON({
9
8
  id: '3.1.1',
10
9
  uri: 'https://glossarist.org/test/concept/3.1.1',
@@ -24,7 +23,7 @@ function makeConcept(): Concept {
24
23
  });
25
24
  }
26
25
 
27
- function makeConceptWithNonVerbal(): Concept {
26
+ function makeConceptWithNonVerbal(): any {
28
27
  return Concept.fromJSON({
29
28
  id: '3.1.2',
30
29
  uri: 'https://glossarist.org/test/concept/3.1.2',
@@ -0,0 +1,57 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { renderContent } from '../../utils/content-renderer';
3
+
4
+ describe('content-renderer XSS protection', () => {
5
+ it('blocks javascript: URLs in {{link:}} (no anchor tag)', () => {
6
+ const html = renderContent('{{link:javascript:alert(1)}}', {});
7
+ expect(html).not.toContain('<a');
8
+ expect(html).not.toContain('href=');
9
+ });
10
+
11
+ it('blocks javascript: URLs in {{link:}} with label (renders as text)', () => {
12
+ const html = renderContent('{{link:javascript:alert(1), click me}}', {});
13
+ expect(html).not.toContain('<a');
14
+ expect(html).not.toContain('href=');
15
+ expect(html).toContain('click me');
16
+ });
17
+
18
+ it('blocks data: URLs in {{link:}} (no anchor tag)', () => {
19
+ const html = renderContent('{{link:data:text/html,<script>alert(1)</script>}}', {});
20
+ expect(html).not.toContain('<a');
21
+ expect(html).not.toContain('href=');
22
+ expect(html).not.toContain('<script>');
23
+ });
24
+
25
+ it('allows https: URLs in {{link:}}', () => {
26
+ const html = renderContent('{{link:https://example.com}}', {});
27
+ expect(html).toContain('href="https://example.com"');
28
+ });
29
+
30
+ it('blocks javascript: URLs in {{image:}} (no img tag)', () => {
31
+ const html = renderContent('{{image:javascript:alert(1)}}', {});
32
+ expect(html).not.toContain('<img');
33
+ });
34
+
35
+ it('blocks data: URLs in {{image:}} (no img tag with executable src)', () => {
36
+ const html = renderContent('{{image:data:text/html,<script>}}', {});
37
+ expect(html).not.toContain('<img');
38
+ expect(html).not.toContain('data:text/html');
39
+ });
40
+
41
+ it('allows relative paths in {{image:}}', () => {
42
+ const html = renderContent('{{image:diagram.png}}', {});
43
+ expect(html).toContain('src="diagram.png"');
44
+ });
45
+
46
+ it('escapes HTML in unordered list items', () => {
47
+ const html = renderContent('* <script>alert(1)</script>\n* safe item', {});
48
+ expect(html).not.toContain('<script>');
49
+ expect(html).toContain('&lt;script&gt;');
50
+ });
51
+
52
+ it('escapes HTML in ordered list items', () => {
53
+ const html = renderContent('1. <img onerror=alert(1)>\n2. safe', {});
54
+ expect(html).not.toContain('<img onerror');
55
+ expect(html).toContain('&lt;img');
56
+ });
57
+ });
@@ -4,7 +4,7 @@ import { join, dirname } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
 
6
6
  const __dirname = dirname(fileURLToPath(import.meta.url));
7
- const SCRIPT = join(__dirname, '..', '..', 'scripts', 'validate-shacl.mjs');
7
+ const SCRIPT = join(__dirname, '..', '..', 'scripts', 'validate-shacl.ts');
8
8
  const FIXTURES = join(__dirname, '..', '__fixtures__', 'shacl');
9
9
  const SHAPES = join(__dirname, '..', '__fixtures__', 'concept-shape.ttl');
10
10
 
@@ -31,13 +31,13 @@ function execCaught(cmd: string, args: string[], opts: Parameters<typeof execFil
31
31
  }
32
32
 
33
33
  function runValidate(dataDir: string, { env, shapes }: RunOptions = {}): RunResult {
34
- return execCaught('node', [SCRIPT, '--shapes', shapes ?? SHAPES, dataDir], {
34
+ return execCaught('node', ['--import', 'tsx', SCRIPT, '--shapes', shapes ?? SHAPES, dataDir], {
35
35
  env: { ...process.env, ...env },
36
36
  });
37
37
  }
38
38
 
39
39
  function runValidateRaw(args: string[], { env }: RunOptions = {}): RunResult {
40
- return execCaught('node', [SCRIPT, ...args], {
40
+ return execCaught('node', ['--import', 'tsx', SCRIPT, ...args], {
41
41
  env: { ...process.env, ...env },
42
42
  stdio: ['ignore', 'pipe', 'pipe'],
43
43
  });
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Wire-format types barrel — single import path for all wire types.
3
+ *
4
+ * Three MECE layers:
5
+ * - YAML author format (INPUT to build pipeline)
6
+ * - JSON-LD browser format (OUTPUT of build pipeline)
7
+ * - Manifest / site-config metadata
8
+ *
9
+ * Usage:
10
+ * import type { HarmonizedConcept, JsonLdConcept, Manifest } from '@/adapters/wire'
11
+ *
12
+ * Or layer-specific:
13
+ * import type { HarmonizedConcept } from '@/adapters/wire/yaml'
14
+ * import type { JsonLdConcept } from '@/adapters/wire/jsonld'
15
+ */
16
+
17
+ // YAML author format — what dataset authors write
18
+ export type {
19
+ YamlContent,
20
+ YamlTerm,
21
+ YamlOrigin,
22
+ YamlGrammarInfo,
23
+ YamlSource,
24
+ YamlDate,
25
+ YamlRelation,
26
+ YamlPartitiveMember,
27
+ YamlPartitiveRelation,
28
+ YamlGenericRelation,
29
+ YamlLocalization,
30
+ HarmonizedConcept,
31
+ YamlManagedConceptDoc,
32
+ YamlManifestSection,
33
+ YamlManifest,
34
+ YamlBibliographyEntry,
35
+ YamlNewsFrontmatter,
36
+ YamlContentPage,
37
+ ShaclConstraint,
38
+ PrefixMap,
39
+ } from '../../../scripts/lib/yaml-types';
40
+
41
+ // JSON-LD browser format — what the SPA reads
42
+ export type {
43
+ JsonLdContent,
44
+ JsonLdDate,
45
+ JsonLdPronunciation,
46
+ JsonLdGrammarInfo,
47
+ JsonLdRef,
48
+ JsonLdLocality,
49
+ JsonLdOrigin,
50
+ JsonLdSource,
51
+ JsonLdRelated,
52
+ JsonLdDesignation,
53
+ JsonLdLocalizedConcept,
54
+ JsonLdPartitiveMember,
55
+ JsonLdPartitiveRelation,
56
+ JsonLdConcept,
57
+ } from '../jsonld-types';
58
+
59
+ // Manifest + summary types
60
+ export type {
61
+ Manifest,
62
+ ManifestSection,
63
+ ConceptSummary,
64
+ CitationClassification,
65
+ CiteResolution,
66
+ } from '../types';
67
+
68
+ // Wire-key constants (runtime value, not just type)
69
+ export { GL } from '../wire-keys';
@@ -26,6 +26,7 @@
26
26
  * The v-math directive upgrades the placeholders to Plurimath at runtime.
27
27
  */
28
28
  import { escapeHtml, escapeAttr } from './escape';
29
+ import { sanitizeUrl, isSafeUrl } from './url-safety';
29
30
  import { parseMention } from 'glossarist';
30
31
  import type { NonVerbalKind } from '../adapters/non-verbal/types';
31
32
  import { entityKindFromMentionKind } from '../adapters/non-verbal/kind';
@@ -135,7 +136,7 @@ function convertLists(text: string): string {
135
136
  items.push(m[1].trim());
136
137
  }
137
138
  if (!items.length) return _;
138
- const lis = items.map(item => `<li>${item}</li>`).join('');
139
+ const lis = items.map(item => `<li>${escapeHtml(item)}</li>`).join('');
139
140
  return `\n<ul class="concept-list">${lis}</ul>`;
140
141
  });
141
142
 
@@ -147,7 +148,7 @@ function convertLists(text: string): string {
147
148
  items.push(m[1].trim());
148
149
  }
149
150
  if (!items.length) return _;
150
- const lis = items.map(item => `<li>${item}</li>`).join('');
151
+ const lis = items.map(item => `<li>${escapeHtml(item)}</li>`).join('');
151
152
  return `\n<ol class="concept-list concept-list-ordered">${lis}</ol>`;
152
153
  });
153
154
 
@@ -225,7 +226,9 @@ function resolveMentions(text: string, opts: RenderOptions): string {
225
226
  const url = (commaIdx > 0 ? rest.slice(0, commaIdx).trim() : rest.trim());
226
227
  const label = commaIdx > 0 ? rest.slice(commaIdx + 1).trim() : url;
227
228
  if (opts.linkResolver) return opts.linkResolver(url, label);
228
- return `<a href="${escapeAttr(url)}" target="_blank" rel="noopener" class="ext-link">${escapeHtml(label)}</a>`;
229
+ const safeUrl = sanitizeUrl(url);
230
+ if (!safeUrl) return escapeHtml(label);
231
+ return `<a href="${escapeAttr(safeUrl)}" target="_blank" rel="noopener" class="ext-link">${escapeHtml(label)}</a>`;
229
232
  }
230
233
 
231
234
  const imageMatch = body.match(/^image:(.+)$/i);
@@ -235,6 +238,7 @@ function resolveMentions(text: string, opts: RenderOptions): string {
235
238
  const src = (commaIdx > 0 ? rest.slice(0, commaIdx).trim() : rest.trim());
236
239
  const alt = commaIdx > 0 ? rest.slice(commaIdx + 1).trim() : '';
237
240
  if (opts.imageResolver) return opts.imageResolver(src, alt);
241
+ if (!isSafeUrl(src)) return escapeHtml(alt || src);
238
242
  return `<img src="${escapeAttr(src)}" alt="${escapeAttr(alt)}" class="inline-image" />`;
239
243
  }
240
244
 
@@ -18,11 +18,7 @@ export function isSafeUrl(url: string): boolean {
18
18
  }
19
19
  }
20
20
 
21
- if (trimmed.startsWith('#') || trimmed.startsWith('/') || trimmed.startsWith('./') || trimmed.startsWith('../')) {
22
- return true;
23
- }
24
-
25
- return false;
21
+ return true;
26
22
  }
27
23
 
28
24
  export function sanitizeUrl(url: string): string {
package/tsconfig.json CHANGED
@@ -20,6 +20,6 @@
20
20
  },
21
21
  "baseUrl": "."
22
22
  },
23
- "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "cli/**/*.ts", "env.d.ts", ".astro/types.d.ts"],
24
- "exclude": ["src/content.config.ts", "src/pages/**", "src/layouts/**", "src/astro-islands/**", "src/lib/site-config.ts"]
23
+ "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "env.d.ts", ".astro/types.d.ts"],
24
+ "exclude": ["src/content.config.ts", "src/pages/**", "src/layouts/**", "src/astro-islands/**", "src/lib/site-config.ts", "src/__tests__/**", "scripts/**", "cli/**"]
25
25
  }
package/vite.config.ts CHANGED
@@ -183,7 +183,7 @@ export default defineConfig({
183
183
  globals: true,
184
184
  include: [
185
185
  'src/__tests__/**/*.test.ts',
186
- 'scripts/__tests__/**/*.test.mjs',
186
+ 'scripts/__tests__/**/*.test.ts',
187
187
  ],
188
188
  },
189
189
  })
@@ -1,167 +0,0 @@
1
- // @ts-nocheck — TODO.typescript/12: remove after glossarist TS migration
2
- import { describe, it, expect } from 'vitest';
3
- // glossarist 0.4.33 renamed PartitiveRelation → PartitiveHyperedge.
4
- // Import from glossarist/models (extended by our local augmentation).
5
- import type {
6
- PartitiveHyperedge,
7
- PartitiveMember,
8
- ConceptRef,
9
- } from 'glossarist/models';
10
- import {
11
- PartitiveHyperedge as PartitiveHyperedgeModel,
12
- PartitiveMember as PartitiveMemberModel,
13
- ConceptRef as ConceptRefModel,
14
- } from 'glossarist/models';
15
-
16
- /**
17
- * Verifies glossarist-js 0.4.26's MECE-native PartitiveRelation model
18
- * (ISO 704:2022 presence × count + is_delimiting).
19
- *
20
- * The concept-browser consumes these fields directly (no migration
21
- * bridge). These specs pin the runtime contract so a regression in
22
- * upstream is caught here.
23
- */
24
-
25
- function makeRef(source: string, id: string): ConceptRef {
26
- return ConceptRefModel.fromJSON({ source, id }) as ConceptRef;
27
- }
28
-
29
- function makePartitive(
30
- source: string,
31
- id: string,
32
- fields: { presence?: 'required' | 'optional'; count?: 'exactly_one' | 'at_least_one' | 'multiple'; isDelimiting?: boolean } = {},
33
- ): PartitiveMember {
34
- const data: {
35
- ref: ConceptRef;
36
- presence?: 'required' | 'optional';
37
- count?: 'exactly_one' | 'at_least_one' | 'multiple';
38
- is_delimiting?: boolean;
39
- } = { ref: makeRef(source, id) };
40
- if (fields.presence) data.presence = fields.presence;
41
- if (fields.count) data.count = fields.count;
42
- if (fields.isDelimiting) data.is_delimiting = fields.isDelimiting;
43
- return new (PartitiveMemberModel as unknown as new (data: unknown) => PartitiveMember)(data) as PartitiveMember;
44
- }
45
-
46
- function makeRelation(opts: {
47
- comprehensive: ConceptRef;
48
- partitives: PartitiveMember[];
49
- completeness?: 'complete' | 'partial';
50
- criterion?: Record<string, string>;
51
- }): PartitiveHyperedge {
52
- return new (PartitiveHyperedgeModel as unknown as new (data: unknown) => PartitiveHyperedge)(opts) as PartitiveHyperedge;
53
- }
54
-
55
- describe('useConceptEdges — MECE PartitiveRelation (glossarist 0.4.26)', () => {
56
- describe('PartitiveMember (ISO 704:2022 presence × count + delimiting)', () => {
57
- it('presence defaults to required, count defaults to exactly_one', () => {
58
- const m = makePartitive('test', '1.1');
59
- expect(m.presence).toBe('required');
60
- expect(m.presence === "required").toBe(true);
61
- expect(m.presence === "optional").toBe(false);
62
- });
63
-
64
- it('count defaults to exactly_one', () => {
65
- const m = makePartitive('test', '1.1');
66
- expect(m.count).toBe('exactly_one');
67
- });
68
-
69
- it('presence=optional is preserved', () => {
70
- const m = makePartitive('test', '1.1', { presence: 'optional' });
71
- expect(m.presence).toBe('optional');
72
- expect(m.presence === "optional").toBe(true);
73
- expect(m.presence === "required").toBe(false);
74
- });
75
-
76
- it('count=multiple is preserved', () => {
77
- const m = makePartitive('test', '1.1', { count: 'multiple' });
78
- expect(m.count).toBe('multiple');
79
- });
80
-
81
- it('count=at_least_one is preserved', () => {
82
- const m = makePartitive('test', '1.1', { count: 'at_least_one' });
83
- expect(m.count).toBe('at_least_one');
84
- });
85
-
86
- it('is_delimiting defaults to false', () => {
87
- const m = makePartitive('test', '1.1');
88
- expect((m as any).is_delimiting).toBe(false);
89
- expect((m as any).is_delimiting).toBe(false);
90
- });
91
-
92
- it('is_delimiting is preserved when set to true', () => {
93
- const m = makePartitive('test', '1.1', { isDelimiting: true });
94
- expect((m as any).is_delimiting).toBe(true);
95
- expect((m as any).is_delimiting).toBe(true);
96
- });
97
-
98
- it('rejects the vacuous (presence=optional, count=at_least_one)', () => {
99
- expect(() =>
100
- makePartitive('test', '1.1', { presence: 'optional', count: 'at_least_one' }),
101
- ).toThrow(/collapses to optional \+ multiple/);
102
- });
103
- });
104
-
105
- describe('PartitiveRelation', () => {
106
- it('accepts 2 members with default axes', () => {
107
- const r = makeRelation({
108
- comprehensive: makeRef('test', '1.3'),
109
- partitives: [makePartitive('test', '1.4'), makePartitive('test', '1.5')],
110
- });
111
- expect((r as any).members).toHaveLength(2);
112
- expect((r as any).completeness).toBe('complete');
113
- expect((r as any).isComplete).toBe(true);
114
- expect((r as any).isPartial).toBe(false);
115
- });
116
-
117
- it('completeness=partial is preserved', () => {
118
- const r = makeRelation({
119
- comprehensive: makeRef('test', '1.3'),
120
- partitives: [makePartitive('test', '1.4'), makePartitive('test', '1.5')],
121
- completeness: 'partial',
122
- });
123
- expect((r as any).completeness).toBe('partial');
124
- expect((r as any).isPartial).toBe(true);
125
- });
126
-
127
- it('isCoordinate true when ≥2 partitives', () => {
128
- const r = makeRelation({
129
- comprehensive: makeRef('test', '1.3'),
130
- partitives: [makePartitive('test', '1.4'), makePartitive('test', '1.5')],
131
- });
132
- expect((r as any).isCoordinate).toBe(true);
133
- });
134
-
135
- it('rejects < 2 partitives (ISO 704 requirement)', () => {
136
- expect(() => makeRelation({
137
- comprehensive: makeRef('test', '1.3'),
138
- partitives: [makePartitive('test', '1.4')],
139
- })).toThrow(/≥2 partitives/);
140
- });
141
-
142
- it('round-trips through toJSON with MECE fields', () => {
143
- const r = makeRelation({
144
- comprehensive: makeRef('test', '1.3'),
145
- partitives: [
146
- makePartitive('test', '1.4', { presence: 'required', count: 'multiple' }),
147
- makePartitive('test', '1.5', { presence: 'optional', isDelimiting: true }),
148
- ],
149
- completeness: 'partial',
150
- criterion: { eng: 'physical structure' },
151
- });
152
- const json = r.toJSON() as {
153
- comprehensive: unknown;
154
- partitives: Array<{ presence?: string; count?: string; is_delimiting?: boolean; ref: unknown }>;
155
- completeness: string;
156
- criterion?: Record<string, string>;
157
- };
158
- expect(json.comprehensive).toBeDefined();
159
- expect(json.partitives).toHaveLength(2);
160
- expect(json.partitives[0].count).toBe('multiple');
161
- expect(json.partitives[1].presence).toBe('optional');
162
- expect(json.partitives[1].is_delimiting).toBe(true);
163
- expect(json.completeness).toBe('partial');
164
- expect(json.criterion).toEqual({ eng: 'physical structure' });
165
- });
166
- });
167
- });