Read an operator their own MSSP back, and never call it a fault - #33
Conversation
§8.5 asks for an MSSP linter scorecard, continuous rather than one-shot,
flagging missing fields, wrong types and non-standard values. MsspLint.Inspect
is a static over the rows the crawler already wrote: no stored score, no button,
no job. Fix mush.cnf and the page is clean on the next probe.
It reads measurements and writes nothing about them — the method takes a list
and a predicate and has nothing in reach to write with, which is asserted,
because a lint result is a decision of OURS and rule 5 forbids recording one as
a fact about somebody's game.
Three things it refuses to say.
Silence is never a fault. A game we hold no MSSP report from gets HasReport
false and an empty finding list, never twenty-seven "missing" lines: we did not
measure an absence of variables, we have not read a report. The page says that
about our crawl, in those words, to the one reader who could tell the difference.
An unlisted value is not an error. MSSP's enumerations are not exhaustive and a
game running a genre nobody wrote down in 2011 is doing nothing wrong — so the
finding names the consequence instead, which is true: our facets do not
recognise it and the game lands in the unknown bucket.
No capability variable is linted. Telling an operator to declare GMCP 1 would be
advising them to make exactly the assertion this site is built to distrust, and
the handshake already answers it by measurement.
The placeholder test is borrowed, not copied. MsspDefaults already knows that
NAME "PennMUSH" means nobody edited that line; it lives in MUI.Crawl, which
MUI.Catalog may never reference, so it is injected at the composition root
rather than duplicated into a second list that would drift.
Owner-only, at /g/{slug}/mssp, and that is about what the thing is rather than
about secrecy — every value on it is already public. A scorecard per game would
be a league table of how well operators fill in a config file, which is a rating
affordance by another route, and §2 does not allow one.
846 tests over five suites, Postgres exercised.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
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 |
|
CI does not run on PRs whose base is not Re-target to |
|
@coderabbitai review |
|
Ninth link, and the first of the three that were based on feat/claim-wiring rather than on main: main → #27 → #32 → #31 → #21 → #28 → #30 → #26 → #29 → #33. Two conflicts, and one of them is the reason these three were flagged. Account.razor — this branch put the scorecard link in the <li> a claimed game used to be, and #29 has since turned each claimed game into a <details> with an owner panel inside it. Textually the two edits are the same lines; what the branch meant was "one link per verified claim, on the dashboard and nowhere else", so the link moved into the <summary> beside the game's name and the verified date. Taking either side wholesale would have lost the owner panel or lost the only route to the scorecard. app.css is two independent blocks, both kept. Read the branch's tests rather than trusting them: MsspLintTests and MsspScorecardSurfaceTests assert MsspLint's judgements and that the page is CLOSED to a stranger, neither of which touches the dashboard's markup, so nothing here went stale. They also do not cover the dashboard link itself — noted rather than fixed, because Account.razor has no render harness and building one for one anchor is a bigger change than this merge.
Tenth link: main → #27 → #32 → #31 → #21 → #28 → #30 → #26 → #29 → #33 → #34. THE BADGE WAS PUBLISHING A GAME'S OWN ASSERTION AS OUR MEASUREMENT, and this merge is where that became visible. This branch added GameSummary.PlayersNowAt — "when the count was measured" — and said so in its own doc comment: "not the whole of §10.1's fix, the codebase still has no chip". #27 landed the whole fix while this branch sat on feat/claim-wiring, so the summary now carries PlayersNowProvenance, a chip with the count's source on it. PlayersNowAt is dropped and the badge reads the instant off the chip; keeping both would have been two answers to one question, which is the thing #27 exists to stop. But an instant is not the interesting half. PlayersNow can come from MSSP PLAYERS — PresenceChoice ranks WHO, then MSSP, then the connect screen — and this badge writes "N players measured 4m ago", returns state "measured" in its JSON, and paints the accent that means measured on every other surface here. On somebody else's front page, where we cannot correct it. That is rule 5 broken by a format string, which is very nearly the sentence §10.1 uses about the unlabelled listing. So Counted now requires ProvenanceChip.IsMeasured — the same predicate ApiMapper.Counted already uses to decide playersNowState, so the badge and /api/games/{slug} cannot disagree about one game. A declared count reads as unknown: three states, no new vocabulary, and the badge says only what we measured. What the game says about itself is on its page, attributed. Nothing covered this: BadgeApiTests used ashen-court only to assert two URLs, and ashen-court is the fixture's MSSP-declared row — put there by #27 precisely as the argument for labelling. ADeclaredCountIsNotPublishedAsA- MeasuredOne now pins it, with m-u-s-h as the measured control, and it fails if the IsMeasured guard is relaxed. Account.razor for the third time: the badge snippet was written against the <li> a claimed game used to be. It is now in the <details> body beside the owner panel rather than in the <summary>, because a <details> nested inside a <summary> is interactive content inside a control. NEEDS A HUMAN: ProvenanceChip.IsMeasured is Handshake-or-Who, so a count read off a connect screen is "declared" — while migration 0003 says in terms that a banner count "is still a measurement of ours". The two disagree, the disagreement predates this chain, and it now decides whether Aardwolf's badge shows a number. I have kept the badge consistent with the API rather than picking a side.
Eleventh and last link of the restack: main → #27 → #32 → #31 → #21 → #28 → #30 → #26 → #29 → #33 → #34 → #35. MIGRATION RENUMBERED. 0012_claim_intent.sql → 0013_claim_intent.sql, since #30's slug history now holds 0012. Thirteen migrations are embedded in MUI.Catalog.dll, 0013 sorts last, and nothing referred to it by number. Three conflicts and one clean merge that did not compile. Account.razor, for the third merge running, and this one had the most in it. The dashboard's status banner is now one else-if chain — resigned, then saved, then refused — because a redirect carries exactly one outcome and two banners at once would be two answers to one action. A claimed game's block holds, in the order an owner would want them: who else owns it, the enrichment panel, the badge snippet, the history, and giving up the claim last. #35 was written when a claimed game was an <li>, so all of it had to move inside the <details> #29 introduced; the co-owner line in particular was inside the <summary>, which is not somewhere a <p> may live. Passkeys.cs maps both write surfaces rather than choosing: MapMuiOwnerWrites is §8.5's enrichment and §11's suppression, MapMuiOwnership is §8.4's counter-claim and resignation. They are different routes. Claim.razor merged clean and broke the build, which is the useful kind of failure. #21 changed this page from IGameQueries to IGameStore on purpose — a submitted game is hidden from the public read until somebody claims it, so looking it up through the listing's own query made claiming the one thing a hidden game could never do — and #35 added three uses of the old Page.Summary against the view model that is no longer loaded. They now read the row, which is what the rest of the page already did. Read #35's tests rather than trusting them, as asked: OwnershipPostgresTests and OwnershipSchemaTests assert ClaimService and the claim_intent schema against a real database, neither of which touches the dashboard markup or the page's lookup, so nothing in them went stale. The dashboard markup itself has no render harness on any of these three branches.
Carries the shared measured/declared predicate and /submit's opt-out refusal down the chain.
§8.5's MSSP linter scorecard: continuous rather than one-shot, flagging missing fields, wrong types and non-standard values.
Continuous means derived, not scheduled
MsspLint.Inspectis a static over themssp-sourced rows the crawler already wrote. No stored score, no button, no queue, no job. An operator fixesmush.cnfand the page is clean on the next probe with nothing to press — and there is no number that can go stale against the report it describes.It reads measurements and writes nothing about them. The method takes a list and a predicate and has nothing in reach to write with, which is asserted rather than promised (
TheLinterHasNothingToWriteWith). A lint result is a decision of ours, and rule 5 forbids recording one as a fact about somebody's game.Three things it refuses to say
Silence is never a fault. A game we hold no MSSP report from gets
HasReport: falseand an empty finding list — never twenty-seven "missing" lines. We did not measure an absence of variables; we have not read a report. The page says exactly that, about our crawl:Getting this backwards publishes our own gap as somebody's neglect, on the page of the one person who could tell the difference.
An unlisted value is not an error. MSSP's enumerations are not exhaustive, and a game running a genre nobody wrote down in 2011 is doing nothing wrong. So the finding names the consequence, which is true and is actionable:
"Cyberpunk Noir" is not one of MSSP's listed values, so our facets do not recognise it and the game lands in the unknown bucket.The test asserts the words "invalid" and "wrong" never appear.No capability variable is linted. Advising an operator to declare
GMCP 1would be advising them to make precisely the assertion this site exists to distrust — and the handshake already answers the question by measurement.Borrowed, not copied
MsspDefaultsalready knows thatNAME "PennMUSH"means nobody edited that line. It lives inMUI.Crawl, whichMUI.Catalogmay never reference, soInspecttakes the placeholder test as a parameter and the composition supplies the real one. A second copy of that list inMUI.Catalogwould be a second spelling of the same judgement, and the two would drift the first time somebody added a placeholder to one. A test pins the two together.Owner-only, and why
At
/g/{slug}/mssp, reachable from the dashboard and nowhere else. This is a judgement about what the thing is, not about secrecy — every value on it is already public on the game's own page. A scorecard published per game would be a league table of how well operators fill in a config file, which is a rating affordance by another route, and §2 does not permit one by any route. So it goes to the person who can act on it.A reader without a verified claim gets not found rather than forbidden: whether a particular account holds a claim is not a fact this page owes a stranger, and an explicit refusal would confirm it.
Verified
dotnet build MUIndex.slnx -c Releaseclean, each suite run directly — 846 tests, 0 failed:Catalog and Crawler against real PostgreSQL via Testcontainers/Podman. 16 new tests: 12 over the linter, 4 over the surface and the borrowed placeholder test.
Left undone
Account.razorlink is one line and will conflict trivially with Owning a game: add what MSSP has no field for, and take your screen back #29, which restructures that loop.INTERMUD,AREAS/ROOMS/MOBILESand the pay-to-play flags are not linted. Nothing on this site reads them, and a variable no surface renders is optional however much MSSP likes it.🤖 Generated with Claude Code