Skip to content

Commit 0272901

Browse files
authored
Merge pull request #14 from sensein/claude/schema-registry-meta-model-2upt56
fix: update index.html + export_json for current data model
2 parents 84c04c2 + e4db9ea commit 0272901

2 files changed

Lines changed: 147 additions & 49 deletions

File tree

index.html

Lines changed: 143 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ <h1><span class="accent">Neuro</span>Registry</h1>
236236
<!-- GRAPH SCHEMA -->
237237
<div id="view-graph" class="view">
238238
<div class="section-pad">
239-
<p style="color:var(--muted);font-size:12px;margin-bottom:12px">Node types and relationships in the LadybugDB graph. Click a node to inspect its fields.</p>
239+
<p style="color:var(--muted);font-size:12px;margin-bottom:12px">Node types and relationships in the LadybugDB graph. Identity is content-addressed (<code style="font-family:var(--mono);color:var(--teal)">hash_id = sha256:…</code>). Click a node to inspect its fields.</p>
240240
<div id="graph-canvas">
241241
<svg class="graph-svg" id="graph-svg"></svg>
242242
<div class="g-legend"><h4>Legend</h4><div class="g-legend-item"><div class="g-legend-dot" style="background:var(--teal)"></div>Node table</div><div class="g-legend-item"><div class="g-legend-dot" style="background:var(--purple)"></div>Relationship</div></div>
@@ -253,32 +253,97 @@ <h5 id="g-detail-name" style="color:var(--teal);font-size:13px"></h5>
253253
<!-- TRANSFORM -->
254254
<div id="view-transform" class="view">
255255
<div class="transform-panel">
256-
<h2 style="font-size:18px;font-weight:600;margin-bottom:8px">Transform CSV between schemas</h2>
257-
<p style="color:var(--text-dim);font-size:12.5px;margin-bottom:20px;line-height:1.6">Upload a CSV whose columns match one schema, and convert to another. The registry uses stored alignments to auto-map columns.</p>
258-
259-
<div class="transform-section">
260-
<h3>1. Upload CSV</h3>
261-
<label class="file-drop">
262-
<input type="file" id="tr-file" accept=".csv" onchange="trOnFile(event)"/>
263-
<div id="tr-drop-label" style="color:var(--muted);font-size:13px">Click to select a CSV file, or drag here</div>
264-
</label>
265-
<div id="tr-file-info" style="margin-top:10px;font-size:11.5px;color:var(--text-dim);display:none"></div>
256+
<div class="query-tabs" style="margin-bottom:20px">
257+
<button class="active" onclick="trTabSwitch('browser',this)">In-browser CSV</button>
258+
<button onclick="trTabSwitch('api',this)">API reference</button>
266259
</div>
267260

268-
<div class="transform-section" id="tr-schema-picker" style="display:none">
269-
<h3>2. Pick schemas</h3>
270-
<div class="builder-row"><label>Source schema (CSV format)</label><select id="tr-src" onchange="trBuildMapping()"></select></div>
271-
<div class="builder-row"><label>Target schema (convert to)</label><select id="tr-tgt" onchange="trBuildMapping()"></select></div>
272-
</div>
261+
<!-- In-browser CSV transform -->
262+
<div id="tr-tab-browser">
263+
<p style="color:var(--text-dim);font-size:12.5px;margin-bottom:20px;line-height:1.6">Upload a CSV whose columns match one schema, and convert to another. Uses stored alignments to auto-map columns. Runs entirely in your browser — no data leaves your machine.</p>
264+
265+
<div class="transform-section">
266+
<h3>1. Upload CSV</h3>
267+
<label class="file-drop">
268+
<input type="file" id="tr-file" accept=".csv" onchange="trOnFile(event)"/>
269+
<div id="tr-drop-label" style="color:var(--muted);font-size:13px">Click to select a CSV file, or drag here</div>
270+
</label>
271+
<div id="tr-file-info" style="margin-top:10px;font-size:11.5px;color:var(--text-dim);display:none"></div>
272+
</div>
273+
274+
<div class="transform-section" id="tr-schema-picker" style="display:none">
275+
<h3>2. Pick schemas</h3>
276+
<div class="builder-row"><label>Source schema (CSV format)</label><select id="tr-src" onchange="trBuildMapping()"></select></div>
277+
<div class="builder-row"><label>Target schema (convert to)</label><select id="tr-tgt" onchange="trBuildMapping()"></select></div>
278+
</div>
273279

