Skip to content

Claiming was complete, tested, and wired to nothing - #32

Merged
HarryCordewener merged 5 commits into
feat/api-provenancefrom
feat/claim-wiring
Aug 15, 2026
Merged

Claiming was complete, tested, and wired to nothing#32
HarryCordewener merged 5 commits into
feat/api-provenancefrom
feat/claim-wiring

Conversation

@HarryCordewener

@HarryCordewener HarryCordewener commented Aug 15, 2026

Copy link
Copy Markdown
Member

§8.5 closes by saying nothing sets game.is_claimed, so the listing's claimed badge and ArchivePolicy's ceiling-grace (§7.5) have never been exercised. Claiming shipped in #16 and that note still read true, so I went looking for why.

The claim logic was not the problem. It is complete and it has tests. The composition was, and not one of the faults could fail loudly.

What was broken

The site has two compositions of the same objects. mui-crawl builds the crawl loop by hand and passes every collaborator explicitly; the deployed site assembles it through DI. Everything below is true only of the second, which is why every existing test passed.

1. The crawl graph registered no ClaimService. CrawlCycle takes it as an optional parameter — deliberately, because a crawl with no database behind it should do slightly less rather than refuse to run. The site only ever had one because the accounts module happened to register the same type for the dashboard.

2. ClaimService was scoped; the crawl loop that needs it is a singleton BackgroundService.

Cannot consume scoped service 'MUI.Catalog.Persistence.ClaimService'
from singleton 'MUI.Crawler.CrawlCycle'

With scope validation on — what dotnet run does in Development — the container refuses to build and the site does not start with a connection string set. Production leaves validation off, so there it resolved the scoped service from the root and worked. That is the answer to §8.5's note: is_claimed was being set, in Production, by accident, and nowhere else.

3. IClaimStore was registered nowhere at all. Account.razor service-locates it and reads a null as "this site has no database" — so every operator's dashboard was empty on a site holding their claims, and /g/{slug}/claim/check threw on request. In both environments.

4. The on-demand check moved nothing. RequestCheckAsync wrote last_checked_at and a check_requested event. Due-ness comes only from crawl_target.next_probe_at, so no probe ever came of it — while the page told the operator the button dialled a real server. A rate limiter on an action that does not happen is the most convincing possible no-op.

The fix

ClaimService and IClaimStore are stateless over a pooled NpgsqlDataSource, so both become TryAddSingleton, registered by the crawl graph and the accounts graph alike.

IOnDemandProbes brings the game's crawl targets forward — LEAST, so an ask can only ever make a probe sooner, and every target of the game moves because a game is claimed and a target is an address. The crawl loop still does the dialling, under CRAWL DELAY and §7.2's address gate, which is what keeps a button on a page from becoming a way to make us connect to a stranger's server. The button now says what it does: Look sooner.

The test that was missing

CompositionTests resolves the graph Program builds — by calling the same AddMuiSite the deployable calls. It restated the registrations first time round, and a restatement is a second copy that agrees with the first only until somebody edits one of them; the edit that breaks it is precisely the one this file exists to catch. So the graph moved out of Program's top-level statements into SiteComposition, and Program.cs is now the side effects only — connection string, migrations, the log line that says which of the two worlds it is in.

Measured, not asserted. Reverting the three source files to main:

  • the original seven tests fail five ways (an earlier revision of this description said seven — that was wrong);
  • with the four added since, ten tests fail seven ways.

The three that pass on the parent are the two Production controls and the demo composition, none of which were ever broken.

This is the only kind of test that could have caught any of it. Every part was correct in isolation and had tests proving it. An optional dependency and a service-located one both fail silently when the composition is wrong, and the claim path has one of each.

§8.5's closing note is rewritten to say what is true and to name that hazard, since it generalises past claiming.

Also closed here

  • The demo composition had no test. It has one: the claim surfaces are absent rather than present and broken, and the page still admits nothing on it was measured.
  • Two NpgsqlAvailabilityStore instances. The web tier registered its own with AddSingleton, so the crawler's TryAdd for IAvailabilityStore was skipped and the concrete type and IReachableHistory pointed at a second one. Harmless on a shared pool, and a direct contradiction of the crawler's own comment that two would be two connection paths answering one question. AddPostgresCatalogue is TryAdd throughout now, and one object answers to all three names.

Passkey sign-in: the #30 report does not reproduce

#30 reported that a JSON POST to a minimal API mapped after UseAntiforgery() is refused with 400 in a slim host, even with .DisableAntiforgery() — which would mean sign-in is broken, and §8.2 leaves no other way in. Measured against the real host: accounts registered, Postgres behind them, migrations applied, ASPNETCORE_ENVIRONMENT=Production.

request status
POST /account/passkey/assertion-options 200
POST /account/passkey/sign-in (the body passkey.js sends) 401
POST /account/passkey/registration-options 200
POST /account — Razor route, no token — control 400

The control is what makes the rest mean anything: anti-forgery is live in that pipeline and refuses an untokened POST one route over. The 401 is the handler answering — a bogus credential rejected — not the middleware. (The first attempt, skipping the ceremony, returned 500 from SignInManager: "No passkey assertion is underway". Also the handler.)

The mechanism. A minimal API is given anti-forgery metadata only when it binds form data — IFormCollection, IFormFile, [FromForm]. The passkey endpoints bind JSON or a query string, so they carry none and the middleware passes them through. MapRazorComponents puts that metadata on every component route, which is why the control is refused and these are not.

And .DisableAntiforgery() sets RequiresValidation to false, so a 400 that survives it was never anti-forgery's to begin with — that detail in the report is itself the evidence the diagnosis was mis-attributed. Whatever the slim host was returning, it was not this middleware.

PasskeyEndpointTests is added anyway, and not as a formality: sign-in is the only door in the building and no suite opened it — the ones that exercise sign-in are the ones that stub it, and the composition tests stop at the graph. It drives the real routes through AddMuiSite/UseMuiSite with the control alongside, so it fails if anti-forgery ever stops being live and it would catch the reported failure if it ever became real.

Verified

dotnet build MUIndex.slnx -c Release clean, each suite run directly — 860 tests, 0 failed:

suite total
MUI.Catalog.Tests 217
MUI.Crawl.Tests 136
MUI.Crawler.Tests 82
MUI.Discovery.Tests 183
MUI.Web.Tests 242

Catalog and Crawler against real PostgreSQL via Testcontainers/Podman.

Notes for review

🤖 Generated with Claude Code

§8.5 says nothing sets game.is_claimed, so the listing badge and §7.5's ceiling
grace have never been exercised. Claiming shipped in #16 and the note still read
true, so I went looking for why. The claim logic was not the problem — it is
complete and it has tests. The composition was.

The site has two compositions of the same objects. mui-crawl builds the crawl
loop by hand and passes every collaborator; the deployed site assembles it
through DI. Three things were wrong with the second, and none of them could fail
loudly:

CrawlCycle takes its ClaimService as an OPTIONAL parameter, deliberately — a
crawl with no database should do slightly less rather than refuse to run. The
crawler graph registered no ClaimService, so a crawler-only deployment settled
no beacons and said nothing about it.

ClaimService was registered scoped, and the crawl loop that needs it is a
singleton BackgroundService. A scoped dependency is one CrawlCycle can never
legally be given: with scope validation on, which is what `dotnet run` does, the
container refuses to build and THE SITE DOES NOT START with a connection string
set. Production leaves validation off, so there it worked — by accident, and
only there. That is also the answer to §8.5's note: is_claimed was being set in
production and nowhere else.

IClaimStore was registered nowhere at all. Account.razor service-locates it and
reads a null as "this site has no database", so every operator's dashboard was
empty on a site that had their claims, and /g/{slug}/claim/check threw on
request. A service-located dependency fails silently by construction.

Both services are stateless over a pooled NpgsqlDataSource, so both are now
TryAddSingleton, registered by the crawler graph and the accounts graph alike —
the one deployment that runs both gets one of each.

CompositionTests resolves the graph Program builds, under scope validation, in
both environments, and asserts the claim path is really joined. It fails on the
parent commit in seven ways and is the only kind of test that could have caught
any of this: every part was correct and the wiring between them was not.

§8.5's closing note is rewritten to say what is actually true, and to name the
general hazard — an optional dependency and a service-located one both fail
silently when the composition is wrong, and the claim path has one of each.

830 tests over five suites, Postgres exercised. Testcontainers 4.13.0 -> 4.14.0
is the same one-line pickup as #26: SSH.NET 2025.1.0 is now advised against and
NU1903 fails restore on main without it.

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: 2c72d464-8b92-47a6-962c-089b22c9ae99

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

@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.

Review findings on the composition fix. All four held; each is verified rather
than taken.

CompositionTests restated Program's registrations instead of running them, so a
future divergence — a scoped service consumed by a singleton, AddMuiAccounts
moving — would break the site while all the tests passed. That is exactly the
failure this file exists to catch. The graph moves out of Program's top-level
statements into SiteComposition.AddMuiSite/UseMuiSite, which the deployable and
the test now both call, so there is one copy and nothing to diverge from.

The on-demand check moved nothing. RequestCheckAsync wrote last_checked_at and a
check_requested event; due-ness comes only from crawl_target.next_probe_at, so
no probe ever came of it while the page said the button dialled a real server. A
rate limiter on an action that does not happen is the most convincing possible
no-op. IOnDemandProbes brings the game's targets forward with LEAST — an ask can
only make a probe sooner — and the crawl loop still does the dialling under
CRAWL DELAY and §7.2's address gate. Five Postgres tests assert on the schedule
rather than on the audit log. The button now says what it does: "Look sooner",
brings your game to the front of the queue, we dial on our own schedule.

The remark on TheHostedCrawlerCanBeResolvedInProductionToo described a failure
that never happened — it passes on the parent commit, because Production leaves
scope validation off and resolved the scoped ClaimService from the root. It is a
control, not a finding, and now says so. Measured: the original seven tests fail
five ways on the parent, not seven. With the four added here, ten fail seven.

"A crawler-only deployment" justified the new registrations in three places and
describes nothing: AddMuiCrawler has exactly one caller, MUI.Web's Program, and
mui-crawl builds its graph by hand. §4.11 has one deployable. The real consumer
is the web tier's in-process CrawlCycle, and all three now say so.

Two more while here. The demo composition had no test at all; it has one, and it
asserts the claim surfaces are absent rather than broken and that the page still
admits nothing on it was measured. And the web tier registered its own
NpgsqlAvailabilityStore with AddSingleton, so the crawler's TryAdd was skipped
and the concrete type and IReachableHistory pointed at a second instance —
harmless on one pool and a direct contradiction of the crawler's own comment
that two would be two connection paths answering one question. AddPostgresCatalogue
is TryAdd throughout now, and one object answers to all three names.

838 tests over five suites, Postgres exercised.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@HarryCordewener
HarryCordewener changed the base branch from main to feat/api-provenance August 15, 2026 16:10
HarryCordewener pushed a commit that referenced this pull request Aug 15, 2026
Fourth link of the restack: main → #27#32#31#21.

Four conflicts, and one of them was a hole rather than a collision.

NpgsqlGameQueries — #21 put a visibility predicate on every read of the
`game` table, so an unclaimed submission is off every public surface; #27
added a lookup by id and factored the projection into one `GameSelect`
constant. Textually they touched the same lines; semantically #27's new
lookup had no gate on it at all, and merging either side wholesale would
have served an unclaimed submission to anyone holding its identifier while
the slug lookup beside it refused. The predicate is now inside GameSelect,
where a lookup added on top of it cannot forget it, and the three feed
queries keep #27's `id AS Id` alongside #21's gate.

Program.cs and SiteComposition — #32 moved the pipeline into UseMuiSite,
#21 added two lines to the pipeline it moved. The lines moved with it
rather than staying behind in Program: one composition, one spelling.

CrawlerServiceCollectionExtensions and Passkeys.cs were both additive; the
opt-out registrations and the scope-guard registration coexist, as do the
two halves of the comment over the on-demand check.

SubmissionPostgresTests builds a CrawlCycle by hand and #31 gave the cycle
a non-optional OptOutGate — a submitted address is still an address whose
operator may have asked us to stop, and the form does not override that.

Two fixes the merge earned:

- MUI.Crawler.Tests' Postgres fixture now lifts max_connections the way
  MUI.Catalog.Tests' copy already did. With the opt-out, on-demand-probe
  and submission tests in one suite it hit the hundred-client ceiling and
  failed six tests, differently each run. The fixtures are meant to be kept
  in step and one of them had the fix.

- CompositionTests asserts that UseMuiSite maps both POST routes. The graph
  had tests and the pipeline had none, so losing a Map call during exactly
  this merge would have been silent — and it is asserted on the endpoint's
  display name, because /submit is also a Razor page whose SSR form answers
  POST, so the path (and the path with the verb) both pass with the line
  deleted. Both weaker forms were written first and both went green.
#30 reported that a JSON POST to a minimal API mapped after UseAntiforgery() is
refused with 400 in a slim host, even with .DisableAntiforgery(), which would
mean passkey sign-in is broken and §8.2 leaves no other way in.

IT DOES NOT REPRODUCE. Measured against the real host — accounts registered,
Postgres behind them, migrations applied, ASPNETCORE_ENVIRONMENT=Production:

  POST /account/passkey/assertion-options     -> 200
  POST /account/passkey/sign-in  (as sent)    -> 401
  POST /account/passkey/registration-options  -> 200
  POST /account  (Razor route, no token)      -> 400   <- control

The control is the part that makes the rest mean anything: anti-forgery IS live
in that pipeline and refuses an untokened POST one route over. The 401 is the
handler answering, not the middleware — the first attempt without the ceremony's
cookie got a 500 from SignInManager saying no assertion was underway, which is
the handler too.

The mechanism: a minimal API is given anti-forgery metadata only when it binds
FORM data. These bind JSON or a query string, so they carry none and the
middleware passes them through; MapRazorComponents puts metadata on component
routes, which is why the control is refused. And .DisableAntiforgery() sets
RequiresValidation false — a 400 surviving it was never anti-forgery's.

Adding the test anyway. Sign-in is the only door in the building and no suite
opened it: the ones that exercise sign-in are the ones that stub it, and the
composition tests stop at the graph. These drive the real routes through
AddMuiSite/UseMuiSite, with a control that fails if anti-forgery ever stops
being live and would catch the reported failure if it ever became real.

841 tests over five suites, Postgres exercised.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
HarryCordewener pushed a commit that referenced this pull request Aug 15, 2026
Fifth and last link of this round: main → #27#32#31#21#28.

Two conflicts, both in the crawler's options, and both purely additive:
CrawlerOptions.Validate now validates the maintenance and salt settings
this branch added and the submission settings #21 added, and the builder's
Build carries all three through. Neither side was choosing between them.

Everything else merged: the presence rollup registrations sit beside the
claim, opt-out and submission registrations in one graph, and the two
rotating salts are unrelated to each other — MUI.Crawl's hashes a player
name for §11's aggregates, MUI.Crawler's hashes a submitter address for
§7.6's per-source bound.

Migrations arrive in chain order with nothing to renumber: 0009 opt-out,
0010 submitted games, 0011 presence rollup. This branch's edit to 0003 is
a comment, so a database that has already applied it is unaffected.
HarryCordewener pushed a commit that referenced this pull request Aug 15, 2026
Sixth link, and the first of round two: the chain now runs main → #27#32#31#21#28#30.

MIGRATION RENUMBERED. 0009_slug_history.sql → 0012_slug_history.sql: #31
took 0009, #21 took 0010, #28 took 0011. MigrationRunner sorts embedded
resource names ordinally and MUI.Catalog.csproj globs migrations/*.sql into
LogicalName MUI.Catalog.Migrations.%(Filename)%(Extension), so a rename is
the whole change — the file is still embedded and now sorts after 0011.
Nothing in the tree referred to it by number.

Six conflicts.

PostgresData — #30 added the former-slug store with AddSingleton, into a
method #32 had converted to TryAdd throughout precisely because
AddMuiCrawler registers the same objects and one deployable calls both. The
store is registered TryAdd for the same reason the availability store above
it is: two of them would be two pools answering one question.

Program.cs and SiteComposition — the same shape as #21's merge. #30 added
UseMuiNotFoundPage and UseFormerSlugRedirects to a pipeline #32 had moved
into UseMuiSite; they move with it, at the positions #30 put them in — the
404 page before the static files, the redirect after the account routes and
before the route that would answer "not found".

SiteHost now calls AddMuiSite/UseMuiSite instead of restating them, which
its author flagged as a one-call follow-up. Verified it binds rather than
merely compiling: commenting out either middleware inside UseMuiSite fails
seven of #30's tests by name.

ApiHost — #27 added an IGameQueries parameter and #30 added an
Action<IServiceCollection> one, to the same optional slot. Both are kept
and SlugRedirectTests names its argument.

CrawlCyclePostgresTests took both new optional parameters, the spec's §10.1
took #27's prose with #30's paragraph in place of the "one gap remains"
sentence that is no longer true — after this merge all three of the gaps
that section lists are closed.

And the compile breaks a restack earns: #30 gave ProbeIngestor a SlugMinter
and CatalogueBinder an ISlugHistoryStore, which #31's and #21's Postgres
tests construct by hand; Render's StubGameStore grew RenameAsync, throwing,
because it throws for every other writer and a surface reaching for one is
a surface in the wrong layer.
HarryCordewener pushed a commit that referenced this pull request Aug 15, 2026
Seventh link: main → #27#32#31#21#28#30#26.

Two conflicts.

Arguments.cs — this branch moved seed parsing out to CrawlSeed.Parse so
the CLI, the environment variable and compose.yaml all read an address the
same way; #31 added ParseAddress beside the ParseSeed that moved. Kept
ParseAddress where it is and let ParseSeed stay gone: an opt-out address
comes only from the CLI, and it takes an optional port, so it is not the
same parse.

Program.cs — the same shape as the two merges before it. This branch added
configure.Apply(builder.Configuration) to the AddMuiCrawler call, which #32
had moved into SiteComposition.AddMuiSite; the line moved with it.

That move earned a test. CrawlerSettings.Apply has thorough tests and every
one of them calls it on a builder it constructed itself, so all of them
pass on a site that never calls it — and the call has now moved between
files during a restack, which is exactly how it would be lost. The new
CompositionTests case resolves CrawlerOptions out of the site's own graph
with Crawler:Enabled=false and a configured seed, and fails when the line
is commented out. It also pins the half configuration may not do: a seed
that arrives this way is never an operator seed, because §7.2's exemption
is a claim a human makes about one address and an environment variable
copied between deployments is not that human.

Verified after the migration rename in the link below this one: all twelve
migrations are still embedded in MUI.Catalog.dll and 0012_slug_history
sorts last. This branch's new EnsureTheMigrationsWereFound target and the
runner's empty-set refusal both still hold.
HarryCordewener pushed a commit that referenced this pull request Aug 15, 2026
Eighth link: main → #27#32#31#21#28#30#26#29.

THE ANTI-FORGERY REORDER, WHICH WOULD HAVE VANISHED. This branch moves
UseAntiforgery after UseAuthentication, and it was written against the
Program.cs #32 deleted — so it merged clean and applied to nothing. The
rule is reapplied inside the composition, and it is now a named method
rather than three lines: UseMuiAntiforgeryAfterAuthentication.

A method because the test that proves the rule matters built its own copy
of the three lines. OwnerEndpointTests' harness said "Program's order" in a
comment and restated it in code, so it asserted its own ordering and would
have gone on passing with the site's reversed — which is the failure it
exists to catch, one level up. The harness now calls the site's own method
for the correct order and hand-builds only the wrong one, because there is
no other way to build a thing that is not supposed to exist. Verified:
swapping the two lines inside the method fails five of this branch's tests
by name, and it failed none of them before.

The ordering is worth that trouble. An anti-forgery token issued to a
signed-in operator carries their identity; validating before authentication
compares it against nobody and every owner's form post is refused as
forged, while every public page — all GET — goes on working perfectly.

PlainText — #27 factored the plain label into PlainText.Label, this branch
added a third word to it at one of the four call sites. The three-way
choice moved into Label: an owner's answer is owner-declared on the listing,
on the game page and in the archive alike, and "declared" alone would put
what an operator typed into our form and what their config file emits under
one word. That was this branch's own argument for making the distinction at
all; it just has one place to live now.

app.css is two separate blocks, both kept. InMemoryFieldStore grew #30's
LastChangedAtAsync, answering null — which is true of a store whose
RecordChangeAsync already discards what it is handed.

DeclaredOf's empty-value filter survived intact, and the ladder conflict
its author expected with #28 did not happen: #28 never touched
NpgsqlGameQueries. A cleared owner row is still filtered before the ladder
rather than after it, so it cannot win its group and silence the MSSP value
underneath.
HarryCordewener pushed a commit that referenced this pull request Aug 15, 2026
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 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 down the chain: a count read
off the connect screen is a count we measured.
@HarryCordewener
HarryCordewener merged commit 433d40e into main Aug 15, 2026
3 checks passed
@HarryCordewener
HarryCordewener deleted the feat/claim-wiring 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.

1 participant