Write down the dials we refuse, and let staff honour "unlist us" - #189
Merged
Merged
Conversation
#187) Two defects found auditing the registry, both of which had been true since the code was written and neither of which anything could see. **A refused dial left no record (#185).** `RefuseAsync` records it through `RecordAttemptAsync(succeeded: true)` — right, because the far end did not fail — so the target reads as flawless: no failures, a recent attempt, no availability row. The only trace was a log line with about thirty minutes of retention. Two of 1,677 production targets were in that state and neither was findable except by noticing that `next_probe_at` happened to be exactly seven days after `last_probed_at`, which is an accident of the backoff rather than a record. `crawl_refusal` (migration 0039) is our note about our own decision, on the `icon_attempt` precedent: nothing in it reaches a game page, the API or the change feed, and rule 5 is what keeps it out of `availability_interval`, where `refused` already means an RST from a real host. It lists standing refusals rather than history — a row goes the moment the address is dialled again — so it cannot become the queue nobody reads that `icon_attempt`'s ancestor was. Read it with the new `crawl_refusals` tool; `crawl_summary` counts the same rows. **An opt-out could not actually unlist a game (#187).** Recording one and unlisting are two acts, deliberately — the first stops the dial, and most asks are only that. Convergence MUSH asked for the second on 2026-08-16, got the first, and stayed listed for a month: indexable, publishing `telnet://` as schema.org `GameServer` under a page titled "how to connect". Nothing could fix it, because `game_unlisting_is_attributed` demanded an `app_user` and the person who asked had no account. Attribution becomes a person *or* an explanation, never neither (migration 0040, `UnlistedBy`) — the shape `excluded_reason`, `crawl_opt_out.detail` and `--because` already use. `game_unlist` and `game_relist` are staff's route to it, the second so it is not a one-way door: an opted-out address is refused before the dial, so the probe that would otherwise relist a game never happens. And the page stops being promoted while it is withheld: `noindex, nofollow`, and no structured data at all, for `unlisted` and `excluded` alike. §7.5 is untouched — the page, the URL, the history and the change feed all go on answering. **Archived is deliberately not included**: an archived game is a real game one successful probe away from being active again, and staying findable is the whole difference between going dark and being taken out. 3,206 tests pass across all six suites. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Warning Review paused — included plan limit reachedKeep your review moving with free on-demand reviews.
On-demand reviews are free for the next 22 days.
Reviews can continue after your included limit without a manual trigger. An admin must approve usage-based billing. Promotion and pricing detailsOn-demand reviews are free for the next 22 days. After that, they cost $0.25 per reviewed file. Review limit detailsOr wait 16 seconds for your next included review. Limit details: You’ve used all 2 included reviews currently available. Your 52 included PR review attempts over the past 7 days set your current allowance at 2 reviews per hour. Review configuration: ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Essentials Run ID: 📒 Files selected for processing (32)
Comment |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #185. Closes #187.
Two defects found auditing the registry after #184. Both had been true since the code was written, and neither was visible from anywhere.
A refused dial left no record (#185)
CrawlCycle.RefuseAsyncrecords a refusal throughRecordAttemptAsync(succeeded: true)— correct, the far end did not fail — so the target it leaves behind reads as flawless: no failures, a recent attempt, no availability row. The only trace was a log line with about thirty minutes of retention.Two of 1,677 production targets were in that state (HellMOO, refused by the address gate; Convergence MUSH, opted out) and neither was findable except by noticing that
next_probe_athappened to be exactly seven days afterlast_probed_at— an accident of the backoff rather than a record.crawl_refusal(migration 0039) is our note about our own decision, on theicon_attemptprecedent: nothing in it reaches a game page, the API or the change feed, and rule 5 is what keeps it out ofavailability_interval, whererefusedalready means an RST from a real host — a measurement of them.It lists standing refusals, not history: a row goes the moment the address is dialled again, so it cannot become the queue nobody reads that
icon_attempt's ancestor was. Read it with the newcrawl_refusalstool;crawl_summarygained arefusals standingtotal over the same rows.An opt-out could not actually unlist a game (#187)
Recording an opt-out and unlisting a game are two acts, deliberately — the first stops the dial, and most asks are only that. Convergence MUSH asked for the second on 2026-08-16, got the first, and stayed listed for a month: indexable, publishing
telnet://game.convergencemush.org:10000as schema.orgGameServerunder a page titled "how to connect".Nothing could have fixed it.
game_unlisting_is_attributeddemanded anapp_user,UnlistAsyncwas reachable only from the owner dashboard, and the person who asked had no account. The request was granted and its point was not.Attribution becomes a person or an explanation, never neither (migration 0040,
UnlistedBy) — the shapeexcluded_reason,crawl_opt_out.detailand--becausealready use here.game_unlistandgame_relistare staff's route to it; the second exists so this is not a one-way door, because an opted-out address is refused before the dial and the probe that would otherwise relist a game never happens.And the page stops being promoted while it is withheld —
noindex, nofollow, and no structured data at all, forunlistedandexcludedalike. §7.5 is untouched: the page, the URL, the history and the change feed all go on answering. Keeping a record and advertising a game are different acts, and this is where they part.Archived is deliberately not included. An archived game is a real game one successful probe away from being active again, and staying findable is the whole difference between going dark and being taken out.
Notes
SitePreviewnow treats an empty graph as an absent one. An emptyld+jsonblock is worse than none: a machine reading it gets a parse error where it would otherwise have read an absence.NpgsqlCrawlRefusalStore.Rowis a class withinitproperties rather than a positional record, following every other store here. Dapper matches a record's constructor by parameter type and Npgsql hands it aDateTimefor atimestamptz, so a positionalDateTimeOffsetfails to materialise at run time with nothing to see at compile time — which is exactly how it failed while being written.Testing
TDD throughout: eleven new tests, each watched fail first. 3,206 pass across all six suites, Postgres exercised rather than skipped.
Not in this PR
AAAA) is a decision about the scope guard plus an email to their operator, not a code change — andcrawl_refusalsis what makes it visible in the first place.game_unlisthere is what will settle Convergence; the110-10-160-150-4001half wantsLifecycleState.Excluded, which already has a route.🤖 Generated with Claude Code