Skip to content

Document where NodeNorm's data comes from, and serve the API docs that were being discarded#403

Open
gaurav wants to merge 6 commits into
mainfrom
improve-documentation
Open

Document where NodeNorm's data comes from, and serve the API docs that were being discarded#403
gaurav wants to merge 6 commits into
mainfrom
improve-documentation

Conversation

@gaurav

@gaurav gaurav commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

NodeNorm is a thin frontend over data whose complexity is almost entirely Babel's, and that inheritance was undocumented in one direction and stale in the other. This splits the two clearly, and fixes the largest documentation hole in the API along the way.

The division

Three buckets, decided once so it stays decided:

  • NodeNorm's own — API surface, response shape, conflation flags and defaults, Redis layout, the loader, deployment. Stays here.
  • Babel's, but a user hits it on day one — cliques, preferred identifiers, preferred labels, information content, conflation ordering, descriptions. Stated in full here, in our own words, with a link out. Not truncated to a link, so the docs still stand alone.
  • Babel's, and users can find it themselves — how cliques are actually built, per-source ingest, the pipeline, DuckDB exports. One link to Babel's index, then we stop.

The new documentation/Babel.md holds the middle bucket and is deliberately the only file in this repository that links into Babel's docs/ tree. Everything else links to it. A reorganisation on the Babel side now breaks one file instead of twenty.

The OpenAPI fix

construct_open_api_schema() regenerates paths from the route decorators and copies back only info, tags and servers. Everything under paths: in resources/openapi.yml — 235 lines — has therefore never been served to anyone. That included the only written statement of the conflation ordering guarantees ("the gene(s) always appear first", "the active ingredient appears before any formulations") and the /get_setid stability guarantee, neither of which existed anywhere else in the repo.

That prose now lives on the decorators, and the dead block is deleted. Every operation gains a real 200 description instead of FastAPI's default "Successful Response", and /get_normalized_nodes goes from documenting its output as an untyped object to a full schema plus a worked example.

The response models are attached via responses={200: {"model": ...}} rather than response_model=, deliberately — create_node() builds those dicts by hand and omits keys that don't apply, so letting FastAPI filter output through a model risks silently dropping a field the model forgot. Verified response_field is None on both routes: schema documented, serialization untouched.

Also fixed: the MIT license was parsed then dropped from the served schema; apidocs.py called app.openapi_schema() on a dict; CurieList used title= instead of description=, so Swagger showed no help text for any of its fields; and both TRAPI summaries claimed to normalize a "response object".

Stale content corrected

The README's compendia example predated Biolink-prefixed types (chemical_substance, bare PUBCHEM:) and was a normalization response, not a compendium line. Its config.json example listed Redis credentials, which the loader reads only from redis_config.yaml. API.md was missing include_taxa, taxa, all parameter defaults, the biolink_model block from /status, and had a sentence that trailed off after "These methods".

Root path is now recorded as /; the TRAPI-versioned prefixes are deprecated, and the only place one should still appear is the OpenAPI servers block.

Two corrections found by checking rather than trusting

  • DrugChemicalSmaller.txt.gz is a SapBERT training export, not a conflation file — so Babel's "only two conflation files" is right, and I dropped a planned edit.
  • "NodeNorm reports the lowest IC" was wrong. min(ICs) happens in Babel (src/node.py:443); NodeNorm returns the stored per-clique value unchanged. Corrected in both repos.

Verification

  • pytest -m "not integration" — 27 passed, 4 skipped.
  • Booted the app and inspected /openapi.json: license, x-translator, x-trapi and the SERVER_ROOT/TRAPI_VERSION server override all still present; all 8 paths carry real descriptions.
  • Confirmed by grep that only documentation/Babel.md links into Babel's docs/ tree, and that all six link targets exist.

black was not run: the repo is not black-clean at baseline (10 files, including several this PR never touches), so reformatting would bury the diff. Confirmed these changes add no new violations.

Companion PR

Babel-side fixes are in NCATSTranslator/Babel#973, including correcting its claim that NodeNorm does not use preferred_name.

