Skip to content

Seed the catalogue from other people's directories, and keep measured apart from recorded - #8

Merged
HarryCordewener merged 2 commits into
mainfrom
feat/backfill-importers
Jul 31, 2026
Merged

Seed the catalogue from other people's directories, and keep measured apart from recorded#8
HarryCordewener merged 2 commits into
mainfrom
feat/backfill-importers

Conversation

@HarryCordewener

Copy link
Copy Markdown
Member

Implements Plan 04 — backfill importers (spec §7.6): src/MUI.Import, a new class library that
reads the existing MU* directories so the catalogue has a day-one seed, and splits everything it
finds by whether the source measured or merely recorded.

Composition is not wired here — MUI.Web/Program.cs is owned by another branch. AddMuiImporters(…)
is exposed and ready to call.

The rule, enforced rather than remembered

imported_measured imported_asserted
Seeds discovery and endpoints
presence_sample / availability_interval
Archive grace half weight (§7.5) none

AssertedHistorySink takes no constructor parameters and holds no store, no writer and no clock,
so the asserted tier's "no history, no presence, no grace" is a fact about the type rather than a
rule at eight call sites. It still counts what it refused — a silent refusal reads identically to a
source that had nothing to offer, and only one of those is worth an email.

MeasuredHistorySink writes through IImportedAvailabilityWriter, never
IAvailabilityStore.OpenAsync, whose origin column defaults to first_party. Reaching for the
wrong write path would credit a third party's decade at full weight, and it would look correct in
every fake — so the stored value is asserted against a real PostgreSQL as well as in memory.

import_provenance (migration 0100) carries §7.6's other requirement, the originating site and
the import date on every value, for the provenance chip and the about page. It is not on the grace
path
; a second calculator reading it would count the same history twice.

An import may not mint a game. Identity is one signal — an endpoint we already measured — so below it
a listing becomes a CrawlTarget and stops. That is §7.2 arrived at by having no mechanism to break
it, and the seeded target never mentions IsOperatorSeed, so a stranger's list gets the guarded
default without anybody having to remember.

