Files
data-meta-model/generated/pr_metamodel_viewer.html
T

990 lines
115 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PR Data Meta Model &middot; T-Box</title>
<style>
:root{
--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;
--sans:-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif;
--serif:Georgia,"Times New Roman",serif;
}
*{box-sizing:border-box;-webkit-tap-highlight-color:transparent}
html,body{margin:0;height:100%}
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}
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;
text-transform:uppercase;font-weight:600}
.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%;
background:var(--panel);border-left:1px solid var(--rule);
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}
#insp.open{transform:translateX(0)}
#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;
text-transform:uppercase;font-weight:600}
.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}
.jump{background:none;border:none;padding:0;font:inherit;color:var(--navy);
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}
}
@media(prefers-reduced-motion:reduce){*{transition:none!important}}
</style>
</head>
<body>
<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>
<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-dep">Deprecated</button>
</div>
</header>
<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">&times;</button>
<div id="insp-body"></div>
</div>
<footer>
<span>Build artefact of pr_metamodel.ttl &mdash; regenerate, never edit</span>
<span id="f-count"></span>
</footer>
<script>
var M = {"nodes":[{"id":"pr:MetaModelObject","kind":"Class","label":"Meta Model Object","shortLabel":"Meta model object","layer":"roots","prov":"structural","depth":0,"parent":"","comment":"Anything the meta model describes. Actors are deliberately NOT Meta Model Objects.","harvest":"","authoring":"","acronym":"","deprecated":false,"replacedBy":"","abstract":false,"members":[]},{"id":"pr:DefinedObject","kind":"Class","label":"Defined Object","shortLabel":"Defined","layer":"roots","prov":"defined","depth":1,"parent":"pr:MetaModelObject","comment":"Somebody decided it. Written by governance, edited through a pull request on the domain TTL file, and subject to the full identity and lifecycle shape: identifier, name, owner, owning Data Domain, status, version.","harvest":"","authoring":"ASSERTED","acronym":"","deprecated":false,"replacedBy":"","abstract":true,"members":[]},{"id":"pr:CuratedObject","kind":"Class","label":"Curated Object (deprecated)","shortLabel":"","layer":"roots","prov":"structural","depth":0,"parent":"","comment":"v1.4 term. Renamed because 'curated' is governance jargon; 'defined' says the same thing to a business reader. No instance ever named this class directly.","harvest":"","authoring":"","acronym":"","deprecated":true,"replacedBy":"pr:DefinedObject","abstract":false,"members":[]},{"id":"pr:CapturedObject","kind":"Class","label":"Captured Object","shortLabel":"Captured","layer":"roots","prov":"captured","depth":1,"parent":"pr:MetaModelObject","comment":"Read from a running system and rebuilt on every run. Never hand-edited. Carries sourceIdentifier, fullyQualifiedName and harvestedOn instead of declared ownership: attribution is INFERRED through the Data Element it is mapped to, never asserted on the node.","harvest":"","authoring":"HARVESTED","acronym":"","deprecated":false,"replacedBy":"","abstract":true,"members":[]},{"id":"pr:ObservedObject","kind":"Class","label":"Observed Object (deprecated)","shortLabel":"","layer":"roots","prov":"structural","depth":0,"parent":"","comment":"v1.4 term. Renamed because 'observed' is passive and technical; 'captured' says where it came from. No instance ever named this class directly.","harvest":"","authoring":"","acronym":"","deprecated":true,"replacedBy":"pr:CapturedObject","abstract":false,"members":[]},{"id":"pr:OwnershipLayerObject","kind":"Class","label":"Ownership & Categorization Layer Object","shortLabel":"","layer":"ownership","prov":"defined","depth":2,"parent":"pr:DefinedObject","comment":"","harvest":"","authoring":"","acronym":"","deprecated":false,"replacedBy":"","abstract":false,"members":[]},{"id":"pr:BusinessLayerObject","kind":"Class","label":"Business Layer Object","shortLabel":"","layer":"business","prov":"defined","depth":2,"parent":"pr:DefinedObject","comment":"","harvest":"","authoring":"","acronym":"","deprecated":false,"replacedBy":"","abstract":false,"members":[]},{"id":"pr:LogicalLayerObject","kind":"Class","label":"Logical Layer Object","shortLabel":"","layer":"logical","prov":"defined","depth":2,"parent":"pr:DefinedObject","comment":"How the data is structured.","harvest":"","authoring":"","acronym":"","deprecated":false,"replacedBy":"","abstract":false,"members":[]},{"id":"pr:DeliveryLayerObject","kind":"Class","label":"Delivery Layer Object","shortLabel":"","layer":"delivery","prov":"defined","depth":2,"parent":"pr:DefinedObject","comment":"What we promise, to whom, until when. A different question from how the data is structured, hence a layer of its own: version, owner, changelog, support channel, consumers to warn.","harvest":"","authoring":"","acronym":"","deprecated":false,"replacedBy":"","abstract":false,"members":[]},{"id":"pr:PhysicalLayerObject","kind":"Class","label":"Physical Layer Object","shortLabel":"","layer":"physical","prov":"structural","depth":1,"parent":"pr:MetaModelObject","comment":"\"\"The one layer whose members do NOT share a provenance, which is why this class -- unlike the other five layer roots -- says nothing about it.\n\nA System is DEFINED: somebody decides that MDM Product is a system worth tracking, and names it. Nobody harvests the list of systems that matter. Everything beneath a system is CAPTURED: databases, schemas, data structures, fields, keys and transformations are read from what is running.\n\nUntil v1.4 this class was itself declared a kind of Captured Object, which made every physical class captured by construction and put the five seeded Systems in breach of the harvest rules. The two axes are meant to be orthogonal; that shortcut collapsed them.\"\"","harvest":"","authoring":"","acronym":"","deprecated":false,"replacedBy":"","abstract":false,"members":[]},{"id":"pr:ConsumptionLayerObject","kind":"Class","label":"Consumption Layer Object","shortLabel":"Consumption","layer":"consumption","prov":"captured","depth":2,"parent":"pr:CapturedObject","comment":"The BI estate. Separate from Physical because BI assets have their own lifecycle and their own owner: a dashboard belongs to its author, not to the Data Domain that owns the underlying data.","harvest":"","authoring":"","acronym":"","deprecated":false,"replacedBy":"","abstract":false,"members":[]},{"id":"pr:DataDomain","kind":"Class","label":"Data Domain","shortLabel":"Domain","layer":"ownership","prov":"defined","depth":3,"parent":"pr:OwnershipLayerObject","comment":"","harvest":"","authoring":"","acronym":"","deprecated":false,"replacedBy":"","abstract":false,"members":[]},{"id":"pr:SubDomain","kind":"Class","label":"Sub-Domain","shortLabel":"Sub-domain","layer":"ownership","prov":"defined","depth":3,"parent":"pr:OwnershipLayerObject","comment":"","harvest":"","authoring":"","acronym":"","deprecated":false,"replacedBy":"","abstract":false,"members":[]},{"id":"pr:BusinessObject","kind":"Class","label":"Business Object","shortLabel":"Business object","layer":"ownership","prov":"defined","depth":3,"parent":"pr:OwnershipLayerObject","comment":"The single anchor of accountability, which is why it stays in the Ownership layer: it is the one object where a steward is declared, and Data Objects and Data Elements inherit from it. It carries NO business definition of its own -- its meaning comes from its aboutConcept. What it does carry is a perimeter: the subject it is about, and the notions needed to read it.","harvest":"","authoring":"","acronym":"","deprecated":false,"replacedBy":"","abstract":false,"members":[]},{"id":"pr:BusinessConcept","kind":"Class","label":"Business Concept","shortLabel":"Business concept","layer":"business","prov":"defined","depth":3,"parent":"pr:BusinessLayerObject","comment":"A shared business notion WITHOUT a formula. The presence of a formula is the sole discriminator against Metric (rule BR-003).","harvest":"","authoring":"","acronym":"","deprecated":false,"replacedBy":"","abstract":false,"members":[]},{"id":"pr:Metric","kind":"Class","label":"Metric","shortLabel":"Metric","layer":"business","prov":"defined","depth":3,"parent":"pr:BusinessLayerObject","comment":"A quantifiable measurement WITH a formula, a unit and a grain. Under BR-013 a Metric carries the harmonised CALCULATION RULE; variants that differ only by analysis context are Data Elements linked back via computedBy.","harvest":"","authoring":"","acronym":"","deprecated":false,"replacedBy":"","abstract":true,"members":[]},{"id":"pr:KPI","kind":"Class","label":"Key Performance Indicator","shortLabel":"KPI","layer":"business","prov":"defined","depth":4,"parent":"pr:Metric","comment":"An elevated Metric. Use this class rather than the deprecated isKPI flag.","harvest":"","authoring":"","acronym":"","deprecated":false,"replacedBy":"","abstract":false,"members":[]},{"id":"pr:DataObject","kind":"Class","label":"Data Object","shortLabel":"Data object","layer":"logical","prov":"defined","depth":3,"parent":"pr:LogicalLayerObject","comment":"The pivot: represents exactly one Business Object, is composed of Data Elements, and is materialized as one or more Physical Relations. The single junction between governance and technology.","harvest":"","authoring":"","acronym":"","deprecated":false,"replacedBy":"","abstract":false,"members":[]},{"id":"pr:DataElement","kind":"Class","label":"Data Element","shortLabel":"Data element","layer":"logical","prov":"defined","depth":3,"parent":"pr:LogicalLayerObject","comment":"The finest defined unit. Its mapping to a physical Field is the ONLY place where the defined graph and the captured graph meet: proposed automatically by name and type matching, confirmed by a Data Steward.","harvest":"","authoring":"PROPOSED_CONFIRMED","acronym":"","deprecated":false,"replacedBy":"","abstract":false,"members":[]},{"id":"pr:DataAsset","kind":"Class","label":"Data Asset (deprecated)","shortLabel":"","layer":"logical","prov":"defined","depth":3,"parent":"pr:LogicalLayerObject","comment":"v1.0 term, defined as 'directly consumable by a business user' -- a judgement, not a structural test, which is why it collected zero instances in a year. Use DataInterface.","harvest":"","authoring":"","acronym":"","deprecated":true,"replacedBy":"pr:DataInterface","abstract":false,"members":[]},{"id":"pr:DataProduct","kind":"Class","label":"Data Product","shortLabel":"Data product","layer":"delivery","prov":"defined","depth":3,"parent":"pr:DeliveryLayerObject","comment":"The unit of delivery: what carries a version number, an owner, a changelog, a support channel and a list of consumers to warn. It has no single technical counterpart, and should not: physically it often lands on a Snowflake schema or a dbt project, but that is not what defines it. Operated by exactly one Sub-Domain -- it is a consumable, and there must be no ambiguity about who runs it. Candidate alignment: dcat:Dataset / DPROD.","harvest":"","authoring":"","acronym":"","deprecated":false,"replacedBy":"","abstract":false,"members":[]},{"id":"pr:DataInterface","kind":"Class","label":"Data Interface","shortLabel":"Data interface","layer":"delivery","prov":"defined","depth":3,"parent":"pr:DeliveryLayerObject","comment":"The channel through which a consumer actually gets the data: a Snowflake secure share or listing, a consumption schema with its grants, a file extract to a panel partner, an API or reverse-ETL feed. Not a Data Object (that is structure) and not a BI Data Source (that is one consumer among others). Verifiable: a grant, a share or an endpoint either exists or it does not. The output port in data-mesh terms.","harvest":"","authoring":"","acronym":"","deprecated":false,"replacedBy":"","abstract":false,"members":[]},{"id":"pr:DataContract","kind":"Class","label":"Data Contract","shortLabel":"Data contract","layer":"delivery","prov":"defined","depth":3,"parent":"pr:DeliveryLayerObject","comment":"The service commitment attached to a Data Product: schema, freshness, quality thresholds, support, breaking-change notice period. Candidate alignment: ODCS (Bitol).","harvest":"","authoring":"","acronym":"","deprecated":false,"replacedBy":"","abstract":false,"members":[]},{"id":"pr:System","kind":"Class","label":"System","shortLabel":"System","layer":"physical","prov":"defined","depth":2,"parent":"pr:PhysicalLayerObject","comment":"A real system: Snowflake, MDM Product, MDM Customer, a panel provider feed, Tableau Server, Looker. Making this a node rather than a string is what makes 'what depends on MDM Product?' answerable.","harvest":"","authoring":"ASSERTED","acronym":"","deprecated":false,"replacedBy":"","abstract":false,"members":[]},{"id":"pr:Database","kind":"Class","label":"Database","shortLabel":"Database","layer":"physical","prov":"captured","depth":2,"parent":"pr:PhysicalLayerObject","comment":"","harvest":"SNOWFLAKE.INFORMATION_SCHEMA.DATABASES","authoring":"","acronym":"","deprecated":false,"replacedBy":"","abstract":false,"members":[]},{"id":"pr:Schema","kind":"Class","label":"Schema","shortLabel":"Schema","layer":"physical","prov":"captured","depth":2,"parent":"pr:PhysicalLayerObject","comment":"","harvest":"SNOWFLAKE.INFORMATION_SCHEMA.SCHEMATA","authoring":"","acronym":"","deprecated":false,"replacedBy":"","abstract":false,"members":[]},{"id":"pr:DataStructure","kind":"Class","label":"Data Structure","shortLabel":"Table or view","layer":"physical","prov":"captured","depth":2,"parent":"pr:PhysicalLayerObject","comment":"\"\"Anything that carries Fields and reads as a set of rows. Never instantiated directly: a harvest types the concrete kind, and a viewer shows that kind rather than this parent.\n\nWHY THE PARENT EXISTS. Three properties need exactly this set as their scope: storedIn from a Data Object, hasField and its inverse livesIn, and dependsOn. Without it each would have to be tripled, once per concrete kind, and the checks would go with them.\n\nWHY THE NAME CHANGED. It was PhysicalRelation, from Codd, where a relation IS a table. In an ontology \"relation\" reads as a property, and the word locks the class into the tabular world. A Neo4j node label, a Mongo collection and a Kafka topic all carry fields; none is a relation.\n\nEXTENSION POINT for non-tabular storage. Nothing at Pernod Ricard is non-tabular today, so no such class is declared -- four terms were deprecated in v1.2 precisely because nobody instantiated them. The day a graph enters scope, the insertion is additive and touches no instance, because subClassOf is transitive:\n\n pr:Dataset rdfs:subClassOf pr:DataStructure .\n pr:Graph rdfs:subClassOf pr:DataStructure .\n pr:BaseTable rdfs:subClassOf pr:Dataset . # instead of DataStructure\n pr:inSchema rdfs:domain pr:Dataset . # instead of nothing\n\nExisting BaseTable assertions stay DataStructure by entailment, every shape targeting DataStructure keeps working, and storedIn does not move. The extension point is paid once; the extension is paid when it arrives.\"\"","harvest":"SNOWFLAKE.INFORMATION_SCHEMA.TABLES","authoring":"","acronym":"","deprecated":false,"replacedBy":"","abstract":true,"members":[]},{"id":"pr:PhysicalRelation","kind":"Class","label":"Physical Relation (deprecated)","shortLabel":"","layer":"physical","prov":"captured","depth":2,"parent":"pr:PhysicalLayerObject","comment":"","harvest":"","authoring":"","acronym":"","deprecated":true,"replacedBy":"pr:DataStructure","abstract":false,"members":[]},{"id":"pr:BaseTable","kind":"Class","label":"Base Table","shortLabel":"Base table","layer":"physical","prov":"captured","depth":3,"parent":"pr:DataStructure","comment":"","harvest":"","authoring":"","acronym":"","deprecated":false,"replacedBy":"","abstract":false,"members":[]},{"id":"pr:View","kind":"Class","label":"View","shortLabel":"View","layer":"physical","prov":"captured","depth":3,"parent":"pr:DataStructure","comment":"Depends on other relations through dependsOn, so impact is transitive.","harvest":"","authoring":"","acronym":"","deprecated":false,"replacedBy":"","abstract":false,"members":[]},{"id":"pr:MaterializedView","kind":"Class","label":"Materialized View (deprecated)","shortLabel":"","layer":"physical","prov":"captured","depth":3,"parent":"pr:DataStructure","comment":"A class standing in for an attribute: what distinguishes it is being derived, which dependsOn already states.","harvest":"","authoring":"","acronym":"","deprecated":true,"replacedBy":"pr:View","abstract":false,"members":[]},{"id":"pr:ExternalTable","kind":"Class","label":"External Table","shortLabel":"External table","layer":"physical","prov":"captured","depth":3,"parent":"pr:DataStructure","comment":"Backed by files outside the warehouse. Marks the boundary where lineage leaves Snowflake.","harvest":"","authoring":"","acronym":"","deprecated":false,"replacedBy":"","abstract":false,"members":[]},{"id":"pr:Field","kind":"Class","label":"Field","shortLabel":"Field","layer":"physical","prov":"captured","depth":2,"parent":"pr:PhysicalLayerObject","comment":"A physical column. The anchor of impact analysis: everything upstream reaches a Field, everything downstream starts from one.","harvest":"SNOWFLAKE.INFORMATION_SCHEMA.COLUMNS","authoring":"","acronym":"","deprecated":false,"replacedBy":"","abstract":false,"members":[]},{"id":"pr:KeyConstraint","kind":"Class","label":"Key Constraint","shortLabel":"","layer":"physical","prov":"captured","depth":2,"parent":"pr:PhysicalLayerObject","comment":"Gives foreign keys a home. Rule OW-006 says an FK is not a Data Element - true, but it still needs somewhere to live, and it lets the asserted DataObject-references-DataObject edge be checked against reality.","harvest":"SNOWFLAKE.INFORMATION_SCHEMA.TABLE_CONSTRAINTS + KEY_COLUMN_USAGE","authoring":"","acronym":"","deprecated":false,"replacedBy":"","abstract":true,"members":[]},{"id":"pr:PrimaryKey","kind":"Class","label":"Primary Key","shortLabel":"","layer":"physical","prov":"captured","depth":3,"parent":"pr:KeyConstraint","comment":"","harvest":"","authoring":"","acronym":"","deprecated":false,"replacedBy":"","abstract":false,"members":[]},{"id":"pr:ForeignKeyConstraint","kind":"Class","label":"Foreign Key Constraint","shortLabel":"","layer":"physical","prov":"captured","depth":3,"parent":"pr:KeyConstraint","comment":"","harvest":"","authoring":"","acronym":"","deprecated":false,"replacedBy":"","abstract":false,"members":[]},{"id":"pr:Transformation","kind":"Class","label":"Transformation","shortLabel":"Transformation","layer":"physical","prov":"captured","depth":2,"parent":"pr:PhysicalLayerObject","comment":"The code that turns inputs into outputs: a dbt model, a Snowflake task, an ADF pipeline. Subclass of prov:Activity so that lineage is expressed in a standard vocabulary rather than a private one.","harvest":"dbt manifest.json, Snowflake TASKS, Azure Data Factory pipelines","authoring":"","acronym":"","deprecated":false,"replacedBy":"","abstract":false,"members":[]},{"id":"pr:BIWorkspace","kind":"Class","label":"BI Workspace","shortLabel":"BI workspace","layer":"consumption","prov":"captured","depth":3,"parent":"pr:ConsumptionLayerObject","comment":"","harvest":"Tableau Metadata API (Project/Site) ; Looker API (Model)","authoring":"","acronym":"","deprecated":false,"replacedBy":"","abstract":false,"members":[]},{"id":"pr:BIDataSource","kind":"Class","label":"BI Data Source","shortLabel":"BI data source","layer":"consumption","prov":"captured","depth":3,"parent":"pr:ConsumptionLayerObject","comment":"The semantic surface a report is built on. This is where BI meets the warehouse.","harvest":"Tableau Published Datasource ; Looker Explore","authoring":"","acronym":"","deprecated":false,"replacedBy":"","abstract":false,"members":[]},{"id":"pr:BIField","kind":"Class","label":"BI Field","shortLabel":"BI field","layer":"consumption","prov":"captured","depth":3,"parent":"pr:ConsumptionLayerObject","comment":"A field as a business user sees it. exposesMetric is what turns a certified Metric from a definition on paper into something measurable against the real BI estate.","harvest":"Tableau calculated fields ; LookML measures and dimensions","authoring":"","acronym":"","deprecated":false,"replacedBy":"","abstract":false,"members":[]},{"id":"pr:BIReport","kind":"Class","label":"BI Report","shortLabel":"BI report","layer":"consumption","prov":"captured","depth":3,"parent":"pr:ConsumptionLayerObject","comment":"What a Data Governance Lead actually cares about in an impact analysis: not that a column changed, but which reports break and who to warn.","harvest":"Tableau Workbook/Sheet ; Looker Look/Dashboard","authoring":"","acronym":"","deprecated":false,"replacedBy":"","abstract":false,"members":[]},{"id":"pr:Actor","kind":"Class","label":"Actor","shortLabel":"Actor","layer":"actors","prov":"structural","depth":0,"parent":"","comment":"A person or a team. Deliberately NOT a MetaModelObject: people are not subject to ownership and lifecycle rules.","harvest":"","authoring":"","acronym":"","deprecated":false,"replacedBy":"","abstract":true,"members":[]},{"id":"pr:DataDomainOwner","kind":"Class","label":"Data Domain Owner","shortLabel":"Domain owner","layer":"actors","prov":"structural","depth":1,"parent":"pr:Actor","comment":"Accountable for a Data Domain. Exactly one per domain.","harvest":"","authoring":"","acronym":"","deprecated":false,"replacedBy":"","abstract":false,"members":[]},{"id":"pr:SubDomainOwner","kind":"Class","label":"Sub-Domain Owner","shortLabel":"Sub-domain owner","layer":"actors","prov":"structural","depth":1,"parent":"pr:Actor","comment":"Accountable for a Sub-Domain. Exactly one per sub-domain.","harvest":"","authoring":"","acronym":"","deprecated":false,"replacedBy":"","abstract":false,"members":[]},{"id":"pr:DataGovernanceLead","kind":"Class","label":"Data Governance Lead","shortLabel":"Data governance lead","layer":"actors","prov":"structural","depth":1,"parent":"pr:Actor","comment":"Runs governance for a Data Domain. The acronym belongs in hasAcronym, not in the label (rule NR-005).","harvest":"","authoring":"","acronym":"DGL","deprecated":false,"replacedBy":"","abstract":false,"members":[]},{"id":"pr:DataProductOwner","kind":"Class","label":"Data Product Owner","shortLabel":"Product owner","layer":"actors","prov":"structural","depth":1,"parent":"pr:Actor","comment":"Runs a Data Product day to day: releases, changelog, support, consumer communication. Belongs to the Sub-Domain that operates the product.","harvest":"","authoring":"","acronym":"","deprecated":false,"replacedBy":"","abstract":false,"members":[]},{"id":"pr:DataSteward","kind":"Class","label":"Data Steward","shortLabel":"Steward","layer":"actors","prov":"structural","depth":1,"parent":"pr:Actor","comment":"Monitors a Business Object. Declared there and nowhere else: Data Objects and Data Elements inherit their steward, so that no column can ever have two answers to 'who watches this'.","harvest":"","authoring":"","acronym":"","deprecated":false,"replacedBy":"","abstract":false,"members":[]},{"id":"pr:ActivationStatus","kind":"Class","label":"Activation Status","shortLabel":"","layer":"vocab","prov":"structural","depth":0,"parent":"","comment":"","harvest":"","authoring":"","acronym":"","deprecated":false,"replacedBy":"","abstract":false,"members":[{"id":"pr:NotActivated","label":"Not Activated"},{"id":"pr:LightActivation","label":"Light Activation"},{"id":"pr:FullActivation","label":"Full Activation"}]},{"id":"pr:Environment","kind":"Class","label":"Environment","shortLabel":"","layer":"vocab","prov":"structural","depth":0,"parent":"","comment":"Same logical object, several physical incarnations. Kept as a vocabulary rather than folded into the name so that DEV drift never pollutes PROD lineage.","harvest":"","authoring":"","acronym":"","deprecated":false,"replacedBy":"","abstract":false,"members":[{"id":"pr:Development","label":"Development"},{"id":"pr:UserAcceptance","label":"User Acceptance"},{"id":"pr:Production","label":"Production"}]},{"id":"pr:SystemType","kind":"Class","label":"System Type","shortLabel":"","layer":"vocab","prov":"structural","depth":0,"parent":"","comment":"","harvest":"","authoring":"","acronym":"","deprecated":false,"replacedBy":"","abstract":false,"members":[{"id":"pr:SourceApplication","label":"Source Application"},{"id":"pr:MasterDataSystem","label":"Master Data System"},{"id":"pr:DataPlatform","label":"Data Platform"},{"id":"pr:BITool","label":"BI Tool"},{"id":"pr:ExternalProvider","label":"External Provider"}]}],"relations":[{"id":"pr:belongsTo","kind":"Relation","label":"belongs to","shortLabel":"","comment":"Position in the categorization tree. One property for the whole tree: SubDomain belongs to DataDomain, BusinessObject belongs to SubDomain. Scope is enforced per class by SHACL rather than by two near-identical properties, and the same relation will serve if a domain family ever appears. Deliberately NOT transitive: entailment would produce two values and break the maxCount 1 shape. To walk the ancestry, use the property path belongsTo+ in the query.","domain":"","range":"","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":true,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:belongsToDomain","kind":"Relation","label":"belongs to domain (deprecated)","shortLabel":"","comment":"","domain":"","range":"","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":true,"transitive":false,"deprecated":true,"replacedBy":"pr:belongsTo","seam":false,"prov":"","layer":""},{"id":"pr:belongsToSubDomain","kind":"Relation","label":"belongs to sub-domain (deprecated)","shortLabel":"","comment":"","domain":"","range":"","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":true,"transitive":false,"deprecated":true,"replacedBy":"pr:belongsTo","seam":false,"prov":"","layer":""},{"id":"pr:aboutConcept","kind":"Relation","label":"is about","shortLabel":"","comment":"The single subject of this Business Object, and where its meaning comes from -- a Business Object carries no definition of its own. Exactly one. If the subject cannot be named, the object is not scoped, and that is the most useful thing the rule can tell you. Replaces the 3..10 minimum of BR-004, which was satisfied in v0.6 by attaching unrelated concepts.","domain":"pr:BusinessObject","range":"pr:BusinessConcept","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":true,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:usesConcept","kind":"Relation","label":"uses concept","shortLabel":"","comment":"The other notions required to read this object correctly. Zero or more, and zero is a legitimate answer.","domain":"pr:BusinessObject","range":"pr:BusinessConcept","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:composedOf","kind":"Relation","label":"composed of (deprecated)","shortLabel":"","comment":"Carried three different meanings depending on the object: dependency (Sell Out Volume), subject (Retailer Stock), part-whole (Calendar). One word for three relations is what allowed the v0.6 padding. Split into aboutConcept and usesConcept.","domain":"","range":"","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":true,"replacedBy":"pr:usesConcept","seam":false,"prov":"","layer":""},{"id":"pr:isComponentOf","kind":"Relation","label":"is component of (deprecated)","shortLabel":"","comment":"","domain":"","range":"","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":true,"replacedBy":"pr:usesConcept","seam":false,"prov":"","layer":""},{"id":"pr:describes","kind":"Relation","label":"describes (deprecated)","shortLabel":"","comment":"Removed as a genuine relation. Its only use was as a false inverse of composedOf.","domain":"","range":"","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":true,"replacedBy":"pr:isComponentOf","seam":false,"prov":"","layer":""},{"id":"pr:hasMetric","kind":"Relation","label":"has metric","shortLabel":"","comment":"","domain":"pr:BusinessObject","range":"pr:Metric","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:measures","kind":"Relation","label":"measures","shortLabel":"","comment":"","domain":"pr:Metric","range":"pr:BusinessConcept","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:hasGrainElement","kind":"Relation","label":"has grain element","shortLabel":"","comment":"The elements whose combination makes a row unique. Grain belongs to a DATASET, not to a calculation rule: a Metric has no rows, so it cannot have a grain of its own -- its grain is derivable from the Data Objects it lands in. Stated at element level rather than object level because it is then CHECKABLE against the harvested PrimaryKey: a declared grain that does not match the real key is an automatic finding.","domain":"pr:DataObject","range":"pr:DataElement","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:hasGrainDimension","kind":"Relation","label":"has grain dimension (deprecated)","shortLabel":"","comment":"Put grain on the Metric, which confounded a declarative statement with a structural fact. See hasGrainElement.","domain":"","range":"","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":true,"replacedBy":"pr:hasGrainElement","seam":false,"prov":"","layer":""},{"id":"pr:computedBy","kind":"Relation","label":"computed by","shortLabel":"","comment":"BR-013. Links a mother Metric to the granular Data Elements that implement it in a given analysis context (period, promo status, scope, unit, currency).","domain":"pr:Metric","range":"pr:DataElement","inverseOf":"","subPropertyOf":"prov:wasDerivedFrom","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:represents","kind":"Relation","label":"represents","shortLabel":"","comment":"\"\"The business notion a logical artefact is the data-side counterpart of. One relation at two granularities:\n DataObject represents BusinessObject (exactly one -- the pivot, BR-006)\n DataElement represents BusinessConcept (at most one)\nRenamed from 'implements', which carried software-engineering baggage: a Data Object does not implement a Business Object the way a class implements an interface.\n\nRange is deliberately left open and scoped by SHACL per class, because the two uses have different targets.\n\nA Data Element uses this ONLY when it is dimensional. A measure element reaches meaning through Metric -> computedBy, which says more: it names WHICH metric the meaning travels through. Never both routes on one element.\n\nFunctional at DataObject level only -- not declared owl:FunctionalProperty here, since a single global cardinality would be wrong for one of the two uses.\"\"","domain":"","range":"","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:implements","kind":"Relation","label":"implements (deprecated)","shortLabel":"","comment":"","domain":"","range":"","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":true,"transitive":false,"deprecated":true,"replacedBy":"pr:represents","seam":false,"prov":"","layer":""},{"id":"pr:hasElement","kind":"Relation","label":"has element","shortLabel":"","comment":"","domain":"pr:DataObject","range":"pr:DataElement","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:qualifiedBy","kind":"Relation","label":"qualified by","shortLabel":"","comment":"The Data Objects that give this one its analysis context: a fact dataset is qualified by Product, Customer, Currency and Date. Physically a foreign key, which is why it is checkable against the harvested ForeignKeyConstraint objects -- but the name says the reason rather than the mechanism. NOT ownership. It is also what carries the grain of a fact object, whose own elements are measures and whose keys are not Data Elements (OW-006).","domain":"pr:DataObject","range":"pr:DataObject","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:references","kind":"Relation","label":"references (deprecated)","shortLabel":"","comment":"","domain":"","range":"","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":true,"replacedBy":"pr:qualifiedBy","seam":false,"prov":"","layer":""},{"id":"pr:packages","kind":"Relation","label":"packages","shortLabel":"","comment":"The scope of the product. Through these Data Objects and their represents, the Business Objects a product makes available are derivable -- which is why a product needs no direct link to a Business Object.","domain":"pr:DataProduct","range":"pr:DataObject","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:exposes","kind":"Relation","label":"exposes","shortLabel":"","comment":"The channels through which the product is actually consumed.","domain":"pr:DataProduct","range":"pr:DataInterface","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:servedBy","kind":"Relation","label":"served by","shortLabel":"","comment":"What physically backs the interface: the shared relations, the extract source.","domain":"pr:DataInterface","range":"pr:DataStructure","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:governedBy","kind":"Relation","label":"governed by","shortLabel":"","comment":"","domain":"pr:DataProduct","range":"pr:DataContract","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":true,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:operatedBy","kind":"Relation","label":"operated by","shortLabel":"","comment":"The single Sub-Domain that runs this product. A Data Product spans several Business Objects and therefore cannot inherit accountability the way a Data Object does -- so it declares its own, and exactly one. It is a consumable: there must be no ambiguity about who runs it.","domain":"pr:DataProduct","range":"pr:SubDomain","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":true,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:hasProductOwner","kind":"Relation","label":"has product owner","shortLabel":"","comment":"Exactly one. Belongs to the operating Sub-Domain -- checkable, not just conventional.","domain":"pr:DataProduct","range":"pr:DataProductOwner","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":true,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:usesDataObject","kind":"Relation","label":"uses Data Object (deprecated)","shortLabel":"","comment":"","domain":"","range":"","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":true,"replacedBy":"pr:packages","seam":false,"prov":"","layer":""},{"id":"pr:materializedAs","kind":"Relation","label":"materialized as (deprecated)","shortLabel":"","comment":"Redundant once storedIn serves two granularities. One verb fewer, and the seam counts one relation instead of two.","domain":"","range":"","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":true,"replacedBy":"pr:storedIn","seam":true,"prov":"","layer":""},{"id":"pr:storedIn","kind":"Relation","label":"stored in","shortLabel":"","comment":"\"\"Where a logical artefact actually sits. One relation at two granularities, like represents:\n DataObject storedIn DataStructure (a table, a view, later a node label)\n DataElement storedIn Field (a column, later a node property)\n\nRange left open and scoped by SHACL per class, because the two uses have different targets.\n\nTHE SEAM. This is the one relation crossing from what governance defines to what a harvester captures, and the one thing a steward confirms: the reconciliation job proposes the mapping on fully qualified name and type, a person accepts it. It is also what makes end-to-end impact analysis possible.\n\nNot functional: the same object exists in DEV, UAT and PROD, and a Data Element may sit in several Fields -- see primaryLocation for the authoritative one. Renamed from physicalizedIn, which was jargon.\"\"","domain":"","range":"","inverseOf":"","subPropertyOf":"","authoring":"PROPOSED_CONFIRMED","harvest":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":"","seam":true,"prov":"","layer":""},{"id":"pr:primaryLocation","kind":"Relation","label":"primary location","shortLabel":"","comment":"\"\"The authoritative Field among the several a Data Element may sit in -- the others being copies, replicas or downstream materialisations.\n\nA SUB-PROPERTY of storedIn on purpose, not a parallel relation. Entailment then guarantees the primary location is always among the locations, so the two can never disagree; two independent properties would allow a Field marked primary here and absent from storedIn there.\n\nPrimary and replica rather than master and slave: the industry has moved on, and it reads better to the business.\"\"","domain":"pr:DataElement","range":"pr:Field","inverseOf":"","subPropertyOf":"pr:storedIn","authoring":"PROPOSED_CONFIRMED","harvest":"","functional":true,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:physicalizedIn","kind":"Relation","label":"physicalized in (deprecated)","shortLabel":"","comment":"","domain":"","range":"","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":true,"replacedBy":"pr:storedIn","seam":true,"prov":"","layer":""},{"id":"pr:sourcedFrom","kind":"Relation","label":"sourced from","shortLabel":"","comment":"\"\"The System holding the authoritative data for this object. Replaces the free-text hasSource, and answers a question that text could not: which objects depend on MDM Product.\n\nDomain deliberately omitted -- it applies to Data Elements, Data Structures, Metrics and BI Data Sources alike, and the scope lives in the shapes.\n\nIt also settles a naming collision. As a string, \"source\" meant a system here and a BI surface in BIDataSource, two readings of one word. As a relation towards System, the two are different kinds of thing and the ambiguity disappears.\"\"","domain":"","range":"pr:System","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:hasSystemType","kind":"Relation","label":"has system type","shortLabel":"","comment":"","domain":"pr:System","range":"pr:SystemType","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":true,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:hostedOn","kind":"Relation","label":"hosted on","shortLabel":"","comment":"","domain":"pr:Database","range":"pr:System","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":true,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:inDatabase","kind":"Relation","label":"in database","shortLabel":"","comment":"","domain":"pr:Schema","range":"pr:Database","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":true,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:inSchema","kind":"Relation","label":"in schema","shortLabel":"","comment":"Domain deliberately not declared, and the shapes must keep this OPTIONAL: Neo4j has no schema and BigQuery calls the level a dataset. A minCount 1 would break on the first harvest outside Snowflake. It is also the only property that would distinguish a tabular Dataset from a Graph, which is why that intermediate level is not declared yet -- one client does not pay for a level of hierarchy.","domain":"","range":"pr:Schema","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":true,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:hasField","kind":"Relation","label":"has field","shortLabel":"","comment":"Domain widened from Table to PhysicalRelation so that views and external tables carry columns too. Inverse now declared - in v0.9 hasField and livesIn were inverses in fact but not in the model, so they were free to diverge.","domain":"pr:DataStructure","range":"pr:Field","inverseOf":"pr:livesIn","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:livesIn","kind":"Relation","label":"lives in","shortLabel":"","comment":"","domain":"pr:Field","range":"pr:DataStructure","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":true,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:deployedIn","kind":"Relation","label":"deployed in","shortLabel":"","comment":"","domain":"pr:CapturedObject","range":"pr:Environment","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":true,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:constrainsRelation","kind":"Relation","label":"constrains relation (deprecated)","shortLabel":"","comment":"Derivable: constrainsField then livesIn gives the relation. An asserted edge redundant with an existing path is a divergence waiting to happen.","domain":"","range":"","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":true,"transitive":false,"deprecated":true,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:constrainsField","kind":"Relation","label":"constrains field","shortLabel":"","comment":"Several fields for a composite key.","domain":"pr:KeyConstraint","range":"pr:Field","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:fromField","kind":"Relation","label":"from field","shortLabel":"","comment":"","domain":"pr:ForeignKeyConstraint","range":"pr:Field","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:toField","kind":"Relation","label":"to field","shortLabel":"","comment":"","domain":"pr:ForeignKeyConstraint","range":"pr:Field","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:consumes","kind":"Relation","label":"consumes","shortLabel":"","comment":"","domain":"pr:Transformation","range":"pr:DataStructure","inverseOf":"","subPropertyOf":"prov:used","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:produces","kind":"Relation","label":"produces","shortLabel":"","comment":"","domain":"pr:Transformation","range":"pr:DataStructure","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:dependsOn","kind":"Relation","label":"depends on","shortLabel":"","comment":"Relation-level dependency, view to table. Transitive by nature; in practice compute the closure at query time in Snowflake rather than materialising it with a reasoner.","domain":"pr:DataStructure","range":"pr:DataStructure","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"SNOWFLAKE.ACCOUNT_USAGE.OBJECT_DEPENDENCIES","functional":false,"transitive":true,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:derivedFrom","kind":"Relation","label":"derived from","shortLabel":"","comment":"Column-to-column lineage. The single highest-value edge in the model, and largely free: Snowflake already computes it.","domain":"pr:Field","range":"pr:Field","inverseOf":"","subPropertyOf":"prov:wasDerivedFrom","authoring":"","harvest":"SNOWFLAKE.ACCOUNT_USAGE.ACCESS_HISTORY (column lineage) ; dbt manifest","functional":false,"transitive":true,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:inWorkspace","kind":"Relation","label":"in workspace","shortLabel":"","comment":"","domain":"pr:ConsumptionLayerObject","range":"pr:BIWorkspace","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":true,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:biConsumes","kind":"Relation","label":"BI consumes","shortLabel":"","comment":"","domain":"pr:BIDataSource","range":"pr:DataStructure","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"Tableau Metadata API (GraphQL) ; Looker Explore SQL","functional":false,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:hasBIField","kind":"Relation","label":"has BI field","shortLabel":"","comment":"","domain":"pr:BIDataSource","range":"pr:BIField","inverseOf":"pr:inBIDataSource","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:inBIDataSource","kind":"Relation","label":"in BI data source","shortLabel":"","comment":"","domain":"pr:BIField","range":"pr:BIDataSource","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":true,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:biDerivedFrom","kind":"Relation","label":"BI derived from","shortLabel":"","comment":"Closes the last gap in the impact chain: from a dashboard field back to the warehouse column.","domain":"pr:BIField","range":"pr:Field","inverseOf":"","subPropertyOf":"prov:wasDerivedFrom","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:exposesMetric","kind":"Relation","label":"exposes metric","shortLabel":"","comment":"\"\"The harmonisation measure: how many distinct BI Fields expose one certified Metric, and do their expressions agree. Turns 'we harmonised the definitions' from a claim into a count over the real BI estate.\n\nIn principle this is derivable from the lineage chain Metric -> DataElement -> Field -> BIField, since lineage carries semantic inheritance. In practice that chain will have holes -- four hops, one of them PROPOSED_CONFIRMED, another harvested from an API that does not resolve custom SQL, extracts or cascaded calculated fields -- and the question it answers is too important to depend on them.\n\nSo: TRANSITIONAL CONTROL. Asserted directly, and that is all. No rule is written against the lineage chain: making the assertion conditional on intermediate mappings would create confusion and discourage people from filling it in.\n\nRetirement criterion, to be measured rather than debated: when the lineage chain reproduces the asserted link for more than 95% of certified BI fields across three consecutive reconciliation cycles, the direct link is derivable and can go.\"\"","domain":"pr:BIField","range":"pr:Metric","inverseOf":"","subPropertyOf":"","authoring":"PROPOSED_CONFIRMED","harvest":"","functional":true,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:usesBIDataSource","kind":"Relation","label":"uses BI data source","shortLabel":"","comment":"","domain":"pr:BIReport","range":"pr:BIDataSource","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:hasBIOwner","kind":"Relation","label":"has BI owner","shortLabel":"","comment":"The author or publisher. This is who gets notified by an impact analysis, and it is NOT the Data Domain owner.","domain":"pr:ConsumptionLayerObject","range":"pr:Actor","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:ownedBy","kind":"Relation","label":"owned by","shortLabel":"","comment":"","domain":"","range":"pr:Actor","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:owningDomain","kind":"Relation","label":"owning Data Domain","shortLabel":"","comment":"\"\"The single Data Domain with authority to define the object's rule. Absent on Captured Objects: a Snowflake column is attributed through the Data Element mapped to it, never asserted directly.\n\nUnlike the steward, this one is RE-DECLARABLE on a Data Element. It is inherited by default from the parent chain, but OW-002 attributes an element to the domain that defines its RULE, and a borrowed element -- Brand Code sitting in a Sell Out fact object -- has its rule elsewhere. A Data Element whose owningDomain differs from its parent's is a borrowed element: a signal to check against its represents, not an anomaly to fix.\"\"","domain":"","range":"pr:DataDomain","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":true,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:monitoredBy","kind":"Relation","label":"monitored by","shortLabel":"","comment":"\"\"Declared on BusinessObject, and on DataProduct, and NOWHERE ELSE. Data Objects inherit through represents, Data Elements through hasElement. Declaring a steward at several levels would allow two answers to 'who watches this column'.\n\nSays what the steward DOES, where the deprecated hasSteward only stated a membership.\n\nDataProduct is the one exception to inheritance: it packages Data Objects belonging to several Business Objects, so it cannot inherit a single steward and declares its own.\"\"","domain":"","range":"pr:DataSteward","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:hasSteward","kind":"Relation","label":"has Data Steward (deprecated)","shortLabel":"","comment":"","domain":"","range":"","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":true,"replacedBy":"pr:monitoredBy","seam":false,"prov":"","layer":""},{"id":"pr:hasDGL","kind":"Relation","label":"has DGL","shortLabel":"","comment":"","domain":"pr:DataDomain","range":"pr:DataGovernanceLead","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":true,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:hasDomainOwner","kind":"Relation","label":"has domain owner","shortLabel":"","comment":"","domain":"pr:DataDomain","range":"pr:DataDomainOwner","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":true,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:hasSubDomainOwner","kind":"Relation","label":"has sub-domain owner","shortLabel":"","comment":"","domain":"pr:SubDomain","range":"pr:SubDomainOwner","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":true,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:hasActivationStatus","kind":"Relation","label":"activation status","shortLabel":"","comment":"","domain":"pr:DataDomain","range":"pr:ActivationStatus","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":true,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""}],"attributes":[{"id":"pr:hasIdentifier","kind":"Attribute","label":"identifier","shortLabel":"","comment":"","domain":"","range":"xsd:string","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":true,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:hasName","kind":"Attribute","label":"canonical name","shortLabel":"","comment":"","domain":"","range":"xsd:string","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":true,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:hasShortLabel","kind":"Attribute","label":"short label","shortLabel":"","comment":"\"\"The name screens use, where the canonical name is the exhaustive one: Domain for Data Domain, SODH Gold for Sell Out Data Hub - Gold.\n\nDeclared rather than left to each interface. The same object was being called two different things depending on where you looked, because every viewer kept a display dictionary of its own. One name is authoritative, the other is short, and both live in the model.\"\"","domain":"","range":"xsd:string","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":true,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:hasStatus","kind":"Attribute","label":"status","shortLabel":"","comment":"","domain":"","range":"xsd:string","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:hasVersion","kind":"Attribute","label":"version","shortLabel":"","comment":"","domain":"","range":"xsd:string","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:createdOn","kind":"Attribute","label":"created on","shortLabel":"","comment":"","domain":"","range":"xsd:date","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:lastReviewedOn","kind":"Attribute","label":"last reviewed on","shortLabel":"","comment":"","domain":"","range":"xsd:date","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:hasSource","kind":"Attribute","label":"source of truth (deprecated)","shortLabel":"","comment":"\"\"The system holding the authoritative data for this object: SODH Gold, MDM Product, Group Finance.\n\nONE MEANING ONLY. It had drifted into naming the document a record came from (","domain":"","range":"xsd:string","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":true,"replacedBy":"pr:sourcedFrom","seam":false,"prov":"","layer":""},{"id":"pr:arbitrationStatus","kind":"Attribute","label":"arbitration status","shortLabel":"","comment":"TO_ARBITRATE blocks publication (OW-007). Use this on the object rather than tracking arbitration in a side spreadsheet.","domain":"","range":"xsd:string","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:hasBusinessDefinition","kind":"Attribute","label":"business definition","shortLabel":"","comment":"\"\"The meaning of a business notion. Belongs to the BusinessConcept.\n\nMUST NOT be carried by a BusinessObject: an object gets its meaning through aboutConcept. Two definitions for one notion are two definitions free to diverge. Enforced by SHACL rather than by rdfs:domain, because Data Elements legitimately carry one too and a declared domain would make them entail BusinessConcept -- which the disjointness axioms would then reject.\n\nMIGRATION: text currently on a Business Object moves to the Concept named by its aboutConcept.\"\"","domain":"","range":"xsd:string","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:hasTechnicalDefinition","kind":"Attribute","label":"technical definition","shortLabel":"","comment":"","domain":"","range":"xsd:string","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:hasAcronym","kind":"Attribute","label":"acronym","shortLabel":"","comment":"","domain":"","range":"xsd:string","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:hasSynonym","kind":"Attribute","label":"synonym","shortLabel":"","comment":"","domain":"","range":"xsd:string","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:hasBusinessRule","kind":"Attribute","label":"business rule","shortLabel":"","comment":"","domain":"","range":"xsd:string","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:hasFormula","kind":"Attribute","label":"formula","shortLabel":"","comment":"","domain":"","range":"xsd:string","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:hasUnit","kind":"Attribute","label":"unit","shortLabel":"","comment":"From the Units LoV. Note the LoV currently mixes physical units, currencies (ISO 4217) and value types (Index, Rate, Count) - three different concepts that should eventually be split.","domain":"","range":"xsd:string","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:hasGranularity","kind":"Attribute","label":"granularity (deprecated)","shortLabel":"","comment":"Free-text grain label on a Metric, e.g. 'SKU x Customer x Day'. Kept a second, unverifiable copy of something now stated structurally on the Data Object. Grain is a property of a dataset, not of a calculation rule.","domain":"","range":"xsd:string","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":true,"replacedBy":"pr:hasGrainElement","seam":false,"prov":"","layer":""},{"id":"pr:hasTimeAggregation","kind":"Attribute","label":"time aggregation","shortLabel":"","comment":"","domain":"","range":"xsd:string","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:isKPI","kind":"Attribute","label":"is KPI (deprecated)","shortLabel":"","comment":"Duplicated the KPI class. Two ways to say the same thing are two ways to diverge.","domain":"","range":"xsd:boolean","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":true,"replacedBy":"pr:KPI","seam":false,"prov":"","layer":""},{"id":"pr:hasFormat","kind":"Attribute","label":"logical format","shortLabel":"","comment":"","domain":"","range":"xsd:string","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:hasExampleValue","kind":"Attribute","label":"example value","shortLabel":"","comment":"","domain":"","range":"xsd:string","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:hasSourceSystem","kind":"Attribute","label":"source system (deprecated)","shortLabel":"","comment":"Free text. 'Which objects depend on MDM Product?' was unanswerable because MDM Product was a string, not a node.","domain":"","range":"xsd:string","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":true,"replacedBy":"pr:sourcedFrom","seam":false,"prov":"","layer":""},{"id":"pr:hasSchemaName","kind":"Attribute","label":"schema name (deprecated)","shortLabel":"","comment":"","domain":"","range":"xsd:string","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":true,"replacedBy":"pr:Schema","seam":false,"prov":"","layer":""},{"id":"pr:physicalName","kind":"Attribute","label":"physical name","shortLabel":"","comment":"Recorded verbatim, never altered (NR-009). Must NOT be carried by a Metric - that is a layer leak; under BR-013 the physical name belongs on the Data Element.","domain":"","range":"xsd:string","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":true,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:fullyQualifiedName","kind":"Attribute","label":"fully qualified name","shortLabel":"","comment":"DATABASE.SCHEMA.RELATION[.COLUMN]. The actual join key against INFORMATION_SCHEMA: a bare physical name is ambiguous as soon as the same column name exists in two tables.","domain":"","range":"xsd:string","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":true,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:sourceIdentifier","kind":"Attribute","label":"source identifier","shortLabel":"","comment":"Native immutable id in the source system (Snowflake object id, Tableau LUID, Looker id). Makes reconciliation idempotent and survives renames, which names do not.","domain":"","range":"xsd:string","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":true,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:harvestedOn","kind":"Attribute","label":"harvested on","shortLabel":"","comment":"Staleness marker. A Captured Object not refreshed by the last run is a deletion candidate, not a fact.","domain":"","range":"xsd:dateTime","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:hasPhysicalDataType","kind":"Attribute","label":"physical data type","shortLabel":"","comment":"","domain":"","range":"xsd:string","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:isNullable","kind":"Attribute","label":"is nullable","shortLabel":"","comment":"","domain":"","range":"xsd:boolean","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:ordinalPosition","kind":"Attribute","label":"ordinal position","shortLabel":"","comment":"","domain":"","range":"xsd:integer","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:hasViewDefinition","kind":"Attribute","label":"view definition","shortLabel":"","comment":"","domain":"","range":"xsd:string","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:hasVendor","kind":"Attribute","label":"vendor (deprecated)","shortLabel":"","comment":"","domain":"","range":"xsd:string","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":true,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:definedBy","kind":"Attribute","label":"defined by (code URI)","shortLabel":"","comment":"Git URI of the code behind a Transformation. Connects the graph to the repository.","domain":"","range":"xsd:anyURI","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:approximateRowCount","kind":"Attribute","label":"approximate row count (deprecated)","shortLabel":"","comment":"Volumetry with no identified use. queryCount30d and lastQueriedOn carry the rationalisation argument; row count does not.","domain":"","range":"xsd:integer","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":true,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:lastQueriedOn","kind":"Attribute","label":"last queried on","shortLabel":"","comment":"Lets the model say 'documented but unused for eight months'. A rationalisation argument that costs nothing and is denominated in warehouse spend.","domain":"","range":"xsd:dateTime","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"SNOWFLAKE.ACCOUNT_USAGE.ACCESS_HISTORY","functional":false,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:queryCount30d","kind":"Attribute","label":"query count (30 days)","shortLabel":"","comment":"","domain":"","range":"xsd:integer","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:hasExpression","kind":"Attribute","label":"BI expression","shortLabel":"","comment":"The calculated-field formula, Tableau or LookML. Compared against the Metric formula to detect definition drift.","domain":"","range":"xsd:string","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""}],"bridges":[{"id":"pr:denotes","kind":"Bridge","label":"denotes","shortLabel":"","comment":"Links a documentation record (Business Object, Business Concept, Data Object) to the CLASS it describes in a domain ontology. Example: ex:BO_16_03_001 pr:denotes fin:Invoice .","domain":"","range":"","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:denotesProperty","kind":"Bridge","label":"denotes property","shortLabel":"","comment":"Links a Data Element or Business Concept to the PROPERTY it describes in a domain ontology. Example: ex:DE_16_03_0001 pr:denotesProperty fin:netAmount .","domain":"","range":"","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""},{"id":"pr:denotesActivity","kind":"Bridge","label":"denotes activity","shortLabel":"","comment":"Links a Metric or Business Object to the PROCESS or EVENT it describes, typically a prov:Activity subclass. Example: ex:M_16_03_001 pr:denotesActivity fin:InvoiceIssuance .","domain":"","range":"","inverseOf":"","subPropertyOf":"","authoring":"","harvest":"","functional":false,"transitive":false,"deprecated":false,"replacedBy":"","seam":false,"prov":"","layer":""}],"layers":[{"id":"roots","title":"Model roots","tag":"how a term got here"},{"id":"ownership","title":"Ownership & Categorization","tag":"who is accountable"},{"id":"business","title":"Business","tag":"what it means"},{"id":"logical","title":"Logical","tag":"how it is structured"},{"id":"delivery","title":"Delivery","tag":"what we promise, to whom, until when"},{"id":"physical","title":"Physical","tag":"what actually runs"},{"id":"consumption","title":"Consumption","tag":"what people open in the morning"},{"id":"actors","title":"Actors","tag":"people and teams"},{"id":"vocab","title":"Vocabularies","tag":"controlled value lists"}]};
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,"&amp;").replace(/</g,"&lt;"); }
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){
return p === "captured" ? "#8A6A28" : p === "defined" ? "#1E4B9B" : "#6B6B6B";
}
/* 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"]
];
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: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"],
["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;
}
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;
/* 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);
keep[n.id] = {n:n, x:0, y:0,
layer: M.layers.map(function(l){return l.id;}).indexOf(n.layer),
row: rank > -1 ? rank : 90 + Math.min(n.depth, 5)};
});
var E = [];
/* 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"]
];
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});
});
live(M.nodes).forEach(function(n){
if(n.parent && keep[n.id] && keep[n.parent])
E.push({s:n.id, t:n.parent, l:"type of", cross:false});
});
G.nodes = Object.keys(keep).map(function(k){ return keep[k]; });
G.edges = E;
return keep;
}
/* 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; }); });
/* 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. */
var laneW = {}, x = GUT;
G._lanes = [];
lanes.forEach(function(code){
var w = NW + LANEPAD*2;
laneW[code] = {x:x, w:w};
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;
});
G._width = x;
var y = 0;
M.layers.forEach(function(b, li){
b._y = y;
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;
});
rows = Math.max(rows, g.length);
});
b._h = rows*(NH+ROWGAP) + 48;
y += b._h + BANDGAP;
});
G._height = y;
}
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;
}
a.x += (600-a.x)*.003*k; a.y += (400-a.y)*.004*k;
}
links.forEach(function(e){
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;
});
}
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;
}
}
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){
var isSubject = l.code === "defined";
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});
t.textContent = l.label;
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"});
g.textContent=b.tag; bL.appendChild(g);
});
}
/* 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);
}
});
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"}));
var lines = wrapText(n.n.shortLabel || n.n.label, 21);
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"});
e.textContent = t; g.appendChild(e);
});
nL.appendChild(g);
});
applyHot();
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";
}
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(); });
if(cur) out.push(cur);
return out.slice(0,2);
}
function applyHot(){
var id = G.sel || G.hot;
var ns=document.querySelectorAll(".gnode"), es=document.querySelectorAll(".gedge"),
ls=document.querySelectorAll(".elabel,.elbg");
if(!id){
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"); });
return;
}
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);
var cur=cls.filter(function(n){return n.prov==="defined";});
var obs=cls.filter(function(n){return n.prov==="captured";});
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>');
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 '
+ '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>');
h.push('<h2 class="sec">Defined against captured</h2><div class="bar">'
+'<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">'
+'<span><i style="background:#1E4B9B"></i>Defined \u2014 '+cur.length+'</span>'
+'<span><i style="background:#8A6A28"></i>Captured \u2014 '+obs.length+'</span>'
+'<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 '
+'graph is either pure governance or pure machine, which is what keeps the mapping effort bounded.</div>');
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>');
});
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;
g.sort(function(a,b){
return a.depth - b.depth
|| ((ORDER.indexOf(a.id)+1)||99) - ((ORDER.indexOf(b.id)+1)||99)
|| 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>'
+'<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>');
});
return h.join("");
}
/* ------------------------------------------------------------ RELATIONS */
function renderProps(){
var h=['<p class="lede">'+live(M.relations).length+' relations and '+live(M.attributes).length+' attributes.</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">'
+'<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("");
}
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); });
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>'];
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");
if(p.authoring) t.push(p.authoring.toLowerCase().replace(/_/g," "));
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>'
+'<td class="mono">'+(t.join(" \u00b7 ")||"\u2014")+'</td></tr>');
});
h.push('</tbody></table><p class="sublede" style="margin-top:12px">'+all.length+' shown.</p>');
return h.join("");
}
/* ------------------------------------------------------------ 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("");
}
/* ------------------------------------------------------------ SHELL */
function renderGraphShell(){
var h=['<p class="lede">The meta model as a graph.</p>'
+'<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 '
+'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("");
}
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); });
/* 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]);
});
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");
}
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>');
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>');
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");
at("Authoring mode", (n.authoring||"").toLowerCase().replace(/_/g," "));
at("Acronym", n.acronym);
} else {
at("Cardinality", n.functional ? "exactly one" : "many");
at("Transitive", n.transitive ? "yes" : "");
at("Crosses the seam", n.seam ? "yes" : "");
at("Authoring mode", (n.authoring||"").toLowerCase().replace(/_/g," "));
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 */
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>');
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){
/* 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"); });
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>');
}
link("Type of", [n.parent, n.subPropertyOf]);
link("Types", CHILDREN[n.id]);
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"); }; });
}
function bindRows(){
host.querySelectorAll("tr[data-id]").forEach(function(tr){
tr.onclick=function(){ open(tr.dataset.id); }; });
}
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;
}
["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();
</script>
</body>
</html>