Skip to content

Read an operator their own MSSP back, and never call it a fault - #33

Merged
HarryCordewener merged 3 commits into
feat/owner-enrichmentfrom
feat/mssp-scorecard
Aug 15, 2026
Merged

Read an operator their own MSSP back, and never call it a fault#33
HarryCordewener merged 3 commits into
feat/owner-enrichmentfrom
feat/mssp-scorecard

Conversation

@HarryCordewener

Copy link
Copy Markdown
Member

§8.5's MSSP linter scorecard: continuous rather than one-shot, flagging missing fields, wrong types and non-standard values.

Stacked on #32 (feat/claim-wiring), because the page resolves IClaimStore to check ownership and nothing registers it on main — that is the bug #32 fixes. Base this on main once #32 lands and the diff is unchanged.

Continuous means derived, not scheduled

MsspLint.Inspect is a static over the mssp-sourced rows the crawler already wrote. No stored score, no button, no queue, no job. An operator fixes mush.cnf and 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: 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 exactly that, about our crawl:

We have not read an MSSP report from this game. That is a fact about our crawl rather than about your server — we may not have reached it yet, or your server may not offer MSSP at all.

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 1 would 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

MsspDefaults already knows that NAME "PennMUSH" means nobody edited that line. It lives in MUI.Crawl, which MUI.Catalog may never reference, so Inspect takes the placeholder test as a parameter and the composition supplies the real one. A second copy of that list in MUI.Catalog would 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 Release clean, each suite run directly — 846 tests, 0 failed:

suite total
MUI.Catalog.Tests 223
MUI.Crawl.Tests 136
MUI.Crawler.Tests 77
MUI.Discovery.Tests 183
MUI.Web.Tests 227

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

🤖 Generated with Claude Code

§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>
@coderabbitai

coderabbitai Bot commented Aug 15, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8d413df5-f8a5-4847-a3d4-c2bb5bcc1aac

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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

@HarryCordewener

Copy link
Copy Markdown
Member Author

CI does not run on PRs whose base is not main, so the checks tab here is empty. Dispatched the same workflow against the branch instead — green on both legs: https://github.com/SharpMUSH/MUIndex/actions/runs/31869717007

Re-target to main once #32 lands and the checks will attach normally.

@HarryCordewener

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 15, 2026

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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.
@HarryCordewener
HarryCordewener changed the base branch from feat/claim-wiring to feat/owner-enrichment August 15, 2026 16:50
HarryCordewener pushed a commit that referenced this pull request Aug 15, 2026
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.
HarryCordewener pushed a commit that referenced this pull request Aug 15, 2026
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.
@HarryCordewener
HarryCordewener merged commit 92a8d6c into main Aug 15, 2026
3 checks passed
@HarryCordewener
HarryCordewener deleted the feat/mssp-scorecard branch August 16, 2026 23:52
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.

2 participants