Skip to content
Merged
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

All notable changes to this project are documented in this file.

## Unreleased

### Added
- **`validate-infores` checks every emitted knowledge-source identifier against the Translator registry.** The new command classifies each `infores:` CURIE on the edges (`primary_knowledge_source`, `sources[].resource_id`, `sources[].upstream_resource_ids`), the nodes (`provided_by`), and optionally the RIG document against `biolink/information-resource-registry`'s `infores_catalog.yaml` -- the authoritative catalog translator-ingests' SOP points at but never checks programmatically. The registry ships as a bundled offline snapshot (vendored from upstream commit `a72dbc3b55d5`), `--registry refresh` fetches the live catalog through the bounded-retry network layer, and `--registry off` reports structure only. The default posture is advisory (exit 0 with warnings) because Tablassert legitimately mints graph-local CURIEs; repeatable `--allow-infores` exempts them and `--strict` makes unregistered CURIEs fail in CI. Values without the `infores:` prefix are malformed, not unregistered. This closes the validation gap that only the CURIE prefix was ever checked.

## 19.6.0 - 2026-10-05

### Added
Expand Down
45 changes: 43 additions & 2 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

Tablassert extracts knowledge assertions from tabular data into KGX NDJSON. The `tablassert` app
requires the `[cli]` extra (`pip install "tablassert[cli]"` or `uv tool install "tablassert[cli]"`) and
exposes **eight subcommands**: `agent`, `build-fullmap`, `build-kg`, `distill-export`, `distill-weigh`, `quick-map`, `validate`,
and `validate-kgx`, plus an app-level `--version` flag. Run `tablassert --help` (or `<command> --help`)
exposes **nine subcommands**: `agent`, `build-fullmap`, `build-kg`, `distill-export`, `distill-weigh`, `quick-map`, `validate`,
`validate-infores`, and `validate-kgx`, plus an app-level `--version` flag. Run `tablassert --help` (or `<command> --help`)
for the live surface.

