Skip to content

Commit d5cf62b

Browse files
authored
Merge pull request #12 from sensein/claude/schema-registry-meta-model-2upt56
Claude/schema registry meta model 2upt56
2 parents 42a5c9c + 33bea55 commit d5cf62b

2 files changed

Lines changed: 139 additions & 114 deletions

File tree

README.md

Lines changed: 28 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -100,198 +100,112 @@ the entire registry, or a CSV diff between two schemas.
100100

101101
## API Reference
102102

103-
**Base URL:** `https://sensein.group/NeuroGhost`
103+
Static JSON served by GitHub Pages — no auth, no rate limits, CORS open.
104104

105-
The registry exposes two kinds of endpoints:
106-
107-
| Type | Transport | Auth | Status |
108-
|------|-----------|------|--------|
109-
| Read (schemas, alignments, provenance) | Static JSON via GitHub Pages | None | ✅ Live |
110-
| Transform (field mapping between schemas) | Serverless function (planned) | None | 🔜 Planned |
111-
112-
CORS is open on all endpoints. No API key required.
105+
| Method | URL | Description | Status |
106+
|--------|-----|-------------|--------|
107+
| `GET` | [`/data/registry.json`](https://sensein.group/NeuroGhost/data/registry.json) | Full registry: all schemas, classes, properties, alignments | ✅ Live |
108+
| `GET` | [`/data/versions/{version}.json`](https://sensein.group/NeuroGhost/data/versions/1.7.0.json) | Frozen snapshot at a specific registry version | ✅ Live |
109+
| `GET` | [`/data/provenance.json`](https://sensein.group/NeuroGhost/data/provenance.json) | Changelog of every schema ingestion | ✅ Live |
110+
| `GET` | `/api/transform?from={schema}&to={schema}` | Field-to-field mapping between two schemas | 🔜 Planned |
111+
| `POST` | `/api/transform` | Transform a dataset from one schema format to another | 🔜 Planned |
113112

114113
---
115114

116115
### `GET /data/registry.json`
117116

118-
Returns the full registry at the current version: all sources, classes,
119-
properties, and pre-computed cross-schema alignments.
120-
121117
```bash
122118
curl https://sensein.group/NeuroGhost/data/registry.json
123119
```
124120

125-
**Response**
126121
```json
127122
{
128123
"registry_version": "1.7.0",
129124
"generated_at": "2026-07-23T12:40:24Z",
130125
"sources": [
131-
{ "label": "bbqs", "version": "1.0.0", "class_count": 29 },
132-
{ "label": "bids", "version": "1.9.0", "class_count": 1 },
133-
{ "label": "dandi", "version": "0.6.8", "class_count": 20 },
134-
{ "label": "nwb", "version": "2.7.0", "class_count": 53 }
126+
{ "label": "bbqs", "version": "1.0.0", "class_count": 29 }
135127
],
136128
"classes": [
137129
{
138-
"uid": "8003dcad-...",
130+
"hash_id": "sha256:abc123...",
139131
"iri": "https://registry.sensein.io/obj/Subject",
140132
"name": "Subject",
141133
"definition": "A research participant.",
142-
"abstract": false,
143134
"source": "bbqs",
144135
"properties": [
145-
{
146-
"uid": "807f8fec-...",
147-
"name": "age",
148-
"definition": "Age in years.",
149-
"datatype": "xsd:integer",
150-
"multivalued": false,
151-
"required": false,
152-
"source": "bbqs"
153-
}
136+
{ "hash_id": "sha256:def456...", "name": "age", "value_range": "xsd:integer", "multivalued": false, "required": false }
154137
],
155138
"alignments": [
156-
{
157-
"target_uid": "f4f8e4a4-...",
158-
"target_name": "Participant",
159-
"target_source": "bids",
160-
"distance": 0.12,
161-
"method": "composite",
162-
"scores": { "iri": 0.0, "name": 0.08, "desc": 0.19, "slot": 0.0 }
163-
}
139+
{ "target_name": "Participant", "target_source": "bids", "distance": 0.12, "method": "composite" }
164140
]
165141
}
166142
]
167143
}
168144
```
169145

170-
`distance` ranges from **0.0** (identical) to **1.0** (unrelated).
146+
`distance`: **0.0** = identical · **1.0** = unrelated.
171147

172148
---
173149

174150
### `GET /data/versions/{version}.json`
175151

176-
Frozen snapshot of the registry at a specific version. Snapshots never change.
152+
Same shape as `registry.json`. Snapshots are permanent — they never change.
177153

178154
```bash
179155
curl https://sensein.group/NeuroGhost/data/versions/1.2.0.json
180156
```
181157

182-
Same shape as `registry.json`. To list available versions, check the
183-
`registry_version` field of the live registry and the `data/versions/`
184-
directory.
185-
186158
---
187159

188160
### `GET /data/provenance.json`
189161

190-
Changelog — every schema ingestion: who submitted it, when, and which registry
191-
version it produced.
192-
193162
```bash
194163
curl https://sensein.group/NeuroGhost/data/provenance.json
195164
```
196165

197166
---
198167

199-
### Client-side filtering
200-
201-
There are no server-side query parameters (static files). Filter in your
202-
client after fetching:
203-
204-
```js
205-
const reg = await fetch("https://sensein.group/NeuroGhost/data/registry.json")
206-
.then(r => r.json());
207-
208-
// All classes from one schema
209-
const bbqs = reg.classes.filter(c => c.source === "bbqs");
210-
211-
// Close alignments across any two schemas (distance < 0.35)
212-
const pairs = reg.classes.flatMap(c =>
213-
c.alignments
214-
.filter(a => a.distance < 0.35)
215-
.map(a => ({ from: `${c.source}/${c.name}`, to: `${a.target_source}/${a.target_name}`, distance: a.distance }))
216-
);
217-
```
218-
219-
---
220-
221-
### `GET /api/transform` — field mapping *(planned)*
168+
### `GET /api/transform?from={schema}&to={schema}` *(planned)*
222169

223-
Returns the computed field-to-field mapping between two schemas, derived from
224-
the alignment graph. No data is sent — this is purely a schema-level lookup.
170+
Returns the pre-computed field mapping between two schemas. Derived from the
171+
alignment graph; can be served as a static file with no extra infrastructure.
225172

226-
```
227-
GET /api/transform?from=bbqs&to=bids
173+
```bash
174+
curl "https://sensein.group/NeuroGhost/api/transform?from=bbqs&to=bids"
228175
```
229176

230-
**Planned response**
231177
```json
232178
{
233-
"from": "bbqs",
234-
"to": "bids",
179+
"from": "bbqs", "to": "bids",
235180
"mappings": [
236181
{
237-
"from_class": "Subject",
238-
"to_class": "Participant",
239-
"distance": 0.12,
182+
"from_class": "Subject", "to_class": "Participant", "distance": 0.12,
240183
"field_mappings": [
241-
{ "from_field": "age", "to_field": "age", "confidence": 0.97 },
242-
{ "from_field": "species", "to_field": "species", "confidence": 0.91 },
243-
{ "from_field": "subject_id", "to_field": "participant_id","confidence": 0.85 }
184+
{ "from_field": "subject_id", "to_field": "participant_id", "confidence": 0.85 },
185+
{ "from_field": "age", "to_field": "age", "confidence": 0.97 }
244186
]
245187
}
246188
]
247189
}
248190
```
249191

250-
Because the alignment data is already in `registry.json`, this endpoint can be
251-
pre-computed at export time and served as a static file — no compute layer
252-
needed.
253-
254192
---
255193

256-
### `POST /api/transform` — data transform *(planned)*
194+
### `POST /api/transform` *(planned)*
257195

258-
Send a dataset in one schema's format; receive it mapped to another. Unlike the
259-
GET above, this requires a live compute layer (a serverless function that reads
260-
the field-mapping and rewrites the payload).
196+
Send a record in one schema's format; receive it remapped to another.
197+
Requires a serverless compute layer (Cloudflare Worker or equivalent).
261198

262199
```bash
263200
curl -X POST https://sensein.group/NeuroGhost/api/transform \
264201
-H "Content-Type: application/json" \
265-
-d '{
266-
"from": "bbqs",
267-
"to": "bids",
268-
"data": {
269-
"subject_id": "sub-01",
270-
"age": 24,
271-
"species": "Homo sapiens"
272-
}
273-
}'
202+
-d '{ "from": "bbqs", "to": "bids", "data": { "subject_id": "sub-01", "age": 24 } }'
274203
```
275204

276-
**Planned response**
277205
```json
278-
{
279-
"from": "bbqs",
280-
"to": "bids",
281-
"result": {
282-
"participant_id": "sub-01",
283-
"age": 24,
284-
"species": "Homo sapiens"
285-
},
286-
"unmapped_fields": [],
287-
"warnings": []
288-
}
206+
{ "from": "bbqs", "to": "bids", "result": { "participant_id": "sub-01", "age": 24 }, "unmapped_fields": [], "warnings": [] }
289207
```
290208

291-
This will be implemented as a lightweight serverless function (Cloudflare Worker
292-
or equivalent) that reads the static registry alignment map and applies field
293-
renaming. Not yet live.
294-
295209
---
296210

297211
## Adding your own schema

neuro_ghost/pipeline.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
"""
2+
pipeline.py — Run the full NeuroGhost ingestion pipeline in one command.
3+
4+
Steps
5+
-----
6+
1. (Optional) Delete the database file for a clean rebuild
7+
2. Seed schema.org as the base vocabulary
8+
3. (Optional) Fetch + convert external schemas via converters/run_all.py
9+
4. Ingest every .yml file found in the schemas directory
10+
5. Compute cross-schema alignments
11+
6. Export registry.json + version snapshot
12+
13+
Usage
14+
-----
15+
# Fresh rebuild — wipe DB, fetch external schemas, ingest everything
16+
python neuro_ghost/pipeline.py --fresh
17+
18+
# Re-ingest local schemas only (skip external fetch)
19+
python neuro_ghost/pipeline.py --fresh --skip-converters
20+
21+
# Incremental — add one schema to an existing DB
22+
python neuro_ghost/pipeline.py --skip-converters --schemas schemas/bbqs.yml
23+
"""
24+
25+
from __future__ import annotations
26+
import subprocess
27+
import sys
28+
from pathlib import Path
29+
30+
import click
31+
32+
HERE = Path(__file__).parent
33+
ROOT = HERE.parent
34+
SCHEMAS = ROOT / "schemas"
35+
DB_PATH = str(ROOT / "registry.lbug")
36+
37+
38+
def _run(cmd: list[str]) -> None:
39+
print(f"\n$ {' '.join(cmd)}")
40+
result = subprocess.run(cmd, check=False)
41+
if result.returncode != 0:
42+
print(f"\nERROR: command exited with code {result.returncode}")
43+
sys.exit(result.returncode)
44+
45+
46+
@click.command()
47+
@click.option("--db", default=DB_PATH, show_default=True,
48+
help="Path to the LadybugDB file.")
49+
@click.option("--fresh", is_flag=True,
50+
help="Delete the DB before starting (clean rebuild).")
51+
@click.option("--skip-converters", is_flag=True,
52+
help="Skip fetching external schemas (BIDS, NWB, DANDI, …).")
53+
@click.option("--schemas", default=None, multiple=True,
54+
help="Specific .yml files to ingest. Defaults to all files in schemas/.")
55+
@click.option("--bump", default="minor", show_default=True,
56+
type=click.Choice(["major", "minor", "patch"]),
57+
help="Version bump type for the registry export.")
58+
@click.option("--agent", default="local", show_default=True,
59+
help="Who is running this pipeline (recorded in provenance).")
60+
def cli(db: str, fresh: bool, skip_converters: bool,
61+
schemas: tuple, bump: str, agent: str) -> None:
62+
"""Run the full NeuroGhost ingestion pipeline."""
63+
64+
py = sys.executable
65+
66+
# 1. Fresh wipe
67+
if fresh:
68+
db_path = Path(db)
69+
if db_path.exists():
70+
db_path.unlink()
71+
print(f"Deleted {db_path}")
72+
73+
# 2. Seed schema.org
74+
_run([py, str(HERE / "seed.py"), "--db", db])
75+
76+
# 3. External converters
77+
if not skip_converters:
78+
_run([py, str(HERE / "converters" / "run_all.py")])
79+
80+
# 4. Ingest schemas
81+
targets: list[Path] = (
82+
[Path(s) for s in schemas]
83+
if schemas
84+
else sorted(p for p in SCHEMAS.glob("*.yml")
85+
if p.name != "meta_model.yaml")
86+
)
87+
88+
if not targets:
89+
print("No schemas found to ingest.")
90+
sys.exit(1)
91+
92+
for schema_file in targets:
93+
_run([py, str(HERE / "ingest_linkml.py"),
94+
"--file", str(schema_file),
95+
"--db", db,
96+
"--agent", agent])
97+
98+
# 5. Align
99+
_run([py, str(HERE / "align.py"), "--db", db])
100+
101+
# 6. Export
102+
_run([py, str(HERE / "export_json.py"),
103+
"--db", db,
104+
"--bump", bump,
105+
"--agent", agent])
106+
107+
print("\nPipeline complete.")
108+
109+
110+
if __name__ == "__main__":
111+
cli()

0 commit comments

Comments
 (0)