274-
<div class="transform-section" id="tr-mapping" style="display:none">
275-
<h3>3. Column mapping</h3>
276-
<p style="color:var(--muted);font-size:11.5px;margin-bottom:10px">Auto-matches shown. Adjust any dropdowns to override.</p>
277-
<table class="mapping-table" id="tr-mapping-table"></table>
280+
<div class="transform-section" id="tr-mapping" style="display:none">
281+
<h3>3. Column mapping</h3>
282+
<p style="color:var(--muted);font-size:11.5px;margin-bottom:10px">Auto-matches shown. Adjust any dropdowns to override.</p>
283+
<table class="mapping-table" id="tr-mapping-table"></table>
284+
</div>
285+
286+
<div class="transform-section" id="tr-convert" style="display:none">
287+
<button class="btn" onclick="trConvert()">Convert and download</button>
288+
</div>
278289
</div>
279290

280-
<div class="transform-section" id="tr-convert" style="display:none">
281-
<button class="btn" onclick="trConvert()">Convert and download</button>
291+
<!-- API reference -->
292+
<div id="tr-tab-api" style="display:none">
293+
<p style="color:var(--text-dim);font-size:12.5px;margin-bottom:20px;line-height:1.6">
294+
The registry exposes read endpoints today. Transform endpoints are planned — field mapping as a static file, data transform via a serverless function.
295+
</p>
296+
297+
<div class="transform-section">
298+
<h3 style="display:flex;align-items:center;gap:8px;color:var(--text)">
299+
<span style="background:var(--teal-soft);color:var(--teal);border:1px solid var(--teal-line);border-radius:4px;padding:2px 7px;font-family:var(--mono);font-size:10px">GET</span>
300+
Field mapping between two schemas
301+
<span style="background:var(--green-soft);color:var(--green);border:1px solid rgba(52,211,153,.3);border-radius:4px;padding:1px 6px;font-size:9px;font-family:var(--mono)">PLANNED</span>
302+
</h3>
303+
<div class="cypher-preview" id="tr-api-get-url" style="cursor:pointer;user-select:all" title="click to copy"></div>
304+
<p style="color:var(--muted);font-size:11.5px;margin:10px 0 8px">Returns the pre-computed field-to-field mapping derived from alignment scores. Servable as a static file — no extra infrastructure needed.</p>
305+
<pre style="background:var(--bg-elev);border:1px solid var(--border);border-radius:6px;padding:12px 14px;font-family:var(--mono);font-size:11.5px;color:var(--text-dim);white-space:pre-wrap;line-height:1.7">{
306+
"from": "bbqs", "to": "bids",
307+
"mappings": [
308+
{
309+
"from_class": "Subject", "to_class": "Participant", "distance": 0.12,
310+
"field_mappings": [
311+
{ "from_field": "subject_id", "to_field": "participant_id", "confidence": 0.85 },
312+
{ "from_field": "age", "to_field": "age", "confidence": 0.97 }
313+
]
314+
}
315+
]
316+
}</pre>
317+
</div>
318+
319+
<div class="transform-section">
320+
<h3 style="display:flex;align-items:center;gap:8px;color:var(--text)">
321+
<span style="background:var(--purple-soft);color:var(--purple);border:1px solid var(--purple-line);border-radius:4px;padding:2px 7px;font-family:var(--mono);font-size:10px">POST</span>
322+
Transform a dataset
323+
<span style="background:var(--yellow-soft);color:var(--yellow);border:1px solid rgba(251,191,36,.3);border-radius:4px;padding:1px 6px;font-size:9px;font-family:var(--mono)">NEEDS COMPUTE</span>
324+
</h3>
325+
<div class="cypher-preview">POST https://sensein.group/NeuroGhost/api/transform</div>
326+
<p style="color:var(--muted);font-size:11.5px;margin:10px 0 8px">Accepts a record in one schema's format and returns it remapped. Requires a serverless compute layer (Cloudflare Worker or equivalent).</p>
327+
<pre style="background:var(--bg-elev);border:1px solid var(--border);border-radius:6px;padding:12px 14px;font-family:var(--mono);font-size:11.5px;color:var(--text-dim);white-space:pre-wrap;line-height:1.7"># Request
328+
{ "from": "bbqs", "to": "bids", "data": { "subject_id": "sub-01", "age": 24 } }
329+
330+
# Response
331+
{ "from": "bbqs", "to": "bids",
332+
"result": { "participant_id": "sub-01", "age": 24 },
333+
"unmapped_fields": [], "warnings": [] }</pre>
334+
</div>
335+
336+
<div class="transform-section">
337+
<h3 style="color:var(--text)">Live read endpoints</h3>
338+
<table style="width:100%;font-size:12px;border-collapse:collapse">
339+
<thead><tr style="border-bottom:1px solid var(--border)">
340+
<th style="text-align:left;padding:6px 10px;color:var(--muted);font-size:10px;text-transform:uppercase;letter-spacing:.06em">Method</th>
341+
<th style="text-align:left;padding:6px 10px;color:var(--muted);font-size:10px;text-transform:uppercase;letter-spacing:.06em">URL</th>
342+
<th style="text-align:left;padding:6px 10px;color:var(--muted);font-size:10px;text-transform:uppercase;letter-spacing:.06em">Description</th>
343+
</tr></thead>
344+
<tbody id="tr-api-endpoints"></tbody>
345+
</table>
346+
</div>
282347
</div>
283348
</div>
284349
</div>
@@ -447,7 +512,8 @@ <h2>Register a Schema</h2>
447512
<script>
448513
const REGISTRY_JSON="./data/registry.json";
449514
const PROVENANCE_JSON="./data/provenance.json";
450-
const GITHUB_REPO="sensein/NeuroRegistry";
515+
const GITHUB_REPO="sensein/NeuroGhost";
516+
const API_BASE="https://sensein.group/NeuroGhost";
451517

