Skip to content

Improve architecture health signals and dependency depth - #366

Draft
thodson-usgs wants to merge 7 commits into
DOI-USGS:mainfrom
thodson-usgs:ci/health-report-signals
Draft

Improve architecture health signals and dependency depth#366
thodson-usgs wants to merge 7 commits into
DOI-USGS:mainfrom
thodson-usgs:ci/health-report-signals

Conversation

@thodson-usgs

@thodson-usgs thodson-usgs commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • make the periodic code-health report emphasize actionable package signals while retaining the full advisory PySCN report
  • clarify OGC API policy and adapter boundaries across Water Data and NGWMN
  • remove an unnecessary OGC chunk-execution dependency hop
  • record accepted and rejected architecture experiments, score evidence, and convergence rationale

Architecture health result

PySCN 1.29.0 improved from 82 to 83 during the iterative pass:

  • dependency score: 80 -> 85
  • maximum dependency depth: 8 -> 7
  • internal dependency edges: 139 -> 138
  • dead code: 0
  • new clone groups: 0

Three subsequent experiments retained a composite score of 83 and were reverted, satisfying the requested convergence condition. The experiment log explains their subsidiary effects and why they were not retained.

Validation

  • pytest -q tests/ — 780 passed, 12 deselected
  • ruff check .
  • ruff format --check .
  • mypy dataretrieval
  • lint-imports — 7 contracts kept
  • pre-commit hooks
  • final pyscn analyze --json --no-open dataretrieval

The new architecture pages parse successfully. A warning-as-error Sphinx build still reports two pre-existing global warnings: duplicate indexing for WQP_Metadata.site_info and unpicklable nbsphinx_custom_formats configuration.

thodson-usgs and others added 5 commits August 10, 2026 09:15
The weekly report led with a number whose remaining gap is this package's API
surface. pyscn grades six sub-scores and four of them do not describe this
codebase: Complexity counts synthetic per-file rows that are not functions, CBO
and LCOM sit pinned at 100 in a package that is deliberately function-oriented,
and Architecture penalises a leaf for being depended upon -- extracting
BaseMetadata to a dependency-free leaf, an unambiguous improvement, lowered it.
Averaging all six produces a composite that moves for reasons unrelated to the
work.

Three signals do apply, and they are now the headline. Dead code, which nothing
else in the stack checks. Dependency depth, which .importlinter enforces the
direction of but not the length of. And a NEW clone group -- the event actually
worth catching, and until now invisible: five accepted getter families dominate
the duplication score, so a sixth group moves it about two points and nobody
looks. .pyscn-known-clones.json was committed for exactly this and nothing read
it; tools/health_signals.py now diffs against it and names the offending
functions. Verified by dropping a group from the baseline and watching the run
report it.

Added alongside them: the count of public functions carrying required
arguments. It is the closest automatable proxy for interface cost, and measures
something no other check does -- every Water Data getter is currently fully
keyword-optional, which is what makes a 157-parameter module deep rather than
shallow. A getter growing its first required argument is a real event.

The full pyscn scores stay, in a collapsed block with a note on why the
composite is not the headline, and in the artifact for trend. No .pyscn.toml:
tuning similarity_threshold to 0.86 does take duplication to 100 and the
composite to 87, but that value is fitted just above the five known groups and
globally blinds the tool to any future clone below it. The number would improve
because the tool stopped looking.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSMyMqQ4uuQ9TAbJLnJ6Yi
base_url, dialect and extra_id_cols always travel together and are constant for
a service, but were threaded separately through get_ogc_data and _finalize_ogc.
OgcApi bundles them, so an adapter declares its API once -- WATERDATA_API,
NGWMN_API -- and the engine and the shaper take one value.

The invariant was already leaking. get_cql passed extra_id_cols and dialect but
not base_url, relying on the ambient it happened to have entered; the value it
fell back to was right by luck rather than by construction. Passing two of three
is now unrepresentable.

Honest about the measurement: the interface-cost signal this PR added barely
moves -- ogc/engine.py drops two optional parameters, the package total 581 to
579. That is because _finalize_ogc is private, so the metric does not see it,
and the metric therefore under-reports depth won at an internal seam. Worth
knowing about the signal as much as about this change: it measures the public
surface, which is where interface cost is paid by users, not where it is paid
by maintainers.

The gain that is not in the number: a third OGC service is now one object
rather than three arguments to thread through two layers and keep consistent,
and the partial-application bug above cannot recur.

780 tests, mypy clean across 58 files, 7/7 contracts, all gates.
output_id is a fact about an API, not something a caller should have to look
up -- yet three call sites each resolved it a different way. The Water Data
wrapper defaulted it from a module-level map, get_cql imported the same map to
look it up *and* validate against it, and NGWMN passed a constant because every
one of its collections uses the same name.