Follow-ups filed

#398 (drug_chemical_conflate defaults to true on GET but false on POST — a real behavioural inconsistency, documented here as a known divergence), #399 (Retry(method_whitelist=) removed in urllib3 2.0), #400 (no v2.5.x release notes), #401 (docker-compose-redis.yml does not exist), #402 (duplicate handler names, OpenAPI version mismatch).

🤖 Generated with Claude Code

gaurav and others added 3 commits July 27, 2026 02:13
NodeNorm does not decide which identifiers are equivalent, which one is
preferred, or what a clique is called -- Babel does. That means most "why did
I get this answer?" questions are really questions about Babel, and until now
nothing in this repository said so.

This adds a single page covering the Babel behaviour that shows up directly in
a NodeNorm response: cliques and preferred identifiers, why the label is not
necessarily the preferred identifier's label, information content, the
conflation ordering guarantees, where descriptions come from, four gotchas
that surprise people, and which tracker to file against.

It is also deliberately the *only* file in this repository that links into
Babel's docs/ tree. Everything else links here instead, so a reorganisation on
the Babel side breaks one file rather than twenty.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The README carried two examples that no longer describe anything real. The
"compendia file" sample predated Biolink-prefixed types (it showed
chemical_substance and a bare PUBCHEM: prefix) and was in any case a
normalization *response*, not a compendium line. The config.json sample listed
Redis credentials, which the loader reads only from redis_config.yaml, and
old-style filenames the pipeline no longer produces. Both are replaced by
pointers to the documents that are actually maintained.

