2026-07-26 21:28:14 +02:00
#!/usr/bin/env python3
"""
PR Data Meta Model - T-Box viewer generator
2026-07-29 09:03:59 +02:00
============================================
Reads the T-Box Turtle file and emits a self-contained HTML view.
Build artefact: regenerate, never edit.
2026-07-26 21:28:14 +02:00
USAGE
2026-07-29 09:03:59 +02:00
python3 scripts/generate_tbox_viewer.py [in.ttl] [out.html]
defaults: ontology/pr_metamodel.ttl -> generated/pr_metamodel_viewer.html
2026-07-26 21:28:14 +02:00
2026-07-29 09:03:59 +02:00
Same visual language as the SODH viewer, with one substitution. Where SODH
lays its columns out by owning domain, the T-Box has no domains -- its
orthogonal axis is PROVENANCE: what governance asserts against what a
harvester observes. The two-colour code carries that instead, blue for
tbox: v1.5 - PhysicalRelation devient DataStructure et point d'extension declare pour le non-tabulaire, hasSource devient sourcedFrom vers un System instancie, storedIn sert deux granularites et deprecie materializedAs, references devient qualifiedBy, primaryLocation sous-propriete de storedIn, hasShortLabel sur 34 classes. CuratedObject devient DefinedObject et ObservedObject devient CapturedObject jusque dans les IRI, curationMode devient authoringMode. Correction de fond : PhysicalLayerObject ne porte plus la provenance, chaque classe de la couche la declare - un System est defined, tout ce qui est en dessous est captured. instances: 5 systemes crees, 151 sourcedFrom, 15 BO nettoyes de hasSource, labels courts, versions alignees
2026-07-30 15:13:03 +02:00
defined and dark gold for captured, and the seam is what crosses between them.
2026-07-26 21:28:14 +02:00
2026-07-29 09:03:59 +02:00
No dependency: parses the Turtle subset this file uses, so it runs on the DSM
system python without a venv.
2026-07-26 21:28:14 +02:00
"""
import json
2026-07-29 09:03:59 +02:00
import os
2026-07-26 21:28:14 +02:00
import re
import sys
from collections import OrderedDict
PR = " https://ontology.pernod-ricard.com/metamodel/ "
WANTED = {
" a " : " type " , " rdf:type " : " type " ,
" rdfs:label " : " label " , " rdfs:comment " : " comment " ,
" rdfs:subClassOf " : " subClassOf " , " rdfs:subPropertyOf " : " subPropertyOf " ,
" rdfs:domain " : " domain " , " rdfs:range " : " range " ,
" owl:inverseOf " : " inverseOf " , " owl:deprecated " : " deprecated " ,
" dcterms:isReplacedBy " : " isReplacedBy " ,
tbox: v1.5 - PhysicalRelation devient DataStructure et point d'extension declare pour le non-tabulaire, hasSource devient sourcedFrom vers un System instancie, storedIn sert deux granularites et deprecie materializedAs, references devient qualifiedBy, primaryLocation sous-propriete de storedIn, hasShortLabel sur 34 classes. CuratedObject devient DefinedObject et ObservedObject devient CapturedObject jusque dans les IRI, curationMode devient authoringMode. Correction de fond : PhysicalLayerObject ne porte plus la provenance, chaque classe de la couche la declare - un System est defined, tout ce qui est en dessous est captured. instances: 5 systemes crees, 151 sourcedFrom, 15 BO nettoyes de hasSource, labels courts, versions alignees
2026-07-30 15:13:03 +02:00
" pr:harvestSource " : " harvestSource " , " pr:authoringMode " : " authoringMode " ,
2026-08-05 09:13:12 +02:00
" pr:acronym " : " acronym " , " pr:shortLabel " : " shortLabel " ,
" pr:isAbstract " : " isAbstract " ,
2026-07-26 21:28:14 +02:00
}
2026-07-29 09:03:59 +02:00
LAYER_ROOT = OrderedDict ( [
( " pr:OwnershipLayerObject " , ( " ownership " , " Ownership & Categorization " ,
" who is accountable " ) ) ,
( " pr:BusinessLayerObject " , ( " business " , " Business " , " what it means " ) ) ,
( " pr:LogicalLayerObject " , ( " logical " , " Logical " ,
" how it is structured " ) ) ,
tbox: v1.5 - PhysicalRelation devient DataStructure et point d'extension declare pour le non-tabulaire, hasSource devient sourcedFrom vers un System instancie, storedIn sert deux granularites et deprecie materializedAs, references devient qualifiedBy, primaryLocation sous-propriete de storedIn, hasShortLabel sur 34 classes. CuratedObject devient DefinedObject et ObservedObject devient CapturedObject jusque dans les IRI, curationMode devient authoringMode. Correction de fond : PhysicalLayerObject ne porte plus la provenance, chaque classe de la couche la declare - un System est defined, tout ce qui est en dessous est captured. instances: 5 systemes crees, 151 sourcedFrom, 15 BO nettoyes de hasSource, labels courts, versions alignees
2026-07-30 15:13:03 +02:00
( " pr:DeliveryLayerObject " , ( " delivery " , " Delivery " ,
" what we promise, to whom, until when " ) ) ,
2026-07-29 09:03:59 +02:00
( " pr:PhysicalLayerObject " , ( " physical " , " Physical " ,
" what actually runs " ) ) ,
( " pr:ConsumptionLayerObject " , ( " consumption " , " Consumption " ,
" what people open in the morning " ) ) ,
] )
# The only relations crossing between what is asserted and what is harvested.
SEAM = { " pr:materializedAs " , " pr:storedIn " , " pr:physicalizedIn " }
# ---------------------------------------------------------------- parsing
2026-07-26 21:28:14 +02:00
def strip_comments ( text ) :
out , i , n = [ ] , 0 , len ( text )
while i < n :
c = text [ i ]
if c == ' " ' :
j = i + 1
while j < n and text [ j ] != ' " ' :
j + = 2 if text [ j ] == " \\ " else 1
j = min ( j + 1 , n )
out . append ( text [ i : j ] ) ; i = j ; continue
if c == " # " :
j = text . find ( " \n " , i )
i = n if j == - 1 else j
continue
out . append ( c ) ; i + = 1
return " " . join ( out )
2026-07-29 09:03:59 +02:00
def split_outside ( text , sep ) :
parts , buf , i , n = [ ] , [ ] , 0 , len ( text )
2026-07-26 21:28:14 +02:00
while i < n :
c = text [ i ]
if c == ' " ' :
j = i + 1
while j < n and text [ j ] != ' " ' :
j + = 2 if text [ j ] == " \\ " else 1
j = min ( j + 1 , n )
buf . append ( text [ i : j ] ) ; i = j ; continue
2026-07-29 09:03:59 +02:00
if c == sep :
2026-07-26 21:28:14 +02:00
parts . append ( " " . join ( buf ) ) ; buf = [ ] ; i + = 1 ; continue
buf . append ( c ) ; i + = 1
2026-07-29 09:03:59 +02:00
parts . append ( " " . join ( buf ) )
2026-07-26 21:28:14 +02:00
return parts
def unquote ( tok ) :
tok = tok . strip ( )
if tok . startswith ( ' " ' ) :
end = tok . rindex ( ' " ' )
2026-07-29 09:03:59 +02:00
return tok [ 1 : end ] . replace ( ' \\ " ' , ' " ' ) . replace ( " \\ \\ " , " \\ " )
2026-07-26 21:28:14 +02:00
return tok
2026-07-29 09:03:59 +02:00
def parse ( path ) :
""" Turtle subset reader: enough for an instance file, keeps ordering. """
text = strip_comments ( open ( path , encoding = " utf-8 " ) . read ( ) )
2026-07-26 21:28:14 +02:00
subjects = OrderedDict ( )
2026-07-29 09:03:59 +02:00
for stmt in split_outside ( text , " . " ) :
2026-07-26 21:28:14 +02:00
stmt = stmt . strip ( )
2026-07-29 09:03:59 +02:00
if not stmt or not stmt . startswith ( " ex: " ) :
2026-07-26 21:28:14 +02:00
continue
2026-07-29 09:03:59 +02:00
clauses = split_outside ( stmt , " ; " )
head = clauses [ 0 ] . strip ( )
2026-07-26 21:28:14 +02:00
m = re . match ( r " ^( \ S+) \ s+(.*)$ " , head , re . S )
if not m :
continue
subj , rest = m . group ( 1 ) , m . group ( 2 )
2026-07-29 09:03:59 +02:00
rec = subjects . setdefault ( subj , { " id " : subj } )
for chunk in [ rest ] + [ c . strip ( ) for c in clauses [ 1 : ] ] :
2026-07-26 21:28:14 +02:00
chunk = chunk . strip ( )
if not chunk :
continue
pm = re . match ( r " ^( \ S+) \ s+(.*)$ " , chunk , re . S )
if not pm :
continue
pred , objs = pm . group ( 1 ) , pm . group ( 2 )
2026-07-29 09:03:59 +02:00
key = " type " if pred in ( " a " , " rdf:type " ) else pred . split ( " : " ) [ - 1 ]
vals = [ unquote ( t ) for t in split_outside ( objs , " , " ) if t . strip ( ) ]
if key in MULTI or key in rec :
rec . setdefault ( key , [ ] )
if not isinstance ( rec [ key ] , list ) :
rec [ key ] = [ rec [ key ] ]
rec [ key ] + = vals
else :
rec [ key ] = vals [ 0 ] if len ( vals ) == 1 else vals
2026-07-26 21:28:14 +02:00
return subjects
2026-07-29 09:03:59 +02:00
def as_list ( v ) :
if v is None :
return [ ]
return v if isinstance ( v , list ) else [ v ]
2026-07-26 21:28:14 +02:00
2026-07-29 09:03:59 +02:00
# ---------------------------------------------------------------- model
2026-07-26 21:28:14 +02:00
def first ( rec , key , default = " " ) :
v = rec . get ( key )
return v [ 0 ] if v else default
2026-07-29 09:03:59 +02:00
def parse_tbox ( path ) :
""" Turtle subset reader for the T-Box: pr: subjects, WANTED predicates. """
text = strip_comments ( open ( path , encoding = " utf-8 " ) . read ( ) )
subjects = OrderedDict ( )
for stmt in split_outside ( text , " . " ) :
stmt = stmt . strip ( )
if not stmt . startswith ( " pr: " ) :
continue
chunks = split_outside ( stmt , " ; " )
m = re . match ( r " ^( \ S+) \ s+(.*)$ " , chunks [ 0 ] . strip ( ) , re . S )
if not m :
continue
rec = subjects . setdefault ( m . group ( 1 ) , { } )
for chunk in [ m . group ( 2 ) ] + [ c . strip ( ) for c in chunks [ 1 : ] ] :
chunk = chunk . strip ( )
if not chunk :
continue
pm = re . match ( r " ^( \ S+) \ s+(.*)$ " , chunk , re . S )
if not pm :
continue
key = WANTED . get ( pm . group ( 1 ) )
if key :
rec . setdefault ( key , [ ] ) . extend (
unquote ( t ) for t in split_outside ( pm . group ( 2 ) , " , " ) if t . strip ( ) )
return subjects
2026-07-26 21:28:14 +02:00
def build ( subjects ) :
2026-07-29 09:03:59 +02:00
classes , objp , datp , annp , individuals = { } , { } , { } , { } , { }
2026-07-26 21:28:14 +02:00
for term , rec in subjects . items ( ) :
types = rec . get ( " type " , [ ] )
if " owl:Class " in types :
classes [ term ] = rec
elif " owl:ObjectProperty " in types :
2026-07-29 09:03:59 +02:00
objp [ term ] = rec
2026-07-26 21:28:14 +02:00
elif " owl:DatatypeProperty " in types :
2026-07-29 09:03:59 +02:00
datp [ term ] = rec
2026-07-26 21:28:14 +02:00
elif " owl:AnnotationProperty " in types :
2026-07-29 09:03:59 +02:00
annp [ term ] = rec
2026-07-26 21:28:14 +02:00
elif types and types [ 0 ] . startswith ( " pr: " ) :
individuals . setdefault ( types [ 0 ] , [ ] ) . append ( term )
def ancestry ( term , seen = None ) :
seen = seen or set ( )
if term in seen :
return [ ]
seen . add ( term )
out = [ term ]
for parent in classes . get ( term , { } ) . get ( " subClassOf " , [ ] ) :
out + = ancestry ( parent , seen )
return out
2026-07-29 09:03:59 +02:00
parents = { c : first ( classes [ c ] , " subClassOf " ) for c in classes }
nodes = { }
2026-07-26 21:28:14 +02:00
for term , rec in classes . items ( ) :
anc = ancestry ( term )
2026-07-29 09:03:59 +02:00
layer = next ( ( LAYER_ROOT [ a ] [ 0 ] for a in anc if a in LAYER_ROOT ) , None )
if layer is None :
if " pr:Actor " in anc :
layer = " actors "
elif term in ( " pr:ActivationStatus " , " pr:Environment " , " pr:SystemType " ) :
layer = " vocab "
2026-07-26 21:28:14 +02:00
else :
2026-07-29 09:03:59 +02:00
layer = " roots "
tbox: v1.5 - PhysicalRelation devient DataStructure et point d'extension declare pour le non-tabulaire, hasSource devient sourcedFrom vers un System instancie, storedIn sert deux granularites et deprecie materializedAs, references devient qualifiedBy, primaryLocation sous-propriete de storedIn, hasShortLabel sur 34 classes. CuratedObject devient DefinedObject et ObservedObject devient CapturedObject jusque dans les IRI, curationMode devient authoringMode. Correction de fond : PhysicalLayerObject ne porte plus la provenance, chaque classe de la couche la declare - un System est defined, tout ce qui est en dessous est captured. instances: 5 systemes crees, 151 sourcedFrom, 15 BO nettoyes de hasSource, labels courts, versions alignees
2026-07-30 15:13:03 +02:00
# provenance is INFERRED from the type-of chain, never asserted:
# a Sub-domain is Defined because it is a kind of Ownership Layer
# Object, which is a kind of Defined Object. No separate edge exists
# because none is needed -- the subclass chain already says it.
prov = ( " captured " if " pr:CapturedObject " in anc
else " defined " if " pr:DefinedObject " in anc else " structural " )
2026-07-29 09:03:59 +02:00
depth , p , guard = 0 , parents . get ( term ) , 0
while p and p in classes and guard < 8 :
depth + = 1 ; guard + = 1 ; p = parents . get ( p )
nodes [ term ] = {
" id " : term , " kind " : " Class " ,
2026-07-26 21:28:14 +02:00
" label " : first ( rec , " label " , term . split ( " : " ) [ 1 ] ) ,
tbox: v1.5 - PhysicalRelation devient DataStructure et point d'extension declare pour le non-tabulaire, hasSource devient sourcedFrom vers un System instancie, storedIn sert deux granularites et deprecie materializedAs, references devient qualifiedBy, primaryLocation sous-propriete de storedIn, hasShortLabel sur 34 classes. CuratedObject devient DefinedObject et ObservedObject devient CapturedObject jusque dans les IRI, curationMode devient authoringMode. Correction de fond : PhysicalLayerObject ne porte plus la provenance, chaque classe de la couche la declare - un System est defined, tout ce qui est en dessous est captured. instances: 5 systemes crees, 151 sourcedFrom, 15 BO nettoyes de hasSource, labels courts, versions alignees
2026-07-30 15:13:03 +02:00
" shortLabel " : first ( rec , " shortLabel " ) ,
2026-07-29 09:03:59 +02:00
" layer " : layer , " prov " : prov , " depth " : depth ,
" parent " : parents . get ( term , " " ) ,
2026-07-26 21:28:14 +02:00
" comment " : first ( rec , " comment " ) ,
" harvest " : first ( rec , " harvestSource " ) ,
tbox: v1.5 - PhysicalRelation devient DataStructure et point d'extension declare pour le non-tabulaire, hasSource devient sourcedFrom vers un System instancie, storedIn sert deux granularites et deprecie materializedAs, references devient qualifiedBy, primaryLocation sous-propriete de storedIn, hasShortLabel sur 34 classes. CuratedObject devient DefinedObject et ObservedObject devient CapturedObject jusque dans les IRI, curationMode devient authoringMode. Correction de fond : PhysicalLayerObject ne porte plus la provenance, chaque classe de la couche la declare - un System est defined, tout ce qui est en dessous est captured. instances: 5 systemes crees, 151 sourcedFrom, 15 BO nettoyes de hasSource, labels courts, versions alignees
2026-07-30 15:13:03 +02:00
" authoring " : first ( rec , " authoringMode " ) ,
2026-07-29 09:03:59 +02:00
" acronym " : first ( rec , " acronym " ) ,
2026-07-26 21:28:14 +02:00
" deprecated " : first ( rec , " deprecated " ) in ( " true " , " True " ) ,
" replacedBy " : first ( rec , " isReplacedBy " ) ,
2026-08-05 09:13:12 +02:00
# read, not guessed: inferring it from "has subclasses" flagged
# Metric as abstract, and Metric is one of the most instantiated
# classes in the model
" abstract " : first ( rec , " isAbstract " ) in ( " true " , " True " ) ,
2026-07-29 09:03:59 +02:00
" members " : [ { " id " : m , " label " : first ( subjects . get ( m , { } ) , " label " ,
m . split ( " : " ) [ 1 ] ) }
for m in individuals . get ( term , [ ] ) ] ,
}
def prop ( term , rec , kind ) :
return {
" id " : term , " kind " : kind ,
2026-07-26 21:28:14 +02:00
" label " : first ( rec , " label " , term . split ( " : " ) [ 1 ] ) ,
tbox: v1.5 - PhysicalRelation devient DataStructure et point d'extension declare pour le non-tabulaire, hasSource devient sourcedFrom vers un System instancie, storedIn sert deux granularites et deprecie materializedAs, references devient qualifiedBy, primaryLocation sous-propriete de storedIn, hasShortLabel sur 34 classes. CuratedObject devient DefinedObject et ObservedObject devient CapturedObject jusque dans les IRI, curationMode devient authoringMode. Correction de fond : PhysicalLayerObject ne porte plus la provenance, chaque classe de la couche la declare - un System est defined, tout ce qui est en dessous est captured. instances: 5 systemes crees, 151 sourcedFrom, 15 BO nettoyes de hasSource, labels courts, versions alignees
2026-07-30 15:13:03 +02:00
" shortLabel " : first ( rec , " shortLabel " ) ,
2026-07-26 21:28:14 +02:00
" comment " : first ( rec , " comment " ) ,
2026-07-29 09:03:59 +02:00
" domain " : first ( rec , " domain " ) , " range " : first ( rec , " range " ) ,
2026-07-26 21:28:14 +02:00
" inverseOf " : first ( rec , " inverseOf " ) ,
" subPropertyOf " : first ( rec , " subPropertyOf " ) ,
tbox: v1.5 - PhysicalRelation devient DataStructure et point d'extension declare pour le non-tabulaire, hasSource devient sourcedFrom vers un System instancie, storedIn sert deux granularites et deprecie materializedAs, references devient qualifiedBy, primaryLocation sous-propriete de storedIn, hasShortLabel sur 34 classes. CuratedObject devient DefinedObject et ObservedObject devient CapturedObject jusque dans les IRI, curationMode devient authoringMode. Correction de fond : PhysicalLayerObject ne porte plus la provenance, chaque classe de la couche la declare - un System est defined, tout ce qui est en dessous est captured. instances: 5 systemes crees, 151 sourcedFrom, 15 BO nettoyes de hasSource, labels courts, versions alignees
2026-07-30 15:13:03 +02:00
" authoring " : first ( rec , " authoringMode " ) ,
2026-07-29 09:03:59 +02:00
" harvest " : first ( rec , " harvestSource " ) ,
2026-07-26 21:28:14 +02:00
" functional " : " owl:FunctionalProperty " in rec . get ( " type " , [ ] ) ,
" transitive " : " owl:TransitiveProperty " in rec . get ( " type " , [ ] ) ,
" deprecated " : first ( rec , " deprecated " ) in ( " true " , " True " ) ,
" replacedBy " : first ( rec , " isReplacedBy " ) ,
2026-07-29 09:03:59 +02:00
" seam " : term in SEAM ,
" prov " : " " , " layer " : " " ,
2026-07-26 21:28:14 +02:00
}
2026-07-29 09:03:59 +02:00
layers = [ { " id " : v [ 0 ] , " title " : v [ 1 ] , " tag " : v [ 2 ] } for v in LAYER_ROOT . values ( ) ]
layers = ( [ { " id " : " roots " , " title " : " Model roots " , " tag " : " how a term got here " } ]
+ layers
+ [ { " id " : " actors " , " title " : " Actors " , " tag " : " people and teams " } ,
{ " id " : " vocab " , " title " : " Vocabularies " , " tag " : " controlled value lists " } ] )
return { " nodes " : list ( nodes . values ( ) ) ,
" relations " : [ prop ( t , r , " Relation " ) for t , r in objp . items ( ) ] ,
" attributes " : [ prop ( t , r , " Attribute " ) for t , r in datp . items ( ) ] ,
" bridges " : [ prop ( t , r , " Bridge " ) for t , r in annp . items ( )
if t . startswith ( " pr:denotes " ) ] ,
" layers " : layers }
2026-07-26 21:28:14 +02:00
2026-07-29 09:03:59 +02:00
# ---------------------------------------------------------------- output
def main ( ) :
root = os . path . dirname ( os . path . dirname ( os . path . abspath ( __file__ ) ) )
src = sys . argv [ 1 ] if len ( sys . argv ) > 1 else os . path . join (
root , " ontology " , " pr_metamodel.ttl " )
out = sys . argv [ 2 ] if len ( sys . argv ) > 2 else os . path . join (
root , " generated " , " pr_metamodel_viewer.html " )
2026-07-26 21:28:14 +02:00
2026-07-29 09:03:59 +02:00
model = build ( parse_tbox ( src ) )
2026-07-26 21:28:14 +02:00
payload = json . dumps ( model , ensure_ascii = False , separators = ( " , " , " : " ) )
2026-07-29 09:03:59 +02:00
html = TEMPLATE . replace ( " __MODEL__ " , payload ) . replace (
" __SRC__ " , os . path . basename ( src ) )
os . makedirs ( os . path . dirname ( out ) , exist_ok = True )
open ( out , " w " , encoding = " utf-8 " ) . write ( html )
live_c = [ n for n in model [ " nodes " ] if not n [ " deprecated " ] ]
dep = ( [ n for n in model [ " nodes " ] if n [ " deprecated " ] ]
+ [ r for r in model [ " relations " ] + model [ " attributes " ] if r [ " deprecated " ] ] )
print ( " PR META MODEL - T-Box viewer " )
print ( " source : %s " % src )
print ( " output : %s " % out )
print ( " classes %d | relations %d | attributes %d | bridges %d | deprecated %d "
% ( len ( live_c ) ,
len ( [ r for r in model [ " relations " ] if not r [ " deprecated " ] ] ) ,
len ( [ a for a in model [ " attributes " ] if not a [ " deprecated " ] ] ) ,
len ( model [ " bridges " ] ) , len ( dep ) ) )
for l in model [ " layers " ] :
c = len ( [ n for n in live_c if n [ " layer " ] == l [ " id " ] ] )
if c :
print ( " %-28s %d " % ( l [ " title " ] , c ) )
tbox: v1.5 - PhysicalRelation devient DataStructure et point d'extension declare pour le non-tabulaire, hasSource devient sourcedFrom vers un System instancie, storedIn sert deux granularites et deprecie materializedAs, references devient qualifiedBy, primaryLocation sous-propriete de storedIn, hasShortLabel sur 34 classes. CuratedObject devient DefinedObject et ObservedObject devient CapturedObject jusque dans les IRI, curationMode devient authoringMode. Correction de fond : PhysicalLayerObject ne porte plus la provenance, chaque classe de la couche la declare - un System est defined, tout ce qui est en dessous est captured. instances: 5 systemes crees, 151 sourcedFrom, 15 BO nettoyes de hasSource, labels courts, versions alignees
2026-07-30 15:13:03 +02:00
for p in ( " defined " , " captured " , " structural " ) :
2026-07-29 09:03:59 +02:00
c = len ( [ n for n in live_c if n [ " prov " ] == p ] )
print ( " %-28s %d " % ( p , c ) )
2026-07-26 21:28:14 +02:00
TEMPLATE = r """ <!DOCTYPE html>
<html lang= " en " >
<head>
<meta charset= " UTF-8 " >
<meta name= " viewport " content= " width=device-width, initial-scale=1.0 " >
2026-07-29 09:03:59 +02:00
<title>PR Data Meta Model · T-Box</title>
2026-07-26 21:28:14 +02:00
<style>
:root {
2026-07-29 09:03:59 +02:00
--navy:#0A1F44; --gold:#B8935A; --ink:#1A1A1A; --bg:#F7F6F2;
--panel:#fff; --rule:#E4E2DA; --muted:#6B6B6B; --soft:#F1EFE8;
--mono:ui-monospace, " SF Mono " ,SFMono-Regular,Menlo,Consolas,monospace;
2026-07-26 21:28:14 +02:00
--sans:-apple-system,BlinkMacSystemFont, " Segoe UI " ,Helvetica,Arial,sans-serif;
2026-07-29 09:03:59 +02:00
--serif:Georgia, " Times New Roman " ,serif;
2026-07-26 21:28:14 +02:00
}
* { box-sizing:border-box;-webkit-tap-highlight-color:transparent}
2026-07-29 09:03:59 +02:00
html,body { margin:0;height:100 % }
2026-07-26 21:28:14 +02:00
body { font-family:var(--sans);background:var(--bg);color:var(--ink);
display:flex;flex-direction:column;overflow:hidden}
:focus-visible { outline:2px solid var(--gold);outline-offset:2px}
2026-07-29 09:03:59 +02:00
header { background:var(--navy);color:#fff;padding:11px 20px;display:flex;
align-items:center;gap:18px;flex-shrink:0}
header h1 { margin:0;font-family:var(--mono);font-size:12.5px;font-weight:600;
letter-spacing:.17em;text-transform:uppercase}
header .sub { font-size:11px;color:rgba(255,255,255,.6);margin-top:3px}
.tabs { margin-left:auto;display:flex;gap:2px;background:rgba(255,255,255,.08);
padding:3px;border-radius:4px}
.tabs button { padding:6px 14px;border:none;background:transparent;color:rgba(255,255,255,.7);
font-family:var(--mono);font-size:10px;letter-spacing:.09em;text-transform:uppercase;
cursor:pointer;border-radius:3px;font-weight:600}
.tabs button.on { background:#fff;color:var(--navy)}
main { flex:1;overflow-y:auto;padding:22px 20px 60px}
.wrap { max-width:1220px;margin:0 auto}
.lede { font-family:var(--serif);font-size:21px;line-height:1.45;color:var(--navy);
margin:0 0 6px;max-width:860px}
.lede b { color:var(--gold)}
.sublede { font-size:13px;color:var(--muted);margin:0 0 22px;max-width:860px;line-height:1.55}
.kpis { display:flex;gap:26px;flex-wrap:wrap;padding:16px 0 20px;
border-top:1px solid var(--rule);border-bottom:1px solid var(--rule);margin-bottom:24px}
.kpi b { display:block;font-family:var(--serif);font-size:29px;color:var(--navy);line-height:1}
.kpi.warn b { color:var(--gold)}
.kpi span { font-size:9.5px;text-transform:uppercase;letter-spacing:.1em;color:var(--muted);
display:block;margin-top:5px}
h2.sec { font-family:var(--mono);font-size:10px;letter-spacing:.15em;text-transform:uppercase;
color:var(--muted);margin:30px 0 12px;font-weight:600}
.grid { display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:14px}
.card { background:var(--panel);border:1px solid var(--rule);border-radius:4px;
padding:0;overflow:hidden;cursor:pointer;transition:box-shadow .15s}
.card:hover { box-shadow:0 3px 14px rgba(10,31,68,.10)}
.card .top { height:4px}
.card .body { padding:15px 17px 16px}
.card .role { font-family:var(--mono);font-size:8.5px;letter-spacing:.13em;
2026-07-26 21:28:14 +02:00
text-transform:uppercase;font-weight:600}
2026-07-29 09:03:59 +02:00
.card h3 { margin:5px 0 2px;font-family:var(--serif);font-size:19px;color:var(--navy)}
.card .who { font-size:11.5px;color:var(--muted);margin-bottom:12px}
.card .nums { display:flex;gap:15px;flex-wrap:wrap;padding-top:11px;border-top:1px solid var(--rule)}
.card .nums div { font-size:10.5px;color:var(--muted)}
.card .nums b { display:block;font-family:var(--mono);font-size:15px;color:var(--ink)}
.flag { display:inline-block;margin-top:11px;padding:4px 9px;border-radius:3px;
background:#FBF3E4;border:1px solid var(--gold);color:#7A5B2A;
font-family:var(--mono);font-size:9.5px;letter-spacing:.05em}
table { width:100 % ;border-collapse:collapse;background:var(--panel);font-size:12.5px}
th { text-align:left;padding:9px 11px;font-family:var(--mono);font-size:9px;
letter-spacing:.1em;text-transform:uppercase;color:var(--muted);
border-bottom:1.5px solid var(--navy);white-space:nowrap;background:var(--panel);
position:sticky;top:0;cursor:pointer}
td { padding:8px 11px;border-bottom:1px solid var(--rule);vertical-align:top}
tr.arb td { background:#FDFAF3}
tr:hover td { background:var(--soft)}
tr { cursor:pointer}
.dot { display:inline-block;width:8px;height:8px;border-radius:2px;margin-right:7px;
vertical-align:baseline}
.mono { font-family:var(--mono);font-size:10.5px;color:var(--muted)}
.pill { display:inline-block;padding:2px 7px;border-radius:2px;font-family:var(--mono);
font-size:8.5px;letter-spacing:.06em;text-transform:uppercase;border:1px solid}
.bar { display:flex;height:26px;border-radius:3px;overflow:hidden;margin:6px 0 4px}
.bar div { position:relative}
.legend { display:flex;gap:16px;flex-wrap:wrap;font-size:11px;color:var(--muted);margin-bottom:20px}
.legend i { display:inline-block;width:10px;height:10px;border-radius:2px;margin-right:6px}
.filters { display:flex;gap:8px;flex-wrap:wrap;margin-bottom:14px;align-items:center}
.filters input { padding:7px 11px;border:1px solid var(--rule);border-radius:3px;
font-family:var(--mono);font-size:12px;min-width:180px;background:#fff}
.filters button { padding:6px 12px;border:1px solid var(--rule);background:#fff;
border-radius:3px;font-family:var(--mono);font-size:10px;cursor:pointer;
text-transform:uppercase;letter-spacing:.07em;color:var(--muted)}
.filters button.on { background:var(--navy);color:#fff;border-color:var(--navy)}
#insp { position:fixed;top:0;right:0;width:390px;max-width:100 % ;height:100 % ;
2026-07-26 21:28:14 +02:00
background:var(--panel);border-left:1px solid var(--rule);
2026-07-29 09:03:59 +02:00
box-shadow:-8px 0 30px rgba(10,31,68,.13);overflow-y:auto;padding:22px 22px 60px;
transform:translateX(100 % );transition:transform .22s;z-index:50}
2026-07-26 21:28:14 +02:00
#insp.open { transform:translateX(0)}
2026-07-29 09:03:59 +02:00
#insp .x { position:absolute;top:13px;right:15px;border:none;background:none;
font-size:23px;cursor:pointer;color:var(--muted);line-height:1}
.inh { font-style:italic;color:#8A6A28;font-size:9px;text-transform:uppercase;
letter-spacing:.06em;font-family:var(--mono)}
.backlink { background:none;border:none;padding:0 0 10px;font-family:var(--mono);
font-size:10px;color:var(--navy);cursor:pointer;letter-spacing:.05em}
.kind { font-family:var(--mono);font-size:9px;letter-spacing:.14em;
2026-07-26 21:28:14 +02:00
text-transform:uppercase;font-weight:600}
2026-07-29 09:03:59 +02:00
.gctl .gsec { font-family:var(--mono);font-size:8.5px;letter-spacing:.12em;
text-transform:uppercase;color:var(--muted);margin:9px 0 4px;font-weight:600}
.gctl select { width:100 % ;padding:4px;font-family:var(--mono);font-size:10px;
border:1px solid var(--rule);border-radius:3px;margin-top:5px}
.lanelabel { font-family:var(--mono);font-size:10px;letter-spacing:.1em;font-weight:600}
.elabel { font-family:var(--mono);font-size:7.5px;fill:#5A6270;text-anchor:middle;
pointer-events:none}
.elbg { fill:#F4F2EC;pointer-events:none}
.elabel.dim,.elbg.dim {opacity:.06}
#insp h3 { margin:7px 0 2px;font-family:var(--serif);font-size:21px;color:var(--navy)}
#insp .iri { font-family:var(--mono);font-size:10.5px;color:var(--muted);margin-bottom:15px}
#insp h4 { font-family:var(--mono);font-size:9px;letter-spacing:.14em;text-transform:uppercase;
color:var(--muted);margin:19px 0 7px;border-top:1px solid var(--rule);padding-top:11px}
#insp p { font-size:12.5px;line-height:1.6;margin:0 0 8px}
.row { display:flex;justify-content:space-between;gap:12px;padding:5px 0;
border-bottom:1px dotted var(--rule);font-size:12px}
.row span:last-child { color:var(--muted);text-align:right}
2026-07-26 21:28:14 +02:00
.jump { background:none;border:none;padding:0;font:inherit;color:var(--navy);
2026-07-29 09:03:59 +02:00
cursor:pointer;text-decoration:underline;text-underline-offset:2px;font-size:12px}
.note { background:#FBF3E4;border-left:3px solid var(--gold);padding:10px 12px;
font-size:11.5px;line-height:1.55;color:#6B5426}
/* ---- graph ---- */
.gwrap { position:relative;height:calc(100vh - 250px);min-height:420px;
border:1px solid var(--rule);border-radius:4px;overflow:hidden;background:
linear-gradient(90deg,rgba(10,31,68,.03) 1px,transparent 1px) 0 0/26px 26px,
linear-gradient(180deg,#FCFBF7,#F0EEE5);touch-action:none}
.gwrap svg { display:block;width:100 % ;height:100 % ;cursor:grab}
.gwrap svg.drag { cursor:grabbing}
.bandrect {opacity:.5}
.bandlabel { font-family:var(--mono);font-size:9.5px;letter-spacing:.14em;
text-transform:uppercase;font-weight:600}
.bandtag { font-size:9px;fill:var(--muted);font-style:italic}
.gnode { cursor:pointer}
.gnode rect { stroke-width:1.4}
.gnode text { font-size:9px;text-anchor:middle;pointer-events:none;font-weight:500}
.gnode.dim {opacity:.11}
.gnode.sel rect { stroke-width:3}
.gedge { fill:none;stroke:#A6ABB5;stroke-width:1;marker-end:url(#ga)}
.gedge.cross { stroke:#B8935A;stroke-width:1.8;marker-end:url(#gg)}
.gedge.dim {opacity:.05}
.gedge.hot { stroke:var(--navy);stroke-width:2.4;marker-end:url(#gn)}
.gctl { position:absolute;left:12px;top:12px;background:rgba(255,255,255,.94);
border:1px solid var(--rule);border-radius:4px;padding:9px 11px;font-size:11px;
max-width:230px}
.gctl .seg { display:flex;border:1px solid var(--rule);border-radius:3px;overflow:hidden;
margin-bottom:8px}
.gctl .seg button { flex:1;padding:5px;border:none;background:#fff;cursor:pointer;
font-family:var(--mono);font-size:9px;text-transform:uppercase;letter-spacing:.06em;
color:var(--muted)}
.gctl .seg button+button { border-left:1px solid var(--rule)}
.gctl .seg button.on { background:var(--navy);color:#fff}
.gctl label { display:flex;align-items:center;gap:7px;padding:3px 0;cursor:pointer}
.gctl label i { display:inline-block;width:8px;height:8px;border-radius:2px;flex-shrink:0}
.gctl input { accent-color:var(--navy)}
.gzoom { position:absolute;right:12px;bottom:12px;display:flex;gap:3px;
background:#fff;border:1px solid var(--rule);border-radius:4px;padding:3px}
.gzoom button { width:30px;height:30px;border:none;background:#fff;cursor:pointer;
font-size:15px;color:var(--navy);border-radius:3px}
.ghint { position:absolute;left:12px;bottom:12px;font-family:var(--mono);font-size:9px;
color:var(--muted);background:rgba(255,255,255,.85);padding:4px 8px;border-radius:3px}
footer { background:var(--navy);color:rgba(255,255,255,.5);padding:7px 20px;
font-family:var(--mono);font-size:9px;letter-spacing:.06em;flex-shrink:0;
display:flex;justify-content:space-between}
@media(max-width:760px) {
.tabs { width:100 % ;margin:8px 0 0}header { flex-wrap:wrap}
.kpis { gap:18px}.kpi b { font-size:23px}
table { font-size:11.5px}td,th { padding:7px 8px}
.hide-s { display:none}
2026-07-26 21:28:14 +02:00
}
@media(prefers-reduced-motion:reduce) { * { transition:none!important}}
</style>
</head>
<body>
<header>
<div>
<h1>PR Data Meta Model · T-Box</h1>
2026-08-05 09:13:12 +02:00
<div class= " sub " >Six layers, crossed with what governance defines and what a harvester captures</div>
2026-07-26 21:28:14 +02:00
</div>
2026-07-29 09:03:59 +02:00
<div class= " tabs " role= " tablist " >
<button id= " t-scope " class= " on " >Overview</button>
<button id= " t-model " >Layers</button>
<button id= " t-graph " >Graph</button>
2026-08-05 09:13:12 +02:00
<button id= " t-props " >Properties</button>
2026-07-29 09:03:59 +02:00
<button id= " t-dep " >Deprecated</button>
2026-07-26 21:28:14 +02:00
</div>
</header>
2026-07-29 09:03:59 +02:00
<main><div class= " wrap " id= " view " ></div></main>
<div id= " insp " role= " dialog " aria-label= " Details " >
<button class= " x " id= " insp-x " aria-label= " Close " >×</button>
<div id= " insp-body " ></div>
</div>
2026-07-26 21:28:14 +02:00
<footer>
<span>Build artefact of __SRC__ — regenerate, never edit</span>
2026-07-29 09:03:59 +02:00
<span id= " f-count " ></span>
2026-07-26 21:28:14 +02:00
</footer>
<script>
var M = __MODEL__;
2026-07-29 09:03:59 +02:00
var byId = {} ;
M.nodes.forEach(function(n) { byId[n.id] = n; });
M.relations.concat(M.attributes, M.bridges).forEach(function(p) { byId[p.id] = p; });
var view = " scope " , filt = { q: " " , kind: " " };
function esc(s) { return (s|| " " ).replace(/&/g, " & " ).replace(/</g, " < " ); }
function clean(s) { return (s|| " " ).replace( " (deprecated) " , " " ); }
document.getElementById( " f-count " ).textContent =
M.nodes.filter(function(n) { return !n.deprecated;}).length + " classes \ u00b7 "
+ M.relations.filter(function(r) { return !r.deprecated;}).length + " relations " ;
/* ============================================================ GRAPH */
var G = { mode: " strata " , provs:[], sel:null, hot:null, labels:true,
layers: {} ,
view: { x:0,y:0,s:1}, nodes:[], edges:[]};
var NW=126, NH=32, LANEPAD=26, ROWGAP=15, BANDGAP=34, GUT=142;
function provColour(p) {
tbox: v1.5 - PhysicalRelation devient DataStructure et point d'extension declare pour le non-tabulaire, hasSource devient sourcedFrom vers un System instancie, storedIn sert deux granularites et deprecie materializedAs, references devient qualifiedBy, primaryLocation sous-propriete de storedIn, hasShortLabel sur 34 classes. CuratedObject devient DefinedObject et ObservedObject devient CapturedObject jusque dans les IRI, curationMode devient authoringMode. Correction de fond : PhysicalLayerObject ne porte plus la provenance, chaque classe de la couche la declare - un System est defined, tout ce qui est en dessous est captured. instances: 5 systemes crees, 151 sourcedFrom, 15 BO nettoyes de hasSource, labels courts, versions alignees
2026-07-30 15:13:03 +02:00
return p === " captured " ? " #8A6A28 " : p === " defined " ? " #1E4B9B " : " #6B6B6B " ;
2026-07-29 09:03:59 +02:00
}
tbox: v1.5 - PhysicalRelation devient DataStructure et point d'extension declare pour le non-tabulaire, hasSource devient sourcedFrom vers un System instancie, storedIn sert deux granularites et deprecie materializedAs, references devient qualifiedBy, primaryLocation sous-propriete de storedIn, hasShortLabel sur 34 classes. CuratedObject devient DefinedObject et ObservedObject devient CapturedObject jusque dans les IRI, curationMode devient authoringMode. Correction de fond : PhysicalLayerObject ne porte plus la provenance, chaque classe de la couche la declare - un System est defined, tout ce qui est en dessous est captured. instances: 5 systemes crees, 151 sourcedFrom, 15 BO nettoyes de hasSource, labels courts, versions alignees
2026-07-30 15:13:03 +02:00
/* Scoped relations: these carry no rdfs:domain or rdfs:range on purpose,
because each serves several classes with different targets and the scope
belongs in the SHACL shapes. Both the graph and the cards read this list,
so an edge drawn in one is never missing from the other. */
var SCOPED_PAIRS = [
[ " pr:belongsTo " , " pr:SubDomain " , " pr:DataDomain " , " belongs to " ],
[ " pr:belongsTo " , " pr:BusinessObject " , " pr:SubDomain " , " belongs to " ],
[ " pr:represents " , " pr:DataObject " , " pr:BusinessObject " , " represents " ],
[ " pr:represents " , " pr:DataElement " , " pr:BusinessConcept " , " represents " ],
[ " pr:monitoredBy " , " pr:BusinessObject " , " pr:DataSteward " , " monitored by " ],
[ " pr:owningDomain " , " pr:DefinedObject " , " pr:DataDomain " , " owning domain " ],
[ " pr:ownedBy " , " pr:DefinedObject " , " pr:Actor " , " owned by " ]
];
2026-08-05 09:13:12 +02:00
var ORDER = [
" pr:MetaModelObject " , " pr:DefinedObject " , " pr:CapturedObject " ,
tbox: v1.5 - PhysicalRelation devient DataStructure et point d'extension declare pour le non-tabulaire, hasSource devient sourcedFrom vers un System instancie, storedIn sert deux granularites et deprecie materializedAs, references devient qualifiedBy, primaryLocation sous-propriete de storedIn, hasShortLabel sur 34 classes. CuratedObject devient DefinedObject et ObservedObject devient CapturedObject jusque dans les IRI, curationMode devient authoringMode. Correction de fond : PhysicalLayerObject ne porte plus la provenance, chaque classe de la couche la declare - un System est defined, tout ce qui est en dessous est captured. instances: 5 systemes crees, 151 sourcedFrom, 15 BO nettoyes de hasSource, labels courts, versions alignees
2026-07-30 15:13:03 +02:00
" pr:OwnershipLayerObject " , " pr:DataDomain " , " pr:SubDomain " , " pr:BusinessObject " ,
" pr:BusinessLayerObject " , " pr:BusinessConcept " , " pr:Metric " , " pr:KPI " ,
" pr:LogicalLayerObject " , " pr:DataObject " , " pr:DataElement " ,
" pr:DeliveryLayerObject " , " pr:DataProduct " , " pr:DataContract " , " pr:DataInterface " ,
" pr:PhysicalLayerObject " , " pr:System " , " pr:Database " , " pr:Schema " ,
" pr:DataStructure " , " pr:BaseTable " , " pr:View " , " pr:ExternalTable " , " pr:Field " ,
2026-08-05 09:13:12 +02:00
" pr:KeyConstraint " , " pr:PrimaryKey " , " pr:ForeignKey " , " pr:Transformation " ,
tbox: v1.5 - PhysicalRelation devient DataStructure et point d'extension declare pour le non-tabulaire, hasSource devient sourcedFrom vers un System instancie, storedIn sert deux granularites et deprecie materializedAs, references devient qualifiedBy, primaryLocation sous-propriete de storedIn, hasShortLabel sur 34 classes. CuratedObject devient DefinedObject et ObservedObject devient CapturedObject jusque dans les IRI, curationMode devient authoringMode. Correction de fond : PhysicalLayerObject ne porte plus la provenance, chaque classe de la couche la declare - un System est defined, tout ce qui est en dessous est captured. instances: 5 systemes crees, 151 sourcedFrom, 15 BO nettoyes de hasSource, labels courts, versions alignees
2026-07-30 15:13:03 +02:00
" pr:ConsumptionLayerObject " , " pr:BIWorkspace " , " pr:BIDataSource " , " pr:BIField " ,
2026-08-05 09:13:12 +02:00
" pr:BIReport " ,
" pr:Actor " , " pr:DataDomainOwner " , " pr:SubDomainOwner " , " pr:DataGovernanceLead " ,
" pr:DataSteward " , " pr:DataProductOwner "
];
var LANES = [[ " defined " , " Defined " , " #1E4B9B " ],
[ " captured " , " Captured " , " #8A6A28 " ],
2026-07-29 09:03:59 +02:00
[ " structural " , " Structural " , " #6B6B6B " ]];
function live(a) { return a.filter(function(x) { return !x.deprecated; }); }
function layerTitle(id) {
var l = M.layers.filter(function(x) { return x.id === id; })[0];
return l ? l.title : id;
}
2026-07-26 21:28:14 +02:00
2026-07-29 09:03:59 +02:00
function buildGraph() {
var keep = {} ;
live(M.nodes).forEach(function(n) {
if(!G.layers[n.layer]) return;
if(G.provs.length && G.provs.indexOf(n.prov) === -1) return;
tbox: v1.5 - PhysicalRelation devient DataStructure et point d'extension declare pour le non-tabulaire, hasSource devient sourcedFrom vers un System instancie, storedIn sert deux granularites et deprecie materializedAs, references devient qualifiedBy, primaryLocation sous-propriete de storedIn, hasShortLabel sur 34 classes. CuratedObject devient DefinedObject et ObservedObject devient CapturedObject jusque dans les IRI, curationMode devient authoringMode. Correction de fond : PhysicalLayerObject ne porte plus la provenance, chaque classe de la couche la declare - un System est defined, tout ce qui est en dessous est captured. instances: 5 systemes crees, 151 sourcedFrom, 15 BO nettoyes de hasSource, labels courts, versions alignees
2026-07-30 15:13:03 +02:00
/* row = rank inside the layer, so the hierarchy reads DOWNWARD:
layer root, then Domain, then Sub-domain, then Business object.
Depth alone put siblings side by side, which read left to right. */
var rank = ORDER.indexOf(n.id);
2026-07-29 09:03:59 +02:00
keep[n.id] = { n:n, x:0, y:0,
layer: M.layers.map(function(l) { return l.id;}).indexOf(n.layer),
tbox: v1.5 - PhysicalRelation devient DataStructure et point d'extension declare pour le non-tabulaire, hasSource devient sourcedFrom vers un System instancie, storedIn sert deux granularites et deprecie materializedAs, references devient qualifiedBy, primaryLocation sous-propriete de storedIn, hasShortLabel sur 34 classes. CuratedObject devient DefinedObject et ObservedObject devient CapturedObject jusque dans les IRI, curationMode devient authoringMode. Correction de fond : PhysicalLayerObject ne porte plus la provenance, chaque classe de la couche la declare - un System est defined, tout ce qui est en dessous est captured. instances: 5 systemes crees, 151 sourcedFrom, 15 BO nettoyes de hasSource, labels courts, versions alignees
2026-07-30 15:13:03 +02:00
row: rank > -1 ? rank : 90 + Math.min(n.depth, 5)};
2026-07-29 09:03:59 +02:00
});
var E = [];
tbox: v1.5 - PhysicalRelation devient DataStructure et point d'extension declare pour le non-tabulaire, hasSource devient sourcedFrom vers un System instancie, storedIn sert deux granularites et deprecie materializedAs, references devient qualifiedBy, primaryLocation sous-propriete de storedIn, hasShortLabel sur 34 classes. CuratedObject devient DefinedObject et ObservedObject devient CapturedObject jusque dans les IRI, curationMode devient authoringMode. Correction de fond : PhysicalLayerObject ne porte plus la provenance, chaque classe de la couche la declare - un System est defined, tout ce qui est en dessous est captured. instances: 5 systemes crees, 151 sourcedFrom, 15 BO nettoyes de hasSource, labels courts, versions alignees
2026-07-30 15:13:03 +02:00
/* Properties deliberately left without rdfs:domain/range serve several
classes with different targets, and their scope lives in the SHACL
shapes instead. The graph reads domain and range, so those edges were
simply absent -- Business Object to Sub-domain among them. The pairs are
declared here, mirroring the shapes, rather than added to the T-Box where
they would over-constrain the property. */
SCOPED_PAIRS.forEach(function(sc) {
if(byId[sc[0]] && !byId[sc[0]].deprecated && keep[sc[1]] && keep[sc[2]]
&& sc[1] !== sc[2])
E.push( { s:sc[1], t:sc[2], l:sc[3], cross:false});
});
var _unused = [
[ " pr:belongsTo " , " pr:SubDomain " , " pr:DataDomain " , " belongs to " ],
[ " pr:belongsTo " , " pr:BusinessObject " , " pr:SubDomain " , " belongs to " ],
[ " pr:represents " , " pr:DataObject " , " pr:BusinessObject " , " represents " ],
[ " pr:represents " , " pr:DataElement " , " pr:BusinessConcept " , " represents " ],
[ " pr:monitoredBy " , " pr:BusinessObject " , " pr:DataSteward " , " monitored by " ],
[ " pr:owningDomain " , " pr:DefinedObject " , " pr:DataDomain " , " owning domain " ],
[ " pr:ownedBy " , " pr:DefinedObject " , " pr:Actor " , " owned by " ]
];
2026-07-29 09:03:59 +02:00
live(M.relations).forEach(function(r) {
if(keep[r.domain] && keep[r.range] && r.domain !== r.range)
E.push( { s:r.domain, t:r.range, l:r.label, cross:r.seam});
2026-07-26 21:28:14 +02:00
});
tbox: v1.5 - PhysicalRelation devient DataStructure et point d'extension declare pour le non-tabulaire, hasSource devient sourcedFrom vers un System instancie, storedIn sert deux granularites et deprecie materializedAs, references devient qualifiedBy, primaryLocation sous-propriete de storedIn, hasShortLabel sur 34 classes. CuratedObject devient DefinedObject et ObservedObject devient CapturedObject jusque dans les IRI, curationMode devient authoringMode. Correction de fond : PhysicalLayerObject ne porte plus la provenance, chaque classe de la couche la declare - un System est defined, tout ce qui est en dessous est captured. instances: 5 systemes crees, 151 sourcedFrom, 15 BO nettoyes de hasSource, labels courts, versions alignees
2026-07-30 15:13:03 +02:00
2026-07-29 09:03:59 +02:00
live(M.nodes).forEach(function(n) {
if(n.parent && keep[n.id] && keep[n.parent])
tbox: v1.5 - PhysicalRelation devient DataStructure et point d'extension declare pour le non-tabulaire, hasSource devient sourcedFrom vers un System instancie, storedIn sert deux granularites et deprecie materializedAs, references devient qualifiedBy, primaryLocation sous-propriete de storedIn, hasShortLabel sur 34 classes. CuratedObject devient DefinedObject et ObservedObject devient CapturedObject jusque dans les IRI, curationMode devient authoringMode. Correction de fond : PhysicalLayerObject ne porte plus la provenance, chaque classe de la couche la declare - un System est defined, tout ce qui est en dessous est captured. instances: 5 systemes crees, 151 sourcedFrom, 15 BO nettoyes de hasSource, labels courts, versions alignees
2026-07-30 15:13:03 +02:00
E.push( { s:n.id, t:n.parent, l: " type of " , cross:false});
2026-07-29 09:03:59 +02:00
});
G.nodes = Object.keys(keep).map(function(k) { return keep[k]; });
G.edges = E;
return keep;
2026-07-26 21:28:14 +02:00
}
2026-07-29 09:03:59 +02:00
/* Strata = a matrix. Columns are domains, rows are meta model layers.
Everything owned by one domain sits in one vertical lane, so a cross-domain
dependency is literally a line leaving its column. */
function layoutGraph(idx) {
if(G.mode === " network " ) { layoutForce(idx); return; }
var lanes = LANES.map(function(L) { return L[0]; })
.filter(function(c) { return G.nodes.some(function(n) { return n.n.prov === c; }); });
tbox: v1.5 - PhysicalRelation devient DataStructure et point d'extension declare pour le non-tabulaire, hasSource devient sourcedFrom vers un System instancie, storedIn sert deux granularites et deprecie materializedAs, references devient qualifiedBy, primaryLocation sous-propriete de storedIn, hasShortLabel sur 34 classes. CuratedObject devient DefinedObject et ObservedObject devient CapturedObject jusque dans les IRI, curationMode devient authoringMode. Correction de fond : PhysicalLayerObject ne porte plus la provenance, chaque classe de la couche la declare - un System est defined, tout ce qui est en dessous est captured. instances: 5 systemes crees, 151 sourcedFrom, 15 BO nettoyes de hasSource, labels courts, versions alignees
2026-07-30 15:13:03 +02:00
/* One class per row. A lane is one column wide, so a layer reads DOWNWARDS:
the layer class on top, then Domain, then Sub-domain, then Business
Object. That is how a hierarchy is read; laying them left to right made
the reader scan across for something that descends. */
2026-07-29 09:03:59 +02:00
var laneW = {} , x = GUT;
G._lanes = [];
lanes.forEach(function(code) {
tbox: v1.5 - PhysicalRelation devient DataStructure et point d'extension declare pour le non-tabulaire, hasSource devient sourcedFrom vers un System instancie, storedIn sert deux granularites et deprecie materializedAs, references devient qualifiedBy, primaryLocation sous-propriete de storedIn, hasShortLabel sur 34 classes. CuratedObject devient DefinedObject et ObservedObject devient CapturedObject jusque dans les IRI, curationMode devient authoringMode. Correction de fond : PhysicalLayerObject ne porte plus la provenance, chaque classe de la couche la declare - un System est defined, tout ce qui est en dessous est captured. instances: 5 systemes crees, 151 sourcedFrom, 15 BO nettoyes de hasSource, labels courts, versions alignees
2026-07-30 15:13:03 +02:00
var w = NW + LANEPAD*2;
laneW[code] = { x:x, w:w};
2026-07-29 09:03:59 +02:00
var L = LANES.filter(function(z) { return z[0] === code; })[0];
G._lanes.push( { code:code, x:x, w:w, label:L[1], colour:L[2]});
x += w;
2026-07-26 21:28:14 +02:00
});
2026-07-29 09:03:59 +02:00
G._width = x;
var y = 0;
M.layers.forEach(function(b, li) {
b._y = y;
tbox: v1.5 - PhysicalRelation devient DataStructure et point d'extension declare pour le non-tabulaire, hasSource devient sourcedFrom vers un System instancie, storedIn sert deux granularites et deprecie materializedAs, references devient qualifiedBy, primaryLocation sous-propriete de storedIn, hasShortLabel sur 34 classes. CuratedObject devient DefinedObject et ObservedObject devient CapturedObject jusque dans les IRI, curationMode devient authoringMode. Correction de fond : PhysicalLayerObject ne porte plus la provenance, chaque classe de la couche la declare - un System est defined, tout ce qui est en dessous est captured. instances: 5 systemes crees, 151 sourcedFrom, 15 BO nettoyes de hasSource, labels courts, versions alignees
2026-07-30 15:13:03 +02:00
var any = G.nodes.filter(function(n) { return n.layer === li; });
if(!any.length) { b._h = 0; return; }
var cursor = y + 36, rows = 0;
lanes.forEach(function(code) {
var g = any.filter(function(n) { return n.n.prov === code; });
if(!g.length) return;
g.sort(function(p, q) {
return ((ORDER.indexOf(p.n.id)+1)||99) - ((ORDER.indexOf(q.n.id)+1)||99)
|| p.n.label.localeCompare(q.n.label); });
g.forEach(function(n, i) {
n.x = laneW[code].x + laneW[code].w/2;
n.y = cursor + i*(NH+ROWGAP) + NH/2;
2026-07-29 09:03:59 +02:00
});
tbox: v1.5 - PhysicalRelation devient DataStructure et point d'extension declare pour le non-tabulaire, hasSource devient sourcedFrom vers un System instancie, storedIn sert deux granularites et deprecie materializedAs, references devient qualifiedBy, primaryLocation sous-propriete de storedIn, hasShortLabel sur 34 classes. CuratedObject devient DefinedObject et ObservedObject devient CapturedObject jusque dans les IRI, curationMode devient authoringMode. Correction de fond : PhysicalLayerObject ne porte plus la provenance, chaque classe de la couche la declare - un System est defined, tout ce qui est en dessous est captured. instances: 5 systemes crees, 151 sourcedFrom, 15 BO nettoyes de hasSource, labels courts, versions alignees
2026-07-30 15:13:03 +02:00
rows = Math.max(rows, g.length);
2026-07-26 21:28:14 +02:00
});
tbox: v1.5 - PhysicalRelation devient DataStructure et point d'extension declare pour le non-tabulaire, hasSource devient sourcedFrom vers un System instancie, storedIn sert deux granularites et deprecie materializedAs, references devient qualifiedBy, primaryLocation sous-propriete de storedIn, hasShortLabel sur 34 classes. CuratedObject devient DefinedObject et ObservedObject devient CapturedObject jusque dans les IRI, curationMode devient authoringMode. Correction de fond : PhysicalLayerObject ne porte plus la provenance, chaque classe de la couche la declare - un System est defined, tout ce qui est en dessous est captured. instances: 5 systemes crees, 151 sourcedFrom, 15 BO nettoyes de hasSource, labels courts, versions alignees
2026-07-30 15:13:03 +02:00
b._h = rows*(NH+ROWGAP) + 48;
y += b._h + BANDGAP;
2026-07-29 09:03:59 +02:00
});
G._height = y;
2026-07-26 21:28:14 +02:00
}
2026-07-29 09:03:59 +02:00
function layoutForce(idx) {
var links = G.edges.filter(function(e) { return idx[e.s] && idx[e.t]; });
G.nodes.forEach(function(n, i) {
var a = i*2.399963;
n.x = 600 + Math.sqrt(i)*46*Math.cos(a);
n.y = 400 + Math.sqrt(i)*46*Math.sin(a);
});
var iters = G.nodes.length > 90 ? 200 : 380;
for(var it=0; it<iters; it++) {
var k = 1 - it/iters;
for(var i=0; i<G.nodes.length; i++) {
var a = G.nodes[i];
for(var j=i+1; j<G.nodes.length; j++) {
var b = G.nodes[j], dx = b.x-a.x, dy = (b.y-a.y)*1.7;
var d2 = dx*dx+dy*dy || 1, f = Math.min(30000/d2, 8), d = Math.sqrt(d2);
a.x -= dx/d*f; a.y -= dy/d*f*.6; b.x += dx/d*f; b.y += dy/d*f*.6;
2026-07-26 21:28:14 +02:00
}
2026-07-29 09:03:59 +02:00
a.x += (600-a.x)*.003*k; a.y += (400-a.y)*.004*k;
2026-07-26 21:28:14 +02:00
}
links.forEach(function(e) {
2026-07-29 09:03:59 +02:00
var a = idx[e.s], b = idx[e.t], dx = b.x-a.x, dy = b.y-a.y;
var d = Math.sqrt(dx*dx+dy*dy) || 1, f = (d-190)*.02*k;
a.x += dx/d*f; a.y += dy/d*f; b.x -= dx/d*f; b.y -= dy/d*f;
2026-07-26 21:28:14 +02:00
});
}
2026-07-29 09:03:59 +02:00
for(var pass=0; pass<40; pass++) {
var moved = 0;
for(var i=0; i<G.nodes.length; i++) {
for(var j=i+1; j<G.nodes.length; j++) {
var a = G.nodes[i], b = G.nodes[j];
var ox = (NW+10)-Math.abs(b.x-a.x), oy = (NH+8)-Math.abs(b.y-a.y);
if(ox>0 && oy>0) {
moved++;
if(ox<oy) { var s=(b.x>=a.x?1:-1)*ox/2; a.x-=s; b.x+=s; }
else { var t=(b.y>=a.y?1:-1)*oy/2; a.y-=t; b.y+=t; }
}
}
}
if(!moved) break;
2026-07-26 21:28:14 +02:00
}
}
2026-07-29 09:03:59 +02:00
function renderGraph() {
var idx = buildGraph();
G.nodes.forEach(function(n) { idx[n.n.id] = n; });
layoutGraph(idx);
var bL=document.getElementById( " g-b " ), eL=document.getElementById( " g-e " ),
lL=document.getElementById( " g-l " ), nL=document.getElementById( " g-n " );
bL.innerHTML=eL.innerHTML=lL.innerHTML=nL.innerHTML= " " ;
var NS= " http://www.w3.org/2000/svg " ;
function el(t,a) { var e=document.createElementNS(NS,t);
for(var k in a) e.setAttribute(k,a[k]); return e; }
if(G.mode === " strata " && G.nodes.length) {
/* domain lanes: alternating tint, so a column reads as one domain */
(G._lanes||[]).forEach(function(l, i) {
tbox: v1.5 - PhysicalRelation devient DataStructure et point d'extension declare pour le non-tabulaire, hasSource devient sourcedFrom vers un System instancie, storedIn sert deux granularites et deprecie materializedAs, references devient qualifiedBy, primaryLocation sous-propriete de storedIn, hasShortLabel sur 34 classes. CuratedObject devient DefinedObject et ObservedObject devient CapturedObject jusque dans les IRI, curationMode devient authoringMode. Correction de fond : PhysicalLayerObject ne porte plus la provenance, chaque classe de la couche la declare - un System est defined, tout ce qui est en dessous est captured. instances: 5 systemes crees, 151 sourcedFrom, 15 BO nettoyes de hasSource, labels courts, versions alignees
2026-07-30 15:13:03 +02:00
var isSubject = l.code === " defined " ;
2026-07-29 09:03:59 +02:00
bL.appendChild(el( " rect " , { x:l.x, y:-6, width:l.w, height:G._height+10,
fill:l.colour, opacity:isSubject ? .05 : .028}));
if(i) bL.appendChild(el( " line " , { x1:l.x, y1:-6, x2:l.x, y2:G._height+4,
stroke: " #D8D4C8 " , " stroke-width " :1}));
var t = el( " text " , { x:l.x+l.w/2, y:-16, " text-anchor " : " middle " ,
class: " lanelabel " , fill:l.colour});
2026-08-05 09:13:12 +02:00
/* clipped to the column: a label wider than its lane overlapped the
neighbouring one, which read as though it belonged to both */
var cap = Math.floor(l.w / 6.4);
t.textContent = l.label.length > cap ? l.label.slice(0, cap-1) + " \ u2026 "
: l.label;
2026-07-29 09:03:59 +02:00
bL.appendChild(t);
});
M.layers.forEach(function(b) {
if(!b._h) return;
bL.appendChild(el( " line " , { x1:0, y1:b._y, x2:(G._width||900)+10, y2:b._y,
stroke: " #CFCABA " , " stroke-width " :1}));
var t=el( " text " , { x:GUT-16, y:b._y+18, class: " bandlabel " , " text-anchor " : " end " ,
fill: " #0A1F44 " }); t.textContent=b.title; bL.appendChild(t);
var g=el( " text " , { x:GUT-16, y:b._y+31, class: " bandtag " , " text-anchor " : " end " });
2026-07-26 21:28:14 +02:00
g.textContent=b.tag; bL.appendChild(g);
});
}
2026-07-29 09:03:59 +02:00
/* clip both ends to the node rectangle: an arrow head drawn to the centre
disappears under the box, which is what hid every marker until now */
function clip(from, to) {
var dx = to.x-from.x, dy = to.y-from.y;
if(!dx && !dy) return { x:to.x, y:to.y};
var sx = (NW/2+5)/Math.abs(dx||1e-6), sy = (NH/2+5)/Math.abs(dy||1e-6);
var t = Math.min(sx, sy, 1);
return { x: to.x - dx*t, y: to.y - dy*t};
}
G.edges.forEach(function(e) {
var a=idx[e.s], b=idx[e.t]; if(!a||!b) return;
var dx=b.x-a.x, dy=b.y-a.y;
var cx=a.x+dx/2+(-dy)*.09, cy=a.y+dy/2+dx*.035;
var p0=clip(b, a), p1=clip(a, b);
var p=el( " path " , { d: " M " +p0.x+ " , " +p0.y+ " Q " +cx+ " , " +cy+ " " +p1.x+ " , " +p1.y,
class: " gedge " +(e.cross? " cross " : " " )});
p.dataset.a=e.s; p.dataset.b=e.t; eL.appendChild(p);
if(G.labels) {
var mx=(a.x+2*cx+b.x)/4, my=(a.y+2*cy+b.y)/4;
var w=e.l.length*4.3+7;
var bg=el( " rect " , { x:mx-w/2, y:my-6, width:w, height:11, rx:2, class: " elbg " });
bg.dataset.a=e.s; bg.dataset.b=e.t; lL.appendChild(bg);
var tx=el( " text " , { x:mx, y:my+2.4, class: " elabel " });
tx.dataset.a=e.s; tx.dataset.b=e.t; tx.textContent=e.l; lL.appendChild(tx);
2026-07-26 21:28:14 +02:00
}
});
2026-07-29 09:03:59 +02:00
G.nodes.forEach(function(n) {
var c = provColour(n.n.prov);
var g = el( " g " , { class: " gnode " , tabindex: " 0 " , role: " button " ,
transform: " translate( " +(n.x-NW/2)+ " , " +(n.y-NH/2)+ " ) " });
g.dataset.id = n.n.id;
/* dashed outline = abstract: a class that exists to give its
specialisations a shape, never instantiated */
g.appendChild(el( " rect " , { width:NW, height:NH, rx:3, fill: " #FFFFFF " ,
stroke:c, " stroke-dasharray " : n.n.abstract ? " 5 3 " : " none " }));
2026-08-05 09:13:12 +02:00
var lines = wrapText(n.n.shortLabel || n.n.label, 20);
2026-07-29 09:03:59 +02:00
lines.forEach(function(t,i) {
2026-08-05 09:13:12 +02:00
var e = el( " text " , { x:NW/2, y:NH/2+3+(i-(lines.length-1)/2)*9.5, fill: " #1A1A1A " });
2026-07-29 09:03:59 +02:00
e.textContent = t; g.appendChild(e);
2026-07-26 21:28:14 +02:00
});
nL.appendChild(g);
});
applyHot();
2026-07-29 09:03:59 +02:00
fitGraph();
var cd = G.edges.filter(function(e) { return e.cross; }).length;
document.getElementById( " g-count " ).textContent =
G.nodes.length+ " classes \ u00b7 " +G.edges.length+ " edges \ u00b7 " +cd+ " seam " ;
2026-07-26 21:28:14 +02:00
}
2026-08-05 09:13:12 +02:00
/* A node box is NW wide and holds at most three lines. A word longer than a
line is cut rather than allowed to run past the border, and anything beyond
the third line is elided -- overflowing text overlapped the neighbouring
column, which is worse than an ellipsis. */
function wrapText(s, max) {
var out = [], cur = " " ;
s.split( " " ).forEach(function(w) {
while(w.length > max) {
if(cur) { out.push(cur); cur = " " ; }
out.push(w.slice(0, max - 1) + " \ u2011 " );
w = w.slice(max - 1);
}
if((cur + " " + w).trim().length > max) { if(cur) out.push(cur); cur = w; }
else cur = (cur + " " + w).trim();
});
2026-07-29 09:03:59 +02:00
if(cur) out.push(cur);
2026-08-05 09:13:12 +02:00
if(out.length > 3) {
out = out.slice(0, 3);
out[2] = out[2].slice(0, Math.max(0, max - 1)) + " \ u2026 " ;
}
return out;
2026-07-26 21:28:14 +02:00
}
function applyHot() {
2026-07-29 09:03:59 +02:00
var id = G.sel || G.hot;
var ns=document.querySelectorAll( " .gnode " ), es=document.querySelectorAll( " .gedge " ),
ls=document.querySelectorAll( " .elabel,.elbg " );
2026-07-26 21:28:14 +02:00
if(!id) {
2026-07-29 09:03:59 +02:00
ns.forEach(function(n) { n.classList.remove( " dim " , " sel " ); });
es.forEach(function(e) { e.classList.remove( " dim " , " hot " ); });
ls.forEach(function(l) { l.classList.remove( " dim " ); });
2026-07-26 21:28:14 +02:00
return;
}
2026-07-29 09:03:59 +02:00
var keep= {} ; keep[id]=1;
G.edges.forEach(function(e) {
if(e.s===id) keep[e.t]=1; if(e.t===id) keep[e.s]=1; });
es.forEach(function(p) {
var on = p.dataset.a===id || p.dataset.b===id;
p.classList.toggle( " hot " ,on); p.classList.toggle( " dim " ,!on); });
ls.forEach(function(l) {
l.classList.toggle( " dim " , !(l.dataset.a===id || l.dataset.b===id)); });
ns.forEach(function(n) {
n.classList.toggle( " dim " , !keep[n.dataset.id]);
n.classList.toggle( " sel " , n.dataset.id===G.sel); });
}
function clearSel() { G.sel=null; G.hot=null; applyHot(); }
function applyView() {
document.getElementById( " g-vp " ).setAttribute( " transform " ,
" translate( " +G.view.x+ " , " +G.view.y+ " ) scale( " +G.view.s+ " ) " );
}
function fitGraph() {
if(!G.nodes.length) return;
var svg=document.getElementById( " g-svg " ), r=svg.getBoundingClientRect();
var xs=G.nodes.map(function(n) { return n.x;}), ys=G.nodes.map(function(n) { return n.y;});
var pad = G.mode=== " strata " ? GUT+20 : NW;
var mnx=Math.min.apply(null,xs)-pad, mxx=Math.max.apply(null,xs)+NW;
var mny=Math.min.apply(null,ys)-(G.mode=== " strata " ?66:60), mxy=Math.max.apply(null,ys)+50;
G.view.s=Math.min(r.width/(mxx-mnx), r.height/(mxy-mny), 1.15);
G.view.x=(r.width-(mxx-mnx)*G.view.s)/2-mnx*G.view.s;
G.view.y=(r.height-(mxy-mny)*G.view.s)/2-mny*G.view.s;
applyView();
}
function zoomG(f) {
var svg=document.getElementById( " g-svg " ), r=svg.getBoundingClientRect();
var cx=r.width/2, cy=r.height/2;
var ns=Math.max(.1,Math.min(G.view.s*f,3));
G.view.x=cx-(cx-G.view.x)*(ns/G.view.s);
G.view.y=cy-(cy-G.view.y)*(ns/G.view.s);
G.view.s=ns; applyView();
}
/* ------------------------------------------------------------ OVERVIEW */
function kpi(v,l,w) { return ' <div class= " kpi ' +(w? ' warn ' : ' ' )+ ' " ><b> ' +v+ ' </b><span> ' +l+ ' </span></div> ' ; }
function renderScope() {
var cls=live(M.nodes), rel=live(M.relations), att=live(M.attributes);
tbox: v1.5 - PhysicalRelation devient DataStructure et point d'extension declare pour le non-tabulaire, hasSource devient sourcedFrom vers un System instancie, storedIn sert deux granularites et deprecie materializedAs, references devient qualifiedBy, primaryLocation sous-propriete de storedIn, hasShortLabel sur 34 classes. CuratedObject devient DefinedObject et ObservedObject devient CapturedObject jusque dans les IRI, curationMode devient authoringMode. Correction de fond : PhysicalLayerObject ne porte plus la provenance, chaque classe de la couche la declare - un System est defined, tout ce qui est en dessous est captured. instances: 5 systemes crees, 151 sourcedFrom, 15 BO nettoyes de hasSource, labels courts, versions alignees
2026-07-30 15:13:03 +02:00
var cur=cls.filter(function(n) { return n.prov=== " defined " ;});
var obs=cls.filter(function(n) { return n.prov=== " captured " ;});
2026-07-29 09:03:59 +02:00
var str=cls.filter(function(n) { return n.prov=== " structural " ;});
var seam=rel.filter(function(r) { return r.seam;});
var dep=M.nodes.filter(function(n) { return n.deprecated;})
.concat(M.relations.concat(M.attributes).filter(function(r) { return r.deprecated;}));
var h=[];
h.push( ' <p class= " lede " >The layer says what an object <b>is</b>. The provenance says <b>how it got here</b>.</p> ' );
tbox: v1.5 - PhysicalRelation devient DataStructure et point d'extension declare pour le non-tabulaire, hasSource devient sourcedFrom vers un System instancie, storedIn sert deux granularites et deprecie materializedAs, references devient qualifiedBy, primaryLocation sous-propriete de storedIn, hasShortLabel sur 34 classes. CuratedObject devient DefinedObject et ObservedObject devient CapturedObject jusque dans les IRI, curationMode devient authoringMode. Correction de fond : PhysicalLayerObject ne porte plus la provenance, chaque classe de la couche la declare - un System est defined, tout ce qui est en dessous est captured. instances: 5 systemes crees, 151 sourcedFrom, 15 BO nettoyes de hasSource, labels courts, versions alignees
2026-07-30 15:13:03 +02:00
h.push( ' <p class= " sublede " >Defined objects are written by governance and reviewed through a pull request. '
+ ' Captured objects are read from Snowflake, Tableau and Looker, rebuilt on every run and never edited '
2026-07-29 09:03:59 +02:00
+ ' by hand. Without that split the ownership rules would demand an owner, a status and a version on every '
+ ' one of some fifty thousand warehouse columns.</p> ' );
h.push( ' <div class= " kpis " > ' +kpi(cls.length, " Classes " )+kpi(rel.length, " Relations " )
+kpi(att.length, " Attributes " )+kpi(M.bridges.length, " Bridge properties " )
+kpi(seam.length, " Seam edges " ,true)+kpi(dep.length, " Deprecated " ,true)+ ' </div> ' );
tbox: v1.5 - PhysicalRelation devient DataStructure et point d'extension declare pour le non-tabulaire, hasSource devient sourcedFrom vers un System instancie, storedIn sert deux granularites et deprecie materializedAs, references devient qualifiedBy, primaryLocation sous-propriete de storedIn, hasShortLabel sur 34 classes. CuratedObject devient DefinedObject et ObservedObject devient CapturedObject jusque dans les IRI, curationMode devient authoringMode. Correction de fond : PhysicalLayerObject ne porte plus la provenance, chaque classe de la couche la declare - un System est defined, tout ce qui est en dessous est captured. instances: 5 systemes crees, 151 sourcedFrom, 15 BO nettoyes de hasSource, labels courts, versions alignees
2026-07-30 15:13:03 +02:00
h.push( ' <h2 class= " sec " >Defined against captured</h2><div class= " bar " > '
2026-07-29 09:03:59 +02:00
+ ' <div style= " width: ' +(cur.length/cls.length*100)+ ' % ;background:#1E4B9B " ></div> '
+ ' <div style= " width: ' +(obs.length/cls.length*100)+ ' % ;background:#8A6A28 " ></div> '
+ ' <div style= " width: ' +(str.length/cls.length*100)+ ' % ;background:#B9B4A7 " ></div></div> ' );
h.push( ' <div class= " legend " > '
tbox: v1.5 - PhysicalRelation devient DataStructure et point d'extension declare pour le non-tabulaire, hasSource devient sourcedFrom vers un System instancie, storedIn sert deux granularites et deprecie materializedAs, references devient qualifiedBy, primaryLocation sous-propriete de storedIn, hasShortLabel sur 34 classes. CuratedObject devient DefinedObject et ObservedObject devient CapturedObject jusque dans les IRI, curationMode devient authoringMode. Correction de fond : PhysicalLayerObject ne porte plus la provenance, chaque classe de la couche la declare - un System est defined, tout ce qui est en dessous est captured. instances: 5 systemes crees, 151 sourcedFrom, 15 BO nettoyes de hasSource, labels courts, versions alignees
2026-07-30 15:13:03 +02:00
+ ' <span><i style= " background:#1E4B9B " ></i>Defined \ u2014 ' +cur.length+ ' </span> '
+ ' <span><i style= " background:#8A6A28 " ></i>Captured \ u2014 ' +obs.length+ ' </span> '
2026-07-29 09:03:59 +02:00
+ ' <span><i style= " background:#B9B4A7 " ></i>Structural \ u2014 ' +str.length+ ' </span></div> ' );
h.push( ' <div class= " note " style= " max-width:880px;margin-bottom:26px " ><b>Only ' +seam.length
+ ' relations cross between the two.</b> ' +seam.map(function(r) { return esc(r.label);}).join( " and " )
+ ' \ u2014 both proposed by the reconciliation job and confirmed by a steward. Everywhere else the '
tbox: v1.5 - PhysicalRelation devient DataStructure et point d'extension declare pour le non-tabulaire, hasSource devient sourcedFrom vers un System instancie, storedIn sert deux granularites et deprecie materializedAs, references devient qualifiedBy, primaryLocation sous-propriete de storedIn, hasShortLabel sur 34 classes. CuratedObject devient DefinedObject et ObservedObject devient CapturedObject jusque dans les IRI, curationMode devient authoringMode. Correction de fond : PhysicalLayerObject ne porte plus la provenance, chaque classe de la couche la declare - un System est defined, tout ce qui est en dessous est captured. instances: 5 systemes crees, 151 sourcedFrom, 15 BO nettoyes de hasSource, labels courts, versions alignees
2026-07-30 15:13:03 +02:00
+ ' graph is either pure governance or pure machine, which is what keeps the mapping effort bounded.</div> ' );
2026-07-29 09:03:59 +02:00
h.push( ' <h2 class= " sec " >The layers</h2><div class= " grid " > ' );
M.layers.forEach(function(l) {
var g=cls.filter(function(n) { return n.layer===l.id;});
if(!g.length) return;
var c=provColour(g[0].prov);
h.push( ' <div class= " card " data-layer= " ' +l.id+ ' " ><div class= " top " style= " background: ' +c+ ' " ></div> '
+ ' <div class= " body " ><div class= " role " style= " color: ' +c+ ' " > ' +esc(l.tag)+ ' </div> '
+ ' <h3> ' +esc(l.title)+ ' </h3><p> ' +g.slice(0,5).map(function(n) { return esc(n.label);}).join( " \ u00b7 " )
+(g.length>5? " \ u00b7 \ u2026 " : " " )+ ' </p><div class= " nums " ><div><b> ' +g.length+ ' </b>classes</div> '
+ ' <div><b> ' +rel.filter(function(r) { return byId[r.domain]&&byId[r.domain].layer===l.id;}).length
+ ' </b>relations from</div></div></div></div> ' );
2026-07-26 21:28:14 +02:00
});
2026-07-29 09:03:59 +02:00
h.push( ' </div> ' );
return h.join( " " );
}
/* ------------------------------------------------------------ LAYERS */
function pill(t,c) { c=c|| " #8A6A28 " ;
return ' <span class= " pill " style= " color: ' +c+ ' ;border-color: ' +c+ ' ;background:#FBF3E4 " > ' +t+ ' </span> ' ; }
function renderModel() {
var h=[ ' <p class= " lede " >Every class, in the layer it belongs to.</p> '
+ ' <p class= " sublede " >Abstract classes are never instantiated \ u2014 they exist to give their '
+ ' specialisations a common parent and a common shape. Superseded terms are kept rather than deleted '
+ ' so assertions written against them keep classifying; they live on the Deprecated tab.</p> ' ];
M.layers.forEach(function(l) {
var g=live(M.nodes).filter(function(n) { return n.layer===l.id;});
if(!g.length) return;
2026-08-05 09:13:12 +02:00
/* ORDER alone, never depth first: sorting by depth grouped all the
parents together and pushed Base table, View and External table below
Field and Key constraint, away from the Data Structure they specialise. */
tbox: v1.5 - PhysicalRelation devient DataStructure et point d'extension declare pour le non-tabulaire, hasSource devient sourcedFrom vers un System instancie, storedIn sert deux granularites et deprecie materializedAs, references devient qualifiedBy, primaryLocation sous-propriete de storedIn, hasShortLabel sur 34 classes. CuratedObject devient DefinedObject et ObservedObject devient CapturedObject jusque dans les IRI, curationMode devient authoringMode. Correction de fond : PhysicalLayerObject ne porte plus la provenance, chaque classe de la couche la declare - un System est defined, tout ce qui est en dessous est captured. instances: 5 systemes crees, 151 sourcedFrom, 15 BO nettoyes de hasSource, labels courts, versions alignees
2026-07-30 15:13:03 +02:00
g.sort(function(a,b) {
2026-08-05 09:13:12 +02:00
return ((ORDER.indexOf(a.id)+1)||99) - ((ORDER.indexOf(b.id)+1)||99)
|| a.depth - b.depth
tbox: v1.5 - PhysicalRelation devient DataStructure et point d'extension declare pour le non-tabulaire, hasSource devient sourcedFrom vers un System instancie, storedIn sert deux granularites et deprecie materializedAs, references devient qualifiedBy, primaryLocation sous-propriete de storedIn, hasShortLabel sur 34 classes. CuratedObject devient DefinedObject et ObservedObject devient CapturedObject jusque dans les IRI, curationMode devient authoringMode. Correction de fond : PhysicalLayerObject ne porte plus la provenance, chaque classe de la couche la declare - un System est defined, tout ce qui est en dessous est captured. instances: 5 systemes crees, 151 sourcedFrom, 15 BO nettoyes de hasSource, labels courts, versions alignees
2026-07-30 15:13:03 +02:00
|| a.label.localeCompare(b.label); });
2026-07-29 09:03:59 +02:00
h.push( ' <h2 class= " sec " style= " color: ' +provColour(g[0].prov)+ ' " > ' +esc(l.title)+ ' \ u00b7 ' +esc(l.tag)+ ' </h2> ' );
tbox: v1.5 - PhysicalRelation devient DataStructure et point d'extension declare pour le non-tabulaire, hasSource devient sourcedFrom vers un System instancie, storedIn sert deux granularites et deprecie materializedAs, references devient qualifiedBy, primaryLocation sous-propriete de storedIn, hasShortLabel sur 34 classes. CuratedObject devient DefinedObject et ObservedObject devient CapturedObject jusque dans les IRI, curationMode devient authoringMode. Correction de fond : PhysicalLayerObject ne porte plus la provenance, chaque classe de la couche la declare - un System est defined, tout ce qui est en dessous est captured. instances: 5 systemes crees, 151 sourcedFrom, 15 BO nettoyes de hasSource, labels courts, versions alignees
2026-07-30 15:13:03 +02:00
h.push( ' <table><thead><tr><th>Class</th><th>Provenance</th><th class= " hide-s " >Type of</th> '
2026-07-29 09:03:59 +02:00
+ ' <th class= " hide-s " >What it is for</th></tr></thead><tbody> ' );
g.forEach(function(n) {
2026-08-05 09:13:12 +02:00
h.push( ' <tr data-id= " ' +n.id+ ' " ><td style= " padding-left: ' +(11+n.depth*16)+ ' px " > '
+ ' <span class= " dot " style= " background: ' +provColour(n.prov)
+ ' " ></span><b> ' +esc(n.shortLabel||n.label)+ ' </b> '
+(n.abstract? ' ' +pill( " abstract " ): ' ' )+ ' </td> '
2026-07-29 09:03:59 +02:00
+ ' <td class= " mono " > ' +n.prov+ ' </td><td class= " hide-s mono " > '
+(byId[n.parent]?esc(clean(byId[n.parent].label)): " \ u2014 " )+ ' </td> '
+ ' <td class= " hide-s " > ' +esc((n.comment|| " " ).split( " . " )[0])+ ' </td></tr> ' );
});
h.push( ' </tbody></table> ' );
2026-07-26 21:28:14 +02:00
});
2026-07-29 09:03:59 +02:00
return h.join( " " );
2026-07-26 21:28:14 +02:00
}
2026-07-29 09:03:59 +02:00
/* ------------------------------------------------------------ RELATIONS */
function renderProps() {
2026-08-05 09:13:12 +02:00
var h=[ ' <p class= " lede " > ' +live(M.relations).length+ ' relations, ' +live(M.attributes).length+ ' attributes and ' +M.bridges.length+ ' bridge properties.</p> '
2026-07-29 09:03:59 +02:00
+ ' <p class= " sublede " >A property with no declared domain or range is deliberate: it serves several classes '
+ ' with different targets, so the scope lives in the SHACL shapes instead, stated per class.</p> ' ];
h.push( ' <div class= " filters " ><input id= " q " type= " search " placeholder= " filter \ u2026 " > '
+ ' <button data-k= " " class= " on " >All</button><button data-k= " Relation " >Relations</button> '
+ ' <button data-k= " Attribute " >Attributes</button><button data-k= " Bridge " >Bridges</button> '
+ ' </div><div id= " tbl " ></div> ' );
return h.join( " " );
2026-07-26 21:28:14 +02:00
}
2026-08-05 09:13:12 +02:00
/* First sentence only: the table is for scanning, the card carries the full
text. Splitting on " . " alone broke on abbreviations and on the multi-line
comments, which start with a newline. */
function firstSentence(t) {
t = (t|| " " ).replace(/ \ s+/g, " " ).trim();
var m = t.match(/^(. { 0,150}?[.!?])( \ s|$)/);
return m ? m[1] : (t.length > 150 ? t.slice(0,150).replace(/ \ s \ S*$/, " " ) + " \ u2026 " : t);
}
2026-07-29 09:03:59 +02:00
function propRows() {
var all=M.relations.concat(M.attributes,M.bridges).filter(function(p) {
if(p.deprecated) return false;
if(filt.kind && p.kind!==filt.kind) return false;
if(filt.q && (p.label+ " " +p.id+ " " +(p.comment|| " " )).toLowerCase().indexOf(filt.q)===-1) return false;
return true;
});
all.sort(function(a,b) { return a.kind.localeCompare(b.kind)||a.label.localeCompare(b.label); });
2026-08-05 09:13:12 +02:00
var h=[ ' <table><thead><tr><th>Property</th><th class= " hide-s " >From</th> '
+ ' <th class= " hide-s " >To</th><th class= " hide-s " >Traits</th> '
+ ' <th>What it is for</th></tr></thead><tbody> ' ];
2026-07-29 09:03:59 +02:00
all.forEach(function(p) {
var t=[];
if(p.functional) t.push( " exactly one " );
if(p.transitive) t.push( " transitive " );
if(p.seam) t.push( " seam " );
tbox: v1.5 - PhysicalRelation devient DataStructure et point d'extension declare pour le non-tabulaire, hasSource devient sourcedFrom vers un System instancie, storedIn sert deux granularites et deprecie materializedAs, references devient qualifiedBy, primaryLocation sous-propriete de storedIn, hasShortLabel sur 34 classes. CuratedObject devient DefinedObject et ObservedObject devient CapturedObject jusque dans les IRI, curationMode devient authoringMode. Correction de fond : PhysicalLayerObject ne porte plus la provenance, chaque classe de la couche la declare - un System est defined, tout ce qui est en dessous est captured. instances: 5 systemes crees, 151 sourcedFrom, 15 BO nettoyes de hasSource, labels courts, versions alignees
2026-07-30 15:13:03 +02:00
if(p.authoring) t.push(p.authoring.toLowerCase().replace(/_/g, " " ));
2026-07-29 09:03:59 +02:00
h.push( ' <tr data-id= " ' +p.id+ ' " ><td><b> ' +esc(p.label)+ ' </b><div class= " mono " > ' +esc(p.id)+ ' </div></td> '
+ ' <td class= " hide-s " > ' +(byId[p.domain]?esc(clean(byId[p.domain].label)): ' <span class= " mono " >any</span> ' )+ ' </td> '
+ ' <td class= " hide-s " > ' +(byId[p.range]?esc(clean(byId[p.range].label))
: ' <span class= " mono " > ' +esc(p.range|| " \ u2014 " )+ ' </span> ' )+ ' </td> '
2026-08-05 09:13:12 +02:00
+ ' <td class= " hide-s mono " > ' +(t.join( " \ u00b7 " )|| " \ u2014 " )+ ' </td> '
+ ' <td> ' +(p.comment ? esc(firstSentence(p.comment))
: ' <span class= " mono " >no description</span> ' )+ ' </td></tr> ' );
2026-07-29 09:03:59 +02:00
});
h.push( ' </tbody></table><p class= " sublede " style= " margin-top:12px " > ' +all.length+ ' shown.</p> ' );
return h.join( " " );
2026-07-26 21:28:14 +02:00
}
2026-07-29 09:03:59 +02:00
/* ------------------------------------------------------------ DEPRECATED */
function renderDep() {
var dep=M.nodes.filter(function(n) { return n.deprecated;})
.concat(M.relations.concat(M.attributes).filter(function(r) { return r.deprecated;}));
var h=[ ' <p class= " lede " > ' +dep.length+ ' terms kept, none of them to be used.</p> '
+ ' <p class= " sublede " >Rule LC-002: a superseded term is deprecated rather than deleted, so assertions '
+ ' written against it keep classifying while a migration runs. The obligation is symmetric \ u2014 a '
+ ' deprecation that never ends stops meaning anything, which is why Table was removed outright in v1.2 '
+ ' once nothing used it.</p> ' ];
h.push( ' <table><thead><tr><th>Term</th><th>Kind</th><th>Replaced by</th> '
+ ' <th class= " hide-s " >Why</th></tr></thead><tbody> ' );
dep.forEach(function(d) {
h.push( ' <tr class= " dep " data-id= " ' +d.id+ ' " ><td><b> ' +esc(clean(d.label))+ ' </b> '
+ ' <div class= " mono " > ' +esc(d.id)+ ' </div></td><td class= " mono " > '
+(d.kind=== " Class " ? " class " :d.kind.toLowerCase())+ ' </td><td> '
+(byId[d.replacedBy]?esc(clean(byId[d.replacedBy].label)): ' <span class= " mono " > \ u2014</span> ' )
+ ' </td><td class= " hide-s " > ' +esc((d.comment|| " " ).split( " . " )[0])+ ' </td></tr> ' );
});
h.push( ' </tbody></table> ' );
return h.join( " " );
2026-07-26 21:28:14 +02:00
}
2026-07-29 09:03:59 +02:00
/* ------------------------------------------------------------ SHELL */
function renderGraphShell() {
var h=[ ' <p class= " lede " >The meta model as a graph.</p> '
tbox: v1.5 - PhysicalRelation devient DataStructure et point d'extension declare pour le non-tabulaire, hasSource devient sourcedFrom vers un System instancie, storedIn sert deux granularites et deprecie materializedAs, references devient qualifiedBy, primaryLocation sous-propriete de storedIn, hasShortLabel sur 34 classes. CuratedObject devient DefinedObject et ObservedObject devient CapturedObject jusque dans les IRI, curationMode devient authoringMode. Correction de fond : PhysicalLayerObject ne porte plus la provenance, chaque classe de la couche la declare - un System est defined, tout ce qui est en dessous est captured. instances: 5 systemes crees, 151 sourcedFrom, 15 BO nettoyes de hasSource, labels courts, versions alignees
2026-07-30 15:13:03 +02:00
+ ' <p class= " sublede " >Columns are provenance: <b style= " color:#1E4B9B " >defined</b> on the left, '
+ ' <b style= " color:#8A6A28 " >captured</b> on the right. Rows are layers. A <b>navy edge</b> crosses '
2026-07-29 09:03:59 +02:00
+ ' between the two \ u2014 that is the seam, the only place a steward confirms anything. Dashed outlines '
+ ' are abstract classes, never instantiated.</p> ' ];
h.push( ' <div class= " gwrap " ><svg id= " g-svg " xmlns= " http://www.w3.org/2000/svg " ><defs> '
+ ' <marker id= " ga " viewBox= " 0 0 10 10 " refX= " 9 " refY= " 5 " markerWidth= " 5 " markerHeight= " 5 " orient= " auto " > '
+ ' <path d= " M0,0 L10,5 L0,10 z " fill= " #A6ABB5 " /></marker> '
+ ' <marker id= " gg " viewBox= " 0 0 10 10 " refX= " 9 " refY= " 5 " markerWidth= " 5 " markerHeight= " 5 " orient= " auto " > '
+ ' <path d= " M0,0 L10,5 L0,10 z " fill= " #0A1F44 " /></marker> '
+ ' <marker id= " gn " viewBox= " 0 0 10 10 " refX= " 9 " refY= " 5 " markerWidth= " 5 " markerHeight= " 5 " orient= " auto " > '
+ ' <path d= " M0,0 L10,5 L0,10 z " fill= " #B8935A " /></marker></defs> '
+ ' <g id= " g-vp " ><g id= " g-b " ></g><g id= " g-e " ></g><g id= " g-l " ></g><g id= " g-n " ></g></g></svg> ' );
h.push( ' <div class= " gctl " ><div class= " seg " ><button id= " g-strata " class= " on " >Strata</button> '
+ ' <button id= " g-network " >Network</button></div><div class= " gsec " >Layers</div> '
+ M.layers.map(function(l) {
var n=live(M.nodes).filter(function(x) { return x.layer===l.id;}).length;
if(!n) return " " ;
return ' <label><input type= " checkbox " data-ly= " ' +l.id+ ' " ' +(G.layers[l.id]? " checked " : " " )
+ ' > ' +esc(l.title)+ ' <span class= " mono " > ' +n+ ' </span></label> ' ; }).join( " " )
+ ' <div class= " gsec " >Provenance</div> '
+ LANES.map(function(L) {
var n=live(M.nodes).filter(function(x) { return x.prov===L[0];}).length;
if(!n) return " " ;
return ' <label><input type= " checkbox " data-pv= " ' +L[0]+ ' " '
+(!G.provs.length||G.provs.indexOf(L[0])>-1? " checked " : " " )+ ' > <i style= " background: ' +L[2]
+ ' " ></i> ' +L[0]+ ' <span class= " mono " > ' +n+ ' </span></label> ' ; }).join( " " )
+ ' <div class= " gsec " >Options</div> '
+ ' <label><input type= " checkbox " id= " g-lab " checked> Relation names</label> '
+ ' <div class= " mono " id= " g-count " style= " margin-top:8px " ></div></div> ' );
h.push( ' <div class= " gzoom " ><button id= " g-in " >+</button><button id= " g-out " > \ u2212</button> '
+ ' <button id= " g-fit " style= " font-size:12px " > \ u21ba</button></div> ' );
h.push( ' <div class= " ghint " >drag to pan \ u00b7 scroll to zoom \ u00b7 click empty space to clear</div></div> ' );
return h.join( " " );
2026-07-26 21:28:14 +02:00
}
2026-07-29 09:03:59 +02:00
function wireGraph() {
renderGraph();
var svg = document.getElementById( " g-svg " );
function mode(m) {
G.mode = m;
document.getElementById( " g-strata " ).classList.toggle( " on " , m=== " strata " );
document.getElementById( " g-network " ).classList.toggle( " on " , m=== " network " );
renderGraph();
}
document.getElementById( " g-strata " ).onclick = function() { mode( " strata " ); };
document.getElementById( " g-network " ).onclick = function() { mode( " network " ); };
document.querySelectorAll( " [data-ly] " ).forEach(function(cb) {
cb.onchange = function() { G.layers[cb.dataset.ly] = cb.checked; renderGraph(); };
});
document.getElementById( " g-lab " ).onchange = function() {
G.labels = this.checked; renderGraph(); };
document.querySelectorAll( " [data-pv] " ).forEach(function(cb) {
cb.onchange = function() {
var on = [];
document.querySelectorAll( " [data-pv] " ).forEach(function(x) {
if(x.checked) on.push(x.dataset.pv); });
G.provs = (on.length === LANES.length) ? [] : on;
renderGraph();
};
});
document.getElementById( " g-in " ).onclick = function() { zoomG(1.25); };
document.getElementById( " g-out " ).onclick = function() { zoomG(.8); };
document.getElementById( " g-fit " ).onclick = fitGraph;
var drag=false, moved=false, ds, vs, pinch=null;
svg.addEventListener( " mousedown " , function(e) {
drag=true; moved=false; svg.classList.add( " drag " );
ds= { x:e.clientX,y:e.clientY}; vs= { x:G.view.x,y:G.view.y}; });
window.addEventListener( " mousemove " , function(e) {
if(!drag) return;
if(Math.abs(e.clientX-ds.x)+Math.abs(e.clientY-ds.y) > 4) moved=true;
G.view.x=vs.x+(e.clientX-ds.x); G.view.y=vs.y+(e.clientY-ds.y); applyView(); });
window.addEventListener( " mouseup " , function() {
drag=false; svg.classList.remove( " drag " ); });
/* click on empty canvas clears the selection -- a highlight that cannot be
dismissed traps the reader in one node */
svg.addEventListener( " click " , function(e) {
if(!moved && !e.target.closest( " .gnode " )) { clearSel(); insp.classList.remove( " open " ); }
});
svg.addEventListener( " wheel " , function(e) { e.preventDefault();
var f=e.deltaY<0?1.1:.9, r=svg.getBoundingClientRect();
var mx=e.clientX-r.left, my=e.clientY-r.top;
var ns=Math.max(.1,Math.min(G.view.s*f,3));
G.view.x=mx-(mx-G.view.x)*(ns/G.view.s); G.view.y=my-(my-G.view.y)*(ns/G.view.s);
G.view.s=ns; applyView(); }, { passive:false});
svg.addEventListener( " touchstart " , function(e) {
if(e.touches.length===1) { drag=true; moved=false;
ds= { x:e.touches[0].clientX,y:e.touches[0].clientY}; vs= { x:G.view.x,y:G.view.y}; }
else if(e.touches.length===2) { drag=false;
var dx=e.touches[0].clientX-e.touches[1].clientX,
dy=e.touches[0].clientY-e.touches[1].clientY;
pinch= { d:Math.sqrt(dx*dx+dy*dy), s:G.view.s}; } }, { passive:true});
svg.addEventListener( " touchmove " , function(e) {
if(e.touches.length===1 && drag) { moved=true;
G.view.x=vs.x+(e.touches[0].clientX-ds.x);
G.view.y=vs.y+(e.touches[0].clientY-ds.y); applyView(); e.preventDefault(); }
else if(e.touches.length===2 && pinch) {
var dx=e.touches[0].clientX-e.touches[1].clientX,
dy=e.touches[0].clientY-e.touches[1].clientY;
var nd=Math.sqrt(dx*dx+dy*dy), ns=Math.max(.1,Math.min(pinch.s*(nd/pinch.d),3));
var r=svg.getBoundingClientRect();
G.view.x=r.width/2-(r.width/2-G.view.x)*(ns/G.view.s);
G.view.y=r.height/2-(r.height/2-G.view.y)*(ns/G.view.s);
G.view.s=ns; applyView(); e.preventDefault(); } }, { passive:false});
svg.addEventListener( " touchend " , function() { drag=false; pinch=null; });
var nL = document.getElementById( " g-n " );
nL.addEventListener( " click " , function(e) {
var g = e.target.closest( " .gnode " ); if(!g) return;
e.stopPropagation();
if(G.sel === g.dataset.id) { clearSel(); insp.classList.remove( " open " ); return; }
G.sel = g.dataset.id; G.hot = null; applyHot(); openCard(g.dataset.id, true);
});
nL.addEventListener( " keydown " , function(e) {
var g = e.target.closest( " .gnode " );
if(g && (e.key=== " Enter " ||e.key=== " " )) { e.preventDefault();
G.sel=g.dataset.id; applyHot(); openCard(g.dataset.id, true); } });
nL.addEventListener( " mouseover " , function(e) {
var g=e.target.closest( " .gnode " ); if(g && !G.sel) { G.hot=g.dataset.id; applyHot(); } });
nL.addEventListener( " mouseout " , function(e) {
if(!G.sel && (!e.relatedTarget || !e.relatedTarget.closest
|| !e.relatedTarget.closest( " .gnode " ))) { G.hot=null; applyHot(); } });
window.addEventListener( " resize " , fitGraph);
}
/* ------------------------------------------------------------ INSPECTOR */
var insp=document.getElementById( " insp " ), ibody=document.getElementById( " insp-body " );
var HIST=[];
var CHILDREN= {} , FROM= {} , TO= {} , ATTRS= {} ;
M.nodes.forEach(function(n) {
if(n.parent) (CHILDREN[n.parent]=CHILDREN[n.parent]||[]).push(n.id); });
M.relations.forEach(function(r) {
if(r.deprecated) return;
if(r.domain) (FROM[r.domain]=FROM[r.domain]||[]).push(r.id);
if(r.range) (TO[r.range]=TO[r.range]||[]).push(r.id); });
tbox: v1.5 - PhysicalRelation devient DataStructure et point d'extension declare pour le non-tabulaire, hasSource devient sourcedFrom vers un System instancie, storedIn sert deux granularites et deprecie materializedAs, references devient qualifiedBy, primaryLocation sous-propriete de storedIn, hasShortLabel sur 34 classes. CuratedObject devient DefinedObject et ObservedObject devient CapturedObject jusque dans les IRI, curationMode devient authoringMode. Correction de fond : PhysicalLayerObject ne porte plus la provenance, chaque classe de la couche la declare - un System est defined, tout ce qui est en dessous est captured. instances: 5 systemes crees, 151 sourcedFrom, 15 BO nettoyes de hasSource, labels courts, versions alignees
2026-07-30 15:13:03 +02:00
/* The same scoped pairs the graph uses. Without them a Business Object shows
no incoming relation at all, because belongsTo and represents carry no
rdfs:domain -- their scope lives in the shapes. The card was silent about
edges the graph was drawing. */
SCOPED_PAIRS.forEach(function(sc) {
if(!byId[sc[0]] || byId[sc[0]].deprecated) return;
(FROM[sc[1]] = FROM[sc[1]]||[]).push(sc[0]);
(TO[sc[2]] = TO[sc[2]]||[]).push(sc[0]);
});
/* the scoped properties again: without this a Sub-domain card showed no
incoming Business Object, and a Business Object card had no incoming list
at all, because neither relation declares a domain or a range */
[[ " pr:belongsTo " , " pr:SubDomain " , " pr:DataDomain " ],
[ " pr:belongsTo " , " pr:BusinessObject " , " pr:SubDomain " ],
[ " pr:represents " , " pr:DataObject " , " pr:BusinessObject " ],
[ " pr:represents " , " pr:DataElement " , " pr:BusinessConcept " ],
[ " pr:monitoredBy " , " pr:BusinessObject " , " pr:DataSteward " ],
[ " pr:owningDomain " , " pr:DefinedObject " , " pr:DataDomain " ],
[ " pr:ownedBy " , " pr:DefinedObject " , " pr:Actor " ]].forEach(function(sc) {
if(!byId[sc[0]] || byId[sc[0]].deprecated) return;
if((FROM[sc[1]]||[]).indexOf(sc[0])===-1) (FROM[sc[1]]=FROM[sc[1]]||[]).push(sc[0]);
if((TO[sc[2]]||[]).indexOf(sc[0])===-1) (TO[sc[2]]=TO[sc[2]]||[]).push(sc[0]);
});
2026-07-29 09:03:59 +02:00
M.attributes.forEach(function(a) {
if(a.deprecated||!a.domain) return;
(ATTRS[a.domain]=ATTRS[a.domain]||[]).push(a.id); });
function openCard(id, reset) {
if(reset) HIST=[];
var cur=HIST[HIST.length-1];
if(cur&&cur!==id) HIST.push(id); else if(!cur) HIST.push(id);
paint(id);
}
function back() {
HIST.pop();
var prev=HIST[HIST.length-1];
if(prev) { paint(prev); if(G.sel) { G.sel=prev; applyHot(); } }
else insp.classList.remove( " open " );
2026-07-26 21:28:14 +02:00
}
2026-07-29 09:03:59 +02:00
function open(id) { openCard(id,true); }
function paint(id) {
var n=byId[id]; if(!n) return;
var h=[];
if(HIST.length>1)
h.push( ' <button class= " backlink " id= " i-back " > \ u2190 back to '
+ esc(clean(byId[HIST[HIST.length-2]].label)) + ' </button> ' );
var kindLabel = n.kind=== " Class " ? " Class " : n.kind=== " Relation " ? " Relation "
: n.kind=== " Attribute " ? " Attribute " : " Bridge property " ;
h.push( ' <div class= " kind " style= " color: ' +provColour(n.prov|| " structural " )+ ' " > '
+ kindLabel + (n.layer ? ' \ u00b7 ' + esc(layerTitle(n.layer)) : ' ' ) + ' </div> ' );
tbox: v1.5 - PhysicalRelation devient DataStructure et point d'extension declare pour le non-tabulaire, hasSource devient sourcedFrom vers un System instancie, storedIn sert deux granularites et deprecie materializedAs, references devient qualifiedBy, primaryLocation sous-propriete de storedIn, hasShortLabel sur 34 classes. CuratedObject devient DefinedObject et ObservedObject devient CapturedObject jusque dans les IRI, curationMode devient authoringMode. Correction de fond : PhysicalLayerObject ne porte plus la provenance, chaque classe de la couche la declare - un System est defined, tout ce qui est en dessous est captured. instances: 5 systemes crees, 151 sourcedFrom, 15 BO nettoyes de hasSource, labels courts, versions alignees
2026-07-30 15:13:03 +02:00
h.push( ' <h3> ' +esc(n.shortLabel || clean(n.label))+ ' </h3> ' );
if(n.shortLabel && n.shortLabel !== clean(n.label))
h.push( ' <div class= " row " style= " border:0;padding:0 0 6px " ><span class= " mono " > '
+ esc(clean(n.label)) + ' </span><span></span></div> ' );
2026-07-29 09:03:59 +02:00
h.push( ' <div class= " iri " > ' +esc(n.id)+ ' </div> ' );
if(n.deprecated) h.push( ' <div class= " note " ><b>Deprecated.</b> Kept so assertions written '
+ ' against it keep classifying (LC-002) '
+ (byId[n.replacedBy] ? ' , replaced by <b> ' +esc(clean(byId[n.replacedBy].label))+ ' </b> ' : ' ' )
+ ' . Not to be used in new work.</div> ' );
/* 1. attributes, always first and always in the same place */
var rows=[];
function at(l,v) { if(v) rows.push([l,v]); }
if(n.kind=== " Class " ) {
at( " Provenance " , n.prov);
at( " Instantiated " , n.abstract ? " never \ u2014 abstract " : " yes " );
tbox: v1.5 - PhysicalRelation devient DataStructure et point d'extension declare pour le non-tabulaire, hasSource devient sourcedFrom vers un System instancie, storedIn sert deux granularites et deprecie materializedAs, references devient qualifiedBy, primaryLocation sous-propriete de storedIn, hasShortLabel sur 34 classes. CuratedObject devient DefinedObject et ObservedObject devient CapturedObject jusque dans les IRI, curationMode devient authoringMode. Correction de fond : PhysicalLayerObject ne porte plus la provenance, chaque classe de la couche la declare - un System est defined, tout ce qui est en dessous est captured. instances: 5 systemes crees, 151 sourcedFrom, 15 BO nettoyes de hasSource, labels courts, versions alignees
2026-07-30 15:13:03 +02:00
at( " Authoring mode " , (n.authoring|| " " ).toLowerCase().replace(/_/g, " " ));
2026-07-29 09:03:59 +02:00
at( " Acronym " , n.acronym);
} else {
at( " Cardinality " , n.functional ? " exactly one " : " many " );
at( " Transitive " , n.transitive ? " yes " : " " );
at( " Crosses the seam " , n.seam ? " yes " : " " );
tbox: v1.5 - PhysicalRelation devient DataStructure et point d'extension declare pour le non-tabulaire, hasSource devient sourcedFrom vers un System instancie, storedIn sert deux granularites et deprecie materializedAs, references devient qualifiedBy, primaryLocation sous-propriete de storedIn, hasShortLabel sur 34 classes. CuratedObject devient DefinedObject et ObservedObject devient CapturedObject jusque dans les IRI, curationMode devient authoringMode. Correction de fond : PhysicalLayerObject ne porte plus la provenance, chaque classe de la couche la declare - un System est defined, tout ce qui est en dessous est captured. instances: 5 systemes crees, 151 sourcedFrom, 15 BO nettoyes de hasSource, labels courts, versions alignees
2026-07-30 15:13:03 +02:00
at( " Authoring mode " , (n.authoring|| " " ).toLowerCase().replace(/_/g, " " ));
2026-07-29 09:03:59 +02:00
if(!byId[n.range]) at( " Range " , n.range);
if(!n.domain) at( " Domain " , " any \ u2014 scoped by SHACL per class " );
}
if(rows.length) h.push( ' <h4>Attributes</h4> ' +rows.map(function(r) {
return ' <div class= " row " ><span> ' +r[0]+ ' </span><span class= " mono " > ' +esc(r[1])+ ' </span></div> ' ;
}).join( " " ));
/* 2. what it is for */
tbox: v1.5 - PhysicalRelation devient DataStructure et point d'extension declare pour le non-tabulaire, hasSource devient sourcedFrom vers un System instancie, storedIn sert deux granularites et deprecie materializedAs, references devient qualifiedBy, primaryLocation sous-propriete de storedIn, hasShortLabel sur 34 classes. CuratedObject devient DefinedObject et ObservedObject devient CapturedObject jusque dans les IRI, curationMode devient authoringMode. Correction de fond : PhysicalLayerObject ne porte plus la provenance, chaque classe de la couche la declare - un System est defined, tout ce qui est en dessous est captured. instances: 5 systemes crees, 151 sourcedFrom, 15 BO nettoyes de hasSource, labels courts, versions alignees
2026-07-30 15:13:03 +02:00
h.push( ' <h4>What it is for</h4><p> ' + (n.comment
? esc(n.comment).replace(/ \ n/g, " <br> " )
: ' <em style= " color:var(--muted) " >No definition yet \ u2014 this term needs one '
+ ' before it can be published.</em> ' ) + ' </p> ' );
2026-07-29 09:03:59 +02:00
if(n.harvest) h.push( ' <h4>Harvested from</h4><div class= " harv " > ' +esc(n.harvest)+ ' </div> ' );
/* 3. related terms, from the coarsest down */
function link(label, ids) {
tbox: v1.5 - PhysicalRelation devient DataStructure et point d'extension declare pour le non-tabulaire, hasSource devient sourcedFrom vers un System instancie, storedIn sert deux granularites et deprecie materializedAs, references devient qualifiedBy, primaryLocation sous-propriete de storedIn, hasShortLabel sur 34 classes. CuratedObject devient DefinedObject et ObservedObject devient CapturedObject jusque dans les IRI, curationMode devient authoringMode. Correction de fond : PhysicalLayerObject ne porte plus la provenance, chaque classe de la couche la declare - un System est defined, tout ce qui est en dessous est captured. instances: 5 systemes crees, 151 sourcedFrom, 15 BO nettoyes de hasSource, labels courts, versions alignees
2026-07-30 15:13:03 +02:00
/* deprecated terms live on their own tab; showing them in a link list
invites someone to use one */
ids=(ids||[]).filter(function(i) {
return byId[i] && (!byId[i].deprecated || label === " Replaced by " ); });
2026-07-29 09:03:59 +02:00
if(!ids.length) return;
h.push( ' <h4> ' +label+ ' </h4> ' );
ids.slice(0,16).forEach(function(i) {
h.push( ' <div class= " row " ><button class= " jump " data-j= " ' +i+ ' " > ' +esc(clean(byId[i].label))
+ ' </button><span class= " mono " > ' +(byId[i].deprecated? " deprecated "
:byId[i].kind=== " Class " ?(byId[i].prov|| " " ):byId[i].kind.toLowerCase())+ ' </span></div> ' );
});
if(ids.length>16) h.push( ' <div class= " row " ><span class= " mono " > \ u2026 and '
+(ids.length-16)+ ' more</span><span></span></div> ' );
}
tbox: v1.5 - PhysicalRelation devient DataStructure et point d'extension declare pour le non-tabulaire, hasSource devient sourcedFrom vers un System instancie, storedIn sert deux granularites et deprecie materializedAs, references devient qualifiedBy, primaryLocation sous-propriete de storedIn, hasShortLabel sur 34 classes. CuratedObject devient DefinedObject et ObservedObject devient CapturedObject jusque dans les IRI, curationMode devient authoringMode. Correction de fond : PhysicalLayerObject ne porte plus la provenance, chaque classe de la couche la declare - un System est defined, tout ce qui est en dessous est captured. instances: 5 systemes crees, 151 sourcedFrom, 15 BO nettoyes de hasSource, labels courts, versions alignees
2026-07-30 15:13:03 +02:00
link( " Type of " , [n.parent, n.subPropertyOf]);
link( " Types " , CHILDREN[n.id]);
2026-07-29 09:03:59 +02:00
link( " Replaced by " , [n.replacedBy]);
link( " From " , [n.domain]);
link( " To " , [n.range]);
link( " Inverse of " , [n.inverseOf]);
link( " Relations from here " , (FROM[n.id]||[]));
link( " Relations to here " , (TO[n.id]||[]));
link( " Attributes carried " , ATTRS[n.id]);
if(n.members && n.members.length)
h.push( ' <h4>Allowed values</h4> ' +n.members.map(function(m) {
return ' <div class= " row " ><span> ' +esc(m.label)+ ' </span><span class= " mono " > '
+esc(m.id)+ ' </span></div> ' ; }).join( " " ));
ibody.innerHTML=h.join( " " );
insp.classList.add( " open " );
var bk=document.getElementById( " i-back " ); if(bk) bk.onclick=back;
ibody.querySelectorAll( " .jump " ).forEach(function(b) {
b.onclick=function() { HIST.push(b.dataset.j); paint(b.dataset.j);
if(G.sel) { G.sel=b.dataset.j; applyHot(); } }; });
}
document.getElementById( " insp-x " ).onclick=function() {
insp.classList.remove( " open " ); clearSel(); };
document.addEventListener( " keydown " ,function(e) {
if(e.key=== " Escape " ) { insp.classList.remove( " open " ); clearSel(); } });
/* ------------------------------------------------------------ WIRING */
var host=document.getElementById( " view " );
function render() {
host.innerHTML = view=== " scope " ? renderScope()
: view=== " model " ? renderModel()
: view=== " graph " ? renderGraphShell()
: view=== " props " ? renderProps() : renderDep();
if(view=== " graph " ) wireGraph();
if(view=== " props " ) {
document.getElementById( " tbl " ).innerHTML=propRows();
document.getElementById( " q " ).oninput=function() {
filt.q=this.value.trim().toLowerCase();
document.getElementById( " tbl " ).innerHTML=propRows(); bindRows(); };
host.querySelectorAll( " [data-k] " ).forEach(function(b) {
b.onclick=function() {
filt.kind=b.dataset.k;
host.querySelectorAll( " [data-k] " ).forEach(function(x) {
x.classList.toggle( " on " ,x===b); });
document.getElementById( " tbl " ).innerHTML=propRows(); bindRows(); }; });
}
bindRows();
host.querySelectorAll( " .card " ).forEach(function(c) {
c.onclick=function() {
M.layers.forEach(function(l) { G.layers[l.id]=(l.id===c.dataset.layer); });
setView( " graph " ); }; });
2026-07-26 21:28:14 +02:00
}
2026-07-29 09:03:59 +02:00
function bindRows() {
host.querySelectorAll( " tr[data-id] " ).forEach(function(tr) {
tr.onclick=function() { open(tr.dataset.id); }; });
2026-07-26 21:28:14 +02:00
}
2026-07-29 09:03:59 +02:00
function setView(v) {
view=v;
[ " scope " , " model " , " graph " , " props " , " dep " ].forEach(function(k) {
document.getElementById( " t- " +k).classList.toggle( " on " ,k===v); });
render();
document.querySelector( " main " ).scrollTop=0;
2026-07-26 21:28:14 +02:00
}
2026-07-29 09:03:59 +02:00
[ " scope " , " model " , " graph " , " props " , " dep " ].forEach(function(k) {
document.getElementById( " t- " +k).onclick=function() {
if(k=== " graph " ) M.layers.forEach(function(l) { G.layers[l.id]=(l.id!== " vocab " ); });
setView(k); }; });
render();
2026-07-26 21:28:14 +02:00
</script>
</body>
</html>
"""
if __name__ == " __main__ " :
main ( )