@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
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* SidebarSeriesSection — compact version of the dataset series list for the
|
|
4
|
+
* AppSidebar. Shows all multi-edition series as collapsible groups with their
|
|
5
|
+
* editions as clickable items.
|
|
6
|
+
*
|
|
7
|
+
* Designed to fit the sidebar's existing visual language: small text, gold
|
|
8
|
+
* accents, ink-100 borders. Uses Tailwind dark: classes for theme switching.
|
|
9
|
+
*/
|
|
10
|
+
import { computed, ref } from 'vue';
|
|
11
|
+
import { useRouter } from 'vue-router';
|
|
12
|
+
import { useDatasetSeries } from '../composables/useDatasetSeries';
|
|
13
|
+
|
|
14
|
+
const router = useRouter();
|
|
15
|
+
const { series } = useDatasetSeries();
|
|
16
|
+
|
|
17
|
+
const multiEditionSeries = computed(() =>
|
|
18
|
+
series.value.filter(s => s.members.length > 1)
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
/* Collapse state — start expanded for the active series, collapsed otherwise */
|
|
22
|
+
const collapsed = ref<Set<string>>(new Set());
|
|
23
|
+
function toggle(key: string) {
|
|
24
|
+
const next = new Set(collapsed.value);
|
|
25
|
+
if (next.has(key)) next.delete(key);
|
|
26
|
+
else next.add(key);
|
|
27
|
+
collapsed.value = next;
|
|
28
|
+
}
|
|
29
|
+
function isCollapsed(key: string) {
|
|
30
|
+
return collapsed.value.has(key);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function navigate(registerId: string) {
|
|
34
|
+
router.push({ name: 'dataset', params: { registerId } });
|
|
35
|
+
}
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<template>
|
|
39
|
+
<div v-if="multiEditionSeries.length" class="sidebar-series">
|
|
40
|
+
<div class="section-label">Edition Series</div>
|
|
41
|
+
<div class="series-list">
|
|
42
|
+
<div v-for="s in multiEditionSeries" :key="s.key" class="series-block">
|
|
43
|
+
<button
|
|
44
|
+
class="series-header"
|
|
45
|
+
@click="toggle(s.key)"
|
|
46
|
+
>
|
|
47
|
+
<span class="series-chevron">{{ isCollapsed(s.key) ? '▸' : '▾' }}</span>
|
|
48
|
+
<span class="series-title">{{ s.title }}</span>
|
|
49
|
+
<span class="series-count">{{ s.members.length }}</span>
|
|
50
|
+
</button>
|
|
51
|
+
<ol v-if="!isCollapsed(s.key)" class="series-editions">
|
|
52
|
+
<li
|
|
53
|
+
v-for="member in [...s.members].reverse()"
|
|
54
|
+
:key="member.id"
|
|
55
|
+
:class="['edition-row', { active: member.isActive, current: member.isCurrent }]"
|
|
56
|
+
>
|
|
57
|
+
<button class="edition-button" @click="navigate(member.id)">
|
|
58
|
+
<span class="edition-year">{{ member.year ?? '—' }}</span>
|
|
59
|
+
<span class="edition-meta">
|
|
60
|
+
<span v-if="member.isActive" class="edition-mark">●</span>
|
|
61
|
+
<span v-else-if="member.isCurrent" class="edition-mark current">◆</span>
|
|
62
|
+
</span>
|
|
63
|
+
</button>
|
|
64
|
+
</li>
|
|
65
|
+
</ol>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</template>
|
|
70
|
+
|
|
71
|
+
<style scoped>
|
|
72
|
+
.sidebar-series {
|
|
73
|
+
margin-bottom: 1.5rem;
|
|
74
|
+
}
|
|
75
|
+
.section-label {
|
|
76
|
+
font-size: 10px;
|
|
77
|
+
font-weight: 700;
|
|
78
|
+
text-transform: uppercase;
|
|
79
|
+
letter-spacing: 0.18em;
|
|
80
|
+
color: theme('colors.ink.300');
|
|
81
|
+
margin-bottom: 0.5rem;
|
|
82
|
+
}
|
|
83
|
+
:global(.dark) .section-label {
|
|
84
|
+
color: theme('colors.ink.400');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.series-list {
|
|
88
|
+
display: flex;
|
|
89
|
+
flex-direction: column;
|
|
90
|
+
gap: 0.25rem;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.series-block {
|
|
94
|
+
border-radius: 6px;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.series-header {
|
|
98
|
+
display: flex;
|
|
99
|
+
align-items: center;
|
|
100
|
+
gap: 0.375rem;
|
|
101
|
+
width: 100%;
|
|
102
|
+
padding: 0.375rem 0.5rem;
|
|
103
|
+
background: transparent;
|
|
104
|
+
border: none;
|
|
105
|
+
border-radius: 4px;
|
|
106
|
+
cursor: pointer;
|
|
107
|
+
font-family: inherit;
|
|
108
|
+
text-align: left;
|
|
109
|
+
color: theme('colors.ink.700');
|
|
110
|
+
font-size: 12px;
|
|
111
|
+
font-weight: 600;
|
|
112
|
+
transition: background 0.15s;
|
|
113
|
+
}
|
|
114
|
+
.series-header:hover {
|
|
115
|
+
background: theme('colors.ink.50');
|
|
116
|
+
}
|
|
117
|
+
:global(.dark) .series-header {
|
|
118
|
+
color: theme('colors.ink.200');
|
|
119
|
+
}
|
|
120
|
+
:global(.dark) .series-header:hover {
|
|
121
|
+
background: theme('colors.ink.700');
|
|
122
|
+
}
|
|
123
|
+
.series-chevron {
|
|
124
|
+
font-size: 10px;
|
|
125
|
+
width: 0.625rem;
|
|
126
|
+
color: theme('colors.ink.300');
|
|
127
|
+
}
|
|
128
|
+
.series-title {
|
|
129
|
+
flex: 1;
|
|
130
|
+
overflow: hidden;
|
|
131
|
+
text-overflow: ellipsis;
|
|
132
|
+
white-space: nowrap;
|
|
133
|
+
font-family: 'DM Serif Display', Georgia, serif;
|
|
134
|
+
font-size: 13px;
|
|
135
|
+
font-weight: 400;
|
|
136
|
+
letter-spacing: -0.005em;
|
|
137
|
+
}
|
|
138
|
+
.series-count {
|
|
139
|
+
font-family: 'JetBrains Mono', monospace;
|
|
140
|
+
font-size: 10px;
|
|
141
|
+
color: theme('colors.ink.300');
|
|
142
|
+
background: theme('colors.ink.50');
|
|
143
|
+
padding: 1px 6px;
|
|
144
|
+
border-radius: 8px;
|
|
145
|
+
font-weight: 600;
|
|
146
|
+
}
|
|
147
|
+
:global(.dark) .series-count {
|
|
148
|
+
background: theme('colors.ink.700');
|
|
149
|
+
color: theme('colors.ink.300');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.series-editions {
|
|
153
|
+
list-style: none;
|
|
154
|
+
padding: 0 0 0 0.625rem;
|
|
155
|
+
margin: 0;
|
|
156
|
+
display: flex;
|
|
157
|
+
flex-direction: column;
|
|
158
|
+
gap: 0;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.edition-row {
|
|
162
|
+
position: relative;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.edition-button {
|
|
166
|
+
display: flex;
|
|
167
|
+
align-items: center;
|
|
168
|
+
gap: 0.375rem;
|
|
169
|
+
width: 100%;
|
|
170
|
+
padding: 0.25rem 0.5rem 0.25rem 0.875rem;
|
|
171
|
+
background: transparent;
|
|
172
|
+
border: none;
|
|
173
|
+
border-radius: 4px;
|
|
174
|
+
cursor: pointer;
|
|
175
|
+
font-family: inherit;
|
|
176
|
+
text-align: left;
|
|
177
|
+
color: theme('colors.ink.500');
|
|
178
|
+
font-size: 11px;
|
|
179
|
+
transition: all 0.15s;
|
|
180
|
+
position: relative;
|
|
181
|
+
}
|
|
182
|
+
.edition-button::before {
|
|
183
|
+
content: '';
|
|
184
|
+
position: absolute;
|
|
185
|
+
left: 0;
|
|
186
|
+
top: 50%;
|
|
187
|
+
transform: translateY(-50%);
|
|
188
|
+
width: 4px;
|
|
189
|
+
height: 4px;
|
|
190
|
+
border-radius: 50%;
|
|
191
|
+
background: theme('colors.ink.200');
|
|
192
|
+
}
|
|
193
|
+
.edition-button:hover {
|
|
194
|
+
background: theme('colors.ink.50');
|
|
195
|
+
color: theme('colors.ink.800');
|
|
196
|
+
}
|
|
197
|
+
:global(.dark) .edition-button {
|
|
198
|
+
color: theme('colors.ink.400');
|
|
199
|
+
}
|
|
200
|
+
:global(.dark) .edition-button:hover {
|
|
201
|
+
background: theme('colors.ink.700');
|
|
202
|
+
color: theme('colors.ink.100');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.edition-row.active .edition-button {
|
|
206
|
+
color: #B8935A;
|
|
207
|
+
font-weight: 600;
|
|
208
|
+
background: rgba(184, 147, 90, 0.08);
|
|
209
|
+
}
|
|
210
|
+
.edition-row.active .edition-button::before {
|
|
211
|
+
background: #B8935A;
|
|
212
|
+
width: 5px;
|
|
213
|
+
height: 5px;
|
|
214
|
+
box-shadow: 0 0 0 2px rgba(184, 147, 90, 0.20);
|
|
215
|
+
}
|
|
216
|
+
.edition-row.current:not(.active) .edition-button::before {
|
|
217
|
+
background: #B8935A;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.edition-year {
|
|
221
|
+
font-family: 'JetBrains Mono', monospace;
|
|
222
|
+
font-size: 11px;
|
|
223
|
+
font-weight: 500;
|
|
224
|
+
letter-spacing: 0.02em;
|
|
225
|
+
}
|
|
226
|
+
.edition-meta {
|
|
227
|
+
margin-left: auto;
|
|
228
|
+
display: flex;
|
|
229
|
+
align-items: center;
|
|
230
|
+
}
|
|
231
|
+
.edition-mark {
|
|
232
|
+
font-size: 7px;
|
|
233
|
+
color: theme('colors.ink.300');
|
|
234
|
+
}
|
|
235
|
+
.edition-mark.current {
|
|
236
|
+
color: #B8935A;
|
|
237
|
+
font-size: 9px;
|
|
238
|
+
}
|
|
239
|
+
</style>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref } from 'vue';
|
|
3
|
+
|
|
4
|
+
const props = defineProps<{
|
|
5
|
+
uri: string;
|
|
6
|
+
conceptId: string;
|
|
7
|
+
}>();
|
|
8
|
+
|
|
9
|
+
defineEmits<{
|
|
10
|
+
(e: 'copy', uri: string): void;
|
|
11
|
+
}>();
|
|
12
|
+
|
|
13
|
+
const copied = ref(false);
|
|
14
|
+
|
|
15
|
+
async function copy() {
|
|
16
|
+
try {
|
|
17
|
+
await navigator.clipboard.writeText(props.uri);
|
|
18
|
+
} catch {
|
|
19
|
+
// clipboard may be unavailable; ignore
|
|
20
|
+
}
|
|
21
|
+
copied.value = true;
|
|
22
|
+
setTimeout(() => { copied.value = false; }, 2000);
|
|
23
|
+
}
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<template>
|
|
27
|
+
<div class="min-w-0">
|
|
28
|
+
<div class="text-[10px] uppercase tracking-widest text-ink-300 font-medium mb-2">RDF Instance</div>
|
|
29
|
+
<div class="flex items-center gap-2 flex-wrap">
|
|
30
|
+
<code class="text-sm font-mono text-ink-700 break-all">{{ uri }}</code>
|
|
31
|
+
<button
|
|
32
|
+
type="button"
|
|
33
|
+
@click="copy"
|
|
34
|
+
class="p-1.5 rounded text-ink-300 hover:text-ink-600 hover:bg-ink-50 transition-colors flex-shrink-0"
|
|
35
|
+
:title="copied ? 'Copied!' : 'Copy URI'"
|
|
36
|
+
:aria-label="copied ? 'URI copied to clipboard' : 'Copy URI to clipboard'"
|
|
37
|
+
>
|
|
38
|
+
<svg v-if="!copied" class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10a2 2 0 01-2-2v-1m6 4v-3a2 2 0 00-2-2H8"/></svg>
|
|
39
|
+
<svg v-else class="w-3.5 h-3.5 text-emerald-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
|
|
40
|
+
</button>
|
|
41
|
+
</div>
|
|
42
|
+
<div class="flex gap-1.5 mt-2.5">
|
|
43
|
+
<router-link to="/ontology/class/gloss-Concept" class="inline-flex items-center px-2 py-0.5 rounded text-[11px] font-medium bg-blue-50 text-blue-700 border border-blue-100 hover:bg-blue-100 transition-colors">gloss:Concept</router-link>
|
|
44
|
+
<router-link to="/ontology/class/gloss-Concept" class="inline-flex items-center px-2 py-0.5 rounded text-[11px] font-medium bg-emerald-50 text-emerald-700 border border-emerald-100 hover:bg-emerald-100 transition-colors">skos:Concept</router-link>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</template>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { PropValue } from './use-rdf-document';
|
|
3
|
+
|
|
4
|
+
defineProps<{
|
|
5
|
+
section: {
|
|
6
|
+
classId: string;
|
|
7
|
+
classLabel: string;
|
|
8
|
+
label: string;
|
|
9
|
+
props: PropValue[];
|
|
10
|
+
};
|
|
11
|
+
}>();
|
|
12
|
+
|
|
13
|
+
function accent(classId: string): string {
|
|
14
|
+
if (classId === 'gloss:Concept') return 'bg-blue-500';
|
|
15
|
+
if (classId === 'gloss:LocalizedConcept') return 'bg-emerald-500';
|
|
16
|
+
return 'bg-amber-500';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function classRoute(classId: string): string {
|
|
20
|
+
return `/ontology/class/${classId.replace(/:/g, '-')}`;
|
|
21
|
+
}
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<template>
|
|
25
|
+
<div class="card p-5">
|
|
26
|
+
<div class="flex items-center gap-2 mb-3">
|
|
27
|
+
<div class="w-1 h-4 rounded-full" :class="accent(section.classId)"></div>
|
|
28
|
+
<router-link :to="classRoute(section.classId)" class="text-xs font-semibold text-ink-700 hover:text-blue-600 transition-colors">{{ section.classId }}</router-link>
|
|
29
|
+
<span class="text-xs text-ink-400">·</span>
|
|
30
|
+
<span class="text-xs text-ink-500">{{ section.label }}</span>
|
|
31
|
+
</div>
|
|
32
|
+
<div class="space-y-1.5">
|
|
33
|
+
<div
|
|
34
|
+
v-for="prop in section.props"
|
|
35
|
+
:key="prop.predicate"
|
|
36
|
+
class="grid grid-cols-[160px_1fr] gap-x-3 gap-y-0.5 py-1.5 border-b border-ink-100/30 last:border-0"
|
|
37
|
+
>
|
|
38
|
+
<code class="text-xs text-blue-600 font-medium leading-relaxed self-start pt-0.5">{{ prop.predicate }}</code>
|
|
39
|
+
<div class="flex flex-col gap-0.5">
|
|
40
|
+
<template v-for="(val, vi) in prop.values" :key="vi">
|
|
41
|
+
<span
|
|
42
|
+
v-if="prop.nested"
|
|
43
|
+
class="text-xs text-ink-600 bg-ink-50/60 px-2 py-1 rounded border-l-2 border-ink-200 leading-relaxed break-words"
|
|
44
|
+
>{{ val }}</span>
|
|
45
|
+
<span
|
|
46
|
+
v-else
|
|
47
|
+
class="text-xs text-ink-600 leading-relaxed break-words"
|
|
48
|
+
>{{ val }}</span>
|
|
49
|
+
</template>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
</template>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue';
|
|
3
|
+
import { RDF_PREFIXES } from './rdf-prefixes';
|
|
4
|
+
|
|
5
|
+
const props = defineProps<{
|
|
6
|
+
collapsed?: boolean;
|
|
7
|
+
}>();
|
|
8
|
+
|
|
9
|
+
const rows = computed(() => RDF_PREFIXES);
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<template>
|
|
13
|
+
<details :open="!collapsed" class="card px-4 py-2 text-[11px]">
|
|
14
|
+
<summary class="cursor-pointer text-ink-500 select-none hover:text-ink-700">
|
|
15
|
+
Prefixes <span class="text-ink-300">({{ rows.length }})</span>
|
|
16
|
+
</summary>
|
|
17
|
+
<dl class="mt-2 grid grid-cols-[auto_1fr] gap-x-3 gap-y-0.5">
|
|
18
|
+
<template v-for="p in rows" :key="p.prefix">
|
|
19
|
+
<dt><code class="text-blue-600">{{ p.prefix }}:</code></dt>
|
|
20
|
+
<dd>
|
|
21
|
+
<code class="text-ink-500">{{ p.iri }}</code>
|
|
22
|
+
<span class="text-ink-300 ml-2">{{ p.description }}</span>
|
|
23
|
+
</dd>
|
|
24
|
+
</template>
|
|
25
|
+
</dl>
|
|
26
|
+
</details>
|
|
27
|
+
</template>
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, computed } from 'vue';
|
|
3
|
+
import { RDF_PREFIXES } from './rdf-prefixes';
|
|
4
|
+
|
|
5
|
+
const props = defineProps<{
|
|
6
|
+
turtle: string;
|
|
7
|
+
jsonld: string;
|
|
8
|
+
resourceCount: number;
|
|
9
|
+
defaultFormat?: 'turtle' | 'jsonld';
|
|
10
|
+
}>();
|
|
11
|
+
|
|
12
|
+
const emit = defineEmits<{
|
|
13
|
+
(e: 'format-change', format: 'turtle' | 'jsonld'): void;
|
|
14
|
+
}>();
|
|
15
|
+
|
|
16
|
+
const showSource = ref(false);
|
|
17
|
+
const format = ref<'turtle' | 'jsonld'>(props.defaultFormat ?? 'turtle');
|
|
18
|
+
|
|
19
|
+
const text = computed(() => format.value === 'turtle' ? props.turtle : props.jsonld);
|
|
20
|
+
|
|
21
|
+
function togglePanel() {
|
|
22
|
+
showSource.value = !showSource.value;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function pickFormat(next: 'turtle' | 'jsonld') {
|
|
26
|
+
format.value = next;
|
|
27
|
+
emit('format-change', next);
|
|
28
|
+
}
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<template>
|
|
32
|
+
<div class="card overflow-hidden">
|
|
33
|
+
<button
|
|
34
|
+
type="button"
|
|
35
|
+
class="w-full flex items-center justify-between px-5 py-3.5 text-left hover:bg-ink-50/30 transition-colors"
|
|
36
|
+
:aria-expanded="showSource"
|
|
37
|
+
@click="togglePanel"
|
|
38
|
+
>
|
|
39
|
+
<div class="flex items-center gap-2">
|
|
40
|
+
<svg class="w-4 h-4 text-ink-400 transition-transform" :class="showSource ? 'rotate-90' : ''" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>
|
|
41
|
+
<span class="text-sm font-medium text-ink-700">RDF Source</span>
|
|
42
|
+
</div>
|
|
43
|
+
<div class="flex items-center gap-2">
|
|
44
|
+
<select
|
|
45
|
+
v-model="format"
|
|
46
|
+
class="text-xs border border-ink-200 rounded px-2 py-1 bg-surface text-ink-600 focus:outline-none focus:ring-1 focus:ring-blue-400"
|
|
47
|
+
@click.stop
|
|
48
|
+
@change="pickFormat(format)"
|
|
49
|
+
>
|
|
50
|
+
<option value="turtle">Turtle</option>
|
|
51
|
+
<option value="jsonld">JSON-LD</option>
|
|
52
|
+
</select>
|
|
53
|
+
<span class="text-[10px] text-ink-300">{{ resourceCount }} resources</span>
|
|
54
|
+
</div>
|
|
55
|
+
</button>
|
|
56
|
+
<div v-if="showSource" class="border-t border-ink-100/60">
|
|
57
|
+
<pre
|
|
58
|
+
dir="auto"
|
|
59
|
+
class="p-4 text-xs font-mono text-ink-700 bg-ink-50/30 overflow-x-auto leading-relaxed max-h-[600px] overflow-y-auto"
|
|
60
|
+
>{{ text }}</pre>
|
|
61
|
+
<details class="border-t border-ink-100/60 px-4 py-2 bg-surface/50">
|
|
62
|
+
<summary class="text-[11px] text-ink-400 cursor-pointer hover:text-ink-600 select-none">Prefixes ({{ RDF_PREFIXES.length }})</summary>
|
|
63
|
+
<dl class="mt-2 grid grid-cols-[auto_1fr] gap-x-3 gap-y-0.5 text-[11px]">
|
|
64
|
+
<template v-for="p in RDF_PREFIXES" :key="p.prefix">
|
|
65
|
+
<dt><code class="text-blue-600">{{ p.prefix }}:</code></dt>
|
|
66
|
+
<dd class="text-ink-400">{{ p.iri }}</dd>
|
|
67
|
+
</template>
|
|
68
|
+
</dl>
|
|
69
|
+
</details>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</template>
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { FOAF, PROV, DCTERMS, RDFS } from './predicates';
|
|
2
|
+
import { RdfGraph } from './rdf-graph';
|
|
3
|
+
|
|
4
|
+
export interface AgentInput {
|
|
5
|
+
readonly slug: string;
|
|
6
|
+
readonly name: string;
|
|
7
|
+
readonly role?: string;
|
|
8
|
+
readonly organization?: string;
|
|
9
|
+
readonly url?: string;
|
|
10
|
+
readonly email?: string;
|
|
11
|
+
readonly agentIri: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface AgentEmissionResult {
|
|
15
|
+
readonly graph: RdfGraph;
|
|
16
|
+
readonly personIris: readonly string[];
|
|
17
|
+
readonly organizationIris: readonly string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function slugify(input: string): string {
|
|
21
|
+
return input
|
|
22
|
+
.toLowerCase()
|
|
23
|
+
.normalize('NFKD')
|
|
24
|
+
.replace(/[̀-ͯ]/g, '')
|
|
25
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
26
|
+
.replace(/^-+|-+$/g, '');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function agentsFromContributors(
|
|
30
|
+
contributors: readonly { name: string; role?: string; organization?: string; url?: string; email?: string }[],
|
|
31
|
+
agentBase = 'https://glossarist.org/agent',
|
|
32
|
+
): readonly AgentInput[] {
|
|
33
|
+
return contributors.map(c => ({
|
|
34
|
+
slug: slugify(c.name),
|
|
35
|
+
name: c.name,
|
|
36
|
+
role: c.role,
|
|
37
|
+
organization: c.organization,
|
|
38
|
+
url: c.url,
|
|
39
|
+
email: c.email,
|
|
40
|
+
agentIri: `${agentBase}/${slugify(c.name)}`,
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function emitAgentsGraph(agents: readonly AgentInput[]): AgentEmissionResult {
|
|
45
|
+
const graph = new RdfGraph();
|
|
46
|
+
const personIris: string[] = [];
|
|
47
|
+
const orgSlugs = new Set<string>();
|
|
48
|
+
const organizationIris: string[] = [];
|
|
49
|
+
|
|
50
|
+
for (const a of agents) {
|
|
51
|
+
const w = graph.declare(a.agentIri, {
|
|
52
|
+
types: [FOAF.Person, PROV.Person, PROV.Agent],
|
|
53
|
+
label: a.name,
|
|
54
|
+
classLabel: 'Person',
|
|
55
|
+
classId: FOAF.Person,
|
|
56
|
+
});
|
|
57
|
+
w.literal(FOAF.name, a.name);
|
|
58
|
+
if (a.email) w.iri(FOAF.mbox, `mailto:${a.email}`);
|
|
59
|
+
if (a.url) w.iri(RDFS.seeAlso, a.url);
|
|
60
|
+
if (a.role) w.literal(DCTERMS.description, a.role);
|
|
61
|
+
personIris.push(a.agentIri);
|
|
62
|
+
|
|
63
|
+
if (a.organization) {
|
|
64
|
+
const orgSlug = slugify(a.organization);
|
|
65
|
+
const orgIri = `https://glossarist.org/org/${orgSlug}`;
|
|
66
|
+
w.iri(PROV.actedOnBehalfOf, orgIri);
|
|
67
|
+
if (!orgSlugs.has(orgSlug)) {
|
|
68
|
+
orgSlugs.add(orgSlug);
|
|
69
|
+
const orgW = graph.declare(orgIri, {
|
|
70
|
+
types: [FOAF.Organization, PROV.Organization, PROV.Agent],
|
|
71
|
+
label: a.organization,
|
|
72
|
+
classLabel: 'Organization',
|
|
73
|
+
classId: FOAF.Organization,
|
|
74
|
+
});
|
|
75
|
+
orgW.literal(FOAF.name, a.organization);
|
|
76
|
+
organizationIris.push(orgIri);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return { graph, personIris, organizationIris };
|
|
82
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { DCTERMS, FOAF, RDF } from './predicates';
|
|
2
|
+
import { RdfGraph } from './rdf-graph';
|
|
3
|
+
|
|
4
|
+
export interface BibliographyEntry {
|
|
5
|
+
readonly id: string;
|
|
6
|
+
readonly reference: string;
|
|
7
|
+
readonly title?: string;
|
|
8
|
+
readonly link?: string;
|
|
9
|
+
readonly type?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface BibliographyInput {
|
|
13
|
+
readonly registerId: string;
|
|
14
|
+
readonly entries: readonly BibliographyEntry[];
|
|
15
|
+
readonly baseUri?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type BibliographyDataShape =
|
|
19
|
+
| { [id: string]: BibliographyEntryLike }
|
|
20
|
+
| { bibliography: readonly BibliographyEntryLike[] };
|
|
21
|
+
|
|
22
|
+
interface BibliographyEntryLike {
|
|
23
|
+
readonly id?: string;
|
|
24
|
+
readonly reference?: string;
|
|
25
|
+
readonly title?: string;
|
|
26
|
+
readonly link?: string;
|
|
27
|
+
readonly type?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function bibliographyEntryIri(registerId: string, id: string, baseUri = 'https://glossarist.org'): string {
|
|
31
|
+
return `${baseUri}/${registerId}/bib/${id}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function normalizeBibliographyData(raw: unknown): readonly BibliographyEntry[] {
|
|
35
|
+
if (!raw || typeof raw !== 'object') return [];
|
|
36
|
+
const obj = raw as Record<string, unknown>;
|
|
37
|
+
|
|
38
|
+
if (Array.isArray(obj.bibliography)) {
|
|
39
|
+
return (obj.bibliography as readonly BibliographyEntryLike[]).map(e => entryFromV3(e));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const entries: BibliographyEntry[] = [];
|
|
43
|
+
for (const [id, value] of Object.entries(obj)) {
|
|
44
|
+
if (!value || typeof value !== 'object') continue;
|
|
45
|
+
const entry = entryFromV3(value as BibliographyEntryLike, id);
|
|
46
|
+
entries.push(entry);
|
|
47
|
+
}
|
|
48
|
+
return entries;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function entryFromV3(e: BibliographyEntryLike, fallbackId?: string): BibliographyEntry {
|
|
52
|
+
const id = e.id ?? fallbackId ?? '';
|
|
53
|
+
const reference = e.reference ?? '';
|
|
54
|
+
return {
|
|
55
|
+
id,
|
|
56
|
+
reference,
|
|
57
|
+
title: e.title,
|
|
58
|
+
link: e.link,
|
|
59
|
+
type: e.type,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function emitBibliographyGraph(input: BibliographyInput): RdfGraph {
|
|
64
|
+
const baseUri = input.baseUri ?? 'https://glossarist.org';
|
|
65
|
+
const graph = new RdfGraph();
|
|
66
|
+
for (const entry of input.entries) {
|
|
67
|
+
if (!entry.id || !entry.reference) continue;
|
|
68
|
+
const iri = bibliographyEntryIri(input.registerId, entry.id, baseUri);
|
|
69
|
+
const w = graph.declare(iri, {
|
|
70
|
+
types: ['dcterms:BibliographicResource'],
|
|
71
|
+
label: entry.title ?? entry.reference,
|
|
72
|
+
classLabel: 'BibliographicResource',
|
|
73
|
+
classId: 'dcterms:BibliographicResource',
|
|
74
|
+
});
|
|
75
|
+
w.literal(DCTERMS.identifier, entry.id);
|
|
76
|
+
w.literal(DCTERMS.bibliographicCitation, entry.reference);
|
|
77
|
+
if (entry.title) w.literal(DCTERMS.title, entry.title);
|
|
78
|
+
if (entry.link) w.iri(FOAF.page, entry.link);
|
|
79
|
+
if (entry.type) w.iri(DCTERMS.type, `gloss:bibtype/${entry.type}`);
|
|
80
|
+
w.iri(DCTERMS.isPartOf, `${baseUri}/${input.registerId}/`);
|
|
81
|
+
}
|
|
82
|
+
return graph;
|
|
83
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { GLOSS, DCTERMS, XSD, PROV, FOAF } from './predicates';
|
|
2
|
+
import { RdfGraph, lit, iri, blank, triple } from './rdf-graph';
|
|
3
|
+
|
|
4
|
+
export interface BuildActivityInput {
|
|
5
|
+
readonly runId: string;
|
|
6
|
+
readonly startedAt: string;
|
|
7
|
+
readonly endedAt: string;
|
|
8
|
+
readonly gitSha?: string;
|
|
9
|
+
readonly gitBranch?: string;
|
|
10
|
+
readonly toolId: string;
|
|
11
|
+
readonly toolVersion: string;
|
|
12
|
+
readonly datasetRegisters: readonly string[];
|
|
13
|
+
readonly conceptCount: number;
|
|
14
|
+
readonly associatedAgentIri?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function activityIri(input: BuildActivityInput): string {
|
|
18
|
+
return `activity/build/${input.runId}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function emitBuildActivityGraph(input: BuildActivityInput): RdfGraph {
|
|
22
|
+
const graph = new RdfGraph();
|
|
23
|
+
const iriStr = activityIri(input);
|
|
24
|
+
const w = graph.declare(iriStr, {
|
|
25
|
+
types: [PROV.Activity],
|
|
26
|
+
label: `build ${input.runId}`,
|
|
27
|
+
classLabel: 'Activity',
|
|
28
|
+
classId: PROV.Activity,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
w.literal(PROV.generatedAtTime, input.endedAt, { datatype: XSD.dateTime });
|
|
32
|
+
|
|
33
|
+
const startW = graph.declare(`${iriStr}/start`, {
|
|
34
|
+
types: ['prov:StartingPoint'],
|
|
35
|
+
label: `start ${input.runId}`,
|
|
36
|
+
classLabel: 'StartingPoint',
|
|
37
|
+
classId: 'prov:StartingPoint',
|
|
38
|
+
});
|
|
39
|
+
startW.literal('prov:atTime', input.startedAt, { datatype: XSD.dateTime });
|
|
40
|
+
|
|
41
|
+
const endW = graph.declare(`${iriStr}/end`, {
|
|
42
|
+
types: ['prov:EndingPoint'],
|
|
43
|
+
label: `end ${input.runId}`,
|
|
44
|
+
classLabel: 'EndingPoint',
|
|
45
|
+
classId: 'prov:EndingPoint',
|
|
46
|
+
});
|
|
47
|
+
endW.literal('prov:atTime', input.endedAt, { datatype: XSD.dateTime });
|
|
48
|
+
|
|
49
|
+
if (input.gitSha) {
|
|
50
|
+
const commitIri = `https://glossarist.org/commit/${input.gitSha}`;
|
|
51
|
+
w.iri(PROV.used, commitIri);
|
|
52
|
+
const commitW = graph.declare(commitIri, {
|
|
53
|
+
types: [PROV.Entity],
|
|
54
|
+
label: input.gitSha,
|
|
55
|
+
classLabel: 'Entity',
|
|
56
|
+
classId: PROV.Entity,
|
|
57
|
+
});
|
|
58
|
+
if (input.gitBranch) commitW.literal(DCTERMS.description, `branch: ${input.gitBranch}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const toolIri = `https://glossarist.org/tool/${input.toolId}/${input.toolVersion}`;
|
|
62
|
+
w.iri(PROV.used, toolIri);
|
|
63
|
+
const toolW = graph.declare(toolIri, {
|
|
64
|
+
types: [PROV.Entity, 'prov:SoftwareAgent'],
|
|
65
|
+
label: `${input.toolId} ${input.toolVersion}`,
|
|
66
|
+
classLabel: 'SoftwareAgent',
|
|
67
|
+
classId: 'prov:SoftwareAgent',
|
|
68
|
+
});
|
|
69
|
+
toolW.literal(DCTERMS.identifier, input.toolVersion);
|
|
70
|
+
toolW.literal('prov:version', input.toolVersion);
|
|
71
|
+
|
|
72
|
+
for (const register of input.datasetRegisters) {
|
|
73
|
+
w.iri(PROV.used, `https://glossarist.org/${register}/`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
w.literal('gloss:conceptCount', String(input.conceptCount), { datatype: XSD.integer });
|
|
77
|
+
|
|
78
|
+
if (input.associatedAgentIri) {
|
|
79
|
+
w.iri(PROV.wasAssociatedWith, input.associatedAgentIri);
|
|
80
|
+
graph.declare(input.associatedAgentIri, {
|
|
81
|
+
types: [PROV.Agent, FOAF.Person],
|
|
82
|
+
label: input.associatedAgentIri.split('/').pop() ?? 'agent',
|
|
83
|
+
classLabel: 'Agent',
|
|
84
|
+
classId: PROV.Agent,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return graph;
|
|
89
|
+
}
|