Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
cache-dependency-path: requirements-dev.txt
- name: Install validation dependencies
run: python -m pip install -r requirements-dev.txt
- name: Run review-hardening regression tests
- name: Run regression and generated-view tests
run: python -m unittest discover -s tests -v
- name: Validate schemas, records, and references
run: python tools/validate.py
Expand All @@ -46,3 +46,11 @@ jobs:
run: python tools/build_year.py 2025 --format json > /tmp/modelmap-2025.json
- name: Build 2026 year view
run: python tools/build_year.py 2026 --format json > /tmp/modelmap-2026.json
- name: Build browse catalog
run: python tools/build_catalog.py > /tmp/modelmap-catalog.json
- name: Build core knowledge graph
run: python tools/build_graph.py --scope core > /tmp/modelmap-graph-core.json
- name: Build full knowledge graph
run: python tools/build_graph.py --scope all > /tmp/modelmap-graph-all.json
- name: Build representative model entity view
run: python tools/build_entity.py release:gpt-5.6 > /tmp/modelmap-gpt-5.6.json
26 changes: 26 additions & 0 deletions schemas/catalog-view.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://modelmap.dev/schemas/catalog-view.schema.json",
"title": "ModelMap Generated Catalog View",
"type": "object",
"required": ["generated_from", "filter_type", "counts", "records"],
"properties": {
"generated_from": {"const": "canonical-records"},
"filter_type": {"type": ["string", "null"]},
"counts": {"type": "object", "additionalProperties": {"type": "integer", "minimum": 0}},
"records": {
"type": "array",
"items": {
"type": "object",
"required": ["id", "type", "record_path"],
"properties": {
"id": {"type": "string"},
"type": {"type": "string"},
"record_path": {"type": "string"}
},
"additionalProperties": true
}
}
},
"additionalProperties": false
}
30 changes: 30 additions & 0 deletions schemas/entity-view.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://modelmap.dev/schemas/entity-view.schema.json",
"title": "ModelMap Generated Entity View",
"type": "object",
"required": ["generated_from", "entity", "outgoing_relationships", "incoming_relationships", "claims", "events", "variants", "related_products"],
"properties": {
"generated_from": {"const": "canonical-records"},
"entity": {"type": "object", "required": ["id", "type", "record_path"], "additionalProperties": true},
"outgoing_relationships": {"type": "array", "items": {"type": "object"}},
"incoming_relationships": {"type": "array", "items": {"type": "object"}},
"claims": {"type": "array", "items": {"type": "object"}},
"events": {"type": "array", "items": {"type": "object"}},
"variants": {
"type": "array",
"items": {
"type": "object",
"required": ["entity", "claims", "events"],
"properties": {
"entity": {"type": "object"},
"claims": {"type": "array"},
"events": {"type": "array"}
},
"additionalProperties": false
}
},
"related_products": {"type": "array", "items": {"type": "object"}}
},
"additionalProperties": false
}
51 changes: 51 additions & 0 deletions schemas/graph-view.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://modelmap.dev/schemas/graph-view.schema.json",
"title": "ModelMap Generated Graph View",
"type": "object",
"required": ["generated_from", "scope", "counts", "nodes", "edges"],
"properties": {
"generated_from": {"const": "canonical-records-and-relationships"},
"scope": {"enum": ["core", "all"]},
"counts": {
"type": "object",
"required": ["nodes", "edges", "omitted_edges"],
"properties": {
"nodes": {"type": "integer", "minimum": 0},
"edges": {"type": "integer", "minimum": 0},
"omitted_edges": {"type": "integer", "minimum": 0}
},
"additionalProperties": false
},
"nodes": {
"type": "array",
"items": {
"type": "object",
"required": ["id", "type", "label", "record_path"],
"properties": {
"id": {"type": "string"},
"type": {"type": "string"},
"label": {"type": "string"},
"record_path": {"type": "string"}
},
"additionalProperties": false
}
},
"edges": {
"type": "array",
"items": {
"type": "object",
"required": ["id", "subject", "predicate", "object", "claim_refs"],
"properties": {
"id": {"type": "string"},
"subject": {"type": "string"},
"predicate": {"type": "string"},
"object": {"type": "string"},
"claim_refs": {"type": "array", "items": {"type": "string"}}
},
"additionalProperties": false
}
}
},
"additionalProperties": false
}
63 changes: 63 additions & 0 deletions tests/test_generated_views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from __future__ import annotations

