@directory-builder/core 0.1.0 → 0.1.2

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.
@@ -35,7 +35,7 @@ const criteriaPredicates = (() => {
35
35
  })()
36
36
 
37
37
  // Map<recordIri, Map<predIri, [literalValue]>> for the per-member details modal.
38
- const orgInfo = groupBySubject(parseTtl(mappedTtl), { literalsOnly: true })
38
+ const entityInfo = groupBySubject(parseTtl(mappedTtl), { literalsOnly: true })
39
39
 
40
40
  const manualPairs = parseTtl(matchKnowledgeTtl)
41
41
  .filter(q => q.predicate.value === OWL_SAME_AS)
@@ -52,7 +52,7 @@ function MemberDetailsModal({ clusterId, memberIris, onClose }) {
52
52
  <button onClick={onClose} style={{ border: 0, background: "transparent", fontSize: 18, cursor: "pointer", lineHeight: 1 }}>×</button>
53
53
  </div>
54
54
  {memberIris.map((iri) => {
55
- const info = orgInfo.get(iri)
55
+ const info = entityInfo.get(iri)
56
56
  return (
57
57
  <div key={iri} style={{ marginBottom: 14 }}>
58
58
  <div style={{ fontSize: 11, color: "#666", marginBottom: 4 }}><code>{prefixed(iri)}</code></div>
@@ -98,7 +98,7 @@ export default function MatchGraph() {
98
98
  if (n.isCluster) n.subtitle = n.id.startsWith(CDF_NS) ? `cdf:${n.id.slice(CDF_NS.length)}` : prefixed(n.id)
99
99
  else { // a source (dedup) node
100
100
  n.label = sourceCode(n.id)
101
- n.subtitle = orgInfo.get(n.id)?.get(SCHEMA_IDENTIFIER)?.[0]
101
+ n.subtitle = entityInfo.get(n.id)?.get(SCHEMA_IDENTIFIER)?.[0]
102
102
  }
103
103
  }
104
104
  // Drop columns that ended up empty (schemas with no source duplication when
@@ -1,42 +1,51 @@
1
- // Merge view: every org with its per-source field values and conflict
2
- // highlighting; each org's services are nested (indented) beneath it.
3
- // Reads: mergedOrgs from mergeOrgs.js ( data/pipeline/merged.ttl + provenance.ttl)
4
- // Does: renders the Merge page (compact / wide <OrgCard>, toggleable)
1
+ // Merge view: every entity with its per-source field values and conflict
2
+ // highlighting. An entity referencing another via a relationship the
3
+ // federation declares (mapping :hasRelationship :toTargetField →
4
+ // :targetPredicate) renders nested beneath it.
5
+ // Reads: mergedEntities from mergeEntities.js (← merged.ttl + provenance.ttl),
6
+ // config/federation.ttl (relationship predicates)
7
+ // Does: renders the Merge page (compact / wide <EntityCard>, toggleable)
5
8
 
6
- import OrgCard from "./OrgCard.jsx"
7
- import { mergedOrgs } from "./mergeOrgs.js"
9
+ import { CDP, parseTtl } from "@directory-builder/core/utils"
10
+ import { mergedEntities } from "./mergeEntities.js"
11
+ import { federationTtl } from "./instanceData.js"
12
+ import EntityCard from "./EntityCard.jsx"
8
13
  import React, { useState } from "react"
9
14
 
10
- const SCHEMA_SERVICE = "schema:Service"
11
- const PROVIDER = "http://schema.org/provider"
12
- const providerOf = (e) => e.fields.find((f) => f.predicate === PROVIDER)?.values[0]?.raw
15
+ const fedQuads = parseTtl(federationTtl)
16
+ const relFields = new Set(fedQuads.filter((q) => q.predicate.value === `${CDP}toTargetField`).map((q) => q.object.value))
17
+ const REL_PREDS = new Set(fedQuads.filter((q) => relFields.has(q.subject.value) && q.predicate.value === `${CDP}targetPredicate`).map((q) => q.object.value))
13
18
 
14
- // Top-level orgs keep their existing (conflict-sorted) order; services are
15
- // grouped under their provider org. Any service whose provider isn't a merged
16
- // org falls through as an orphan, rendered at the end.
17
- const orgs = mergedOrgs.filter((e) => e.type !== SCHEMA_SERVICE)
18
- const orgIris = new Set(orgs.map((o) => o.iri))
19
- const servicesByOrg = new Map()
20
- const orphanServices = []
21
- for (const e of mergedOrgs) {
22
- if (e.type !== SCHEMA_SERVICE) continue
23
- const provider = providerOf(e)
24
- if (provider && orgIris.has(provider)) {
25
- if (!servicesByOrg.has(provider)) servicesByOrg.set(provider, [])
26
- servicesByOrg.get(provider).push(e)
27
- } else {
28
- orphanServices.push(e)
29
- }
19
+ // An entity's parent = the first relationship value pointing at another merged
20
+ // entity. Entities keep their (conflict-sorted) order within each level.
21
+ const iris = new Set(mergedEntities.map((e) => e.iri))
22
+ const parentOf = (e) => e.fields.find((f) => REL_PREDS.has(f.predicate) && iris.has(f.values[0]?.raw))?.values[0].raw
23
+ const childrenOf = new Map()
24
+ const hasParent = new Set()
25
+ for (const e of mergedEntities) {
26
+ const p = parentOf(e)
27
+ if (!p || p === e.iri) continue
28
+ if (!childrenOf.has(p)) childrenOf.set(p, [])
29
+ childrenOf.get(p).push(e)
30
+ hasParent.add(e.iri)
30
31
  }
31
32
 
33
+ // Flatten to (entity, depth) rows; the second pass catches reference cycles,
34
+ // which would otherwise never be reached from a top-level entity.
35
+ const ROWS = []
36
+ const seen = new Set()
37
+ const walk = (e, depth) => {
38
+ if (seen.has(e.iri)) return
39
+ seen.add(e.iri)
40
+ ROWS.push({ e, depth })
41
+ for (const c of childrenOf.get(e.iri) ?? []) walk(c, depth + 1)
42
+ }
43
+ for (const e of mergedEntities) if (!hasParent.has(e.iri)) walk(e, 0)
44
+ for (const e of mergedEntities) walk(e, 0)
45
+
32
46
  export default function MergeTables() {
33
47
  const [compact, setCompact] = useState(true)
34
48
  const [highlight, setHighlight] = useState(true)
35
- const service = (svc) => (
36
- <div key={svc.iri} style={{ marginLeft: "1.5rem", borderLeft: "2px solid #e0e0e0", paddingLeft: "0.75rem" }}>
37
- <OrgCard org={svc} compact={compact} highlight={highlight} />
38
- </div>
39
- )
40
49
  return (
41
50
  <div className="page" style={{ overflowY: "auto", height: "100%" }}>
42
51
  <div style={{ display: "flex", gap: "1rem", marginBottom: "0.75rem", fontSize: 13 }}>
@@ -49,13 +58,11 @@ export default function MergeTables() {
49
58
  Highlight conflicts
50
59
  </label>
51
60
  </div>
52
- {orgs.map((org) => (
53
- <React.Fragment key={org.iri}>
54
- <OrgCard org={org} compact={compact} highlight={highlight} />
55
- {(servicesByOrg.get(org.iri) ?? []).map(service)}
56
- </React.Fragment>
61
+ {ROWS.map(({ e, depth }) => (
62
+ <div key={e.iri} style={depth ? { marginLeft: `${depth * 1.5}rem`, borderLeft: "2px solid #e0e0e0", paddingLeft: "0.75rem" } : undefined}>
63
+ <EntityCard entity={e} compact={compact} highlight={highlight} />
64
+ </div>
57
65
  ))}
58
- {orphanServices.map(service)}
59
66
  </div>
60
67
  )
61
68
  }
@@ -5,7 +5,7 @@
5
5
 
6
6
  import { storeFromTurtles } from "@foerderfunke/sem-ops-utils/core"
7
7
  import { queryEngine } from "@foerderfunke/sem-ops-utils/sparql"
8
- import { finalTtl } from "./instanceData.js"
8
+ import { finalTtl, querySparql } from "./instanceData.js"
9
9
  import React, { useEffect, useRef } from "react"
10
10
  import "@zazuko/yasgui/build/yasgui.min.css"
11
11
  import Yasgui from "@zazuko/yasgui"
@@ -18,14 +18,9 @@ const ENDPOINT = "http://local/sparql"
18
18
 
19
19
  const store = storeFromTurtles([finalTtl])
20
20
 
21
- const INITIAL_QUERY = `PREFIX schema: <http://schema.org/>
22
- PREFIX cdf: <https://civic-data.de/federated-directory#>
23
-
24
- SELECT ?org (SAMPLE(?name) AS ?title) WHERE {
25
- ?org schema:name ?name .
26
- }
27
- GROUP BY ?org
28
- ORDER BY ?title`
21
+ // Instances own the editor's starting query via webapp/content/query.sparql
22
+ // (fetched at runtime like the About prose); plain select-all without one.
23
+ const INITIAL_QUERY = querySparql || "SELECT * WHERE { ?s ?p ?o } LIMIT 100"
29
24
 
30
25
  Yasgui.Yasqe.defaults.value = INITIAL_QUERY
31
26
 
@@ -6,7 +6,7 @@
6
6
  // artifact resolves to "" (pages render empty). Top-level await — importing
7
7
  // modules stay synchronous.
8
8
 
9
- import { CDP, objectsOf, parseTtl, PATHS, prefixesOf, sourceName } from "@directory-builder/core/utils"
9
+ import { CDP, enabledSources, objectsOf, parseTtl, PATHS, prefixesOf, sourceName } from "@directory-builder/core/utils"
10
10
 
11
11
  const fetchText = async (path) => {
12
12
  const res = await fetch(`${import.meta.env.BASE_URL}${path}`).catch(() => null)
@@ -16,20 +16,24 @@ const fetchText = async (path) => {
16
16
  export const federationTtl = await fetchText(PATHS.federation)
17
17
 
18
18
  const fedQuads = parseTtl(federationTtl)
19
- const cleanedPaths = objectsOf(fedQuads, `${CDP}hasSource`).map((iri) => PATHS.cleaned(sourceName(iri)))
19
+ const cleanedPaths = enabledSources(fedQuads).map((iri) => PATHS.cleaned(sourceName(iri)))
20
20
  // The instance's repo URL (:federation :repository …) — undefined when not
21
21
  // declared; pages hide their GitHub links then.
22
22
  export const repositoryUrl = objectsOf(fedQuads, `${CDP}repository`)[0]
23
23
  // Display prefixes = the federation's own @prefix declarations; cdp pinned
24
24
  // first so cdp:… wins over the empty ":" prefix bound to the same namespace.
25
25
  export const displayPrefixes = { cdp: CDP, ...prefixesOf(federationTtl) }
26
+ // The federation's display name (:federation rdfs:label) — optional; the
27
+ // webapp keeps its generic title without one.
28
+ export const federationLabel = fedQuads.find((q) =>
29
+ q.subject.value === `${CDP}federation` && q.predicate.value === "http://www.w3.org/2000/01/rdf-schema#label")?.object.value
26
30
 
27
31
  const FIXED = [PATHS.matchKnowledge, PATHS.ingestLog, PATHS.federateLog, PATHS.mapped,
28
- PATHS.matches, PATHS.merged, PATHS.provenance, PATHS.final, PATHS.about]
32
+ PATHS.matches, PATHS.merged, PATHS.provenance, PATHS.final, PATHS.about, PATHS.query]
29
33
  const [fixedTexts, cleanedTexts] = await Promise.all([
30
34
  Promise.all(FIXED.map(fetchText)),
31
35
  Promise.all(cleanedPaths.map(fetchText)),
32
36
  ])
33
37
 
34
- export const [matchKnowledgeTtl, ingestLogTtl, federateLogTtl, mappedTtl, matchesTtl, mergedTtl, provenanceTtl, finalTtl, aboutMd] = fixedTexts
38
+ export const [matchKnowledgeTtl, ingestLogTtl, federateLogTtl, mappedTtl, matchesTtl, mergedTtl, provenanceTtl, finalTtl, aboutMd, querySparql] = fixedTexts
35
39
  export const cleanedByPath = Object.fromEntries(cleanedPaths.map((p, i) => [p, cleanedTexts[i]]))
@@ -1,7 +1,7 @@
1
- // Helpers for the Map view: build the schema-mapping graph and resolve per-org
1
+ // Helpers for the Map view: build the schema-mapping graph and resolve per-entity
2
2
  // source/target field values. Pure (ttl in → data out).
3
3
  // Reads: TTL strings passed by MapGraph.jsx (federation, mapped, cleaned source TTL)
4
- // Does: returns { nodes, edges } plus per-source / per-org value maps
4
+ // Does: returns { nodes, edges } plus per-source / per-entity value maps
5
5
 
6
6
  import { CDP as NS, localName, parseTtl, prefixesOf, shrink, sourceName, subjectsOfType, typesOf } from "@directory-builder/core/utils"
7
7
 
@@ -9,26 +9,26 @@ const RDFS_LABEL = "http://www.w3.org/2000/01/rdf-schema#label"
9
9
  const NODE_TYPES = [`${NS}Source`, `${NS}SourceField`, `${NS}TargetField`, `${NS}TargetSchema`]
10
10
  const SUB_FIELD = `${NS}SubField`
11
11
 
12
- // Group orgs by source. Each org carries a cdp:fromSource triple in mapped.ttl
12
+ // Group entities by source. Each entity carries a cdp:fromSource triple in mapped.ttl
13
13
  // pointing at its Source IRI, so this is a single-pass scan with no prefix
14
14
  // matching.
15
- export function loadOrgsBySource(_federationTtl, mappedTtl) {
15
+ export function loadEntitiesBySource(_federationTtl, mappedTtl) {
16
16
  const SCHEMA_NAME = "http://schema.org/name"
17
17
  const SCHEMA_IDENTIFIER = "http://schema.org/identifier"
18
18
  const FROM_SOURCE = `${NS}fromSource`
19
19
 
20
- const orgSource = new Map() // orgIri -> sourceIri
20
+ const entitySource = new Map() // entityIri -> sourceIri
21
21
  const ids = new Map()
22
22
  const names = new Map()
23
23
  for (const q of parseTtl(mappedTtl)) {
24
24
  const p = q.predicate.value
25
- if (p === FROM_SOURCE) orgSource.set(q.subject.value, q.object.value)
25
+ if (p === FROM_SOURCE) entitySource.set(q.subject.value, q.object.value)
26
26
  else if (p === SCHEMA_IDENTIFIER) ids.set(q.subject.value, q.object.value)
27
27
  else if (p === SCHEMA_NAME) names.set(q.subject.value, q.object.value)
28
28
  }
29
29
 
30
30
  const result = new Map()
31
- for (const [iri, src] of orgSource) {
31
+ for (const [iri, src] of entitySource) {
32
32
  if (!result.has(src)) result.set(src, [])
33
33
  result.get(src).push({
34
34
  iri,
@@ -40,11 +40,11 @@ export function loadOrgsBySource(_federationTtl, mappedTtl) {
40
40
  return result
41
41
  }
42
42
 
43
- // For each org in mapped.ttl, resolve the literal value of each of its
43
+ // For each entity in mapped.ttl, resolve the literal value of each of its
44
44
  // source fields/sub-fields (from the source's lifted/cleaned TTL) AND each
45
45
  // target field (from mapped.ttl, indirected via the field's :targetPredicate).
46
- // Returns Map<orgIri, Map<fieldIri, string>>.
47
- export function loadFieldValuesByOrg(federationTtl, mappedTtl, liftedBySource) {
46
+ // Returns Map<entityIri, Map<fieldIri, string>>.
47
+ export function loadFieldValuesByEntity(federationTtl, mappedTtl, liftedBySource) {
48
48
  const fedQuads = parseTtl(federationTtl)
49
49
  const fieldPathOf = new Map()
50
50
  const fieldsBySource = new Map()
@@ -64,13 +64,13 @@ export function loadFieldValuesByOrg(federationTtl, mappedTtl, liftedBySource) {
64
64
  }
65
65
 
66
66
  const FROM_SOURCE = `${NS}fromSource`
67
- const orgSource = new Map() // orgIri -> sourceIri
68
- const literalsByOrg = new Map() // orgIri -> Map<predicateIri, string>
67
+ const entitySource = new Map() // entityIri -> sourceIri
68
+ const literalsByEntity = new Map() // entityIri -> Map<predicateIri, string>
69
69
  for (const q of parseTtl(mappedTtl)) {
70
- if (q.predicate.value === FROM_SOURCE) orgSource.set(q.subject.value, q.object.value)
70
+ if (q.predicate.value === FROM_SOURCE) entitySource.set(q.subject.value, q.object.value)
71
71
  if (q.object.termType === "Literal") {
72
- if (!literalsByOrg.has(q.subject.value)) literalsByOrg.set(q.subject.value, new Map())
73
- literalsByOrg.get(q.subject.value).set(q.predicate.value, q.object.value)
72
+ if (!literalsByEntity.has(q.subject.value)) literalsByEntity.set(q.subject.value, new Map())
73
+ literalsByEntity.get(q.subject.value).set(q.predicate.value, q.object.value)
74
74
  }
75
75
  }
76
76
 
@@ -88,10 +88,10 @@ export function loadFieldValuesByOrg(federationTtl, mappedTtl, liftedBySource) {
88
88
  }
89
89
 
90
90
  const fields = fieldsBySource.get(sourceIri) ?? []
91
- for (const [orgIri, src] of orgSource) {
91
+ for (const [entityIri, src] of entitySource) {
92
92
  if (src !== sourceIri) continue
93
93
  // Source subject IS the federation IRI post-clean — no lookup needed.
94
- const subjectPreds = graph.get(orgIri)
94
+ const subjectPreds = graph.get(entityIri)
95
95
  if (!subjectPreds) continue
96
96
 
97
97
  const valueMap = new Map()
@@ -115,17 +115,17 @@ export function loadFieldValuesByOrg(federationTtl, mappedTtl, liftedBySource) {
115
115
  }
116
116
  }
117
117
  }
118
- result.set(orgIri, valueMap)
118
+ result.set(entityIri, valueMap)
119
119
  }
120
120
  }
121
121
 
122
122
  // Layer in target-field values: indirect each :targetPredicate through the
123
- // org's literal predicate->value map from mapped.ttl. These are the values
123
+ // entity's literal predicate->value map from mapped.ttl. These are the values
124
124
  // that flow OUT of transform nodes (and equal the source value for direct
125
125
  // 1:1 mappings).
126
- for (const [orgIri, preds] of literalsByOrg) {
127
- if (!result.has(orgIri)) result.set(orgIri, new Map())
128
- const valueMap = result.get(orgIri)
126
+ for (const [entityIri, preds] of literalsByEntity) {
127
+ if (!result.has(entityIri)) result.set(entityIri, new Map())
128
+ const valueMap = result.get(entityIri)
129
129
  for (const [tfIri, predIri] of targetPredicateOf) {
130
130
  const v = preds.get(predIri)
131
131
  if (v) valueMap.set(tfIri, v)
@@ -1,9 +1,9 @@
1
- // Parses merged + provenance TTL into org objects: each field's values and the
1
+ // Parses merged + provenance TTL into entity objects: each field's values and the
2
2
  // :Source(s) that contributed them, ordered by config. Pure (ttl in → data out).
3
- // Reads: TTL strings passed by mergeOrgs.js; resolves sources via sourceMeta.js
4
- // Does: returns org[] (each {iri, label, type, fields[], sources[]})
3
+ // Reads: TTL strings passed by mergeEntities.js; resolves sources via sourceMeta.js
4
+ // Does: returns entity[] (each {iri, label, type, fields[], sources[]})
5
5
 
6
- import { CDP as NS, parseTtl, parseTtlStar, prefixesOf, shrink } from "@directory-builder/core/utils"
6
+ import { CDP as NS, parseTtl, prefixesOf, shrink } from "@directory-builder/core/utils"
7
7
  import { compareSources, loadSourceMeta } from "./sourceMeta.js"
8
8
 
9
9
  const PROV_DERIVED_FROM = "http://www.w3.org/ns/prov#wasDerivedFrom"
@@ -16,13 +16,11 @@ export function loadMerge(mergedTtl, provTtl, federationTtl = "") {
16
16
  const prefixes = { cdp: NS, ...prefixesOf(federationTtl) }
17
17
  const prefixedIri = (iri) => shrink(iri, prefixes)
18
18
  const mergedQuads = parseTtl(mergedTtl)
19
- const provQuads = parseTtlStar(provTtl)
19
+ const provQuads = parseTtl(provTtl)
20
20
  const sourceMeta = federationTtl ? loadSourceMeta(federationTtl) : new Map()
21
21
 
22
- // Each prov:wasDerivedFrom in provenance.ttl annotates a merged triple
23
- // `<<s p o>>` with the source record IRI it came from. n3.js exposes the
24
- // quoted-triple subject either directly as a Quad term, or via an
25
- // auto-generated reifier bnode + rdf:reifies triple — accept both shapes.
22
+ // provenance.ttl is RDF 1.2 reification: one reifier per derivation
23
+ // (`_:r rdf:reifies <<( s p o )>> ; prov:wasDerivedFrom record`).
26
24
  const reifies = new Map()
27
25
  for (const q of provQuads) {
28
26
  if (q.predicate.value === RDF_REIFIES && q.object.termType === "Quad") reifies.set(q.subject.value, q.object)
@@ -30,7 +28,7 @@ export function loadMerge(mergedTtl, provTtl, federationTtl = "") {
30
28
  const annotations = []
31
29
  for (const q of provQuads) {
32
30
  if (q.predicate.value !== PROV_DERIVED_FROM) continue
33
- const t = q.subject.termType === "Quad" ? q.subject : reifies.get(q.subject.value)
31
+ const t = reifies.get(q.subject.value)
34
32
  if (t) annotations.push({ s: t.subject.value, p: t.predicate.value, o: t.object.value, rec: q.object.value })
35
33
  }
36
34
  // Resolve each record to its :Source via cdp:fromSource (reified in
@@ -48,46 +46,46 @@ export function loadMerge(mergedTtl, provTtl, federationTtl = "") {
48
46
  }
49
47
 
50
48
  // Walk merged.ttl in parse order so card order = pipeline order.
51
- const orgs = []
52
- const orgIndex = new Map()
53
- const fieldIndexByOrg = new Map()
49
+ const entities = []
50
+ const entityIndex = new Map()
51
+ const fieldIndexByEntity = new Map()
54
52
  for (const q of mergedQuads) {
55
- const orgIri = q.subject.value
53
+ const entityIri = q.subject.value
56
54
  const predIri = q.predicate.value
57
55
  const value = q.object.value
58
56
 
59
- if (!orgIndex.has(orgIri)) {
60
- orgIndex.set(orgIri, orgs.length)
61
- fieldIndexByOrg.set(orgIri, new Map())
62
- orgs.push({ iri: orgIri, label: prefixedIri(orgIri), fields: [] })
57
+ if (!entityIndex.has(entityIri)) {
58
+ entityIndex.set(entityIri, entities.length)
59
+ fieldIndexByEntity.set(entityIri, new Map())
60
+ entities.push({ iri: entityIri, label: prefixedIri(entityIri), fields: [] })
63
61
  }
64
- const org = orgs[orgIndex.get(orgIri)]
65
- const fieldIndex = fieldIndexByOrg.get(orgIri)
62
+ const entity = entities[entityIndex.get(entityIri)]
63
+ const fieldIndex = fieldIndexByEntity.get(entityIri)
66
64
 
67
65
  // rdf:type carries the entity class — surface it in the card header
68
- // (see OrgCard), not as a field row.
69
- if (predIri === RDF_TYPE) { org.type = prefixedIri(value); continue }
66
+ // (see EntityCard), not as a field row.
67
+ if (predIri === RDF_TYPE) { entity.type = prefixedIri(value); continue }
70
68
 
71
69
  if (!fieldIndex.has(predIri)) {
72
- fieldIndex.set(predIri, org.fields.length)
73
- org.fields.push({ predicate: predIri, predLabel: prefixedIri(predIri), values: [] })
70
+ fieldIndex.set(predIri, entity.fields.length)
71
+ entity.fields.push({ predicate: predIri, predLabel: prefixedIri(predIri), values: [] })
74
72
  }
75
- const field = org.fields[fieldIndex.get(predIri)]
76
- const records = [...(provIndex.get(tripleKey(orgIri, predIri, value)) ?? [])]
73
+ const field = entity.fields[fieldIndex.get(predIri)]
74
+ const records = [...(provIndex.get(tripleKey(entityIri, predIri, value)) ?? [])]
77
75
  const sources = toSources(records)
78
76
  const displayValue = q.object.termType === "NamedNode" ? prefixedIri(value) : value
79
77
  field.values.push({ value: displayValue, raw: value, sources, records })
80
78
  }
81
79
 
82
80
  // Per-field: sort values by source-count desc so the most-supported one is index 0.
83
- // Per-org: one column per contributing record (two records from the same source
81
+ // Per-entity: one column per contributing record (two records from the same source
84
82
  // get two columns), ordered by source then record IRI.
85
- for (const org of orgs) {
86
- for (const f of org.fields) f.values.sort((a, b) => b.sources.length - a.sources.length)
83
+ for (const entity of entities) {
84
+ for (const f of entity.fields) f.values.sort((a, b) => b.sources.length - a.sources.length)
87
85
  const all = new Set()
88
- for (const f of org.fields) for (const v of f.values) for (const r of v.records) all.add(r)
89
- org.columns = [...all].map((r) => ({ record: r, source: sourceOfRecord.get(r) }))
86
+ for (const f of entity.fields) for (const v of f.values) for (const r of v.records) all.add(r)
87
+ entity.columns = [...all].map((r) => ({ record: r, source: sourceOfRecord.get(r) }))
90
88
  .sort((a, b) => compareSources(a.source, b.source, sourceMeta) || a.record.localeCompare(b.record))
91
89
  }
92
- return orgs
90
+ return entities
93
91
  }
@@ -3,7 +3,7 @@
3
3
  // Reads: federation, mapped, ingest-log TTL strings passed by Sources.jsx
4
4
  // Does: returns source[] ({iri, label, format, totalFields, mappedFields, records, …})
5
5
 
6
- import { CDP as NS, formatFamily, parseTtl, PATHS, sourceName, subjectsOfType } from "@directory-builder/core/utils"
6
+ import { CDP as NS, enabledSources, formatFamily, parseTtl, PATHS, sourceName } from "@directory-builder/core/utils"
7
7
 
8
8
  const PROV_AT_TIME = "http://www.w3.org/ns/prov#atTime"
9
9
  const RDFS_LABEL = "http://www.w3.org/2000/01/rdf-schema#label"
@@ -18,7 +18,7 @@ export function loadSources(federationTtl, mappedTtl, ingestLogTtl) {
18
18
  const mappedQuads = mappedTtl ? parseTtl(mappedTtl) : []
19
19
  const logQuads = ingestLogTtl ? parseTtl(ingestLogTtl) : []
20
20
 
21
- const sourceIris = subjectsOfType(fedQuads, `${NS}Source`)
21
+ const sourceIris = new Set(enabledSources(fedQuads))
22
22
 
23
23
  const props = new Map()
24
24
  const get = (iri) => {
@@ -68,7 +68,7 @@ export function loadSources(federationTtl, mappedTtl, ingestLogTtl) {
68
68
  if (!get(sourceIri).fetchUrl) get(sourceIri).staticSource = PATHS.staticDir(sourceName(sourceIri))
69
69
  }
70
70
 
71
- // Records: count distinct orgs in mapped.ttl per source via cdp:fromSource.
71
+ // Records: count distinct entities in mapped.ttl per source via cdp:fromSource.
72
72
  const FROM_SOURCE = `${NS}fromSource`
73
73
  const subjectsBySource = new Map()
74
74
  for (const q of mappedQuads) {
@@ -0,0 +1,15 @@
1
+ // Builds the entity lists for the Merge and Directory views, in one shared order.
2
+ // Reads: data/pipeline/{merged,provenance,final}.ttl, config/federation.ttl (via loadMerge.js)
3
+ // Does: exports mergedEntities and finalEntities (consumed by MergeTables, Directory)
4
+
5
+ import { loadMerge } from "./loadMerge.js"
6
+ import { isConflict } from "./EntityCard.jsx"
7
+ import { federationTtl, provenanceTtl as provTtl, mergedTtl, finalTtl } from "./instanceData.js"
8
+
9
+ const conflictCount = (entity) => entity.fields.reduce((n, f) => n + (isConflict(f) ? 1 : 0), 0)
10
+
11
+ // Merge view sorts by conflict count desc; the directory mirrors that order
12
+ // so the same entity sits in the same visual slot across pages.
13
+ export const mergedEntities = loadMerge(mergedTtl, provTtl, federationTtl).sort((a, b) => conflictCount(b) - conflictCount(a) || a.iri.localeCompare(b.iri))
14
+ const orderIndex = new Map(mergedEntities.map((o, i) => [o.iri, i]))
15
+ export const finalEntities = loadMerge(finalTtl, "", federationTtl).sort((a, b) => (orderIndex.get(a.iri) ?? Infinity) - (orderIndex.get(b.iri) ?? Infinity))
@@ -3,7 +3,7 @@
3
3
  // the PATHS conventions. JS never hardcodes a source name — it resolves records
4
4
  // to a :Source via cdp:fromSource.
5
5
  // Reads: TTL strings passed in (federation, mapped, ingest-log)
6
- // Does: returns lookup maps + helpers (used by loadMerge, OrgCard, MapGraph, MatchGraph)
6
+ // Does: returns lookup maps + helpers (used by loadMerge, EntityCard, MapGraph, MatchGraph)
7
7
 
8
8
  import { CDP as NS, parseTtl, PATHS, sourceName } from "@directory-builder/core/utils"
9
9
 
@@ -9,12 +9,12 @@ nav a:hover { color: #000 }
9
9
  nav a.active { font-weight: bold; color: #000 }
10
10
  main { flex: 1; min-height: 0 }
11
11
  .page { padding: 1rem }
12
- .org-card { margin: 0 0 1rem 0; padding: 0.6rem 0.9rem; border: 1px solid #ddd; border-radius: 4px; background: #fff }
13
- .org-card-header { font-size: 14px; margin-bottom: 0.4rem; color: #444 }
14
- .org-card-header code { background: #f5f5f5; padding: 2px 4px; border-radius: 2px; font-size: 12px }
15
- .org-card table { border-collapse: collapse; font-size: 13px }
16
- .org-card td { padding: 3px 8px 3px 0; vertical-align: middle }
17
- .org-card td:first-child { color: #666; white-space: nowrap; padding-right: 1rem }
12
+ .entity-card { margin: 0 0 1rem 0; padding: 0.6rem 0.9rem; border: 1px solid #ddd; border-radius: 4px; background: #fff }
13
+ .entity-card-header { font-size: 14px; margin-bottom: 0.4rem; color: #444 }
14
+ .entity-card-header code { background: #f5f5f5; padding: 2px 4px; border-radius: 2px; font-size: 12px }
15
+ .entity-card table { border-collapse: collapse; font-size: 13px }
16
+ .entity-card td { padding: 3px 8px 3px 0; vertical-align: middle }
17
+ .entity-card td:first-child { color: #666; white-space: nowrap; padding-right: 1rem }
18
18
  .value-text { display: inline-block; max-width: 60ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: middle }
19
19
  .source-tag { display: inline-block; padding: 1px 6px; margin-left: 4px; border-radius: 3px; background: #eee; color: #666; font-size: 11px; vertical-align: middle }
20
20
  .flip { margin-right: 8px; white-space: nowrap }
package/webapp/vite.js CHANGED
@@ -16,7 +16,7 @@ export function serveInstanceData({ root = process.cwd() } = {}) {
16
16
  // Own the 404: falling through would hit the SPA fallback, which
17
17
  // serves index.html with 200 — instanceData would parse HTML as TTL.
18
18
  if (!existsSync(file)) { res.statusCode = 404; return res.end() }
19
- res.setHeader("Content-Type", { js: "text/javascript", md: "text/markdown" }[rel.split(".").pop()] ?? "text/turtle")
19
+ res.setHeader("Content-Type", { js: "text/javascript", md: "text/markdown", sparql: "application/sparql-query" }[rel.split(".").pop()] ?? "text/turtle")
20
20
  res.end(readFileSync(file))
21
21
  }
22
22
  return {