House rules: numbers through one formatter, no made-up server errors, and the rules fail the build
Build / Build (push) Successful in 33s

- Every number shown on a page goes through NumbersExtensions: counts,
  percentages, the nerd readouts and the values written into a style.
- A failure the client builds itself (offline, timeout, retries run out, an
  answer it can't read) is a 400 that names its kind; only a server's own 5XX
  passes through.
- Login, sign-up, recovery and password confirmations leave autocomplete to
  the browser, so it can offer the saved login.
- The Daisy components start with empty lists and attributes, and a
  decorative image still renders alt="".
- Arasaka.HouseRules.Analyzers checks all of this in every build, together
  with PrivaPub.ClientModels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W4YtEJrgxG12Pc39Fai8Bd
This commit is contained in:
thepraandClaude Opus 5.5 committed 2026-11-10 01:26:39 +01:00
1 parent 697b2c8897
commit 3827d903a3
37 files changed
+165 -111

No files matched your search

+25 -18
View File
@@ -849,22 +849,24 @@ in both builds, kept out of the precache; see **Web Push**): `push` shows the no
the app on that persona's notifications, `pushsubscriptionchange` subscribes again with the same key. They control no
page and have no fetch handler.
## Development rules
## House rules
These rules are adapted from the owner's b2b.next project. They are enforced by review: the build doesn't check them,
so follow them in every change.
The owner's rules for every .NET project (`~/RiderProjects/HouseRules/RULES.md`, repo `Arasaka/HouseRules`) apply
here and win over anything else in this file. `Arasaka.HouseRules.Analyzers` (`Directory.Build.props`) reports the
ones with an HR id as errors in every build and in Rider, here and in `PrivaPub.ClientModels`; the rest are reviewed.
| Area | Rule |
|---|---|
| All | `var` everywhere; uninitialised locals are `var x = default(T);` |
| All | Primary constructors for DI (`class Foo(IBar bar)`), not constructor + fields |
| All | Every async method takes a `CancellationToken` as its last parameter |
| All | `var` everywhere; uninitialised locals are `var x = default(T);` (HR0001) |
| All | Primary constructors for DI (`class Foo(IBar bar)`), not constructor + fields (HR0004) |
| All | Every async method takes a `CancellationToken` as its last parameter (HR0003) |
| All | Early return on error: guard, handle, `return`; never if/else for success vs failure |
| Models / state | Lists and arrays `= []`, never null; no `= string.Empty` / `""` defaults; explicit enum defaults |
| Components | No `@{ }` blocks in markup: locals become `@code` members |
| Components | `[Inject]` in `@code`, never `@inject` |
| Components | Disable controls with `IsDefaultDisabled` (plus local conditions), never bare offline state |
| Components | Every text/number input declares `autocomplete` |
| Models / state | Lists and arrays `= []`, never null (HR0006); no `= string.Empty` / `""` defaults (HR0005); explicit enum defaults |
| Components | No `@{ }` blocks in markup: locals become `@code` members (HR0102) |
| Components | `[Inject]` in `@code`, never `@inject` (HR0101) |
| Components | Disable controls with `IsDefaultDisabled` (plus local conditions), never bare offline state (HR0104) |
| Components | Every text/number input declares `autocomplete`, except credentials (HR0103, HR0105) |
| Services | A failure the client builds itself is a 400 that names its kind in `ErrorCode` (`FailureCodes`), never a 5XX; only a server's own 5XX passes through (HR0007) |
| Services | `Task<WebResult>` for mutations, `Task<List<T>>` (`[]` on error) for lists, `Task<T>` with a default for values |
| Localization | Every user-facing string through `Localizer`, its key in **both** `AllStrings.resx` and `AllStrings.it.resx` |
@@ -881,7 +883,7 @@ List<FeedPost> posts = []; // DO
@(posts?.Count ?? 0) // DON'T
```
`= ""` is allowed only as a documented exception, with the reason in a comment (e.g. a decorative image's `Alt`).
A string that must render as `""` coalesces in the markup instead: a decorative image is `alt="@(Alt ?? "")"`.
**Pages that load data.** Pages fetch in `LoadData` and keep lifecycle concerns in the lifecycle method:
@@ -930,22 +932,27 @@ join a list into one string with `<br/>` or `\n`.
**Autocomplete values:**
- `autocomplete="off"` for search, filter and free text;
- `new-password` when setting a password;
- `new-password` when setting a password; `off` on a password field is wrong, since it doesn't stop saved passwords
(HR0105);
- `one-time-code` for OTP (DOtp sets it);
- `username` and `current-password` on the login form, so password managers fill it;
- omitted on login credentials, contact e-mails and password confirmations, so the browser can help: the files are
listed in `HouseRulesAutocompleteOmit` (Login, Join, Setup, PasswordRecovery, AccountEditor, TotpEditor, Groups,
ActDialog, BackupsPanel);
- inside `Components/Daisy`, attributes pass through from the caller.
**Localization:**
- Keys are English source strings. Grep both resx files for an existing key before adding one; match keys exactly.
- Never add a key that differs from an existing one only by case ("users" next to "Users"): resource lookups collide
on them and one of the two silently shows the other's text. Reuse the existing key.
- Use `Localizer[key, args]`, never `string.Format(Localizer[key], …)`. Don't write `.Value`.
- Use `Localizer[key, args]`, never `string.Format(Localizer[key], …)`. Don't write `.Value` (HR0009).
- Enum members shown through `Localizer[value.ToString()]` are keys too: when you add a member, add its key.
- Language names in the language picker stay in their own language.
**Formatting.** Date formats come from `SUtility.dateTimeFormat` / `timeFormat` / `dateTimeFormatExt`. Relative times
and sizes come from `GetPassedTime` / `GetFileSize` (`Extensions/GenericExtensions.cs`). No inline format strings for
display.
**Formatting.** Numbers go through `NumbersExtensions` (`Extensions/NumbersExtensions.cs`, HR0011): `ToDefaultCount`,
`Compact` (999, 1.2k), `ToDefaultPercent`, `ToWholePercent`, `ToSignedPercent`, `ToReadout` (nerd tips), `ToFixed`,
and `ToCssNumber` for values written into a style. Date formats come from `SUtility.dateTimeFormat` / `timeFormat` /
`dateTimeFormatExt`. Relative times and sizes come from `GetPassedTime` / `GetFileSize`
(`Extensions/GenericExtensions.cs`). No inline format strings for display.
**JSON.** Register every `/clientapi` or localStorage type in `ClientJsonContext.cs` (camelCase: `theme.js` reads
`pageSettings` straight from localStorage), every Mastodon API type in `MastodonJsonContext.cs` (snake_case).