import json
import sys
import unittest
from pathlib import Path

from jsonschema import Draft202012Validator

ROOT = Path(__file__).resolve().parents[1]
TOOLS = ROOT / "tools"
if str(TOOLS) not in sys.path:
sys.path.insert(0, str(TOOLS))

from build_catalog import load_catalog # noqa: E402
from build_entity import build_entity_view # noqa: E402
from build_graph import build_graph # noqa: E402


def load_schema(name: str) -> dict:
return json.loads((ROOT / "schemas" / name).read_text(encoding="utf-8"))


class GeneratedViewsTest(unittest.TestCase):
def test_catalog_is_schema_valid_and_deterministically_sorted(self) -> None:
payload = load_catalog()
Draft202012Validator(load_schema("catalog-view.schema.json")).validate(payload)
keys = [(record["type"], record["id"]) for record in payload["records"]]
self.assertEqual(keys, sorted(keys))
self.assertEqual(sum(payload["counts"].values()), len(payload["records"]))

def test_core_graph_is_schema_valid_and_edges_have_nodes(self) -> None:
payload = build_graph("core")
Draft202012Validator(load_schema("graph-view.schema.json")).validate(payload)
node_ids = {node["id"] for node in payload["nodes"]}
self.assertEqual(payload["counts"]["nodes"], len(payload["nodes"]))
self.assertEqual(payload["counts"]["edges"], len(payload["edges"]))
for edge in payload["edges"]:
self.assertIn(edge["subject"], node_ids)
self.assertIn(edge["object"], node_ids)

def test_all_graph_keeps_every_relationship_edge(self) -> None:
payload = build_graph("all")
Draft202012Validator(load_schema("graph-view.schema.json")).validate(payload)
self.assertEqual(payload["counts"]["omitted_edges"], 0)

def test_release_entity_view_materializes_variants_and_products(self) -> None:
payload = build_entity_view("release:gpt-5.6")
Draft202012Validator(load_schema("entity-view.schema.json")).validate(payload)
variant_ids = {item["entity"]["id"] for item in payload["variants"]}
self.assertTrue(
{"variant:gpt-5.6-sol", "variant:gpt-5.6-terra", "variant:gpt-5.6-luna"}.issubset(variant_ids)
)
product_ids = {item["id"] for item in payload["related_products"]}
self.assertIn("product:gpt-5.6-ultra", product_ids)

def test_unknown_entity_is_rejected(self) -> None:
with self.assertRaises(KeyError):
build_entity_view("release:not-a-real-model")


if __name__ == "__main__":
unittest.main()
73 changes: 73 additions & 0 deletions tools/build_catalog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#!/usr/bin/env python3
"""Build a deterministic browse catalog from canonical ModelMap records."""

from __future__ import annotations

import argparse
import json
from collections import Counter
from pathlib import Path
from typing import Any

from records import iter_records

ROOT = Path(__file__).resolve().parents[1]
DATA = ROOT / "data"

CATALOG_TYPES = {
"architecture_spec",
"benchmark",
"benchmark_version",
"dataset",
"dataset_version",
"evaluation_protocol",
"evaluation_run",
"hardware_system",
"implementation",
"license",
"model_family",
"model_release",
"model_variant",
"organization",
"paper",
"person",
"product_system",
"technique",
"training_run",
}


