Skip to content

fix(insights): stop implausible release years (e.g. year 400) at the source#382

Merged
SimplicityGuy merged 2 commits into
mainfrom
fix/insights-implausible-release-years
Jun 8, 2026
Merged

fix(insights): stop implausible release years (e.g. year 400) at the source#382
SimplicityGuy merged 2 commits into
mainfrom
fix/insights-implausible-release-years

Conversation

@SimplicityGuy

@SimplicityGuy SimplicityGuy commented Jun 8, 2026

Copy link
Copy Markdown
Owner

Problem

The Insights Genre Trends chart plotted releases at decades like 400 / 600 / 800 AD, stretching the x-axis back to antiquity.

This was not stale pre-filter data — the host was provisioned after the year filter shipped, so every ingest ran with the filter active. The filter simply never applied to releases.

Root cause (two gaps, both in the extractor)

A Discogs release stores its date in <released> (a date string like "1969-09-26"), not <year>:

  1. The extractor's nullify_when / year-out-of-range rules key on the year field, which doesn't exist on a release — so they were silent no-ops for releases (masters, which have <year>, were fine).
  2. Even pointed at released, nullify_when's range check did parse::<f64>() on the whole value, so "0400-01-01" failed to parse and was left untouched.

The release year was then derived consumer-side in common/data_normalizer.py_parse_year_int(data.get("released")), which only rejected year == 0. So "0400-01-01"400 → written straight to Neo4j.

Fix — at the rules engine (where bad data is meant to be caught), plus a defensive backstop and cleanup

Extractor (primary):

  • extractor/src/rules.rsnullify_when's range check now falls back to the leading year component (parse_leading_year) when the value isn't a plain number, so date fields like released are range-checked.
  • extractor/extraction-rules.yaml — add a released nullify_when filter for releases (below: 1860, above: 2027), and add the above upper bound to the existing year filters (releases + masters) for parity. Bad release dates are now nullified at the extractor, before reaching any consumer.

Consumer (defensive backstop):

  • common/data_normalizer.py_parse_year_int bounds the parsed year to [MIN_RELEASE_YEAR (1860), current_year + 1] (added MIN_RELEASE_YEAR; corrected the misleading docstring). Catches anything that slips past, for both releases and masters.

Existing data:

  • scripts/cleanup-implausible-years.sh — one-time cleanup for rows already ingested via the buggy path. Dry-run by default; --apply nulls out-of-range years in existing Neo4j (Release/Master) and PostgreSQL (releases/masters JSONB) records. Documented in scripts/README.md.

Verification

  • cargo test (extractor) — all pass incl. new test_nullify_when_released_date_string_year_out_of_range; cargo clippy -D warnings + cargo fmt --check clean
  • uv run pytest tests/common tests/graphinator tests/tableinator — all pass; ruff + mypy clean
  • shellcheck + bash -n on the cleanup script — clean
  • All pre-commit hooks pass

Operator note

After merge, run on the host once to clean existing data:

./scripts/cleanup-implausible-years.sh            # dry run — see counts
./scripts/cleanup-implausible-years.sh --apply    # perform cleanup

Scope

MusicBrainz entities take a different ingest path (no normalize_record, no year write) and are out of scope.

🤖 Generated with Claude Code

…source

Genre Trends plotted releases at decades like 400/600/800 AD. Root cause:
a Discogs *release* stores its date in `<released>` (a date string), not
`<year>`, so the extractor's `nullify_when` / `year-out-of-range` rules —
which key on the `year` field — were silent no-ops for releases. The release
year is derived consumer-side in `_parse_year_int`, which only rejected
year 0, so `released="0400-01-01"` was stored as year 400.

- common/data_normalizer.py: `_parse_year_int` now bounds the parsed year to
  [MIN_RELEASE_YEAR (1860), current_year + 1], the single authoritative gate
  for both release and master years. Add `MIN_RELEASE_YEAR` constant; fix the
  misleading docstring that claimed the extractor nullifies release sentinels.
- extractor/extraction-rules.yaml: document that the `year` filter is a no-op
  for releases (date lives in `released`), bounded consumer-side instead.
- scripts/cleanup-implausible-years.sh: one-time cleanup (dry-run by default,
  `--apply` to act) that nulls out-of-range years in existing Neo4j and
  PostgreSQL data. Documented in scripts/README.md.
- tests/common/test_data_normalizer.py: cover the new plausibility bounds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Jun 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.65217% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
extractor/src/rules.rs 87.50% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

Move the primary year-plausibility filter to where bad data is supposed to be
caught — the extractor's rules engine — instead of relying on the consumer.

Two gaps prevented the existing rules from catching bad release dates:
1. A Discogs release has no `year` field; its date lives in `released` (a date
   string). The year-keyed rules never matched a release.
2. `nullify_when`'s range check did `parse::<f64>()` on the whole value, so a
   date string like "0400-01-01" failed to parse and was left untouched.

- rules.rs: `nullify_when` range now falls back to the leading year component
  (`parse_leading_year`) when the value isn't a plain number, so date fields
  like `released` are range-checked.
- extraction-rules.yaml: add a `released` nullify_when filter for releases
  (below 1860, above 2027) and add the `above` upper bound to the existing
  `year` filters (releases + masters) for parity.
- rules_tests.rs: cover date-string range filtering (antiquity, far-future,
  plausible full/partial dates).

The consumer-side bound in common/data_normalizer.py remains as a defensive
backstop (see prior commit).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

E2E Coverage (webkit)

Totals Coverage
Statements: 46.29% ( 1249 / 2698 )
Lines: 46.29% ( 1249 / 2698 )

StandWithUkraine

@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

E2E Coverage (chromium)

Totals Coverage
Statements: 46.29% ( 1249 / 2698 )
Lines: 46.29% ( 1249 / 2698 )

StandWithUkraine

@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

E2E Coverage (firefox)

Totals Coverage
Statements: 46.29% ( 1249 / 2698 )
Lines: 46.29% ( 1249 / 2698 )

StandWithUkraine

@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

E2E Coverage (webkit - iPhone 15)

Totals Coverage
Statements: 46.29% ( 1249 / 2698 )
Lines: 46.29% ( 1249 / 2698 )

StandWithUkraine

@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

E2E Coverage (webkit - iPad Pro 11)

Totals Coverage
Statements: 46.29% ( 1249 / 2698 )
Lines: 46.29% ( 1249 / 2698 )

StandWithUkraine

@SimplicityGuy SimplicityGuy merged commit 8d2a32d into main Jun 8, 2026
46 of 48 checks passed
@SimplicityGuy SimplicityGuy deleted the fix/insights-implausible-release-years branch June 8, 2026 04:03
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