Make the listing a directory, and never let a facet lie about a click - #14
Merged
Merged
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
Spec §9's faceted search, over one vocabulary and one pass. Activity band, last seen, measured protocols, TLS, negotiated charset, codebase, family, genre and language; a random game that carries whatever is on screen; and the archive toggle that was already there. Every count beside a value is what choosing that value returns, because the counts and the listing come out of the same call. A choice facet is counted with its own selection lifted, since choosing replaces; a tick box is counted against the results, since ticking intersects. Both answer the same question — what happens if I click this — and a value nothing matches is not drawn at all, which is the cheapest way to make a dead end impossible. An unknown is never a no, and the page says so rather than a comment saying so. The protocol boxes read capability.*.measured and nothing else, so an unticked box means we did not see it offered and never that a game declined it — that distinction is why the field is keyed by source. Every declared facet carries its own absence as a choice, spelled three different ways because they are three different facts: a codebase is "not identified" (our parsers), a genre is "not declared" (what the game published), an encoding is "nothing negotiated" (the handshake). Charset reads the handshake row rather than the precedence winner, or a facet labelled measured would answer from MSSP for every server that never negotiates. TLS is an endpoint we opened, never MSSP's SSL line. Nothing writes a TLS endpoint yet — CatalogueBinder records what it dialled and the crawler dials plaintext — so the facet renders nothing today, which is the honest rendering of a measurement nobody has taken. FacetQueriesPostgresTests seeds one and pins the query, so it becomes a real facet the day the crawler takes it. Filtering moved into MUI.Catalog.FacetedSearch, which both IGameQueries implementations now feed rows to. That closed a live divergence: band=archived lifted the archive exclusion in the demo fixture and did not in Postgres, so one filter had two answers and only the fixture's was tested. The page binds through GameFilterBinding, which the read API already used — one parser, two callers, so /games?band=quiet and /api/games?band=quiet cannot drift. The API publishes the facets too, with each group's evidence, and refuses an unreadable band rather than answering the unfiltered catalogue; the page refuses it out loud for the same reason. The plain surface prints the whole panel with the parameter that selects each value, because a text browser cannot work a select and can edit a URL. Also: the plain-text link is rebuilt from the live querystring rather than a hand-kept list of two parameters, which would have dropped every facet added after it.
HarryCordewener
force-pushed
the
feat/faceted-search
branch
from
July 31, 2026 15:23
ce2392f to
7fc9e5c
Compare
HarryCordewener
changed the base branch from
chore/park-the-importer
to
feat/ecosystem-dashboard
July 31, 2026 15:23
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.
Stacks on #11 (
chore/park-the-importer), which ismainplus #9 and #11. Review after those land, or the diff will include their work.Spec §9's faceted search over
/games: activity band, last seen, measured protocol support, TLS, negotiated charset, codebase, family, genre, language — plus random game, and the archived toggle that already existed. Blazor static SSR,method="get", no script: the querystring is the whole state, so a filtered listing is linkable, the back button works and the server recomputes everything.A facet cannot lie about what a click produces
IGameQueries.SearchAsyncreturns the listing and the facet counts from one pass, so the two cannot be answers to two slightly different questions. Each count is measured against the set the click actually lands on:A value nothing matches is never drawn — a facet that can be clicked into an empty listing is a facet lying about the catalogue, and not drawing the click is the cheapest way to make that impossible.
FacetedSearchTests.EveryCountIsExactlyWhatChoosingThatValueReturnsand its Postgres and API counterparts check this by taking every advertised count, running that choice, and comparing sizes.An unknown is not a
no, and the page says soThe protocol facet reads
capability.*.measuredand nothing else. There is deliberately no way to ask for the complement: a capability is writtentruewhen observed and otherwise not written at all, so "not listed" covers a game we never measured as well as one that declined, and only one of those is a fact about them. The panel states this in the markup rather than in a comment.Every declared facet carries its own absence as a selectable value, spelled three ways because they are three facts — not identified (our parsers), not declared (what the game published), nothing negotiated (the handshake).
FacetChoicemodels the absence as a member rather than an empty string so the two cannot be typed for each other.charsetreads the handshake row rather than the precedence winner:CHARSETis one of the few fields both a handshake and MSSP write, so the winner is silently the game's own assertion for every server that never negotiates — a facet labelled measured answering from the declared column.One vocabulary
FacetKeysnames each facet once;GameFilterBindingis the one parser and the page now binds through it, so/games?band=quietand/api/games?band=quietcannot drift.FacetSurfaceTestswalksFacetKeysby reflection in both directions — every key the query names is read by the binding, and every group the query returns is a key the vocabulary names. The API publishes the facets with each group'sevidence, and refuses an unreadable band; the page refuses it out loud rather than answering with the unfiltered catalogue.Plain mode
?plain=1prints the whole panel — every value, its count, and the parameter that selects it — because a text browser cannot operate a<select>but can edit a URL. Width is pinned at 80 columns.Two bugs fixed on the way
band=archivedhad two answers. The demo fixture read it as asking for the archive; Postgres read it as a filter over a listing the archive had already left, so it returned nothing. Both implementations now feed rows to oneFacetedSearch, and only that closed it.qandarchived. The first facet added would have been silently dropped, offering the unfiltered catalogue as "this page, as text". It is rebuilt from the live querystring now.Left out deliberately
CatalogueBinderwritesEndpointKind.Telnetunconditionally and nothing writescapability.tls.measured, so the only honest measured signal — an endpoint of kindtls— is never recorded. The facet queries it correctly and therefore renders nothing on real data; filling it fromcapability.ssl.declaredwould have been the central lie.FacetQueriesPostgresTests.TlsIsAnEndpointWeOpenedAndNeverAnSslLineInMsspseeds a TLS endpoint and pins the query, so it becomes a real facet the day the crawler dials TLS.codebasevalues are raw and capped at the 12 commonest. MSSPCODEBASEis versioned (PennMUSH 1.8.8p0), so normalising it into families would be inventing a value;FAMILYis the coarse facet and is offered beside it. The tail stays reachable by search and by URL, and the panel says so.FieldsForAsyncalready read every field of every listed game — and the note inSearchAsyncrecords what the next shape is and what it would cost.Verifying
dotnet build MUIndex.slnx -c Releaseclean, all five suites green: Catalog 154, Crawl 130, Crawler 77, Discovery 182, Web 130.🤖 Generated with Claude Code
https://claude.ai/code/session_01NrGKmKcRCGktyhRTFbQDMk