Skip to content

Add GitHub-issue-driven test framework, assertion handlers, and csv-to-babeltests CLI - #67

Open
gaurav wants to merge 136 commits into
mainfrom
add-github-issue-tests
Open

Add GitHub-issue-driven test framework, assertion handlers, and csv-to-babeltests CLI#67
gaurav wants to merge 136 commits into
mainfrom
add-github-issue-tests

Conversation

@gaurav

@gaurav gaurav commented Jan 8, 2026

Copy link
Copy Markdown
Collaborator

Introduces a GitHub-issue-driven test framework for babel-validation, alongside a library carve-out and supporting tooling.

Major pieces

  • New library subpackages — builds on the src/babel_validation/ carve-out that landed on main in Reorganize Babel Validation for more extensibility #101, adding sources/github/, assertions/, and tools/ alongside the existing core, services, and sources/google_sheets.
  • Assertion framework — 8 AssertionHandler types (Resolves, DoesNotResolve, ResolvesWith, DoesNotResolveWith, HasLabel, ResolvesWithType, SearchByName, Needed) with an auto-generated assertions/README.md and a drift test that fails CI if it goes stale.
  • GitHub integration (PyGitHub) — parses {{BabelTest|...}} wiki lines and ```yaml babel_tests: blocks from issue bodies. Open issues are expected to fail (strict xfail, so XPASS flags a closeable issue); closed issues are expected to pass.
  • CSV→YAML CLI (csv-to-babeltests) for bulk-authoring assertion blocks, reusing the same handlers and YAML schema as the issue parser.
  • CI workflow running pytest -m unit on PRs.
  • xdist parallelism with FileLock-coordinated CSV/issue caches; the controller refreshes caches, workers share them.

Review fixes — round 1

Addressing the code review on the original revision:

  • pytest -m unit now runs fully offline. Both the GitHub issue tests and the Google Sheet / blocklist tests previously fetched from the network at collection time, because their parametrization is built before pytest applies -m deselection. The marker expression is now evaluated up front (shared tests/_pytest_helpers.deselected_by_markexpr), and the GitHub fetch and Google Sheet downloads are skipped when the test would be deselected. Net effect: pytest -m unit drops from ~2.5s + network to ~0.3s offline. Row IDs (row=N) and xfail marks are unchanged, so -k 'row=42' still works.
  • ResolvesWith / DoesNotResolveWith now enforce their documented 2+ CURIE arity. A single-CURIE param_set previously passed (or failed) vacuously; it now fails loudly with a clear message, matching HasLabel/SearchByName. Adds unit coverage.
  • SearchByName top-N is read from targets.ini (NameResXFailIfInTop) instead of being hardcoded to 5, so it's configurable per environment.
  • CachedNodeNorm.normalize_curie uses .get() to avoid a KeyError if NodeNorm ever omits a requested CURIE, consistent with the rest of the code.

Review fixes — round 2

A second review pass found eight issues, all fixed here (pytest -m unit: 49 → 56 passing).

Failures that were being hidden

  • The open-issue xfail no longer swallows infrastructure errors. xfail(strict=True) with no raises= treats any exception as the expected failure. Since the marker is applied before the assertion loop and most tracked issues are open, a NodeNorm outage, an HTTP 500, or a bug in our own parsing reported XFAIL — a fully green run across every open issue. Now raises=AssertionError; the imperative pytest.xfail() calls are unaffected.
  • DoesNotResolveWith no longer passes vacuously. normalize_curies returned NodeNorm's response as-is, so a CURIE omitted from it was simply absent from the mapping the handler iterates. The result dict is now seeded with every requested CURIE, so "one entry per input" is an invariant rather than an accident of the pre-warm call order.

Crashes on realistic inputs

  • YAML param values are coerced to str. _to_list validated the container type but not element types, while all eight handlers expect list[str]. HasLabel: - [CHEBI:16480, NO] parsed to [..., False] (YAML 1.1 reads bare NO as boolean) → AttributeError; a numeric identifier → TypeError from _CURIE_RE.match. Numbers now stringify; bare NO/YES/ON/OFF and nulls are rejected with a quote-it hint, since the original spelling is unrecoverable by then.
  • SearchByName handles a node with no label (.get('label', '')HasLabel already guarded this).
  • ResolvesWithType handles a node with no type, using the first_type() guard added in add0848.

Smaller

  • ResolvesWith failure messages are deterministic. The "but expected X" id came from dict order, which depends on set iteration and cache state, so the same failing issue reported different expectations across runs. It now follows param order.
  • csv-to-babeltests no longer POSTs unvalidated values. The batch pre-warm skipped the _CURIE_RE filter that test_with_nodenorm has applied since e322040, so a blank --equivalent-curie-column cell was sent to NodeNorm.
  • A stale cache owned by another user no longer aborts the session. the unlink helper (unlink_if_exists after the Reorganize Babel Validation for more extensibility #101 merge) caught only FileNotFoundError; on a shared box or self-hosted runner, PermissionError escaped pytest_configure and no test ran.

New offline coverage: tests/test_assertion_robustness.py (sparse nodes, omitted CURIEs, message determinism) and three YAML-scalar cases in tests/github_issues/test_system.py.

Unrelated changes riding along in this PR

These are intentional and correct, but not part of the GitHub-issue test framework itself:

  • pytest bumped 8.4.2 → 9.0.2 (plus new deps: pygithub, pytest-xdist[psutil], pytest-subtests, click, pyyaml, tqdm, filelock, python-dotenv).
  • tests/targets.ini [ci]/[ci-es] restructure: both CI targets now point NodeNorm at nodenorm-es.ci.transltr.io (the old biothings.ci.transltr.io/nodenorm/ URL is gone); [ci] pairs the ES NodeNorm with the non-ES NameRes, [ci-es] with the ES NameRes. A Repositories list (scanned for embedded assertions) is added to [DEFAULT].

Merged main (#101)

main has since landed its own library reorg in #101, which touched the same shared
files. Merged in and resolved by taking main's structure and re-applying this branch's
work on top:

  • services/nameres.py, services/nodenorm.py — main's docstrings, Protocol
    interfaces, and the clear_curie/delete_queryinvalidate_curie/invalidate_query
    renames. The normalize_curies result seeding from the round-2 fixes is re-applied,
    and the "one entry per requested CURIE" guarantee is now documented in the docstring.
  • sources/google_sheets/google_sheet_test_cases.py — main's cache_ttl_seconds
    constructor parameter and relative import. Nothing referenced the removed
    CACHE_TTL_SECONDS class attribute.
  • tests/conftest.py — main's unlink_if_exists rename, keeping the OSError widening,
    the issue-cache cleanup, and the selected_github_issues fixture.
  • CLAUDE.md, pyproject.toml — this branch is a superset of main's version.
  • uv.lock — regenerated with uv lock.

pytest -m unit: 56 passed post-merge. Note the two renames for anything downstream:
clear_curieinvalidate_curie, delete_queryinvalidate_query.

Follow-up: library carve-out (stacked PRs)

This PR is being kept as the base for two smaller, stacked follow-up PRs that make the GitHub-issue test framework usable as an installable library (so the Babel pipeline and Babel Explorer can consume it). They branch off this one rather than main, since the library code only exists here:

Recommended review/merge order: this PR (#67) → #95#96. Out-of-scope library-usability improvements surfaced along the way are tracked in #97, #98, and #99.

gaurav and others added 10 commits February 15, 2026 02:39
Each assertion type (Resolves, DoesNotResolve, ResolvesWith, ResolvesWithType,
SearchByName, Needed) is now a self-contained class in
src/babel_validation/assertions/, grouped by which service it targets
(nodenorm.py, nameres.py, common.py). A central ASSERTION_HANDLERS registry
in __init__.py maps lowercase assertion names to handler instances, and
NodeNormAssertion / NameResAssertion marker base classes allow isinstance()
checks for applicability. GitHubIssueTest.test_with_nodenorm() and
test_with_nameres() are now 3-line dispatchers. A README.md documents all
supported assertion types with examples in both wiki and YAML syntax.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
gaurav and others added 2 commits February 19, 2026 19:03
…pendently.

Bumps pytest to >=9.0 (which includes built-in subtests support). Each
TestResult is now evaluated in its own subtest block, so a failure no longer
short-circuits the rest. Adds post-loop state-consistency subtests: a closed
issue with failing tests fails with a "consider reopening" message, and an open
issue where all tests pass emits an xfail "consider closing" hint.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously, get_github_issue_id() and GitHubIssueTest.__str__() both resolved
the org/repo name via github_issue.repository.organization.name, which triggers
lazy PyGitHub API calls. Parse org/repo from html_url instead (always present
in the issue JSON, no extra round-trip needed). Also resolves the TODO comment
in get_test_issues_from_issue().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The NameRes 'found in top N' threshold was hardcoded to the handler default of
5, ignoring NameResXFailIfInTop in targets.ini. Read it from target_info so the
threshold is configurable per environment.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@gaurav gaurav changed the title Add GitHub issue tests Add GitHub-issue-driven test framework (+ src/ library carve-out) Jun 10, 2026
gaurav and others added 5 commits June 10, 2026 17:57
Move the marker-expression check out of the github_issues conftest into
tests/_pytest_helpers.py so the Google Sheet test modules can reuse it to defer
their own network-backed parametrization.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The gsheet and blocklist test modules instantiated GoogleSheetTestCases /
load_blocklist_from_gsheet at import time and parametrized via a module-level
@pytest.mark.parametrize, so every collection — including 'pytest -m unit' —
downloaded the sheets even though those tests aren't unit tests.

Move parametrization into pytest_generate_tests with a lazy, cached fetch, and
skip the fetch entirely when a -m filter would deselect the test. Row IDs
(row=N) and xfail marks are unchanged, so -k 'row=42' still works.

With this, 'pytest -m unit' runs fully offline: ~0.3s and no network, versus
~2.5s and a GitHub + Google Sheet round trip before.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A `babel_tests:` value that is a list/scalar (not a mapping) previously
crashed with an opaque AttributeError on `.items()`, and a non-string
assertion key (e.g. `123:`) blew up later when `.lower()` was called.
Both now raise a clear ValueError up front, with unit coverage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resolved-node message formatting indexed result['type'][0] in five
places, which would raise IndexError if NodeNorm ever returned a node
with an empty type list. Route all sites through a NodeNormTest.first_type()
helper and use .get('label', '') for the same robustness.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Importing the tool registered the _FlowList representer on the global
yaml.SafeDumper, changing YAML output process-wide. Move it onto a local
_BabelTestDumper subclass; emitter output is unchanged.

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

gaurav commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator Author

Recommended merge order: #67#95#96.

These are stacked PRs, each based on the previous branch (not main):

Merging in this order keeps each diff small and reviewable. Once #67 merges, GitHub will retarget #95 onto main automatically (and #96 onto library-packaging); review/merge #95 next, then #96. Out-of-scope follow-ups are tracked in #97, #98, #99, and #100.

@gaurav

gaurav commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator Author

To make this reviewable, I've split it into a stack of smaller PRs (review/merge top-down):

  1. Reorganize Babel Validation for more extensibility #101 — Library reorg: carve tests/common into src/babel_validation (base main)
  2. Assertions framework for BabelTest expectations #102 — Assertions framework (base Reorganize Babel Validation for more extensibility #101)
  3. GitHub-issue test parsing + harness (3/4) #103 — GitHub-issue parsing + harness (base Assertions framework for BabelTest expectations #102)
  4. csv-to-babeltests CLI for generating BabelTests YAML (4/4) #104csv-to-babeltests CLI (base Assertions framework for BabelTest expectations #102, independent of GitHub-issue test parsing + harness (3/4) #103)

Each was reconstructed from this branch's final tree (path-based for 1–3; #104 cherry-picks the CSV commits with authorship intact) and verified offline. Plan is to merge these into main, then rebase this branch — it should reduce to little/nothing. Keeping this PR open until then.

Record the non-obvious things: `-m unit` is the offline CI suite and
network-backed modules defer fetches via deselected_by_markexpr; src/ is
importable thanks to the hatch wheel packaging; black isn't strictly
enforced; and commits sign through 1Password's SSH agent. Also fix the
`-k "row=42"` example, which pytest's expression parser rejects.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
gaurav added a commit that referenced this pull request Jun 26, 2026
The repo's `.gitignore` predates the Python rewrite — it only covered
Scala/Giter8/IntelliJ artifacts, so `__pycache__/` and `*.pyc` showed up
as untracked noise throughout the tree and were easy to `git add` by
accident.

Appends the standard [GitHub
`Python.gitignore`](https://github.com/github/gitignore/blob/main/Python.gitignore)
template (bytecode, `build/`/`dist/`, `.pytest_cache/`, `.venv`/`.env`,
mypy/coverage caches, …) below the existing entries, plus a `.DS_Store`
line for macOS. Existing entries are kept at the top, so this is purely
additive.

Independent of the #67 split stack (#101#104) and based on `main`, so
it can merge immediately. No files are currently tracked that the new
patterns would retroactively ignore — `git ls-tree` shows no
`.pyc`/`__pycache__` in the tree — so nothing needs `git rm --cached`.

### Verify
```
git check-ignore -v src/__pycache__/x.pyc .venv .env   # all matched
```

🤖 Generated with [Claude Code](https://claude.com/claude-code)
gaurav added a commit that referenced this pull request Jun 26, 2026
Pure-ish refactor, no service-behavior change. Moves the code under
`tests/common/` into an importable `src/babel_validation` package and
updates the Google Sheet test modules to match.

### What changes
- Split the monolithic `tests/common/google_sheet_test_cases.py` into:
  - `core/testrow.py` — `TestRow` / `TestStatus` / `TestResult`
- `services/{nodenorm,nameres}.py` — `CachedNodeNorm` / `CachedNameRes`
  - `sources/google_sheets/{google_sheet_test_cases,blocklist}.py`
- Gsheet test modules now import from the new locations and parametrize
**lazily** in `pytest_generate_tests` via
`tests/_pytest_helpers.deselected_by_markexpr`, so marker-deselected
runs never hit the network.
- Migrate `tests/pytest.ini` into `[tool.pytest.ini_options]`; add a
hatchling build that packages `src/`; add `filelock` (gsheet disk
cache).
- Move `test_env.py` → `tests/test_environment/`.

Two incidental one-liners ride along in the final test files (NameRes
`biolink_type` passed as a list; description identifiers as lists not
sets) — kept as-is so #67 rebases cleanly.

### Verify
- `uv run pytest -m unit --collect-only -q` — full suite
imports/collects offline (no network).
- `uv run pytest tests/test_environment/test_env.py` — live Google Sheet
download+parse through the moved code.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
gaurav and others added 3 commits August 18, 2026 14:19
PyYAML resolves bare scalars by YAML 1.1 rules, but _to_list only validated the
container type — so element types reached the handlers unchanged. A param_set
like [CHEBI:16480, NO] parsed to [..., False] and killed HasLabelHandler with
AttributeError, and a numeric identifier crashed _CURIE_RE.match with TypeError.
"NO" (nitric oxide) and numeric labels are realistic Babel test content.

_to_param() now coerces each scalar for all eight handlers at once. Numbers
stringify cleanly; bare NO/YES/ON/OFF and nulls are rejected with a quote-it
hint, since by the time PyYAML hands them over the original spelling is lost
and str(False) would silently assert on the wrong label.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
NodeNorm may omit label or type from a node, and may omit a requested CURIE
from its response entirely. Four places assumed otherwise:

- SearchByNameHandler indexed result['id']['label'], raising KeyError on an
  unlabelled node — HasLabelHandler already guards this case.
- ResolvesWithTypeHandler indexed node['type'], bypassing the first_type()
  guard added in add0848 for exactly this.
- normalize_curies returned the raw response, so an omitted CURIE was absent
  from the mapping. DoesNotResolveWithHandler iterates that mapping, so
  omitted CURIEs vanished and the assertion passed vacuously. It is now
  seeded with every requested CURIE, making "one entry per input" an
  invariant rather than an accident of the pre-warming call order.
- _compare_resolutions picked its canonical id from dict order, which depends
  on set iteration and cache state, so "but expected X" varied across runs
  for the same failing issue. It now walks params order.

Also filters the csv-to-babeltests pre-warm through _CURIE_RE, matching what
test_with_nodenorm has done since e322040 — an empty --equivalent-curie-column
cell was being POSTed to NodeNorm.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
pytest.mark.xfail(strict=True) with no raises= treats any exception as the
expected failure. The marker is added before the assertion loop and most
tracked issues are open, so a NodeNorm outage, an HTTP 500, or a bug in our
own issue parsing produced XFAIL and a fully green run across every open
issue. raises=AssertionError restores the signal; the imperative
pytest.xfail() calls at the end of the test are unaffected.

Also widens _silent_unlink to catch OSError. The gsheet and issue caches use
fixed names in the shared temp dir, so on a shared dev box or self-hosted
runner a stale file owned by another user raised PermissionError out of
pytest_configure and no test ran at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@gaurav gaurav changed the title Add GitHub-issue-driven test framework (+ src/ library carve-out) Add GitHub-issue-driven test framework, src/babel_validation library, and csv-to-babeltests CLI Aug 18, 2026
main's #101 landed a parallel library reorg, so the shared files conflicted.
Resolved by taking main's structure and re-applying this branch's additions:

- services/nameres.py, services/nodenorm.py: main's docstrings, Protocol
  interfaces, and clear_curie/delete_query → invalidate_curie/invalidate_query
  renames. Re-applied the normalize_curies result seeding from d1d1590 and
  documented the "one entry per requested CURIE" guarantee in the docstring.
- sources/google_sheets/google_sheet_test_cases.py: main's cache_ttl_seconds
  constructor parameter and relative import. No callers referenced the removed
  CACHE_TTL_SECONDS class attribute.
- tests/conftest.py: main's unlink_if_exists rename, keeping the OSError
  widening from bcbca58 plus the issue-cache cleanup and the
  selected_github_issues fixture.
- CLAUDE.md, pyproject.toml: this branch is a superset of main's version.
- uv.lock: regenerated with `uv lock`.

pytest -m unit: 56 passed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@gaurav gaurav changed the title Add GitHub-issue-driven test framework, src/babel_validation library, and csv-to-babeltests CLI Add GitHub-issue-driven test framework, assertion handlers, and csv-to-babeltests CLI Aug 18, 2026
gaurav added a commit that referenced this pull request Aug 19, 2026
Adds `src/babel_validation/assertions`: the engine that turns a named
BabelTest assertion plus its parameters into a check evaluated against
NodeNorm/NameRes. Nothing consumes it yet — the GitHub issue parser that
produces the parameters arrives later in the stack — so this PR is the
engine, its generated documentation, and its offline tests.

**Stack 2 of 4** splitting #67. Base: `main` (#101 is merged).

### The assertion types

`AssertionHandler` is the base class; `NodeNormTest` and `NameResTest`
specialize it per service. Each concrete handler declares its parameters
and yields `TestResult`s. Registered: `Resolves`, `DoesNotResolve`,
`ResolvesWith`, `DoesNotResolveWith`, `HasLabel`, `ResolvesWithType`,
`SearchByName`, and `Needed` (a placeholder that always fails, marking
an issue as still needing a real test).

Both wiki (`{{BabelTest|Resolves|CHEBI:15365}}`) and YAML syntax are
supported, and an assertion can carry several independent **params
lists**, each evaluated separately so one bad one doesn't sink the rest.

### Shared parameter handling

`AssertionHandler.prepare_params_lists()` strips whitespace from every
param, rejects params lists whose CURIEs are malformed, and warms the
NodeNorm cache for the survivors in a single batched request. Both
`test_with_nodenorm()` and `test_with_nameres()` route through it, so
the NameRes path gets the same validation and the same one-request
warming rather than a NodeNorm round-trip per params list.

Two escape hatches keep that uniform treatment from being wrong for
particular assertions:

- `curie_params()` narrows which params are CURIEs — `HasLabel` to the
first, `ResolvesWithType` to everything after the Biolink type,
`SearchByName` to the expected CURIE only (its first param is a
free-text query).
- `VALIDATE_CURIES = False` opts an assertion out of format validation
entirely. `DoesNotResolve` sets it: an identifier that isn't even a
well-formed CURIE trivially doesn't resolve, which is exactly what that
assertion exists to state, so rejecting it up front would leave the
assertion unable to express its own purpose.

### A NodeNorm bulk-normalization fix

`CachedNodeNorm.normalize_curies()` built its return value from
`response.json()`, so a CURIE that NodeNorm silently omitted from its
response was absent from the returned dict rather than present with a
`None` value. A caller iterating the results would never see it and
would report success for a CURIE it never tested. The warm-cache path
happened to re-add the missing key, so the hole only opened on a cold
lookup.

It now builds the result from the requested CURIEs: exactly one entry
per request, in request order. The ordering guarantee matters
independently — "first CURIE that resolved" logic previously depended on
the server's JSON ordering when cold and on set iteration order
(per-process string hash randomization) when warm, so *which* CURIE got
blamed in a failure message could vary between runs of the same test.

### Types and naming

Parameters are named rather than left as nested `list[str]`:

- `ParamsList` — one assertion invocation's parameters. Position is
significant (`ResolvesWithType` takes its Biolink type first, `HasLabel`
is `[curie, label]`), which is why this is a list.
- `PreparedParamsList` — a frozen `(params, failure)` record.
`prepare_params_lists()` returns a list of these rather than a
`(stripped, failures_by_index)` tuple, so callers don't re-zip two
structures by hand.

Service parameters are annotated with the `NodeNormService` /
`NameResService` Protocols that `services/` already defines for the
purpose, rather than the concrete `CachedNodeNorm` / `CachedNameRes`, so
a future drop-in replacement needs no changes here.

### Guardrails

- **Registration.** `_register()` replaces the `{h.NAME: h for h in
[...]}` comprehension and raises on a `NAME` that isn't lowercase or one
that's already taken. Lowercase is load-bearing — the README promises
users that assertion names are matched case-insensitively, which only
holds if every registry key is lowercase — and a duplicate `NAME` would
otherwise silently drop a handler. Both are mistakes only made while
adding an assertion, so they fail loudly at import.
- **Missing Biolink types.** When NodeNorm returns a node with no type,
messages show `NO TYPE RETURNED`. The earlier placeholder, `unknown
type`, had the shape of a real type — the older Biolink vocabulary was
lowercase prose like `chemical entity` — so it could be misread as
something Babel actually returned.

### Documentation

`gen_docs.py` renders `assertions/README.md` from the handler class
attributes, grouping handlers by the service they test rather than by
their order in `ASSERTION_HANDLERS` — otherwise a handler registered in
the wrong place lands under the wrong heading, and the sync test can't
catch it because it regenerates the same wrong output.
`tests/test_environment/test_assertions_docs.py` asserts the checked-in
README stays in sync.

The "Adding a New Assertion Type" instructions live in that generated
README and nowhere else. There had been a second copy in the package
docstring and the two had already drifted; the docstring now points at
the README and describes the module layout instead.

### Tests

`tests/test_environment/test_assertions.py` stubs `requests.post` rather
than the service, so handlers run against the real `CachedNodeNorm` and
exercise its contract instead of a fake restating it. The fixture DB
drops one CURIE from the response entirely, reproducing what NodeNorm
does for some unknown identifiers. 13 unit tests, all offline.

Also registers the `unit` pytest marker, first used by these tests.

### Outcomes

- The assertion vocabulary is fixed, documented, and enforced, so the
issue parser in the next part of the stack has a stable target.
- Every check here runs offline — no network, no GitHub API, no Google
Sheet.
- Deliberately not here: parsing assertions out of issue bodies, and any
wiring into the existing pytest suites.

### Notes for review

`gh pr diff` shows hunks in `.gitignore`, `services/nameres.py`,
`sources/google_sheets/`, and `tests/conftest.py` that came from #101
and are already on `main` — they merge as no-ops. The net change against
`main` is the `assertions/` package, the `normalize_curies()` fix, the
two test files, and the pytest marker.

`result: dict` is left untyped for NodeNorm response entries. A
`TypedDict` would be more precise, but the response shape is Babel's to
change and a wrong one is worse than an honest `dict`, so the docstrings
say what the dict is instead.

### Verify

- `uv run pytest -m unit -q` → 13 assertion tests pass offline.
- `uv run python -m src.babel_validation.assertions.gen_docs` reproduces
the committed README.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants