Non-Custodial Partner Program — partner statistics behind the normal login - #1262
Non-Custodial Partner Program — partner statistics behind the normal login#1262joshuakrueger-dfx wants to merge 20 commits into
Conversation
|
Measured locally on Coverage on the files this PR touchesAggregate over the PR's own files: 82.36 % statements, 67.41 % branches, 78.87 % functions, 82.15 % lines.
Two of these are worth more than their number. The dashboard's entire error path is untested. Not "thinly covered" — untested. The chart formatters are the uncovered part of the charts. Lines 78-95 and 87-105 are the ApexCharts
Handbook coverage is zero, and the gate cannot fire
The role belongs in the SDK
Hard requirements
Verified and good
|
|
Thank you — the coverage read was accurate to the line. Measured it again independently before Head is now 100 % coverage on every touched file — doneEvery file this PR touches is at 100 % statements, branches, functions and lines. Suite went The error path first, as you asked. It is covered with the failure produced, not simulated: a The chart formatters are called with real values and their return asserted, so the Mutation probes rather than deletion probes — eleven, each applied with an asserted hit count of
One line is covered but not mutation-proven, and I would rather say so than let the percentage Reaching 100 % branches needed four unreachable spots removed, in a separate, behaviour-neutral SDK enum member first — split outDFXswiss/packages#205 adds Checked the consumers rather than assuming additivity: nothing maps the enum exhaustively — This is the one thing that cannot close from this branch: the member needs a published Worth adding as supporting evidence for your point: the string allow-list is the only role Handbook — and why the gate could not fireYou were right that the gate cannot trigger, and the reason turned out to be one layer below the On the filter itself: it covers exactly the surfaces the handbook is built from — screenshots, The baseline itself I cannot produce from here, and I am carrying it as an open item rather than If you would rather have the entry and a fixture baseline now and the real one later, say so and Self-containedTwo of the four are closed in this PR (coverage, and the unreachable-branch cleanup it required). |
0d2b1d0 to
e7c022b
Compare
Reads the aggregated partner statistics endpoints and renders them as a white-label dashboard: KPI tiles, volume and transaction time series, breakdowns by asset, currency, blockchain and payment method, and the referral position. A fixture mode carries demo data so the whole surface runs without an API or a token. Withheld values are shown as a placeholder rather than a zero, and a suppressed day is marked in the charts instead of drawing a gap — a gap reads as "no business that day", which is not what the API means by null. Built as its own entry point next to the existing app, so the shared build is untouched.
The API moved its enum values to PascalCase, so the dashboard was sending `day` where the timeline endpoint now only accepts `Day` — every real timeline call would have returned 400 — and it read `buy` from the asset breakdown where the API now writes `Buy`, so rows were sorted into nothing. Only fixture mode hid it. The values are defined once and derived from there; the JSON field names stay lowercase, since the API maps the enum onto them separately. The period buttons were wired but had nothing to act on: the demo data ignored the requested range and always returned the same month, which is why "365 days" showed thirty buckets. It now honours range and granularity, keeps the withheld buckets and the genuine zero day, and still marks the edge buckets as partial.
The partner dashboard carried its texts hardcoded in German while the rest of the repo runs on English keys with translation files. The first partner is an international product, and more are planned, so the surface was unusable as it stood. It now uses the same mechanism as everything else: English keys, the German wording moved into the existing translation file unchanged, and number, date and currency formats following the selected language rather than staying German. French and Italian fall back to English until their keys are filled in — text work, no code.
Collects the dashboard work that was sitting unsaved in a scratchpad directory: the wallet login gate and its auth hook, the per-partner brand registry with a fallback for partners without a logo, light and dark themes with a switcher, German and English with a switcher, the even-spaced timeline axis, and the table that starts scrolling past twenty rows. Committed as one unit to get it under version control. It is not yet reviewed as a whole.
The API no longer withholds anything, so the dashboard stops drawing withholding. Gone are the hatched bands, the interpolation that bridged withheld days, the dash placeholders and the threshold that produced them. A day with a single transaction is now a low point on the curve, and a day with none sits on the zero line — the shape falls, it does not break. That is enforced by the type rather than by a test: the series carries `[number, number]`, so a gap cannot be expressed at all. Reintroducing one no longer compiles. The completion block went with it. It rendered `statistic.completion`, which the API does not send and never did — against the real endpoint it would have drawn from `undefined`. It belongs to a separate change that is deliberately parked, so it leaves with its types, fixtures and translations rather than waiting as scenery. Edge buckets keep their partial marker. That one is still true: the first and last bucket of a period really can extend past the period's ends.
The API has always returned two figures the dashboard never showed: how many of a wallet's registered users ever traded, and the lifetime volume behind them. The first is the number a partner actually wants — installs are easy, a first trade is not, and the ratio between them says whether their users find the way there at all. It is shown as a conversion rate beside the raw counts. A partner with no installs yet has no ratio rather than a misleading zero percent, and a test pins that the page never renders NaN there. Adding them exposed a problem that was already present. Eight tiles now sat in one six-column grid, leaving a ragged row of two — but the real fault was that they were two different kinds of number wearing the same clothes. Five move when the period selector moves; registered users, trading users and lifetime volume are lifetime counters the API computes without any date range at all. Someone switching from thirty days to a year watched five figures change and three sit still, and had every reason to read that as a bug. They are two labelled groups now: the period figures under the selector that governs them, the all-time figures below and visually subordinate, being context rather than headline. Five and three fill their rows exactly, so the ragged edge goes with it. A test pins which tile belongs to which group by DOM containment, so moving a period metric into the all-time block fails rather than quietly restoring the confusion.
The dashboard had its own build target, its own login screen and its own token handling. That login asked for a wallet signature, which meant anyone wanting to look at a chart needed the partner's wallet key — their master secret, handed to whoever works in operations. It is a screen in the app now, reached from the burger menu, gated by a role the way the support dashboard is, and it reads the two endpoints with the ordinary session token. The separate entry point, the login screen, the partner auth hook, the white-label brand registry and the standalone build and start scripts are gone with it. Branding per partner goes too. Inside the DFX app behind the DFX login, a partner logo would claim something the page no longer is.
The move into the app dropped the language and theme switchers on the argument that the app provides them. It provides language only, and only on the settings page — a partner mid-analysis would have to leave the page to change it. Dark mode it does not provide at all, so the dashboard simply lost it, though its token system had never gone away; only the setter had. Both sit in the header again as one quiet group. Language drives the app's own language state rather than a second copy beside it, so the rest of the app follows along.
The header carried the program name twice: a small spaced-out line above the heading and the heading itself, word for word the same. A kicker earns its place by saying something the heading does not; this one only repeated it. The bold line stays and is now the page's only title, with a test that fails if a second heading with the same text appears.
The language switcher offers all four languages the app ships, but the partner namespace existed only in German — French and Italian users got the English source strings back. Both now carry the full set, matching the German key for key. Three values stay as they are: Blockchains and Date read the same in French, and Referral is left untranslated in Italian as it is in German.
"Non-Custodial Partner Program" wrapped onto two lines in the burger menu while every other entry sat on one. Menu and heading now have their own keys, so the list stays even at "NC Partner Program" and the page still announces itself in full.
The guard's decision was covered: inverting it failed six tests. Its wiring was not. Removing the guard call from the screen outright left all eighty-three green, and so did replacing the menu's role condition with a constant true — the whole access control of the page could be deleted without a red test. Two things caused that. The only test rendering the real screen mocked the guard hook to a no-op, and the menu test rendered a hand-written copy of the condition instead of the real navigation, which no test in the repository imported at all. Both are gone. The screen test lets the real guard run, the menu test renders the real navigation, and both mutations above now fail.
A shared mock helper sat in src/__tests__/helpers/. Create React App treats every file under __tests__ as a suite, so a file with no tests in it failed the whole run — while the test count still read 863 of 863 passing, because a suite that never runs contributes no tests to count. It lives in src/test-helpers/ now, alongside no test matcher. Every other test file in this repository sits flat in __tests__ for exactly this reason.
The shared mock helper had to stay under __tests__, because tsconfig.build.json excludes that directory and the helper uses jest globals — moving it out put it in the production compile, where the namespace does not exist. But Create React App treats every file under __tests__ as a suite, so the helper failed the run while the test count still read 863 of 863 passing: a suite that never runs contributes no tests to count. Jest now ignores the helpers directory. `testMatch` would have been the tidier key and is listed as supported, but react-app-rewired drops it — asking Jest directly which files it collects showed the helper still in the list of ninety-two. `testPathIgnorePatterns` is the key this repository already uses, and it takes: ninety-one now.
The test asked only whether a value was a non-empty string. A key left standing as its English source text passed, and French and Italian were read by no test at all — the two files carrying fifty-five keys each could lose one without a red line anywhere. All three languages are now compared key for key against each other, and a value identical to its English source fails unless it is on a documented list: Blockchains and Date read the same in French, Blockchains and Swap in German, Referral in Italian. Each of those was checked against how the rest of the repository already treats the word.
`useUserRoleGuard`, `NavigationMenu` and the fixture's `makeBucket` are all module-private, and every call site passes the argument explicitly. The defaults were unreachable, so no test could ever cover them and the branch coverage of the three files could not reach 100 percent. Behaviour is unchanged: the callers already pass exactly the values the defaults declared. The same applies to `if (buckets.length > 0)` in the fixture, where `bucketCount = Math.max(1, ...)` guarantees at least one bucket.
The dashboard's error path had no test at all: the catch block that derives the message and clears the data, the ErrorState render, the error boundary. The component even carried a data-testid nobody used. Covered now, with the failure produced rather than simulated: a rejecting getPartnerStatistic, an Error without a message, a non-Error rejection, and a Retry that provably triggers a second load. The ApexCharts formatters are called with real values and their return is asserted, so the '1k' rule, the localised tooltip date and the NaN branch are pinned rather than merely executed. Also covers the pre-existing guards in guard.hook.ts, the route table in App.tsx and the menu in navigation.tsx, which the PR touches and which carried inherited gaps.
…data entry The partner dashboard had no handbook coverage: no Playwright baseline, no scripts/handbook/metadata.json entry, and because the PR touched neither e2e/screenshots/** nor scripts/handbook/**, handbook-check.yaml never triggered for it — the gate could not fail, so its absence was not visible. Adds e2e/partner-dashboard.spec.ts with four product states (filled light, filled dark, error, empty) and the four committed baselines, plus the metadata entry that gives the handbook block its title and description. The screenshots need neither a DEV role grant nor live API data: the role guard is client-side (jwtDecode without signature check), so a synthetic token carries the role, and both statistic endpoints are mocked. The clock is frozen at 2026-06-30T12:00:00Z because the period controls and chart axes derive their ranges from the current time. Viewport is fitted to content per state instead of using a fixed tall viewport with fullPage: on short pages the stitched capture repeated the sticky header at the bottom of the image, so the error and empty baselines showed a state the product never renders.
…e the bars legible
Seven defects, all reachable in production and five of them visible in the
committed handbook baselines:
- The referral hero printed the currency twice ("285.40 EUR EUR"): formatAmount
already appends it, and the component appended it again.
- load() wrote every response unconditionally, so switching period or granularity
mid-request let the slower answer win. Now guarded by a request id checked before
each setter, matching support-dashboard-overview and safe.hook.
- translate sat in load's dependency list, so every language switch refetched both
endpoints and blanked the screen behind the skeleton - against a rate budget of
120 requests per hour shared by all staff of one wallet. The error is now stored
as a marker and translated at render time.
- The aggregated tail row was appended after sorting, so a large tail took the top
of the bar scale and shrank every real row; its label was hard-coded German in a
util that never saw i18n.
- The screen did not pass textStart, so the layout centred every label above its
left-aligned value.
- The bar palette spread across all eight shades: the lowest rows reached 1.05:1
against the track, so a two-row list showed one bar. The palette now cycles four
shades that clear 3:1 in both themes; the referral paid segment likewise.
- useUserRoleGuard kept two unreachable default arguments, which held guard.hook.ts
at 96.15 % branches. Removed; the file is now at 100 % on all four metrics.
Tests pin what was missing rather than what already passed: the route path, the
KPI field mapping, the query parameters, the sell and swap series, the i18n
namespace, the colour steps and the bar widths. Each was verified by landing the
mutation it is meant to catch. The e2e spec pins the timezone, asserts the first
axis tick exactly, fails loudly on an unmocked statistic endpoint, and covers the
negative-role redirect; the four baselines carry invented figures instead of the
fixture's production-checked partner numbers.
e7c022b to
2c557d6
Compare
The full-stack route-coverage gate now really runs since the full-run flag was repaired, and it rightly flags the new route: no registry entry, no suite that opens it. The new suite proves the negative case in the real stack - a plain user is redirected away by the frontend guard. The positive case needs the NonCustodialWalletPartner role, which the API only grants once its side ships, so it stays with the mocked handbook spec and is named as not covered here.
|
@marassteiner please review This is a PR, not an issue: 77 files against |
Offen — nicht aus diesem Branch schließbar
@dfx.swiss/coremitUserRole.NON_CUSTODIAL_WALLET_PARTNERreleasen. Der Member liegtals PR: feat(core): add the non-custodial wallet partner user role packages#205 (eine Zeile, Wert zeichengleich zu
src/shared/auth/user-role.enum.ts:15in DFXswiss/api#4587). Aus diesem Branch nichtmöglich: die Version entsteht über
develop→maininDFXswiss/packagesundnpm publish; ein Branch hier kann keine Paketversion veröffentlichen. Wer: wer inDFXswiss/packagesmergen und releasen darf. Danach hier@dfx.swiss/reactanheben undPARTNER_DASHBOARD_ROLESinsrc/hooks/guard.hook.tsvon der String-Allowlist auf denEnum-Member umstellen — eine Stelle.
die Endpunkte weisen das Token ab. Aus diesem Branch nicht möglich: anderes Repo.
Wer: Reviewer und Deployer von api#4587.
scripts/handbook/metadata.json-Eintrag für/partner/dashboard— geliefert. Die frühere Begründung („braucht die Rolle auf einerlaufenden Umgebung") hielt nicht: der Rollen-Guard ist clientseitig (
jwtDecodeohneSignaturprüfung), die App übernimmt einen Token aus
?session=, und beide Statistik-Endpunktewerden per
page.routebedient — dieselbe Technik wie ine2e/support-issue-receiver-iban.spec.tsund
e2e/compliance-kyc-stats.spec.ts.e2e/partner-dashboard.spec.tsdeckt vierProduktzustände ab (gefüllt hell, gefüllt dunkel, Fehler, leer); der
metadata.json-Eintragliegt zusammen mit den Bildern, also ohne
orphan-Warnung. Damit greift auchhandbook-check.yamlerstmals auf diesem PR.Why
Not symptom-driven: no incident triggered this. Wallet partners integrating DFX have no way to
see how their integration performs. DFXswiss/api#4587 adds the two endpoints that answer it; this is
the page that shows them.
Scale: the first of several wallet partners; the reference partner is in the six-figure range of
registered users. No exact figure here — this body is public, and the repository's own
„production-checked" reference set disagrees with the number this line carried before.
Smaller fix considered: hand partners the raw JSON and let them build their own view. Rejected —
every partner would rebuild the same charts, and the numbers only become useful once someone can
see a trend rather than a payload.
What
A screen at
/partner/dashboard, reached from the burger menu, gated by a role: volume andtransaction counts over time, breakdowns by asset, fiat currency, blockchain and payment method,
the referral position, and the all-time figures behind them.
Nothing partner-specific is coded anywhere. Any wallet gets its own numbers through the same page.
It lives in the app, not beside it
It began as a separate build target with its own entry point, its own login screen and its own token
handling. That login asked for a wallet signature — so anyone who wanted to look at a chart
needed the partner's wallet key, their master secret, in a browser.
It is a screen now. The role guard follows the pattern of the support dashboard
(
useSupportDashboardGuard→usePartnerDashboardGuard), the API calls go through the app's ownuseGuardedApiwith the ordinary session token, and the standalone entry point, login screen, authhook, brand registry and build scripts are gone. Per-partner branding went with them: inside the DFX
app behind the DFX login, a partner logo would claim something the page no longer is.
The frontend's role list is deliberately narrower than the API's. The endpoints accept
ClientCompany,KycClientCompany,NonCustodialWalletPartner,AdminandSuperAdmin; the pageadmits only
NonCustodialWalletPartner. An admin would see their own wallet, since there is no"show me partner X" parameter — a menu entry leading to an empty page or a 403 is worse than no
entry. This is a decision, not an oversight; please do not widen it without deciding the same
question again.
@dfx.swiss/coredoes not yet carryNonCustodialWalletPartner, so the guard still compares theliteral string. The SDK member is now split out as DFXswiss/packages#205 and this side switches to
the enum once that version ships — see the open list at the top. It is also the only role comparison
in
srcthat does not go throughUserRole; every other one (labels.ts:173,navigation.tsx:237,navigation.tsx:246,support-dashboard-issue.screen.tsx:34) already does,which is why the string is a temporary state and not a second pattern.
Every day is shown
The API used to withhold days below a k-anonymity threshold. It no longer does, and neither does the
page: a day with a single transaction is a low point on the curve, a day with none sits on the zero
line. There is no gap, and there cannot be one —
timelineSeriesreturns[number, number], so ahole is not representable and reintroducing one fails to compile.
Breakdown rows with no activity at all disappear rather than showing a permanent zero. The API
cannot emit such a row today (
GROUP BYonly yields values that occur), so the filter is asecond line rather than a fix — it stops the page trusting an invariant it does not own.
Coverage
Every file this PR touches is at 100 % statements, branches, functions and lines, measured on
head
0d2b1d05with--collectCoverageFromrestricted to the PR's own files. This was not truewhen this section was first written:
src/hooks/guard.hook.tssat at 100 / 96.15 / 100 / 100(50 of 52 branches, uncovered line 66), because two unreachable default arguments of
useUserRoleGuardhad never been removed although this body and the commit message ofd89d160bsaid they had. Both are gone now and the file measures 100 on all four metrics.
The one gap that mattered was the error path:
getPartnerStatisticrejecting had never beenexercised by any test,
error-state.tsxwas at 0 %, and the component carried adata-testid="dashboard-error"placed for tests that were never written. It is covered now with thefailure produced rather than simulated — a rejecting call, an
Errorwithout a message, anon-
Errorrejection, and a Retry that provably triggers a second load.The ApexCharts formatters — the
1000 → "1k"rule, the tooltip date viatoLocaleDateString, theNumber.isNaN → ABSENT_LABELbranch — are called with real values and their return is asserted, sothey are pinned rather than merely executed.
Reaching 100 % branches required removing four default arguments no caller can reach plus one
if (buckets.length > 0)thatMath.max(1, …)makes unreachable.d89d160bremoved two of them(
NavigationMenu, the fixture'smakeBucket); the two onuseUserRoleGuardfollowed only in0d2b1d05— the earlier claim that all of them were done was wrong. Each is module-private withevery call site passing the value explicitly, so the change is behaviour-neutral.
src/App.tsxandsrc/components/navigation.tsxare included too — the inherited gaps in the route table and themenu, not only the lines this PR added.
Verification
0d2b1d05(104 / 1001 before the fix round, 94 / 883 beforethe review round),
lintempty under therepo's own
--max-warnings 0,tsc -p tsconfig.build.json --noEmitexit 0.the diff shown, and reverted afterwards. Eleven in total; the ones that matter:
value === allowed→!==in the partner role check — 11 of 1001 tests fail across4 suites, and the suite still runs (100 suites stay green), so it is a semantic failure and
not a crash.
val >= 1000→>= 100in the volume formatter — 1 test fails.Number.isNaNinverted in both chart tooltips — 1 test each.catchsetssetError(null)— 5 tests fail.reloadToken— 1 test fails.MARKETINGremoved fromSUPPORT_DASHBOARD_ROLES— 1 test fails.App.tsx— 4 tests fail.navigation.tsx— 2 tests fail.setStatistic(null)in thecatchis executed by the tests, but removing it keeps the suite green — the UI is gated on
error, noton an empty statistic. The line is defensive, and the test suite does not prove it is needed.
not against the types: enum casing (
Day,Buy), the lowercase JSON keys (volume.buy),nullability, and the fields the sample does not cover are named rather than assumed.
menu entry, and a day carrying a single transaction of 60 CHF rendered as a value.
For reviewers
NonCustodialWalletPartnerrole the guardadmits nobody and the endpoints reject the token.
develop(merge commit): the handbook tooling from feat(handbook): add static handbook served at handbook.app.dfx.swiss #1265/fix(handbook): discover documents instead of listing them, and fix slug collisions #1268 did notexist on the branch when the review was written, which is why the path-filter question below
could not be answered from it.
handbook-check.yamlnever triggered forthis PR, because none of its files touched a filtered surface — the consequence of the missing
baseline, not a fault in the filter. With the baselines and the
metadata.jsonentry in the diffthe job now runs and passes on
0d2b1d05. The filter covers exactly the surfaces the handbook is built from(screenshots, docs, tokens, logo);
src/**is deliberately not among them because the buildreads no application code. One gap is worth naming: the dashboard's own design tokens live in
src/partner-dashboard/styles/tokens.css, while the filter knows onlytailwind.config.jsas atoken source. Harmless while the handbook builds no token page from that file — but that is where
the filter will be too narrow next.
--bg,--text,--primary,--surface— sit on.theme-light/.theme-dark, not on:root; what:rootcarries is namespaced primitives (--navy-*,--n-*,--fs-*) plus six shared ones(
--error,--info,--success,--warning,--font-sans,--font-mono). Nothing outside thedashboard defines any of them — the app has exactly one other stylesheet. Scoping them to the
dashboard element would break chart colours:
readThemeCssVarmeasures through a probe elementattached to the document root carrying only the theme class, which is how the charts get their
palette on a theme switch before the DOM class has caught up.
de,fr,it) viadescribe.each, checks key-setequality in both directions and rejects any value left as its English source outside a documented
six-entry allow-list. The earlier sentence here — „no test reads those two files" — was already
wrong when it was written: that check landed in
9a6652eb, three days before.Fix round on
0d2b1d05— what the review passes foundSeven defects, five of them visible in the handbook baselines that came with the previous commit:
formatAmountalready appends it.load()wrote every response unconditionally, so switching period or granularity mid-request letthe slower answer win. Now guarded by a request id checked before each setter, matching
support-dashboard-overview.screen.tsxandsafe.hook.ts.translatesat inload's dependency list, so every language switch refetched both endpoints andblanked the screen — against a budget of 120 requests/hour shared by all staff of one wallet.
every real row; its label was hard-coded German inside a util that never saw i18n.
textStart, so the layout centred every label above its left-aligned value.track, so a two-row list showed one bar. It now cycles four shades that clear 3 : 1 in both themes
(light 4.54 / 3.68 / 3.27 / 3.08, dark 3.03 / 3.78 / 4.74 / 5.87); the referral paid segment went
from 1.11 : 1 to ≈ 6.2 : 1.
useUserRoleGuard's two unreachable defaults — see the coverage section above.New tests pin what was missing rather than what already passed, and each was verified by landing the
mutation it is meant to catch: the route path (
partner/dashboard→partner/dashboardsleft1001 of 1001 tests green before), the KPI field mapping, the query parameters, the sell and swap
series, the i18n namespace, the colour steps, the bar widths, and
textStart. The e2e spec pinstimezoneId: 'UTC'(the axis labels go throughtoLocaleDateString, so the baselines weremachine-dependent), asserts the first axis tick exactly, fails loudly on an unmocked statistic
endpoint, and covers the negative-role redirect.
The four baselines carry invented figures. They previously showed the values from
src/partner-dashboard/fixtures/partner-statistic.fixture.ts, which describes itself as„production-checked Cake values (not scaled)" — those numbers do not belong on a published handbook
page. The fixture itself still carries them; that is an open owner decision, not something this
branch settled.
Final pass (0d2b1d0):
Coherent: one subject — the partner dashboard behind the normal login: screen, guard, view,
translations, tests, and the handbook surface that documents it. The fix round changes only files
this PR already owned, plus
guard.hook.ts, which it already touched.Nothing extra: the smallest change that answers the request is the dashboard itself; every
addition beyond it is traceable to a stated requirement — the coverage the reviewer demanded, the
handbook baseline he demanded, and the seven defects the review passes found with a named failing
scenario each. Deliberately not built: no standalone partner app (removed in
f5631770), nosuppression/k-anonymity handling (the api revision this consumes does not send it), and the fixture
is left untouched pending the owner decision above.
Sources closed: Issue — none linked. Reviewer (
issues/1262/comments, one comment of2026-08-07;
pulls/1262/reviewsandpulls/1262/commentsare both empty, and the reviewer has nocommits on this branch): 100 % coverage on every touched file — met, and the gap that was hidden
behind a wrong claim is named above; handbook baseline plus metadata entry, and the path-filter
question answered — met, the job runs and passes; SDK enum member first, then consume — open,
DFXswiss/packages#205is a draft and the guard still compares the string, listed as a checkbox atthe top; self-contained — the two remaining items are in other repositories, which is why they are
checkboxes with an owner rather than a follow-up PR. Own plan and commit messages: the claims of
d89d160band of this body were checked against the diff, and three were wrong — each is correctedin place above rather than quietly rewritten.