OgcApi now carries that mapping (or the single name, which is what NGWMN's
constant always was) and answers output_id(collection). The engine resolves it
when the caller does not, so get_ogc_data's third positional argument becomes
optional. reference.py still passes one explicitly, which is the case the map
does not cover, and that is now the only reason to pass it.

get_cql gains from this twice: it validates against WATERDATA_API.output_ids
rather than importing the map separately, so the collection it accepts and the
id it renames can no longer disagree.

The number moves two points -- engine drops one required and one optional
parameter, package totals 70 to 69 and 581 to 580. That undersells it, as the
previous commit's note predicts: the win is that a caller cannot pass a
collection with the wrong id column, because it no longer passes one at all.

780 tests, mypy clean across 58 files, 7/7 contracts, all gates.
Which queryable a collection filters state on, and in which representation, is
a fact about the collection. It was a pair of arguments repeated at five
getters: to="name", into="state_name" three times in metadata, to="fips_us",
into="state_code" twice in the statistics getters. Nothing tied either pair to
the collection it described, so a getter could name the wrong one and no check
would notice.

ngwmn already had the answer. It holds the same fact as data --
{"sites": {"into": "state_name", "to": "name"}, "providers": {"into":
"state", "to": "postal"}} -- and makes one call. Water Data now does the same
through _STATE_QUERYABLE, and _with_state takes the collection rather than the
answer. The generic mechanism stays where it already was, in the
codes.states leaf.

Deliberately service-side rather than a field on OgcApi. OgcApi describes the
protocol -- base URL, dialect, id columns -- and "which queryable holds the
state" is USGS domain vocabulary that would leak into the generic OGC leaf. The
mapping also spans both the OGC collections and the Statistics API's resources,
which OgcApi does not cover.

Beyond the lookup: the two statistics getters bound the collection name to a
local instead of repeating a literal at the request call, so the collection
whose state rule is applied and the collection actually requested are now the
same value rather than two strings that agree by inspection.

780 tests, mypy clean across 58 files, 7/7 contracts, all gates.
Four agents; efficiency near-clean, the other three found real work. One was a
regression I introduced two commits ago.

Moving output_id onto OgcApi turned a strict lookup into a silent default.
_OUTPUT_ID_BY_COLLECTION[collection] used to raise KeyError for an unregistered
Water Data collection; .get(collection, "id") returned "id", which is right for
NGWMN (one id for every collection) and wrong for every Water Data entry. A
forgotten registration would have mis-shaped the result instead of reporting
itself. The mapping form now raises and names the known collections; the string
form still answers directly, which is what it means -- this API applies one id
everywhere.

That also settles whether the Mapping-or-str union earns its keep. It does now:
with a strict lookup the string is not a restatement of the default, it is the
statement that ids do not vary by collection.

The union was leaking regardless. get_cql tested membership against
WATERDATA_API.output_ids directly, which works only because Water Data's form
is a dict -- against the string form "in" silently becomes a substring test
("i" in "id" is True). OgcApi.knows() and .collections answer for both, and
get_cql asks rather than reaching past the method into the field.

Vocabulary: the statistics getters were binding a local named collection for
observationNormals and observationIntervals, using the same idiom as real
collections, but stats.get_data builds its own request against the Statistics
API and never touches the OGC engine. By CONTEXT.md's own test -- the reasoning
that already declared Samples' tags not collections -- those are resources.
They are named resource now, and _STATE_QUERYABLE says why it is keyed by both
kinds.

Also: dropped _DEFAULT_DIALECT, dead since its only use site went and whose
comment claimed tests referenced it (they do not); corrected get_ogc_data's and
_finalize_ogc's docstrings, which still described the three-argument shape;
gave OgcApi.output_ids field(hash=False) so the comment claiming it is excluded
from hashing is true; made health_signals.py parse each file once rather than
twice; and folded the workflow's inline project-root check into that script,
which was reading the same report through a second copy of the same glob.

Skipped: sharing the __all__ AST walk between tools/ and tests/. It is a
near-verbatim four-line fragment, but wiring a shared module between a CI
script and the test suite costs more coupling than the duplication does.

780 tests, mypy clean across 58 files, 7/7 contracts, all gates.
@thodson-usgs
thodson-usgs force-pushed the ci/health-report-signals branch from 939fa71 to 752252c Compare August 10, 2026 15:07
@thodson-usgs thodson-usgs changed the title ci(health): report the signals that apply, demote the composite Improve architecture health signals and dependency depth Aug 10, 2026
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.

1 participant