452518
let allClasses=[],allSources=[],provenance=[],classHistories={},propHistories={},schemaHistories={};
453519
let activeSource="all",treeSrc="all",trCsv=null,trHeaders=[],trRows=[],trMapping={},selectedHist={schema:null,class:null,property:null};
@@ -462,14 +528,19 @@ <h2>Register a Schema</h2>
462528
// this UI reads uid / datatype / version. Map new→old, keeping old as-is.
463529
allClasses=(d.classes||[]).map(c=>({
464530
...c,
465-
uid: c.uid||c.hash_id,
531+
uid: c.uid||c.hash_id||"",
466532
version: c.version||c.registry_version||"",
467-
abstract: c.abstract??c.is_abstract,
468-
alignments: c.alignments||[],
533+
abstract: c.abstract??c.is_abstract??false,
534+
source: c.source||(Array.isArray(c.sources)?c.sources[0]:"")||"",
535+
alignments: (c.alignments||[]).map(a=>({
536+
...a,
537+
target_uid: a.target_uid||a.target_hash_id||"",
538+
})),
469539
properties: (c.properties||[]).map(p=>({
470540
...p,
471-
uid: p.uid||p.hash_id,
541+
uid: p.uid||p.hash_id||"",
472542
datatype: p.datatype||p.value_range||"",
543+
source: p.source||(Array.isArray(p.sources)?p.sources[0]:"")||"",
473544
})),
474545
}));
475546
allSources=d.sources||[];
@@ -501,7 +572,8 @@ <h2>Register a Schema</h2>
501572
function dlClassYAML(uid){
502573
const c=allClasses.find(x=>x.uid===uid);if(!c)return;
503574
const lines=[`# ${c.name} · ${c.source}`,`# IRI: ${c.iri}`,``,`classes:`,` ${c.name}:`,` description: "${(c.definition||"").replace(/"/g,'\\"')}"`,` class_uri: "${c.iri}"`];
504-
if(c.properties.length){lines.push(` slots:`);c.properties.forEach(p=>lines.push(` - ${p.name}`));lines.push(``,`slots:`);c.properties.forEach(p=>{lines.push(` ${p.name}:`);if(p.definition)lines.push(` description: "${p.definition.replace(/"/g,'\\"')}"`);if(p.datatype)lines.push(` range: ${p.datatype.replace("xsd:","")}`);if(p.multivalued)lines.push(` multivalued: true`);if(p.required)lines.push(` required: true`);});}
575+
if(c.abstract)lines.splice(5,0,` abstract: true`);
576+
if(c.properties.length){lines.push(` slots:`);c.properties.forEach(p=>lines.push(` - ${p.name}`));lines.push(``,`slots:`);c.properties.forEach(p=>{lines.push(` ${p.name}:`);if(p.definition)lines.push(` description: "${p.definition.replace(/"/g,'\\"')}"`);const rng=(p.datatype||p.value_range||"").replace("xsd:","");if(rng)lines.push(` range: ${rng}`);if(p.multivalued)lines.push(` multivalued: true`);});}
505577
dlText(lines.join("\n"),c.name+".yml","application/yaml");
506578
}
507579
function dlDiffCSV(){
@@ -582,18 +654,21 @@ <h5>${srcBadge(c.source)} ${c.name} <span style="color:var(--muted);font-family:
582654

583655
// GRAPH SCHEMA
584656
const GN=[
585-
{id:"SchemaClass",fields:["uid PK","iri","uri","version","created_at","name","definition","abstract","source_label","registry_version"],x:.42,y:.10},
586-
{id:"SchemaProperty",fields:["uid PK","iri","uri","version","created_at","name","definition","datatype","range_uri","multivalued","required","source_label","registry_version"],x:.08,y:.35},
587-
{id:"SchemaRule",fields:["uid PK","iri","uri","version","created_at","name","rule_spec","units","min_val","max_val","pattern","multivalued","required","registry_version"],x:.72,y:.35},
588-
{id:"SchemaSource",fields:["uid PK","iri","uri","label","mime_type","registry_version"],x:.24,y:.65},
589-
{id:"SchemaActivity",fields:["uid PK","activity","agent","started_at","issue_number","registry_version"],x:.56,y:.65},
590-
{id:"SchemaVersionSnapshot",fields:["uid PK","schema_label","version","yml_path","class_count","property_count","changes_summary","registry_version"],x:.88,y:.65},
657+
{id:"RegistryClass",fields:["hash_id PK","iri","name","definition","is_abstract"],x:.42,y:.08},
658+
{id:"RegistryProperty",fields:["hash_id PK","iri","name","definition","value_range","multivalued"],x:.08,y:.35},
659+
{id:"SkosMapping",fields:["hash_id PK","predicate","object_iri","confidence"],x:.75,y:.35},
660+
{id:"ProvenanceEntry",fields:["hash_id PK","generated_at","attributed_to","activity","registry_version","issue_number"],x:.25,y:.65},
661+
{id:"SchemaSource",fields:["uid PK","label","iri","mime_type"],x:.55,y:.65},
662+
{id:"SchemaVersionSnapshot",fields:["uid PK","schema_label","version","yml_path","class_count","property_count"],x:.88,y:.65},
591663
];
592664
const GR=[
593-
{f:"SchemaClass",t:"SchemaProperty",l:"HAS_PROPERTY"},{f:"SchemaClass",t:"SchemaClass",l:"SUBCLASS_OF"},
594-
{f:"SchemaClass",t:"SchemaClass",l:"ALIGNED_TO"},{f:"SchemaClass",t:"SchemaClass",l:"PRIOR_VERSION"},
595-
{f:"SchemaProperty",t:"SchemaProperty",l:"PRIOR_VERSION_P"},{f:"SchemaRule",t:"SchemaClass",l:"APPLIES_TO"},
596-
{f:"SchemaClass",t:"SchemaActivity",l:"PROV_GENERATED"},{f:"SchemaClass",t:"SchemaSource",l:"FROM_SOURCE"},
665+
{f:"RegistryClass",t:"RegistryProperty",l:"HAS_PROPERTY"},
666+
{f:"RegistryClass",t:"RegistryClass",l:"SUBCLASS_OF"},
667+
{f:"RegistryClass",t:"RegistryClass",l:"ALIGNED_TO"},
668+
{f:"RegistryClass",t:"ProvenanceEntry",l:"HAS_PROVENANCE"},
669+
{f:"RegistryProperty",t:"ProvenanceEntry",l:"HAS_PROVENANCE"},
670+
{f:"RegistryClass",t:"SkosMapping",l:"HAS_SKOS_MAPPING"},
671+
{f:"RegistryClass",t:"SchemaVersionSnapshot",l:"SNAPSHOTTED_IN"},
597672
];
598673
let selNode=null;
599674
function renderGraph(){
@@ -730,6 +805,12 @@ <h4>${displayName} <span class="ver-badge">v${v.version}</span></h4>
730805
}
731806

732807
// TRANSFORM
808+
function trTabSwitch(tab,btn){
809+
document.querySelectorAll("#view-transform .query-tabs button").forEach(b=>b.classList.remove("active"));
810+
btn.classList.add("active");
811+
document.getElementById("tr-tab-browser").style.display=tab==="browser"?"block":"none";
812+
document.getElementById("tr-tab-api").style.display=tab==="api"?"block":"none";
813+
}
733814
function initTransform(){
734815
const src=document.getElementById("tr-src"),tgt=document.getElementById("tr-tgt");
735816
src.innerHTML="";tgt.innerHTML="";
@@ -738,6 +819,25 @@ <h4>${displayName} <span class="ver-badge">v${v.version}</span></h4>
738819
const o2=document.createElement("option");o2.value=s.label;o2.textContent=s.label;tgt.appendChild(o2);
739820
if(i===1)o1.selected=true; if(i===0)o2.selected=true;
740821
});
822+
// Populate API tab
823+
const firstSrc=allSources[0]?.label||"bbqs";
824+
const secondSrc=allSources[1]?.label||"bids";
825+
const getUrl=`${API_BASE}/api/transform?from=${firstSrc}&to=${secondSrc}`;
826+
const urlEl=document.getElementById("tr-api-get-url");
827+
if(urlEl){urlEl.textContent=`GET ${getUrl}`;urlEl.onclick=()=>cpText(getUrl);}
828+
const tbody=document.getElementById("tr-api-endpoints");
829+
if(tbody){
830+
const endpoints=[
831+
{method:"GET",url:`${API_BASE}/data/registry.json`,desc:"Full registry — all schemas, classes, alignments"},
832+
{method:"GET",url:`${API_BASE}/data/versions/{ver}.json`,desc:"Frozen snapshot at a specific registry version"},
833+
{method:"GET",url:`${API_BASE}/data/provenance.json`,desc:"Changelog of every schema ingestion"},
834+
];
835+
tbody.innerHTML=endpoints.map(e=>`<tr style="border-bottom:1px solid var(--border-soft)">
836+
<td style="padding:7px 10px"><span style="background:var(--teal-soft);color:var(--teal);border:1px solid var(--teal-line);border-radius:4px;padding:1px 6px;font-family:var(--mono);font-size:10px">${e.method}</span></td>
837+
<td style="padding:7px 10px;font-family:var(--mono);font-size:11px;color:var(--teal)"><a href="${e.url}" target="_blank" style="color:var(--teal);text-decoration:none">${e.url.replace(API_BASE,"")}</a></td>
838+
<td style="padding:7px 10px;color:var(--text-dim);font-size:11.5px">${e.desc}</td>
839+
</tr>`).join("");
840+
}
741841
}
742842
function trOnFile(e){
743843
const file=e.target.files[0];if(!file)return;
@@ -816,17 +916,17 @@ <h4>${displayName} <span class="ver-badge">v${v.version}</span></h4>
816916
const name=(document.getElementById("q-name").value||"").toLowerCase();
817917

818918
// Build Cypher preview
819-
let cypher=`MATCH (n:${what==="properties"?"SchemaProperty":"SchemaClass"})`;
919+
let cypher=`MATCH (n:${what==="properties"?"RegistryProperty":"RegistryClass"})`;
820920
const clauses=[];
821-
if(src)clauses.push(`n.source_label = '${src}'`);
921+
if(src)clauses.push(`'${src}' IN n.sources`);
822922
if(name)clauses.push(`n.name CONTAINS '${name}'`);
823923
if(alnSrc&&what==="classes"){
824-
cypher+=`-[r:ALIGNED_TO]->(t:SchemaClass)`;
825-
clauses.push(`t.source_label = '${alnSrc}'`);
924+
cypher+=`-[r:ALIGNED_TO]->(t:RegistryClass)`;
925+
clauses.push(`'${alnSrc}' IN t.sources`);
826926
clauses.push(`r.distance <= ${dist}`);
827927
}
828928
if(clauses.length)cypher+=`\nWHERE ${clauses.join("\n AND ")}`;
829-
cypher+=`\nRETURN n.name, n.iri, n.source_label${alnSrc&&what==="classes"?", t.name, r.distance":""}`;
929+
cypher+=`\nRETURN n.name, n.iri, n.hash_id${alnSrc&&what==="classes"?", t.name, r.distance":""}`;
830930

831931
document.getElementById("q-cypher").style.display="block";
832932
document.getElementById("q-cypher").textContent=cypher;

neuro_ghost/export_json.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,13 @@ def export_snapshot(conn, registry_version: str) -> dict:
6363
# has a single "source" — it has a `sources` list, one per ProvenanceEntry.
6464
cls_rows = conn.execute("""
6565
MATCH (n:RegistryClass)
66-
RETURN n.hash_id, n.class_uri, n.name, n.description,
67-
n.abstract, n.registry_version
66+
RETURN n.hash_id, n.class_uri, n.name, n.description, n.abstract
6867
ORDER BY n.name
6968
""").get_all()
7069

7170
classes = []
7271
for row in cls_rows:
73-
hash_id, class_uri, name, desc, is_abstract, reg_ver = row
72+
hash_id, class_uri, name, desc, is_abstract = row
7473

7574
props = conn.execute("""
7675
MATCH (c:RegistryClass {hash_id: $hash_id})-[:HAS_PROPERTY]->(p:RegistryProperty)
@@ -97,9 +96,8 @@ def export_snapshot(conn, registry_version: str) -> dict:
9796
"hash_id": hash_id,
9897
"iri": class_uri or "",
9998
"name": name or "",
100-
"definition": desc or "",
101-
"registry_version": reg_ver or "",
102-
"is_abstract": bool(is_abstract),
99+
"definition": desc or "",
100+
"is_abstract": bool(is_abstract),
103101
"sources": _attesting_sources(conn, "RegistryClass", "HAS_PROVENANCE", hash_id),
104102
"properties": [
105103
{

0 commit comments

Comments
 (0)