tbox: v1.6 - provenance portee par les classes concretes, isAbstract declare et non plus infere, ForeignKeyConstraint devient ForeignKey, les dix classes sans rdfs:comment completees

This commit is contained in:
Bastien Gourdon
2026-08-05 09:13:12 +02:00
parent f83b7d7491
commit 1c8ab77003
20 changed files with 4770 additions and 1081 deletions
+68 -28
View File
@@ -34,7 +34,8 @@ WANTED = {
"owl:inverseOf": "inverseOf", "owl:deprecated": "deprecated",
"dcterms:isReplacedBy": "isReplacedBy",
"pr:harvestSource": "harvestSource", "pr:authoringMode": "authoringMode",
"pr:hasAcronym": "acronym", "pr:hasShortLabel": "shortLabel",
"pr:acronym": "acronym", "pr:shortLabel": "shortLabel",
"pr:isAbstract": "isAbstract",
}
LAYER_ROOT = OrderedDict([
@@ -200,7 +201,6 @@ def build(subjects):
return out
parents = {c: first(classes[c], "subClassOf") for c in classes}
has_kids = set(parents.values())
nodes = {}
for term, rec in classes.items():
@@ -234,8 +234,10 @@ def build(subjects):
"acronym": first(rec, "acronym"),
"deprecated": first(rec, "deprecated") in ("true", "True"),
"replacedBy": first(rec, "isReplacedBy"),
"abstract": term in has_kids and term not in LAYER_ROOT
and term not in ("pr:MetaModelObject",),
# 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"),
"members": [{"id": m, "label": first(subjects.get(m, {}), "label",
m.split(":")[1])}
for m in individuals.get(term, [])],
@@ -495,13 +497,13 @@ footer{background:var(--navy);color:rgba(255,255,255,.5);padding:7px 20px;
<header>
<div>
<h1>PR Data Meta Model &middot; T-Box</h1>
<div class="sub">Six layers, crossed with what governance asserts and what a harvester observes</div>
<div class="sub">Six layers, crossed with what governance defines and what a harvester captures</div>
</div>
<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>
<button id="t-props">Relations</button>
<button id="t-props">Properties</button>
<button id="t-dep">Deprecated</button>
</div>
</header>
@@ -549,18 +551,22 @@ var SCOPED_PAIRS = [
["pr:owningDomain", "pr:DefinedObject", "pr:DataDomain", "owning domain"],
["pr:ownedBy", "pr:DefinedObject", "pr:Actor", "owned by"]
];
var ORDER = ["pr:MetaModelObject", "pr:DefinedObject", "pr:CapturedObject",
var ORDER = [
"pr:MetaModelObject", "pr:DefinedObject", "pr:CapturedObject",
"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",
"pr:KeyConstraint", "pr:PrimaryKey", "pr:ForeignKeyConstraint", "pr:Transformation",
"pr:KeyConstraint", "pr:PrimaryKey", "pr:ForeignKey", "pr:Transformation",
"pr:ConsumptionLayerObject", "pr:BIWorkspace", "pr:BIDataSource", "pr:BIField",
"pr:BIReport"];
var LANES = [["defined","Defined \u00b7 someone decided it","#1E4B9B"],
["captured","Captured \u00b7 read from a running system","#8A6A28"],
"pr:BIReport",
"pr:Actor", "pr:DataDomainOwner", "pr:SubDomainOwner", "pr:DataGovernanceLead",
"pr:DataSteward", "pr:DataProductOwner"
];
var LANES = [["defined","Defined","#1E4B9B"],
["captured","Captured","#8A6A28"],
["structural","Structural","#6B6B6B"]];
function live(a){ return a.filter(function(x){ return !x.deprecated; }); }
function layerTitle(id){
@@ -726,7 +732,11 @@ function renderGraph(){
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});
t.textContent = l.label;
/* 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;
bL.appendChild(t);
});
M.layers.forEach(function(b){
@@ -776,9 +786,9 @@ function renderGraph(){
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"}));
var lines = wrapText(n.n.shortLabel || n.n.label, 21);
var lines = wrapText(n.n.shortLabel || n.n.label, 20);
lines.forEach(function(t,i){
var e = el("text",{x:NW/2, y:NH/2+3.5+(i-(lines.length-1)/2)*10, fill:"#1A1A1A"});
var e = el("text",{x:NW/2, y:NH/2+3+(i-(lines.length-1)/2)*9.5, fill:"#1A1A1A"});
e.textContent = t; g.appendChild(e);
});
nL.appendChild(g);
@@ -789,13 +799,27 @@ function renderGraph(){
document.getElementById("g-count").textContent =
G.nodes.length+" classes \u00b7 "+G.edges.length+" edges \u00b7 "+cd+" seam";
}
function wrapText(s,max){
var w=s.split(" "), out=[], cur="";
w.forEach(function(x){
if((cur+" "+x).trim().length>max){ if(cur) out.push(cur); cur=x; }
else cur=(cur+" "+x).trim(); });
/* 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();
});
if(cur) out.push(cur);
return out.slice(0,2);
if(out.length > 3){
out = out.slice(0, 3);
out[2] = out[2].slice(0, Math.max(0, max - 1)) + "\u2026";
}
return out;
}
function applyHot(){
var id = G.sel || G.hot;
@@ -904,16 +928,21 @@ function renderModel(){
M.layers.forEach(function(l){
var g=live(M.nodes).filter(function(n){return n.layer===l.id;});
if(!g.length) return;
/* 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. */
g.sort(function(a,b){
return a.depth - b.depth
|| ((ORDER.indexOf(a.id)+1)||99) - ((ORDER.indexOf(b.id)+1)||99)
return ((ORDER.indexOf(a.id)+1)||99) - ((ORDER.indexOf(b.id)+1)||99)
|| a.depth - b.depth
|| a.label.localeCompare(b.label); });
h.push('<h2 class="sec" style="color:'+provColour(g[0].prov)+'">'+esc(l.title)+' \u00b7 '+esc(l.tag)+'</h2>');
h.push('<table><thead><tr><th>Class</th><th>Provenance</th><th class="hide-s">Type of</th>'
+'<th class="hide-s">What it is for</th></tr></thead><tbody>');
g.forEach(function(n){
h.push('<tr data-id="'+n.id+'"><td><span class="dot" style="background:'+provColour(n.prov)
+'"></span><b>'+esc(n.label)+'</b>'+(n.abstract?' '+pill("abstract"):'')+'</td>'
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>'
+'<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>');
@@ -925,7 +954,7 @@ function renderModel(){
/* ------------------------------------------------------------ RELATIONS */
function renderProps(){
var h=['<p class="lede">'+live(M.relations).length+' relations and '+live(M.attributes).length+' attributes.</p>'
var h=['<p class="lede">'+live(M.relations).length+' relations, '+live(M.attributes).length+' attributes and '+M.bridges.length+' bridge properties.</p>'
+'<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">'
@@ -934,6 +963,14 @@ function renderProps(){
+'</div><div id="tbl"></div>');
return h.join("");
}
/* 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);
}
function propRows(){
var all=M.relations.concat(M.attributes,M.bridges).filter(function(p){
if(p.deprecated) return false;
@@ -942,8 +979,9 @@ function propRows(){
return true;
});
all.sort(function(a,b){ return a.kind.localeCompare(b.kind)||a.label.localeCompare(b.label); });
var h=['<table><thead><tr><th>Property</th><th class="hide-s">From</th><th class="hide-s">To</th>'
+'<th>Traits</th></tr></thead><tbody>'];
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>'];
all.forEach(function(p){
var t=[];
if(p.functional) t.push("exactly one");
@@ -954,7 +992,9 @@ function propRows(){
+'<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>'
+'<td class="mono">'+(t.join(" \u00b7 ")||"\u2014")+'</td></tr>');
+'<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>');
});
h.push('</tbody></table><p class="sublede" style="margin-top:12px">'+all.length+' shown.</p>');
return h.join("");