API.md gains the things it had fallen behind on: the include_taxa and taxa
fields, the defaults for every parameter, the note that a CURIE which cannot
be normalized comes back as null rather than being omitted, the biolink_model
block returned by /status, and a finished sentence for the deprecated TRAPI
section, which previously trailed off after "These methods". It also now flags
that drug_chemical_conflate defaults to true on GET but false on POST (#398).

Attribution of the information content calculation is corrected in passing:
Babel takes the lowest value of any identifier in the clique (min() in its
src/node.py) and NodeNorm returns that stored value unchanged.

Finally, the root path is recorded as / rather than /1.3 or /1.5/. The
TRAPI-versioned prefixes are deprecated and will be removed; the only place
one should still appear is the OpenAPI servers block, which is itself
temporary.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ding

construct_open_api_schema() regenerates paths from the FastAPI route
decorators and copies back only info, tags and servers. Everything under
paths: in resources/openapi.yml -- 235 lines of it -- was therefore never
served to anyone. That included the only written statement of the conflation
ordering guarantees ("the gene(s) always appear first", "the active ingredient
appears before any formulations") and the /get_setid stability guarantee,
neither of which existed anywhere else in the repository.

That prose now lives on the decorators, where it is actually rendered, and the
dead paths: block is deleted. Every operation gains a real 200 description in
place of FastAPI's default "Successful Response", and apidocs.py grows a
comment saying where per-endpoint documentation belongs so this does not
happen again.

The response of /get_normalized_nodes is now described by NormalizedNode,
NormalizedNodeId and EquivalentIdentifier, plus a worked example. These are
attached via responses={200: {"model": ...}} rather than response_model=,
deliberately: create_node() builds those dictionaries by hand and omits keys
that do not apply, so letting FastAPI filter its output through a model would
risk silently dropping a field the model forgot. The schema is documented;
serialization is untouched.

Also fixed along the way:

- The MIT license in openapi.yml was parsed and then dropped, so the served
  schema had no license. It is now copied across.
- apidocs.py called app.openapi_schema() on a dict, which would raise
  TypeError. Unreachable today only because server.py assigns the attribute
  directly, but it would fire the moment anything called app.openapi().
- CurieList used title= rather than description= on every field. Swagger
  renders titles as labels, not help text, so those fields showed no
  description at all. Its example also omitted two parameters and contradicted
  a declared default.
- The /query and /asyncquery summaries both claimed to normalize a "TRAPI
  response object"; /query takes a Query, and the two endpoints are not
  identical. Both now say what they do and name their replacement.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@gaurav gaurav added this to the NodeNorm v2.6.0 milestone Jul 27, 2026
gaurav added a commit that referenced this pull request Jul 27, 2026
construct_open_api_schema() regenerates `paths` from the route decorators and
merges back only document-level metadata, so both halves fail silently: metadata
that stops being copied just vanishes from the served schema, and endpoint
documentation written into openapi.yml is never served at all. That is how 235
lines of prose sat unserved until #403.

Five tests: the yml metadata (including the license, which was dropped for a
long time) reaches the schema; openapi.yml has no `paths` section and the routes
supply them; no operation is left on FastAPI's placeholder "Successful Response";
construct_open_api_schema returns the cached schema rather than calling a dict;
and get_app_info reads what it claims to.

Also records the split in CLAUDE.md, next to the note that the Dockerfile has to
COPY ./skills for /llms.txt to work in a deployed image.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
NodeNormalization, Babel and NameResolution all use main as their default
branch. A /blob/master/ URL resolves today only through GitHub's post-rename
redirect, which is exactly the kind of thing that disappears quietly -- so
these are link rot rather than an acceptable alias.

25 links across five files: the endpoint descriptions in server.py (12), this
branch's new Babel.md (6), the /status examples in API.md (2), openapi.yml (2),
and the notebook including its Colab badge (3). Most predate this branch; they
are fixed here because this is the documentation PR and because the skill
branch stacked on it inherits them.

raw.githubusercontent.com/biolink/biolink-model/master/... is deliberately not
touched: biolink-model really does default to master.

Verified: every rewritten github.com URL returns 200, the notebook is still
valid JSON, and tests/frontend passes (22 passed, 4 skipped).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
gaurav added a commit that referenced this pull request Jul 27, 2026
PR #403 fixed the reference problem — every endpoint now carries a real
description and the response schema is documented. What an agent still lacks
is judgement: when to reach for NodeNorm at all, which conflation flags to
set, how to batch a whole column of identifiers, and the handful of behaviours
that silently produce wrong answers rather than errors.

This is that document. Every factual claim in it was measured against the live
RENCI instance rather than written from memory, including the equivalent-
identifier counts used to illustrate what conflation actually does.

Three things it exists to prevent, all of which fail silently:

- GET and POST disagree on the conflation defaults, so the same query returns
  206 equivalent identifiers one way and 30 the other (#398). The rule taught
  is to set both flags on every request.
- /get_setid takes `conflation` (singular) on GET and `conflations` (plural) in
  the POST body, defaults to no conflation unlike GET /get_normalized_nodes,
  and silently ignores the wrong name — returning HTTP 200 and an unconflated
  hash. Anyone comparing a gene against its protein this way concludes they are
  different concepts.
- A conflated clique routinely holds several identifiers sharing one prefix
  (the DMD clique has three ENSEMBL entries: one gene, two proteins), so
  matching on prefix alone is a coin flip. The recipe uses individual_types
  and filters on type.

Format is skills/nodenorm/SKILL.md with YAML frontmatter, which is what Claude
Code actually discovers; the body is plain Markdown usable by any agent. The
`description` names concrete CURIE prefixes, since that is what the model
matches on when deciding whether the skill is relevant.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
gaurav added a commit that referenced this pull request Jul 27, 2026
construct_open_api_schema() regenerates `paths` from the route decorators and
merges back only document-level metadata, so both halves fail silently: metadata
that stops being copied just vanishes from the served schema, and endpoint
documentation written into openapi.yml is never served at all. That is how 235
lines of prose sat unserved until #403.

Five tests: the yml metadata (including the license, which was dropped for a
long time) reaches the schema; openapi.yml has no `paths` section and the routes
supply them; no operation is left on FastAPI's placeholder "Successful Response";
construct_open_api_schema returns the cached schema rather than calling a dict;
and get_app_info reads what it claims to.

Also records the split in CLAUDE.md, next to the note that the Dockerfile has to
COPY ./skills for /llms.txt to work in a deployed image.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
gaurav added a commit that referenced this pull request Jul 27, 2026
The two remaining master URLs, both in this PR's files. The install curl in
LLMs.md matters most: it is the one line a reader actually executes.

Both targets 404 right now, because skills/nodenorm/SKILL.md and
documentation/Babel.md only exist on these two branches. main is still the
right target -- an install instruction should hand people the merged file, and
pinning it to a branch would rot as soon as the branch is deleted. They resolve
once #403 and this PR land.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@gaurav

gaurav commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator Author

Pushed a commit here repointing every cross-repo link at main.

NodeNormalization, Babel and NameResolution all default to main now; a /blob/master/ URL resolves only through GitHub's post-rename redirect, which is the kind of thing that disappears quietly. 25 links across five files — the endpoint descriptions in server.py (12), this branch's new Babel.md (6), the /status examples in API.md (2), openapi.yml (2), and the notebook including its Colab badge (3).

Most of them predate this branch; they are here because this is the documentation PR and because #405 is stacked on it and inherits them. raw.githubusercontent.com/biolink/biolink-model/master/... is deliberately untouched — biolink-model really does default to master.

Verified: every rewritten github.com URL returns 200, the notebook is still valid JSON, and tests/frontend passes (22 passed, 4 skipped).

Worth considering as a follow-up: NameRes now has tests/test_docs_links.py::test_no_master_branch_links, which fails CI on both the blob/ and raw.githubusercontent.com forms. Without something like it these drift back.

Without something failing CI, the master links just fixed drift back. This is
NameResolution's tests/test_docs_links.py adapted to this repo's paths, so the
two services enforce the same rules: relative links resolve, heading anchors
exist, the API.md anchors used in the endpoint descriptions exist, and no link
is pinned to `master`. All offline -- a link check that fails when GitHub is
slow is one people learn to ignore.

It earned its place immediately: it found 12 more links, in releases/, that a
grep for NCATSTranslator/<repo>/blob/master/ had missed because they use the
pre-rename TranslatorSRI org as well as the pre-rename branch. Those are now
NCATSTranslator/Babel/blob/main/ and all five distinct targets return 200.

The other three checks passed on this repo unchanged, so the whole file came
across rather than just the master ban.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
gaurav added a commit that referenced this pull request Jul 27, 2026
PR #403 fixed the reference problem — every endpoint now carries a real
description and the response schema is documented. What an agent still lacks
is judgement: when to reach for NodeNorm at all, which conflation flags to
set, how to batch a whole column of identifiers, and the handful of behaviours
that silently produce wrong answers rather than errors.

This is that document. Every factual claim in it was measured against the live
RENCI instance rather than written from memory, including the equivalent-
identifier counts used to illustrate what conflation actually does.

Three things it exists to prevent, all of which fail silently:

- GET and POST disagree on the conflation defaults, so the same query returns
  206 equivalent identifiers one way and 30 the other (#398). The rule taught
  is to set both flags on every request.
- /get_setid takes `conflation` (singular) on GET and `conflations` (plural) in
  the POST body, defaults to no conflation unlike GET /get_normalized_nodes,
  and silently ignores the wrong name — returning HTTP 200 and an unconflated
  hash. Anyone comparing a gene against its protein this way concludes they are
  different concepts.
- A conflated clique routinely holds several identifiers sharing one prefix
  (the DMD clique has three ENSEMBL entries: one gene, two proteins), so
  matching on prefix alone is a coin flip. The recipe uses individual_types
  and filters on type.

Format is skills/nodenorm/SKILL.md with YAML frontmatter, which is what Claude
Code actually discovers; the body is plain Markdown usable by any agent. The
`description` names concrete CURIE prefixes, since that is what the model
matches on when deciding whether the skill is relevant.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
gaurav added a commit that referenced this pull request Jul 27, 2026
construct_open_api_schema() regenerates `paths` from the route decorators and
merges back only document-level metadata, so both halves fail silently: metadata
that stops being copied just vanishes from the served schema, and endpoint
documentation written into openapi.yml is never served at all. That is how 235
lines of prose sat unserved until #403.

Five tests: the yml metadata (including the license, which was dropped for a
long time) reaches the schema; openapi.yml has no `paths` section and the routes
supply them; no operation is left on FastAPI's placeholder "Successful Response";
construct_open_api_schema returns the cached schema rather than calling a dict;
and get_app_info reads what it claims to.

Also records the split in CLAUDE.md, next to the note that the Dockerfile has to
COPY ./skills for /llms.txt to work in a deployed image.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
gaurav added a commit that referenced this pull request Jul 27, 2026
The two remaining master URLs, both in this PR's files. The install curl in
LLMs.md matters most: it is the one line a reader actually executes.

Both targets 404 right now, because skills/nodenorm/SKILL.md and
documentation/Babel.md only exist on these two branches. main is still the
right target -- an install instruction should hand people the merged file, and
pinning it to a branch would rot as soon as the branch is deleted. They resolve
once #403 and this PR land.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@gaurav

gaurav commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator Author

Added tests/test_docs_links.py, ported from NameRes so both services enforce the same rules: relative links resolve, heading anchors exist, the API.md anchors used in the endpoint descriptions exist, and nothing is pinned to master. All offline — a link check that fails when GitHub is slow is one people learn to ignore.

It earned its place on the first run: 12 more master links in releases/ that my earlier grep missed, because they use the pre-rename TranslatorSRI org as well as the pre-rename branch, so a pattern looking for NCATSTranslator/<repo>/blob/master/ never saw them. Now NCATSTranslator/Babel/blob/main/; all five distinct targets return 200.

The other three checks passed unchanged, so the whole file came across rather than just the master ban.

tests/frontend is unaffected — 26 passed, 4 skipped here, and 35 passed on #405 once rebased, where the new test also covers skills/nodenorm/SKILL.md.

NodeNormalization moved to NCATSTranslator; these four still named the old org
in places a user actually lands on:

- The release badge in README.md linked to TranslatorSRI while its image
  already pointed at NCATSTranslator, so clicking the badge left the org the
  badge itself reported.
- Two PR #323 links in server.py are served in the OpenAPI descriptions, so
  they show up in Swagger.
- One more in documentation/API.md.

An org-rename redirect is more durable than the branch-rename one cleaned up
earlier, so this is consistency rather than an outage waiting to happen. The
~77 remaining references are all in releases/, i.e. historical notes recording
what was true at the time; whether to sweep those is tracked separately in
NameResolution#292, which covers the same question for that repo.

Left alone deliberately: TranslatorSRI/r3 in README.md. That repository really
does still live under TranslatorSRI -- NCATSTranslator/r3 is a 404.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
gaurav added a commit that referenced this pull request Jul 27, 2026
construct_open_api_schema() regenerates `paths` from the route decorators and
merges back only document-level metadata, so both halves fail silently: metadata
that stops being copied just vanishes from the served schema, and endpoint
documentation written into openapi.yml is never served at all. That is how 235
lines of prose sat unserved until #403.

Five tests: the yml metadata (including the license, which was dropped for a
long time) reaches the schema; openapi.yml has no `paths` section and the routes
supply them; no operation is left on FastAPI's placeholder "Successful Response";
construct_open_api_schema returns the cached schema rather than calling a dict;
and get_app_info reads what it claims to.

Also records the split in CLAUDE.md, next to the note that the Dockerfile has to
COPY ./skills for /llms.txt to work in a deployed image.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
gaurav added a commit that referenced this pull request Jul 27, 2026
The two remaining master URLs, both in this PR's files. The install curl in
LLMs.md matters most: it is the one line a reader actually executes.

Both targets 404 right now, because skills/nodenorm/SKILL.md and
documentation/Babel.md only exist on these two branches. main is still the
right target -- an install instruction should hand people the merged file, and
pinning it to a branch would rot as soon as the branch is deleted. They resolve
once #403 and this PR land.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

1 participant