def load_catalog(record_type: str | None = None) -> dict[str, Any]:
records: list[dict[str, Any]] = []
for path, record in iter_records(DATA):
rtype = record.get("type")
if rtype not in CATALOG_TYPES:
continue
if record_type and rtype != record_type:
continue
item = dict(record)
item["record_path"] = path.relative_to(ROOT).as_posix()
records.append(item)

records.sort(key=lambda item: (str(item.get("type", "")), str(item["id"])))
counts = Counter(str(item["type"]) for item in records)
return {
"generated_from": "canonical-records",
"filter_type": record_type,
"counts": dict(sorted(counts.items())),
"records": records,
}


def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--type", dest="record_type", choices=sorted(CATALOG_TYPES))
parser.add_argument("--compact", action="store_true")
args = parser.parse_args()
payload = load_catalog(args.record_type)
print(json.dumps(payload, indent=None if args.compact else 2, sort_keys=True))
return 0


if __name__ == "__main__":
raise SystemExit(main())
108 changes: 108 additions & 0 deletions tools/build_entity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
#!/usr/bin/env python3
"""Build a read-model payload for one canonical ModelMap entity."""

from __future__ import annotations

import argparse
import json
from pathlib import Path
from typing import Any

from records import iter_records

ROOT = Path(__file__).resolve().parents[1]
DATA = ROOT / "data"


def load_index() -> tuple[dict[str, dict[str, Any]], list[dict[str, Any]]]:
index: dict[str, dict[str, Any]] = {}
records: list[dict[str, Any]] = []
for path, record in iter_records(DATA):
item = dict(record)
item["record_path"] = path.relative_to(ROOT).as_posix()
records.append(item)
if item.get("id"):
index[str(item["id"])] = item
return index, records


def build_entity_view(entity_id: str) -> dict[str, Any]:
index, records = load_index()
if entity_id not in index:
raise KeyError(f"Unknown canonical id: {entity_id}")

outgoing = sorted(
[r for r in records if r.get("type") == "relationship" and r.get("subject") == entity_id],
key=lambda r: r["id"],
)
incoming = sorted(
[r for r in records if r.get("type") == "relationship" and r.get("object") == entity_id],
key=lambda r: r["id"],
)
claims = sorted(
[r for r in records if r.get("type") == "claim" and r.get("subject") == entity_id],
key=lambda r: r["id"],
)
events = sorted(
[r for r in records if r.get("type") == "event" and r.get("subject") == entity_id],
key=lambda r: (str(r.get("occurred_at", "")), r["id"]),
)

variant_ids = sorted(
r["subject"]
for r in incoming
if r.get("predicate") == "variant_of" and str(r.get("subject", "")).startswith("variant:")
)
variants: list[dict[str, Any]] = []
for variant_id in variant_ids:
variant = index.get(variant_id)
if not variant:
continue
variant_claims = sorted(
[r for r in records if r.get("type") == "claim" and r.get("subject") == variant_id],
key=lambda r: r["id"],
)
variant_events = sorted(
[r for r in records if r.get("type") == "event" and r.get("subject") == variant_id],
key=lambda r: (str(r.get("occurred_at", "")), r["id"]),
)
variants.append({"entity": variant, "claims": variant_claims, "events": variant_events})

related_products = sorted(
[
r
for r in records
if r.get("type") == "product_system"
and isinstance(r.get("metadata"), dict)
and r["metadata"].get("model_release") == entity_id
],
key=lambda r: r["id"],
)

return {
"generated_from": "canonical-records",
"entity": index[entity_id],
"outgoing_relationships": outgoing,
"incoming_relationships": incoming,
"claims": claims,
"events": events,
"variants": variants,
"related_products": related_products,
}


def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("entity_id")
parser.add_argument("--compact", action="store_true")
args = parser.parse_args()
try:
payload = build_entity_view(args.entity_id)
except KeyError as exc:
parser.error(str(exc))
print(json.dumps(payload, indent=None if args.compact else 2, sort_keys=True))
return 0


if __name__ == "__main__":
raise SystemExit(main())
Loading
Loading