Compare commits
2 commits
f2b10fca19
...
73b3b21017
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
73b3b21017 | ||
|
|
6781073d06 |
33 changed files with 325 additions and 69 deletions
189
.opencode/rules/no-duplicate-ontology-mappings.md
Normal file
189
.opencode/rules/no-duplicate-ontology-mappings.md
Normal file
|
|
@ -0,0 +1,189 @@
|
||||||
|
# Rule 52: No Duplicate Ontology Mappings
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Each ontology URI MUST appear in only ONE mapping category per schema element. A URI cannot simultaneously have multiple semantic relationships to the same class or slot.
|
||||||
|
|
||||||
|
## The Problem
|
||||||
|
|
||||||
|
LinkML provides five mapping annotation types based on SKOS vocabulary alignment:
|
||||||
|
|
||||||
|
| Property | SKOS Predicate | Meaning |
|
||||||
|
|----------|---------------|---------|
|
||||||
|
| `exact_mappings` | `skos:exactMatch` | "This IS that" (equivalent) |
|
||||||
|
| `close_mappings` | `skos:closeMatch` | "This is very similar to that" |
|
||||||
|
| `related_mappings` | `skos:relatedMatch` | "This is conceptually related to that" |
|
||||||
|
| `narrow_mappings` | `skos:narrowMatch` | "This is MORE SPECIFIC than that" |
|
||||||
|
| `broad_mappings` | `skos:broadMatch` | "This is MORE GENERAL than that" |
|
||||||
|
|
||||||
|
These relationships are **mutually exclusive**. A URI cannot simultaneously:
|
||||||
|
- BE the element (`exact_mappings`) AND be broader than it (`broad_mappings`)
|
||||||
|
- Be closely similar (`close_mappings`) AND be more general (`broad_mappings`)
|
||||||
|
|
||||||
|
## Anti-Pattern (WRONG)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# WRONG - schema:url appears in TWO mapping types
|
||||||
|
slots:
|
||||||
|
source_url:
|
||||||
|
slot_uri: prov:atLocation
|
||||||
|
exact_mappings:
|
||||||
|
- schema:url # Says "source_url IS schema:url"
|
||||||
|
broad_mappings:
|
||||||
|
- schema:url # Says "schema:url is MORE GENERAL than source_url"
|
||||||
|
```
|
||||||
|
|
||||||
|
This is a **logical contradiction**: `source_url` cannot simultaneously BE `schema:url` AND be more specific than `schema:url`.
|
||||||
|
|
||||||
|
## Correct Pattern
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# CORRECT - each URI appears in only ONE mapping type
|
||||||
|
slots:
|
||||||
|
source_url:
|
||||||
|
slot_uri: prov:atLocation
|
||||||
|
exact_mappings:
|
||||||
|
- schema:url # source_url IS schema:url
|
||||||
|
close_mappings:
|
||||||
|
- dcterms:source # Similar but not identical
|
||||||
|
```
|
||||||
|
|
||||||
|
## Decision Guide: Which Mapping to Keep
|
||||||
|
|
||||||
|
When a URI appears in multiple categories, keep the **most precise** one:
|
||||||
|
|
||||||
|
### Precedence Order (keep the first match)
|
||||||
|
|
||||||
|
1. **exact_mappings** - Strongest claim: semantic equivalence
|
||||||
|
2. **close_mappings** - Strong claim: nearly equivalent
|
||||||
|
3. **narrow_mappings** / **broad_mappings** - Hierarchical relationship
|
||||||
|
4. **related_mappings** - Weakest claim: conceptual association
|
||||||
|
|
||||||
|
### Decision Matrix
|
||||||
|
|
||||||
|
| If URI appears in... | Keep | Remove |
|
||||||
|
|---------------------|------|--------|
|
||||||
|
| exact + broad | exact | broad |
|
||||||
|
| exact + close | exact | close |
|
||||||
|
| exact + related | exact | related |
|
||||||
|
| close + broad | close | broad |
|
||||||
|
| close + related | close | related |
|
||||||
|
| related + broad | related | broad |
|
||||||
|
| narrow + broad | narrow | broad (contradictory!) |
|
||||||
|
|
||||||
|
### Special Case: narrow + broad
|
||||||
|
|
||||||
|
If a URI appears in BOTH `narrow_mappings` AND `broad_mappings`, this is a **data error** - the same URI cannot be both more specific AND more general. Investigate which is correct based on the ontology definition.
|
||||||
|
|
||||||
|
## Real Examples Fixed
|
||||||
|
|
||||||
|
### Example 1: source_url
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# BEFORE (wrong)
|
||||||
|
slots:
|
||||||
|
source_url:
|
||||||
|
exact_mappings:
|
||||||
|
- schema:url
|
||||||
|
broad_mappings:
|
||||||
|
- schema:url # Duplicate!
|
||||||
|
|
||||||
|
# AFTER (correct)
|
||||||
|
slots:
|
||||||
|
source_url:
|
||||||
|
exact_mappings:
|
||||||
|
- schema:url # Keep exact (strongest)
|
||||||
|
# broad_mappings removed
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 2: Custodian class
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# BEFORE (wrong)
|
||||||
|
classes:
|
||||||
|
Custodian:
|
||||||
|
close_mappings:
|
||||||
|
- cpov:PublicOrganisation
|
||||||
|
narrow_mappings:
|
||||||
|
- cpov:PublicOrganisation # Duplicate!
|
||||||
|
|
||||||
|
# AFTER (correct)
|
||||||
|
classes:
|
||||||
|
Custodian:
|
||||||
|
close_mappings:
|
||||||
|
- cpov:PublicOrganisation # Keep close (Custodian ≈ PublicOrganisation)
|
||||||
|
# narrow_mappings: use for URIs that are MORE SPECIFIC than Custodian
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 3: geonames_id (narrow + broad conflict)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# BEFORE (wrong - logical contradiction!)
|
||||||
|
slots:
|
||||||
|
geonames_id:
|
||||||
|
narrow_mappings:
|
||||||
|
- dcterms:identifier # Says geonames_id is MORE SPECIFIC
|
||||||
|
broad_mappings:
|
||||||
|
- dcterms:identifier # Says geonames_id is MORE GENERAL
|
||||||
|
|
||||||
|
# AFTER (correct)
|
||||||
|
slots:
|
||||||
|
geonames_id:
|
||||||
|
narrow_mappings:
|
||||||
|
- dcterms:identifier # geonames_id IS a specific type of identifier
|
||||||
|
# broad_mappings removed (was contradictory)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Detection Script
|
||||||
|
|
||||||
|
Run this to find duplicate mappings in the schema:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import yaml
|
||||||
|
from pathlib import Path
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
mapping_types = ['exact_mappings', 'close_mappings', 'related_mappings',
|
||||||
|
'narrow_mappings', 'broad_mappings']
|
||||||
|
|
||||||
|
dirs = [
|
||||||
|
Path('schemas/20251121/linkml/modules/slots'),
|
||||||
|
Path('schemas/20251121/linkml/modules/classes'),
|
||||||
|
]
|
||||||
|
|
||||||
|
for d in dirs:
|
||||||
|
for yaml_file in d.glob('*.yaml'):
|
||||||
|
try:
|
||||||
|
with open(yaml_file) as f:
|
||||||
|
content = yaml.safe_load(f)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
if not content:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for section in ['classes', 'slots']:
|
||||||
|
items = content.get(section, {})
|
||||||
|
if not isinstance(items, dict):
|
||||||
|
continue
|
||||||
|
for name, defn in items.items():
|
||||||
|
if not isinstance(defn, dict):
|
||||||
|
continue
|
||||||
|
uri_to_types = defaultdict(list)
|
||||||
|
for mt in mapping_types:
|
||||||
|
for uri in defn.get(mt, []) or []:
|
||||||
|
uri_to_types[uri].append(mt)
|
||||||
|
for uri, types in uri_to_types.items():
|
||||||
|
if len(types) > 1:
|
||||||
|
print(f"{yaml_file}: {name} - {uri} in {types}")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Validation Rule
|
||||||
|
|
||||||
|
**Pre-commit check**: Before committing LinkML schema changes, run the detection script. If any duplicates are found, the commit should fail.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [LinkML Mappings Documentation](https://linkml.io/linkml-model/latest/docs/mappings/)
|
||||||
|
- [SKOS Mapping Properties](https://www.w3.org/TR/skos-reference/#mapping)
|
||||||
|
- Rule 50: Ontology-to-LinkML Mapping Convention (parent rule)
|
||||||
|
- Rule 51: No Hallucinated Ontology References
|
||||||
50
AGENTS.md
50
AGENTS.md
|
|
@ -1528,6 +1528,56 @@ slots:
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### Rule 52: No Duplicate Ontology Mappings
|
||||||
|
|
||||||
|
🚨 **CRITICAL**: Each ontology URI MUST appear in only ONE mapping category per schema element. A URI cannot have multiple semantic relationships to the same class or slot.
|
||||||
|
|
||||||
|
**The Problem**: LinkML mapping properties (`exact_mappings`, `close_mappings`, `related_mappings`, `narrow_mappings`, `broad_mappings`) are mutually exclusive based on SKOS semantics. The same URI appearing in multiple categories creates logical contradictions.
|
||||||
|
|
||||||
|
**Anti-Pattern (WRONG)**:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
slots:
|
||||||
|
source_url:
|
||||||
|
exact_mappings:
|
||||||
|
- schema:url # Says "source_url IS schema:url"
|
||||||
|
broad_mappings:
|
||||||
|
- schema:url # Says "schema:url is MORE GENERAL than source_url"
|
||||||
|
# CONTRADICTION: source_url cannot both BE schema:url AND be more specific than it
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct Pattern**:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
slots:
|
||||||
|
source_url:
|
||||||
|
exact_mappings:
|
||||||
|
- schema:url # Keep only the most precise mapping
|
||||||
|
```
|
||||||
|
|
||||||
|
**Decision Guide** - When duplicates found, keep the MOST PRECISE:
|
||||||
|
|
||||||
|
| Precedence | Mapping Type | Meaning |
|
||||||
|
|------------|--------------|---------|
|
||||||
|
| 1st (keep) | `exact_mappings` | Semantic equivalence |
|
||||||
|
| 2nd | `close_mappings` | Nearly equivalent |
|
||||||
|
| 3rd | `narrow_mappings` | This is more specific |
|
||||||
|
| 4th | `broad_mappings` | This is more general |
|
||||||
|
| 5th | `related_mappings` | Conceptual association |
|
||||||
|
|
||||||
|
**Quick Reference**:
|
||||||
|
|
||||||
|
| If URI in... | Action |
|
||||||
|
|--------------|--------|
|
||||||
|
| exact + broad | Keep exact, remove broad |
|
||||||
|
| close + broad | Keep close, remove broad |
|
||||||
|
| related + broad | Keep related, remove broad |
|
||||||
|
| narrow + broad | ERROR - investigate (contradictory) |
|
||||||
|
|
||||||
|
**See**: `.opencode/rules/no-duplicate-ontology-mappings.md` for complete documentation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Appendix: Full Rule Content (No .opencode Equivalent)
|
## Appendix: Full Rule Content (No .opencode Equivalent)
|
||||||
|
|
||||||
The following rules have no separate .opencode file and are preserved in full:
|
The following rules have no separate .opencode file and are preserved in full:
|
||||||
|
|
|
||||||
|
|
@ -623,8 +623,9 @@ function parseTurtleOntology(content: string): ParsedOntology {
|
||||||
let blankNodeDepth = 0; // Track depth of blank node blocks to skip
|
let blankNodeDepth = 0; // Track depth of blank node blocks to skip
|
||||||
let inMultiLineString = false; // Track if we're inside a multi-line triple-quoted string
|
let inMultiLineString = false; // Track if we're inside a multi-line triple-quoted string
|
||||||
let multiLineQuoteChar = ''; // The quote character(s) for the multi-line string
|
let multiLineQuoteChar = ''; // The quote character(s) for the multi-line string
|
||||||
|
let baseUri: string | null = null; // @base directive for relative URI resolution
|
||||||
|
|
||||||
// First pass: extract prefixes
|
// First pass: extract prefixes and @base
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const trimmed = line.trim();
|
const trimmed = line.trim();
|
||||||
if (trimmed.startsWith('@prefix') || trimmed.startsWith('PREFIX')) {
|
if (trimmed.startsWith('@prefix') || trimmed.startsWith('PREFIX')) {
|
||||||
|
|
@ -632,9 +633,58 @@ function parseTurtleOntology(content: string): ParsedOntology {
|
||||||
if (match) {
|
if (match) {
|
||||||
prefixes[match[1] || ''] = match[2];
|
prefixes[match[1] || ''] = match[2];
|
||||||
}
|
}
|
||||||
|
} else if (trimmed.startsWith('@base') || trimmed.startsWith('BASE')) {
|
||||||
|
// Extract @base directive: @base <http://example.org/> or BASE <http://example.org/>
|
||||||
|
const match = trimmed.match(/@?base\s+<([^>]+)>/i);
|
||||||
|
if (match) {
|
||||||
|
baseUri = match[1];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper to expand URIs with base URI support for relative URIs
|
||||||
|
const expand = (uri: string): string => {
|
||||||
|
if (!uri) return uri;
|
||||||
|
|
||||||
|
// Remove angle brackets if present
|
||||||
|
let cleaned = uri.trim().replace(/^</, '').replace(/>$/, '');
|
||||||
|
|
||||||
|
// Handle empty relative URI <> - refers to the base URI itself
|
||||||
|
if (cleaned === '' && baseUri) {
|
||||||
|
return baseUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle relative URIs starting with # (like #Address)
|
||||||
|
// These resolve against the base URI
|
||||||
|
if (cleaned.startsWith('#') && baseUri) {
|
||||||
|
return baseUri + cleaned;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's already a full URI
|
||||||
|
if (cleaned.startsWith('http://') || cleaned.startsWith('https://')) {
|
||||||
|
return cleaned;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle prefixed URIs (like vcard:Address, owl:Class)
|
||||||
|
const colonIndex = cleaned.indexOf(':');
|
||||||
|
if (colonIndex > 0) {
|
||||||
|
const prefix = cleaned.substring(0, colonIndex);
|
||||||
|
const localName = cleaned.substring(colonIndex + 1);
|
||||||
|
|
||||||
|
if (prefixes[prefix]) {
|
||||||
|
return prefixes[prefix] + localName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle unprefixed URIs with default prefix (empty prefix ":")
|
||||||
|
// e.g., ":Address" with PREFIX : <http://example.org/#>
|
||||||
|
if (cleaned.startsWith(':') && prefixes['']) {
|
||||||
|
return prefixes[''] + cleaned.substring(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return cleaned;
|
||||||
|
};
|
||||||
|
|
||||||
// Second pass: parse triples
|
// Second pass: parse triples
|
||||||
let lineNum = 0;
|
let lineNum = 0;
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
|
|
@ -734,16 +784,16 @@ function parseTurtleOntology(content: string): ParsedOntology {
|
||||||
if (!trimmed.startsWith(';') && !trimmed.startsWith(',') && !isIndentedLine) {
|
if (!trimmed.startsWith(';') && !trimmed.startsWith(',') && !isIndentedLine) {
|
||||||
// Process previous subject if exists
|
// Process previous subject if exists
|
||||||
if (currentSubject && currentTriples.length > 0) {
|
if (currentSubject && currentTriples.length > 0) {
|
||||||
processSubject(currentSubject, currentTriples, prefixes, classes, properties, individuals);
|
processSubject(currentSubject, currentTriples, prefixes, classes, properties, individuals, baseUri);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start new subject
|
// Start new subject
|
||||||
const parts = splitTurtleLine(trimmed, prefixes);
|
const parts = splitTurtleLine(trimmed, prefixes);
|
||||||
if (parts.length >= 3) {
|
if (parts.length >= 3) {
|
||||||
// Full triple on one line: subject predicate object(s)
|
// Full triple on one line: subject predicate object(s)
|
||||||
currentSubject = expandUri(parts[0], prefixes);
|
currentSubject = expand(parts[0]);
|
||||||
// Handle 'a' shorthand for rdf:type
|
// Handle 'a' shorthand for rdf:type
|
||||||
const predicate = parts[1] === 'a' ? NAMESPACES.rdf + 'type' : expandUri(parts[1], prefixes);
|
const predicate = parts[1] === 'a' ? NAMESPACES.rdf + 'type' : expand(parts[1]);
|
||||||
lastPredicate = predicate; // Track for comma continuations
|
lastPredicate = predicate; // Track for comma continuations
|
||||||
// Handle comma-separated values (e.g., "Subject a Class1, Class2")
|
// Handle comma-separated values (e.g., "Subject a Class1, Class2")
|
||||||
currentTriples = [];
|
currentTriples = [];
|
||||||
|
|
@ -752,7 +802,7 @@ function parseTurtleOntology(content: string): ParsedOntology {
|
||||||
}
|
}
|
||||||
} else if (parts.length === 1 && !trimmed.endsWith('.')) {
|
} else if (parts.length === 1 && !trimmed.endsWith('.')) {
|
||||||
// Subject alone on a line (DCAT3 style): dcat:Catalog
|
// Subject alone on a line (DCAT3 style): dcat:Catalog
|
||||||
currentSubject = expandUri(parts[0], prefixes);
|
currentSubject = expand(parts[0]);
|
||||||
currentTriples = [];
|
currentTriples = [];
|
||||||
lastPredicate = null;
|
lastPredicate = null;
|
||||||
}
|
}
|
||||||
|
|
@ -761,7 +811,7 @@ function parseTurtleOntology(content: string): ParsedOntology {
|
||||||
const parts = splitTurtleLine(trimmed.substring(1).trim(), prefixes);
|
const parts = splitTurtleLine(trimmed.substring(1).trim(), prefixes);
|
||||||
if (parts.length >= 2) {
|
if (parts.length >= 2) {
|
||||||
// Handle 'a' shorthand for rdf:type
|
// Handle 'a' shorthand for rdf:type
|
||||||
const predicate = parts[0] === 'a' ? NAMESPACES.rdf + 'type' : expandUri(parts[0], prefixes);
|
const predicate = parts[0] === 'a' ? NAMESPACES.rdf + 'type' : expand(parts[0]);
|
||||||
lastPredicate = predicate; // Track for comma continuations
|
lastPredicate = predicate; // Track for comma continuations
|
||||||
// Handle comma-separated values
|
// Handle comma-separated values
|
||||||
for (let i = 1; i < parts.length; i++) {
|
for (let i = 1; i < parts.length; i++) {
|
||||||
|
|
@ -774,7 +824,7 @@ function parseTurtleOntology(content: string): ParsedOntology {
|
||||||
const parts = splitTurtleLine(trimmed, prefixes);
|
const parts = splitTurtleLine(trimmed, prefixes);
|
||||||
if (parts.length >= 2) {
|
if (parts.length >= 2) {
|
||||||
// Handle 'a' shorthand for rdf:type
|
// Handle 'a' shorthand for rdf:type
|
||||||
const predicate = parts[0] === 'a' ? NAMESPACES.rdf + 'type' : expandUri(parts[0], prefixes);
|
const predicate = parts[0] === 'a' ? NAMESPACES.rdf + 'type' : expand(parts[0]);
|
||||||
lastPredicate = predicate; // Track for comma continuations
|
lastPredicate = predicate; // Track for comma continuations
|
||||||
// Handle comma-separated values (e.g., "a rdfs:Class, owl:Class")
|
// Handle comma-separated values (e.g., "a rdfs:Class, owl:Class")
|
||||||
// Each part after predicate is a separate object
|
// Each part after predicate is a separate object
|
||||||
|
|
@ -894,7 +944,8 @@ function processSubject(
|
||||||
prefixes: Record<string, string>,
|
prefixes: Record<string, string>,
|
||||||
classes: Map<string, OntologyClass>,
|
classes: Map<string, OntologyClass>,
|
||||||
properties: Map<string, OntologyProperty>,
|
properties: Map<string, OntologyProperty>,
|
||||||
individuals: Map<string, OntologyIndividual>
|
individuals: Map<string, OntologyIndividual>,
|
||||||
|
baseUri?: string | null
|
||||||
): void {
|
): void {
|
||||||
const types: string[] = [];
|
const types: string[] = [];
|
||||||
const labels: LangString[] = [];
|
const labels: LangString[] = [];
|
||||||
|
|
@ -915,7 +966,7 @@ function processSubject(
|
||||||
|
|
||||||
for (const triple of triples) {
|
for (const triple of triples) {
|
||||||
const { predicate, object } = triple;
|
const { predicate, object } = triple;
|
||||||
const expandedObject = expandUri(object, prefixes);
|
const expandedObject = expandUri(object, prefixes, baseUri);
|
||||||
|
|
||||||
if (predicate === NAMESPACES.rdf + 'type') {
|
if (predicate === NAMESPACES.rdf + 'type') {
|
||||||
types.push(expandedObject);
|
types.push(expandedObject);
|
||||||
|
|
@ -1506,19 +1557,33 @@ function parseJsonLdOntology(content: string): ParsedOntology {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Expand prefixed URI to full URI
|
* Expand prefixed URI to full URI
|
||||||
|
* @param uri - The URI to expand (can be prefixed like "owl:Class", relative like "#Address", or full)
|
||||||
|
* @param prefixes - Map of prefix to namespace
|
||||||
|
* @param baseUri - Optional base URI for resolving relative URIs (from @base directive)
|
||||||
*/
|
*/
|
||||||
export function expandUri(uri: string, prefixes: Record<string, string>): string {
|
export function expandUri(uri: string, prefixes: Record<string, string>, baseUri?: string | null): string {
|
||||||
if (!uri) return uri;
|
if (!uri) return uri;
|
||||||
|
|
||||||
// Remove angle brackets if present
|
// Remove angle brackets if present
|
||||||
let cleaned = uri.trim().replace(/^</, '').replace(/>$/, '');
|
let cleaned = uri.trim().replace(/^</, '').replace(/>$/, '');
|
||||||
|
|
||||||
|
// Handle empty relative URI <> - refers to the base URI itself
|
||||||
|
if (cleaned === '' && baseUri) {
|
||||||
|
return baseUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle relative URIs starting with # (like #Address)
|
||||||
|
// These resolve against the base URI
|
||||||
|
if (cleaned.startsWith('#') && baseUri) {
|
||||||
|
return baseUri + cleaned;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if it's already a full URI
|
// Check if it's already a full URI
|
||||||
if (cleaned.startsWith('http://') || cleaned.startsWith('https://')) {
|
if (cleaned.startsWith('http://') || cleaned.startsWith('https://')) {
|
||||||
return cleaned;
|
return cleaned;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle prefixed URIs
|
// Handle prefixed URIs (like vcard:Address, owl:Class)
|
||||||
const colonIndex = cleaned.indexOf(':');
|
const colonIndex = cleaned.indexOf(':');
|
||||||
if (colonIndex > 0) {
|
if (colonIndex > 0) {
|
||||||
const prefix = cleaned.substring(0, colonIndex);
|
const prefix = cleaned.substring(0, colonIndex);
|
||||||
|
|
@ -1529,6 +1594,12 @@ export function expandUri(uri: string, prefixes: Record<string, string>): string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle unprefixed URIs with default prefix (empty prefix ":")
|
||||||
|
// e.g., ":Address" with PREFIX : <http://example.org/#>
|
||||||
|
if (cleaned.startsWith(':') && prefixes['']) {
|
||||||
|
return prefixes[''] + cleaned.substring(1);
|
||||||
|
}
|
||||||
|
|
||||||
return cleaned;
|
return cleaned;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"generated": "2026-01-13T12:50:30.701Z",
|
"generated": "2026-01-13T14:57:26.896Z",
|
||||||
"schemaRoot": "/schemas/20251121/linkml",
|
"schemaRoot": "/schemas/20251121/linkml",
|
||||||
"totalFiles": 2886,
|
"totalFiles": 2886,
|
||||||
"categoryCounts": {
|
"categoryCounts": {
|
||||||
|
|
|
||||||
|
|
@ -146,7 +146,6 @@ classes:
|
||||||
- pico:Person
|
- pico:Person
|
||||||
- schema:Person
|
- schema:Person
|
||||||
- schema:Organization
|
- schema:Organization
|
||||||
- cpov:PublicOrganisation
|
|
||||||
- rico:CorporateBody
|
- rico:CorporateBody
|
||||||
- org:Organization
|
- org:Organization
|
||||||
- foaf:Person
|
- foaf:Person
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,6 @@ classes:
|
||||||
- foaf:homepage
|
- foaf:homepage
|
||||||
close_mappings:
|
close_mappings:
|
||||||
- schema:WebApplication
|
- schema:WebApplication
|
||||||
- schema:SoftwareApplication
|
|
||||||
- dcat:Catalog
|
- dcat:Catalog
|
||||||
- dcat:DataService
|
- dcat:DataService
|
||||||
- crm:E73_Information_Object
|
- crm:E73_Information_Object
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,6 @@ classes:
|
||||||
related_mappings:
|
related_mappings:
|
||||||
- schema:Museum
|
- schema:Museum
|
||||||
- schema:ArtGallery
|
- schema:ArtGallery
|
||||||
- aat:300005768
|
|
||||||
slots:
|
slots:
|
||||||
- has_or_had_admission_fee
|
- has_or_had_admission_fee
|
||||||
- current_exhibition
|
- current_exhibition
|
||||||
|
|
|
||||||
|
|
@ -28,5 +28,3 @@ slots:
|
||||||
description: Natural history specimen data standard
|
description: Natural history specimen data standard
|
||||||
related_mappings:
|
related_mappings:
|
||||||
- dcterms:conformsTo
|
- dcterms:conformsTo
|
||||||
broad_mappings:
|
|
||||||
- dcterms:conformsTo
|
|
||||||
|
|
|
||||||
|
|
@ -29,5 +29,3 @@ slots:
|
||||||
'
|
'
|
||||||
close_mappings:
|
close_mappings:
|
||||||
- dcterms:type
|
- dcterms:type
|
||||||
broad_mappings:
|
|
||||||
- dcterms:type
|
|
||||||
|
|
|
||||||
|
|
@ -14,5 +14,3 @@ slots:
|
||||||
description: The extracted value from the web source. This is the actual content claimed to exist at the XPath location.
|
description: The extracted value from the web source. This is the actual content claimed to exist at the XPath location.
|
||||||
close_mappings:
|
close_mappings:
|
||||||
- rdf:value
|
- rdf:value
|
||||||
broad_mappings:
|
|
||||||
- rdf:value
|
|
||||||
|
|
|
||||||
|
|
@ -14,5 +14,3 @@ slots:
|
||||||
- schema:description
|
- schema:description
|
||||||
related_mappings:
|
related_mappings:
|
||||||
- dcterms:description
|
- dcterms:description
|
||||||
broad_mappings:
|
|
||||||
- dcterms:description
|
|
||||||
|
|
|
||||||
|
|
@ -21,5 +21,3 @@ slots:
|
||||||
- dcterms:coverage
|
- dcterms:coverage
|
||||||
close_mappings:
|
close_mappings:
|
||||||
- schema:about
|
- schema:about
|
||||||
broad_mappings:
|
|
||||||
- dcterms:coverage
|
|
||||||
|
|
|
||||||
|
|
@ -71,5 +71,3 @@ slots:
|
||||||
- schema:numberOfItems
|
- schema:numberOfItems
|
||||||
close_mappings:
|
close_mappings:
|
||||||
- dcterms:extent
|
- dcterms:extent
|
||||||
broad_mappings:
|
|
||||||
- dcterms:extent
|
|
||||||
|
|
|
||||||
|
|
@ -41,5 +41,3 @@ slots:
|
||||||
'
|
'
|
||||||
close_mappings:
|
close_mappings:
|
||||||
- dcterms:type
|
- dcterms:type
|
||||||
broad_mappings:
|
|
||||||
- dcterms:type
|
|
||||||
|
|
|
||||||
|
|
@ -11,5 +11,3 @@ slots:
|
||||||
'
|
'
|
||||||
close_mappings:
|
close_mappings:
|
||||||
- prov:wasGeneratedBy
|
- prov:wasGeneratedBy
|
||||||
broad_mappings:
|
|
||||||
- prov:wasGeneratedBy
|
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ slots:
|
||||||
conflict_status:
|
conflict_status:
|
||||||
status: destroyed
|
status: destroyed
|
||||||
date: "2023-12-08"
|
date: "2023-12-08"
|
||||||
description: "Destroyed by Israeli airstrike\ \ during Gaza conflict"
|
description: "Destroyed by Israeli airstrike during Gaza conflict"
|
||||||
sources:
|
sources:
|
||||||
- "LAP Gaza Report 2024"
|
- "LAP Gaza Report 2024"
|
||||||
|
|
||||||
|
|
@ -49,7 +49,7 @@ slots:
|
||||||
status: damaged
|
status: damaged
|
||||||
date: "2022-03-01"
|
date: "2022-03-01"
|
||||||
is_rebuilding: true
|
is_rebuilding: true
|
||||||
description: "Damaged\ \ by shelling, currently under restoration"
|
description: "Damaged by shelling, currently under restoration"
|
||||||
sources:
|
sources:
|
||||||
- "UNESCO Ukraine heritage monitoring"
|
- "UNESCO Ukraine heritage monitoring"
|
||||||
|
|
||||||
|
|
@ -62,5 +62,3 @@ slots:
|
||||||
- hc:time_of_destruction
|
- hc:time_of_destruction
|
||||||
- hc:ConflictStatus
|
- hc:ConflictStatus
|
||||||
- hc:ConflictStatusEnum
|
- hc:ConflictStatusEnum
|
||||||
broad_mappings:
|
|
||||||
- adms:status
|
|
||||||
|
|
|
||||||
|
|
@ -26,5 +26,3 @@ slots:
|
||||||
exact_mappings:
|
exact_mappings:
|
||||||
- schema:email
|
- schema:email
|
||||||
- vcard:hasEmail
|
- vcard:hasEmail
|
||||||
broad_mappings:
|
|
||||||
- schema:email
|
|
||||||
|
|
|
||||||
|
|
@ -19,5 +19,3 @@ slots:
|
||||||
- schema:startDate
|
- schema:startDate
|
||||||
related_mappings:
|
related_mappings:
|
||||||
- dcterms:date
|
- dcterms:date
|
||||||
broad_mappings:
|
|
||||||
- dcterms:date
|
|
||||||
|
|
|
||||||
|
|
@ -38,5 +38,3 @@ slots:
|
||||||
- CustodianTimelineEvent overrides range to TimelineExtractionMethodEnum
|
- CustodianTimelineEvent overrides range to TimelineExtractionMethodEnum
|
||||||
close_mappings:
|
close_mappings:
|
||||||
- prov:wasGeneratedBy
|
- prov:wasGeneratedBy
|
||||||
broad_mappings:
|
|
||||||
- prov:wasGeneratedBy
|
|
||||||
|
|
|
||||||
|
|
@ -16,5 +16,3 @@ slots:
|
||||||
'
|
'
|
||||||
close_mappings:
|
close_mappings:
|
||||||
- skos:note
|
- skos:note
|
||||||
broad_mappings:
|
|
||||||
- skos:note
|
|
||||||
|
|
|
||||||
|
|
@ -38,8 +38,6 @@ slots:
|
||||||
- gn:geonamesID
|
- gn:geonamesID
|
||||||
narrow_mappings:
|
narrow_mappings:
|
||||||
- dcterms:identifier
|
- dcterms:identifier
|
||||||
broad_mappings:
|
|
||||||
- dcterms:identifier
|
|
||||||
comments:
|
comments:
|
||||||
- Used by Settlement, AuxiliaryPlace, and GeoSpatialPlace classes
|
- Used by Settlement, AuxiliaryPlace, and GeoSpatialPlace classes
|
||||||
- 'Lookup URL: https://www.geonames.org/{geonames_id}/'
|
- 'Lookup URL: https://www.geonames.org/{geonames_id}/'
|
||||||
|
|
|
||||||
|
|
@ -32,5 +32,3 @@ slots:
|
||||||
\n iiif_image_api_version: \"3.0\"\n```\n"
|
\n iiif_image_api_version: \"3.0\"\n```\n"
|
||||||
close_mappings:
|
close_mappings:
|
||||||
- dcat:endpointURL
|
- dcat:endpointURL
|
||||||
broad_mappings:
|
|
||||||
- dcat:endpointURL
|
|
||||||
|
|
|
||||||
|
|
@ -29,5 +29,3 @@ slots:
|
||||||
range: string
|
range: string
|
||||||
related_mappings:
|
related_mappings:
|
||||||
- dcterms:source
|
- dcterms:source
|
||||||
broad_mappings:
|
|
||||||
- dcterms:source
|
|
||||||
|
|
|
||||||
|
|
@ -9,5 +9,3 @@ slots:
|
||||||
\ (ISIL)\n- 'Q190804' (Wikidata)\n- '148691498' (VIAF)\n- '0000 0001 2146 5765' (ISNI with spaces)\n"
|
\ (ISIL)\n- 'Q190804' (Wikidata)\n- '148691498' (VIAF)\n- '0000 0001 2146 5765' (ISNI with spaces)\n"
|
||||||
exact_mappings:
|
exact_mappings:
|
||||||
- rdf:value
|
- rdf:value
|
||||||
broad_mappings:
|
|
||||||
- rdf:value
|
|
||||||
|
|
|
||||||
|
|
@ -54,9 +54,8 @@ slots:
|
||||||
|
|
||||||
close_mappings:
|
close_mappings:
|
||||||
- schema:parentOrganization
|
- schema:parentOrganization
|
||||||
- schema:memberOf
|
|
||||||
- rico:isOrWasSubordinateTo
|
- rico:isOrWasSubordinateTo
|
||||||
|
|
||||||
broad_mappings:
|
broad_mappings:
|
||||||
- schema:memberOf
|
- schema:memberOf
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,5 +32,3 @@ slots:
|
||||||
- org:linkedTo
|
- org:linkedTo
|
||||||
close_mappings:
|
close_mappings:
|
||||||
- prov:wasAttributedTo
|
- prov:wasAttributedTo
|
||||||
broad_mappings:
|
|
||||||
- prov:wasAttributedTo
|
|
||||||
|
|
|
||||||
|
|
@ -37,5 +37,3 @@ slots:
|
||||||
'
|
'
|
||||||
close_mappings:
|
close_mappings:
|
||||||
- dcterms:conformsTo
|
- dcterms:conformsTo
|
||||||
broad_mappings:
|
|
||||||
- dcterms:conformsTo
|
|
||||||
|
|
|
||||||
|
|
@ -43,5 +43,3 @@ slots:
|
||||||
range: string
|
range: string
|
||||||
close_mappings:
|
close_mappings:
|
||||||
- dcterms:source
|
- dcterms:source
|
||||||
broad_mappings:
|
|
||||||
- dcterms:source
|
|
||||||
|
|
|
||||||
|
|
@ -19,5 +19,3 @@ slots:
|
||||||
\ types (e.g., digital archive + aggregator).\n"
|
\ types (e.g., digital archive + aggregator).\n"
|
||||||
close_mappings:
|
close_mappings:
|
||||||
- dcterms:type
|
- dcterms:type
|
||||||
broad_mappings:
|
|
||||||
- dcterms:type
|
|
||||||
|
|
|
||||||
|
|
@ -27,5 +27,3 @@ slots:
|
||||||
- PersonWebClaim overrides range to RetrievalAgentEnum
|
- PersonWebClaim overrides range to RetrievalAgentEnum
|
||||||
close_mappings:
|
close_mappings:
|
||||||
- prov:wasAttributedTo
|
- prov:wasAttributedTo
|
||||||
broad_mappings:
|
|
||||||
- prov:wasAttributedTo
|
|
||||||
|
|
|
||||||
|
|
@ -13,5 +13,3 @@ slots:
|
||||||
'
|
'
|
||||||
related_mappings:
|
related_mappings:
|
||||||
- dcterms:type
|
- dcterms:type
|
||||||
broad_mappings:
|
|
||||||
- dcterms:type
|
|
||||||
|
|
|
||||||
|
|
@ -46,8 +46,6 @@ slots:
|
||||||
exact_mappings:
|
exact_mappings:
|
||||||
- schema:url
|
- schema:url
|
||||||
- dcterms:source
|
- dcterms:source
|
||||||
broad_mappings:
|
|
||||||
- schema:url
|
|
||||||
comments:
|
comments:
|
||||||
- Maps to pav:retrievedFrom for provenance tracking
|
- Maps to pav:retrievedFrom for provenance tracking
|
||||||
- Essential for web claim verification workflows
|
- Essential for web claim verification workflows
|
||||||
|
|
|
||||||
|
|
@ -40,5 +40,3 @@ slots:
|
||||||
- schema:sameAs
|
- schema:sameAs
|
||||||
narrow_mappings:
|
narrow_mappings:
|
||||||
- dcterms:identifier
|
- dcterms:identifier
|
||||||
broad_mappings:
|
|
||||||
- dcterms:identifier
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue