@@ -392,6 +392,120 @@ def mcp() -> None:
392392 run_mcp ()
393393
394394
395+ # ---------------------------------------------------------------------------
396+ # registry — federate multiple bundles (meta-lokf); see SPEC §11
397+ # ---------------------------------------------------------------------------
398+ registry_app = typer .Typer (
399+ help = "Federate bundles: a registry of LOKF bundles (meta-lokf)." ,
400+ no_args_is_help = True ,
401+ )
402+ app .add_typer (registry_app , name = "registry" )
403+
404+
405+ @registry_app .command ("init" )
406+ def registry_init (
407+ registry : Path = typer .Option (
408+ Path ("lokf-registry.yaml" ), "--registry" , "-r" , help = "Manifest path to create."
409+ ),
410+ catalog_id : str = typer .Option ("" , "--id" , help = "Registry IRI (dcat:Catalog @id)." ),
411+ title : str = typer .Option ("" , "--title" , help = "Registry title." ),
412+ ) -> None :
413+ """Scaffold an empty registry manifest."""
414+ from lokf .registry import Registry
415+
416+ if registry .exists ():
417+ _err (f"{ registry } already exists" )
418+ raise typer .Exit (1 )
419+ Registry (path = registry , id = catalog_id , title = title ).save ()
420+ typer .echo (f"wrote { registry } " )
421+
422+
423+ @registry_app .command ("add" )
424+ def registry_add (
425+ bundle_dir : Path = typer .Argument (
426+ ..., exists = True , file_okay = False , help = "A local LOKF bundle directory."
427+ ),
428+ registry : Path = typer .Option (
429+ Path ("lokf-registry.yaml" ), "--registry" , "-r" , help = "Manifest to append to."
430+ ),
431+ source_base : Optional [str ] = typer .Option (
432+ None ,
433+ "--source-base" ,
434+ help = "URL prefix for source .md files (defaults to the bundle's file:// path)." ,
435+ ),
436+ ) -> None :
437+ """Register a local bundle: derive its base_iri, VoID index, and id_index."""
438+ from lokf .registry import entry_for_bundle , load_registry
439+
440+ if not registry .exists ():
441+ _err (f"{ registry } not found — run `lokf registry init` first" )
442+ raise typer .Exit (1 )
443+ reg = load_registry (registry )
444+ try :
445+ entry = entry_for_bundle (bundle_dir , source_base = source_base )
446+ reg .add (entry )
447+ except ValueError as exc :
448+ _err (f"cannot register { bundle_dir } : { exc } " )
449+ raise typer .Exit (1 )
450+ reg .save ()
451+ typer .echo (
452+ f"registered { entry .base_iri } "
453+ f"({ entry .void .get ('triples' , 0 )} triples) in { registry } "
454+ )
455+
456+
457+ @registry_app .command ("list" )
458+ def registry_list (
459+ registry : Path = typer .Option (
460+ Path ("lokf-registry.yaml" ), "--registry" , "-r" , help = "Manifest to read."
461+ ),
462+ ) -> None :
463+ """Print the routing table: base_iri, title, triples, status."""
464+ from lokf .registry import load_registry
465+
466+ if not registry .exists ():
467+ _err (f"{ registry } not found — run `lokf registry init` first" )
468+ raise typer .Exit (1 )
469+ reg = load_registry (registry )
470+ if not reg .repos :
471+ typer .echo ("(no members registered)" )
472+ return
473+ for e in sorted (reg .repos , key = lambda e : e .base_iri ):
474+ triples = e .void .get ("triples" , "?" )
475+ typer .echo (f"{ e .base_iri } \t { e .title } \t { triples } triples\t { e .status } " )
476+
477+
478+ @registry_app .command ("resolve" )
479+ def registry_resolve (
480+ iri : str = typer .Argument (..., help = "An absolute concept IRI to resolve." ),
481+ registry : Path = typer .Option (
482+ Path ("lokf-registry.yaml" ), "--registry" , "-r" , help = "Manifest to read."
483+ ),
484+ ) -> None :
485+ """Resolve an IRI to its owning bundle, Concept ID, and source URL (offline).
486+
487+ Exits non-zero for an IRI no member owns — a tolerated dangling cross-link,
488+ not owned here.
489+ """
490+ from lokf .registry import load_registry
491+
492+ if not registry .exists ():
493+ _err (f"{ registry } not found — run `lokf registry init` first" )
494+ raise typer .Exit (1 )
495+ res = load_registry (registry ).resolve (iri )
496+ if res .external :
497+ typer .echo (f"{ iri } \n external: not owned by any registered bundle" )
498+ raise typer .Exit (1 )
499+ typer .echo (iri )
500+ typer .echo (f" owner: { res .entry .base_iri } ({ res .entry .title } )" )
501+ if res .concept_id :
502+ typer .echo (f" concept_id: { res .concept_id } " )
503+ typer .echo (f" source_url: { res .source_url or '(none)' } " )
504+ else :
505+ typer .echo (" concept_id: (namespace root — not a concept)" )
506+ typer .echo (f" via: { res .via } " )
507+
508+
395509def main (argv : list [str ] | None = None ) -> int :
396510 """Programmatic entry point (tests use ``typer.testing.CliRunner``).
397511
0 commit comments