@@ -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 >
448513const REGISTRY_JSON = "./data/registry.json" ;
449514const 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
452518let allClasses = [ ] , allSources = [ ] , provenance = [ ] , classHistories = { } , propHistories = { } , schemaHistories = { } ;
453519let 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>
501572function 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}
507579function dlDiffCSV ( ) {
@@ -582,18 +654,21 @@ <h5>${srcBadge(c.source)} ${c.name} <span style="color:var(--muted);font-family:
582654
583655// GRAPH SCHEMA
584656const 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] ;
592664const 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] ;
598673let selNode = null ;
599674function 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+ }
733814function 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}
742842function 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 ;
0 commit comments