## Command index
Expand All @@ -17,6 +17,7 @@ for the live surface.
| [`distill-weigh`](#distill-weigh) | Join distillation records to outcomes and prepare LoRA-SFT training rows (experimental) |
| [`quick-map`](#quick-map) | Show what fullmap entity resolution does with one or more terms |
| [`validate`](#validate) | Validate a graph or table configuration without executing it |
| [`validate-infores`](#validate-infores) | Validate emitted infores CURIEs against the Translator registry |
| [`validate-kgx`](#validate-kgx) | Validate built KGX NDJSON against the Biolink Model |

## App flags
Expand Down Expand Up @@ -468,6 +469,46 @@ once a model release drops the requirement.

---

## validate-infores

Use this to prove the knowledge-source identifiers a build emits are real. `validate-kgx` checks
*structure* against the Biolink Model; `validate-infores` checks *identity*: every `infores:` CURIE
on your edges (`primary_knowledge_source`, `sources[].resource_id`, `sources[].upstream_resource_ids`),
nodes (`provided_by`), and, with `--rig`, the RIG document, is classified against the NCATS
Translator registry ([`biolink/information-resource-registry`](https://github.com/biolink/information-resource-registry)'s
`infores_catalog.yaml`). The registry ships as a bundled snapshot (`validate-infores` is offline by
default); `--registry refresh` fetches the live catalog instead. Values without the `infores:`
prefix are reported as malformed, not unregistered.

```bash
tablassert validate-infores --nodes MY_KG_1.0.0.nodes.ndjson --edges MY_KG_1.0.0.edges.ndjson --rig MY_KG_1.0.0.RIG.yaml
```

```text
registry: .../infores_catalog.yaml (508 entries)
curies: 3 registered, 1 allowed, 1 unregistered, 0 malformed
e.g. infores:totally-made-up (edges.primary_knowledge_source): unregistered
infores warnings present; pass --strict to fail on them.
```

The default posture is advisory and exits 0: Tablassert legitimately mints graph-local CURIEs (the
graph's own `infores:` id from the `rig:` section's name) that will never sit in the registry. Allow
those with repeatable `--allow-infores`, and pass `--strict` in CI to exit non-zero while any
unregistered CURIE remains. A missing or misspelled path is reported as `file not found` and exits
non-zero in every mode: a file that was never read must never count as a pass.

| Option | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `--nodes`, `-n` | Path | Yes | n/a | Built `*.nodes.ndjson` file to validate |
| `--edges`, `-e` | Path | Yes | n/a | Built `*.edges.ndjson` file to validate |
| `--rig` | Path | No | n/a | RIG yaml to include in the scan (its `source_info.infores_id` and target `primary_knowledge_sources` are checked) |
| `--allow-infores` | list[str] | No | n/a | Registry-exempt CURIE, repeatable; use for the locally-minted graph identifier and other deliberate locals |
| `--registry` | snapshot\\|refresh\\|off | No | `snapshot` | `snapshot` validates offline against the bundled copy; `refresh` fetches the live upstream catalog (network) and caches it under the user cache dir; `off` skips membership classification and reports structure only |
| `--strict` | Flag | No | `False` | Exit non-zero while any unregistered CURIE remains; without it the run is advisory |
| `--limit` | int | No | `20` | Maximum example unregistered/malformed CURIEs retained for the `e.g.` report lines |

---

## Typical workflow

1. Author a table config, then a graph config that references it.
Expand Down
3 changes: 2 additions & 1 deletion llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Current CLI behavior queries an embedded redb database whose primary file is `fu
- [README](README.md): high-level overview, install snippets, and one-command graph build.
- [Documentation Site Home](docs/index.md): docs landing page and navigation context.
- [Installation Guide](docs/installation.md): three install paths (PyPI, GitHub main, development from source), platform and Rust-toolchain notes, and the missing-extra behavior. Optional extras: `cli`, `rt`, `aria2`, `qc`, `agent`, `optimize`, `distill`, and `log`; the last three back experimental surfaces whose APIs may change.
- [CLI Reference](docs/cli.md): the eight commands (`agent`, `build-fullmap`, `build-kg`, `distill-export`, `distill-weigh`, `quick-map`, `validate`, and `validate-kgx`) plus the app-level `--version` flag.
- [CLI Reference](docs/cli.md): the nine commands (`agent`, `build-fullmap`, `build-kg`, `distill-export`, `distill-weigh`, `quick-map`, `validate`, `validate-infores`, and `validate-kgx`) plus the app-level `--version` flag.
- [Tutorial](docs/tutorial.md): first end-to-end run from CSV input to KGX NDJSON output.

## YAML Authoring
Expand Down Expand Up @@ -54,6 +54,7 @@ Current CLI behavior queries an embedded redb database whose primary file is `fu
- [Fullmap Module](src/tablassert/fullmap.py): embedded redb synonym resolution and ranking logic.
- [Graph Target](src/tablassert/graph_target.py): graph output target preparation and locking.
- [YAML Ingestion](src/tablassert/ingests.py): `from_yaml()`, `to_sections()`, template/section merge behavior, YAML loading, and section expansion.
- [InfoRes Registry](src/tablassert/infores_registry.py): bundled infores registry snapshot loader, KGX/RIG CURIE collection, and three-way registered/allowed/unregistered classification.
- [Core Pipeline](src/tablassert/lib.py): extraction transforms, statement assembly, and graph compilation.
- [Logging](src/tablassert/log.py): optional loguru logging abstraction.
- [Pydantic Models](src/tablassert/models.py): authoritative schema for `Section` and `Graph`: table, graph, and RIG configuration models.
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ module-name = "tablassert.rs"
bindings = "pyo3"
manifest-path = "rust/Cargo.toml"
features = ["extension-module"]
include = ["src/tablassert/data/experimental_taxa.yaml"]
include = ["src/tablassert/data/experimental_taxa.yaml", "src/tablassert/data/infores_catalog.yaml"]

[project.scripts]
tablassert = "tablassert.cli_entry:main"
Expand Down
81 changes: 81 additions & 0 deletions src/tablassert/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1045,6 +1045,87 @@ def validate_kgx_command(
print("KGX output is Biolink-compliant.", file=sys.stderr)


@APP.command(name="validate-infores")
def validate_infores_command(
nodes: Annotated[Path, cyclopts.Parameter(name=["--nodes", "-n"])],
edges: Annotated[Path, cyclopts.Parameter(name=["--edges", "-e"])],
rig: Annotated[Path | None, cyclopts.Parameter(name=["--rig"])] = None,
allow_infores: Annotated[list[str] | None, cyclopts.Parameter(name=["--allow-infores"], negative="", allow_leading_hyphen=False)] = None,
registry: Annotated[Literal["snapshot", "refresh", "off"], cyclopts.Parameter(name=["--registry"])] = "snapshot",
strict: Annotated[bool, cyclopts.Parameter(name=["--strict"])] = False,
limit: Annotated[int, cyclopts.Parameter(name=["--limit"])] = 20,
) -> None:
"""Validate emitted infores CURIEs against the NCATS Translator registry.

Every knowledge-source identifier the build emits -- edge ``primary_knowledge_source``,
``sources[].resource_id`` and ``sources[].upstream_resource_ids``, node ``provided_by``,
and, with ``--rig``, the RIG document's infores values -- is classified against
``biolink/information-resource-registry``'s ``infores_catalog.yaml`` (vendored as the
bundled snapshot, so the default path is offline). Values without the ``infores:``
prefix are malformed, not unregistered.

The default posture is advisory: unregistered CURIEs are reported and the command
exits 0, because Tablassert legitimately mints graph-local CURIEs that will never sit
in the registry. ``--strict`` (CI gates) exits 1 when any unregistered CURIE remains;
allowlist legitimate local ids with repeatable ``--allow-infores``. Missing input
files fail in every mode: a typo'd path must never read as a clean bill of health.

Args:
nodes: Path to the built nodes NDJSON file (``<name>_<version>.nodes.ndjson``).
edges: Path to the built edges NDJSON file (``<name>_<version>.edges.ndjson``).
rig: Optional RIG yaml (``<name>_<version>.RIG.yaml``); its infores values are
classified too.
allow-infores: Registry-exempt CURIE, repeatable; use for the locally-minted
graph identifier (``rig.infores()``) and other deliberate locals.
registry: Registry source. ``snapshot`` (default) validates offline against the
bundled copy; ``refresh`` fetches the live upstream catalog (network) and
caches it under the user cache dir; ``off`` skips membership classification
and reports structure only.
strict: Exit non-zero when any unregistered CURIE remains. Without it the run
is advisory and always exits 0 once inputs exist.
limit: Maximum number of example unregistered/malformed CURIEs retained for the
``e.g.`` report lines; every observation is still classified regardless.
"""
from tablassert.infores_registry import REGISTRY_RAW_URL, refresh_registry, validate_infores

ids: frozenset[str] | None = None
if registry == "refresh":
ids = refresh_registry()
report: dict[str, Any] = validate_infores(
nodes, edges, rig_path=rig, allow=tuple(allow_infores or ()), registry=ids, check_registry=registry != "off", limit=limit
)
if ids is not None:
report["registry"]["source"] = f"{REGISTRY_RAW_URL} (refreshed)"
for label, absent in report["missing"].items():
if absent:
path_label = {"nodes": nodes, "edges": edges, "rig": rig}.get(label)
print(f"{label}: file not found ({path_label})", file=sys.stderr)
if any(report["missing"].values()):
print("infores validation failed: missing input files.", file=sys.stderr)
raise SystemExit(1)
print(f"registry: {report['registry']['source']} ({report['registry']['entries']} entries)", file=sys.stderr)
counts: str = (
f"curies: {report['registered']} registered, {report['allowed']} allowed, "
f"{report['unregistered']} unregistered, {report['malformed']} malformed"
)
print(counts, file=sys.stderr)
logger.info(f"validate-infores: {counts}")
for example in report["examples"][:3]:
print(f" e.g. {example['curie']} ({example['where']}.{example['field']}): {example['problem']}", file=sys.stderr)
clean: bool = not report["unregistered"] and not report["malformed"]
passed: str = "infores membership check skipped (--registry off)." if registry == "off" else "All emitted infores CURIEs are registered."
if strict:
if report["ok_strict"]:
print(passed if clean else "infores validation passed (strict).", file=sys.stderr)
return
print("infores validation failed (strict): unregistered CURIEs remain.", file=sys.stderr)
raise SystemExit(1)
if not clean:
print("infores warnings present; pass --strict to fail on them.", file=sys.stderr)
return
print(passed, file=sys.stderr)


@APP.command(name="quick-map")
def quick_map_command(
# negative="" suppresses cyclopts' auto-generated --empty-terms help row: the reset
Expand Down
Loading
Loading