Etiquette as code

  • Bulk export ▸ documented API ▸ scrape, decided in one place. A configured ScrapeUri is
    genuinely unreachable while a better route exists — not merely unreached by a correctly written
    importer.
  • robots.txt before the first content fetch, and the gate is shut until it is adopted. Longest
    match, Allow over Disallow, */$, and a group naming us beats the wildcard.
  • Rate limit on an injected TimeProvider. A longer Crawl-delay replaces our minimum; a shorter
    one does not licence us to go faster. (tintin.mudhalla.net says Crawl-delay: 0. It does not mean
    what a naive reader would do with it.)
  • MUIndex-Importer/1.0 (+https://muindex.org/crawler) on every request, and an anonymous user
    agent is refused before any route is even considered.
  • A scrape-only source is refused outright until a human has emailed its maintainer. MudStats ships
    with that gate closed, and the run says so out loud instead of running.
  • Attribution is derived from the registry, so "read but not credited" is not a reachable state.

The two sources, behind IDirectorySource

TinTin++ MSSP Mud Crawlerimported_measured, read as a bulk export (one page, one request).
Its fixed-width box table is parsed with the markup's colours and links kept, because both carry
data the text does not: a website URL exists only in an href (the cell's text is the game's name
again), and the crawler's own bright-red invalid data verdict exists only in a colour. A value its
author distrusted is not imported. Capabilities land as capability.x.declared and never .measured
— TinTin measured that the server answered; the game declared what was in the answer.

MudStatsimported_measured, a scrape and therefore gated. Values are read by element id,
never by label
: the page puts a label and its value in separate elements, so a regex anchored on
Address: captures the whitespace between two closing tags and every field comes back empty.

Neither yields availability spans. Both pages are snapshots: they say the host answered at an instant
and nothing about for how long, and a span invented around that credits archive grace nobody measured.
Player counts are imported only where the source dates them — TinTin's generated on stamp,
MudStats' (9 minutes ago) — because dating somebody else's measurement to the moment we read the
page puts a fabricated timestamp into the day × hour heatmap.

Verification

Six suites green and a warning-free build. 88 tests in MUI.Import.Tests, six of them a real
end-to-end import into PostgreSQL 17 via Testcontainers (they skip by name where no container runtime
exists; CI's Linux leg demands one). Every byte any test parses comes from a committed fixture — there
is no HttpClient in the suite that can reach the internet.

HistoryTierTests.AnAssertedSourceStuffedWithHistoryWritesNoneOfItAndIsCountedForTrying is the pin
for the two-tier rule, with TheAssertedSinkHoldsNothingItCouldWriteWith holding the enforcement
mechanism in place by reflection and
ImportAgainstPostgresTests.AnImportedReachableSpanIsStoredWithTheOriginThatEarnsItHalfWeight
pinning the column the half weight is actually computed from.

Run live once, politely (tools/live-tintin-import, in the solution so it cannot rot, never run
by CI): 115 hosts found, 144 crawl targets, 114 dated player counts, 88 codebases, 69 websites.
Against a real PostgreSQL with one matching game seeded, that game received 25 fields, 4 endpoints and
1 presence row, every one labelled imported_measured, plus 30 provenance rows.

Notes for review

  • The plan's TinTin entry was wrong on both the URL and the tier. It assumed a hand-maintained TSV
    at /mudlist/mudlist.tsv and filed it imported_asserted. The real artefact is the MSSP crawler's
    own HTML listing, and a site that connects and negotiates MSSP has measured. Tier corrected.
  • Two of the plan's names differ: MUI.Backfillsrc/MUI.Import, and the plan's DirectorySource
    (the HTTP surface) is DirectoryFetcher, freeing IDirectorySource for the source abstraction.
  • MUI.Catalog, MUI.Crawl and MUI.Discovery are untouched. The only file added outside the two
    new projects is the migration.

🤖 Generated with Claude Code

…r by tier

MUI.Import reads existing MU* directories (spec §7.6) and splits what it finds
by whether the source measured or merely recorded.

- The two tiers, and the rule enforced by construction rather than by care.
  AssertedHistorySink takes no constructor parameters and holds no store, no
  writer and no clock, so "no history, no presence, no grace" is a fact about
  the type. It still counts what it refused, because a silent refusal reads
  identically to a source that offered nothing.
- imported_measured history is written through IImportedAvailabilityWriter and
  never IAvailabilityStore.OpenAsync, whose origin column DEFAULTs to
  'first_party' — getting that wrong would credit a third party's decade at
  full weight, silently, and only a real database can tell you.
- import_provenance (migration 0100) carries §7.6's other requirement: the
  originating site and the import date on every value. It is NOT on the grace
  path; the half weight is availability_interval.origin, computed once in
  ArchiveSweeper.
- Etiquette is configuration and refusals, not prose: bulk export ▸ documented
  API ▸ scrape, robots.txt read before the first content fetch, rate limit on
  an injected TimeProvider that adopts a longer Crawl-delay and never a shorter
  one, and an identifying User-Agent without which nothing fetches at all. A
  scrape-only source is refused outright until a human has written to its
  maintainer. The about page's attribution list is derived from the registry,
  so a source that is read and not credited is unreachable.
- An import may not mint a game. Identity is one signal — an endpoint we
  already measured — so below it a listing becomes a CrawlTarget and stops,
  which is spec §7.2 arrived at by having no mechanism to break it.

Two sources, behind IDirectorySource so a third is a class:

- TinTin++ MSSP Mud Crawler, imported_measured, read as a bulk export. Its
  fixed-width box table is parsed with the markup's colours and links kept:
  the website URL exists only in an href, and the crawler's own bright-red
  "invalid data" verdict exists only in a colour — a value its author
  distrusted is not imported. Capabilities land as capability.x.declared,
  never .measured: TinTin measured that the server answered, the game declared
  what was in the answer.
- MudStats, imported_measured, a scrape and therefore gated. Values are read
  by element id, never by their label — the page puts the two in separate
  elements, so the obvious regex captures the gap between two closing tags.

Neither yields availability spans: both pages are snapshots, and a span
invented around an instant credits grace nobody measured. Player counts are
imported only when the source dates them — TinTin's generation stamp, MudStats'
"(9 minutes ago)".

88 tests, six suites green, and one end-to-end import into a real PostgreSQL
through Testcontainers. Verified once against the live page:
115 hosts, 144 endpoints, 114 dated counts (tools/live-tintin-import).

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

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 36 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9e1104df-2ae4-4e76-aa82-ed8533c91a5f

📥 Commits

Reviewing files that changed from the base of the PR and between 6a6b823 and ab508be.

📒 Files selected for processing (50)
  • .github/workflows/ci.yml
  • Directory.Packages.props
  • MUIndex.slnx
  • migrations/0100_import_provenance.sql
  • src/MUI.Import/DirectoryFetcher.cs
  • src/MUI.Import/EtiquettePlanner.cs
  • src/MUI.Import/HistorySink.cs
  • src/MUI.Import/IDirectorySource.cs
  • src/MUI.Import/IImportWriter.cs
  • src/MUI.Import/ImportEtiquette.cs
  • src/MUI.Import/ImportIdentity.cs
  • src/MUI.Import/ImportPipeline.cs
  • src/MUI.Import/ImportProvenance.cs
  • src/MUI.Import/ImportReport.cs
  • src/MUI.Import/ImportRunner.cs
  • src/MUI.Import/ImportTier.cs
  • src/MUI.Import/ImportedAvailabilityWriter.cs
  • src/MUI.Import/ImportedGame.cs
  • src/MUI.Import/MUI.Import.csproj
  • src/MUI.Import/MeasuredHistorySink.cs
  • src/MUI.Import/NpgsqlImportProvenanceStore.cs
  • src/MUI.Import/PolitenessGate.cs
  • src/MUI.Import/RobotsPolicy.cs
  • src/MUI.Import/ServiceCollectionExtensions.cs
  • src/MUI.Import/SourceAttribution.cs
  • src/MUI.Import/Sources/HtmlLine.cs
  • src/MUI.Import/Sources/MsspCrawlerTable.cs
  • src/MUI.Import/Sources/MudStatsSource.cs
  • src/MUI.Import/Sources/TinTinMsspCrawlerSource.cs
  • tests/MUI.Import.Tests/AttributionTests.cs
  • tests/MUI.Import.Tests/EtiquetteTests.cs
  • tests/MUI.Import.Tests/Fixtures/mudstats-index.html
  • tests/MUI.Import.Tests/Fixtures/mudstats-world-4dimensions.html
  • tests/MUI.Import.Tests/Fixtures/tintin-mssp-mudlist.html
  • tests/MUI.Import.Tests/HistoryTierTests.cs
  • tests/MUI.Import.Tests/ImportAgainstPostgresTests.cs
  • tests/MUI.Import.Tests/ImportPipelineTests.cs
  • tests/MUI.Import.Tests/ImportTierTests.cs
  • tests/MUI.Import.Tests/MUI.Import.Tests.csproj
  • tests/MUI.Import.Tests/RobotsAndRateLimitTests.cs
  • tests/MUI.Import.Tests/Sources/MudStatsSourceTests.cs
  • tests/MUI.Import.Tests/Sources/TinTinMsspCrawlerSourceTests.cs
  • tests/MUI.Import.Tests/Support/FakeHttp.cs
  • tests/MUI.Import.Tests/Support/Fixture.cs
  • tests/MUI.Import.Tests/Support/Harness.cs
  • tests/MUI.Import.Tests/Support/InMemoryStores.cs
  • tests/MUI.Import.Tests/Support/ManualTimeProvider.cs
  • tests/MUI.Import.Tests/Support/PostgresFixture.cs
  • tools/live-tintin-import/Program.cs
  • tools/live-tintin-import/live-tintin-import.csproj

Comment @coderabbitai help to get the list of available commands.

Both PRs added a project, a package and a CI step, so the three shared
registration files collided. The resolution is additive on every hunk -- the
crawler needs hosting abstractions and a console sink, the importer needs the DI
abstractions it shares with it, and both suites want a real Postgres on the Linux
leg for the same reason the Catalog suite does.

Six suites green after resolving: 711 tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@HarryCordewener
HarryCordewener merged commit d295176 into main Jul 31, 2026
3 checks passed
@HarryCordewener
HarryCordewener deleted the feat/backfill-importers branch July 31, 2026 03:30
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