House rules: the build checks them, and every refusal from the server answers 400, 403 or 404
Build / Desktop, server and tools (push) Successful in 1m8s
Build / Desktop, server and tools (push) Successful in 1m8s
The server no longer answers 409, 413 or 429: a busy probe, a share or upload that is too large and a request over the limit now answer 400 with the same reason as before, and still say when to try again. The app reads the reason, so nothing changes for it; versions up to 0.15.4 report a limited speed test as unmeasured until they update. The app's commands can now be cancelled all the way down, and the house rules are written into PATTERNS.md and checked on every build, Android included. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W4YtEJrgxG12Pc39Fai8Bd
This commit is contained in:
1 parent
5b2210a8a7
commit
d408edea98
28 files changed
+235
-145
No files matched your search
+49
-5
@@ -1,7 +1,7 @@
|
||||
# PATTERNS.md — MeWeb house rulings
|
||||
|
||||
The canonical standards doc. **On any conflict about how to write code, a ruling here wins**; cite it in review as
|
||||
"PATTERNS #n". `AGENT.md` is the operational contract, `CLAUDE.md` the quick reference and pitfalls.
|
||||
The canonical standards doc. **On any conflict about how to write code, a ruling here wins**, except over the owner's
|
||||
house rules (next section), which win over everything; cite a ruling in review as "PATTERNS #n". `AGENT.md` is the operational contract, `CLAUDE.md` the quick reference and pitfalls.
|
||||
|
||||
Numbering is shared with the house lineage (SimpleSchool, passolo, odysseus, zigzag.academy): **#42, #51, #86–#93
|
||||
mean the same thing in every repo**, and the text below follows passolo's wording wherever the subject is the same.
|
||||
@@ -10,6 +10,49 @@ only MeWeb needs start at **#120**.
|
||||
|
||||
Scope: `src/`, `tools/`. Out of scope: generated files (`obj/`, `*.g.cs`), third-party trees, the licence text.
|
||||
|
||||
## House rules
|
||||
|
||||
The owner's rules for every .NET project (`~/RiderProjects/HouseRules/RULES.md`, repo `Arasaka/HouseRules`) apply
|
||||
here. `Arasaka.HouseRules.Analyzers` (a `GlobalPackageReference` in `Directory.Packages.props`) reports the checkable
|
||||
ones as `HRxxxx` warnings, they are listed in `WarningsAsErrors`, and `tools/ci.sh` builds with `-warnaserror` too.
|
||||
MeWeb builds with none and carries no suppression. Applied to MeWeb:
|
||||
|
||||
- **Coding style:**
|
||||
- `var` for every local, `var x = default(T)` when there is no value yet (HR0001, #92).
|
||||
- `IOptionsMonitor<T>.CurrentValue` (HR0002, #87).
|
||||
- Primary constructors, using the parameters directly (HR0004, #1).
|
||||
- Work a constructor used to do becomes a field initializer through a static helper (`RegistryWriter.Reserve`).
|
||||
- A collection built from instance methods is a `field ??=` property (`MainViewModel.Cards`, `Shared`).
|
||||
- A `CancellationToken` last on every method that returns a task, passed down (HR0003, #88).
|
||||
- Relay commands take one.
|
||||
- Where an API takes none, the wait takes it (`SetTextAsync(link).WaitAsync(ct)`).
|
||||
- Best-effort and fire-and-forget work passes `CancellationToken.None` on purpose.
|
||||
- **DTOs:**
|
||||
- `View*` responses, `*Request` create/update requests, `*Form` other input (HR0010 over `MeWeb.Shared`, #34).
|
||||
- `Show*` booleans and `*Formatted` strings for display (#44).
|
||||
- Lists are `= []` (HR0006, #75), and strings have no `= string.Empty` default (HR0005, #89).
|
||||
- **Server:**
|
||||
- Routes live only in `Endpoints` (#9).
|
||||
- A service that can refuse returns `WebResult<T>` (`MeWeb.Shared`). `Invalidate(reason)` answers 400; 403 and
|
||||
404 are passed explicitly. The endpoint answers through `Answer(context, result, type)`: the data on success,
|
||||
otherwise a `ViewRefusal` with its `Reason`, plus `Retry-After` when `RetryAfterSeconds` is set.
|
||||
- Services take the caller's address from the endpoint, never `IHttpContextAccessor` (HR0008, #86).
|
||||
- **Status codes** (owner, 2026-11-10):
|
||||
- 400 for every refusal: bad ports, shape, size, busy, too many.
|
||||
- 403 for a caller the probe will not serve (`not-public`, `other-address`) and a delete token that does not match.
|
||||
- 404 for an unknown ticket or submission.
|
||||
- Nothing answers 409, 413, 429 or any 5XX (HR0007), and MeWeb has no exception.
|
||||
- Clients read `ViewRefusal.Reason`, never the status. Installed apps up to 0.15.4 read 429 on the speed download,
|
||||
so until they update a limited speed test shows as "couldn't measure" rather than "limited".
|
||||
- **Client:** `ServerProbe` returns the same `WebResult<T>` and never throws (#27). A transport failure is the refusal
|
||||
`unreachable`, and a failure without a body is `http-<status>`.
|
||||
- **Numbers:** formatted only by `NumbersExtensions` (`MeWeb.Shared`) and `Texts` (`MeWeb.App`) (HR0011).
|
||||
- `NumbersExtensions` holds the invariant values that fill a localized template: `ToOneDecimal`, `ToWholeMegabytes`.
|
||||
- `Texts` holds what the App formats in the reader's culture: money and dates.
|
||||
- **JSON:** one source-generated context per assembly (#84).
|
||||
- **Not applicable:** the `.razor` rules (HR0101–HR0106, HR0201), since the App is Avalonia. Their counterparts are
|
||||
#27, #43, #44 and #93. `Localizer.Instance[key]` already returns a string (#91).
|
||||
|
||||
---
|
||||
|
||||
## A. Dependency injection and services
|
||||
@@ -26,7 +69,8 @@ Scope: `src/`, `tools/`. Out of scope: generated files (`obj/`, `*.g.cs`), third
|
||||
|
||||
## B. Web boundary
|
||||
|
||||
5. **`WebResult` envelope + early return** on every server endpoint that can refuse; the happy path stays flat.
|
||||
5. **`WebResult<T>` + early return** in every server service that can refuse, answered through `Answer`; the happy
|
||||
path stays flat (see House rules).
|
||||
6. **Pure or internal code may return tuples** (`(value, error)`), never across the web boundary.
|
||||
7. **`try/catch` placement: few at the top, few at the bottom, none in the middle.** Top: host startup, background
|
||||
loops. Bottom: sockets, DNS, UPnP/PCP, file IO, platform APIs — the IO edges where a check meets the network.
|
||||
@@ -53,7 +97,7 @@ Scope: `src/`, `tools/`. Out of scope: generated files (`obj/`, `*.g.cs`), third
|
||||
55. n/a — no model output.
|
||||
63. **An inventory that lives in a check rots; keep one map and assert it against reality.**
|
||||
64. **A sweep asserts what must hold and records what is a judgement call.**
|
||||
65. **Prefer a source lint to a runtime check when the bug is visible in the source** (`tools/lint.cs`).
|
||||
65. **Prefer a source lint to a runtime check when the bug is visible in the source** (the house-rules analyzer).
|
||||
66. **A check that has always failed is a decision nobody made** — fix it or delete it.
|
||||
67. **Two paths that resolve the same thing must degrade the same way** (the v4 and v6 echo, the system resolver and
|
||||
the server-side blocklist fallback).
|
||||
@@ -117,7 +161,7 @@ Scope: `src/`, `tools/`. Out of scope: generated files (`obj/`, `*.g.cs`), third
|
||||
32. **Subscription discipline:** every `+=` has its `-=` in the matching unload/dispose; timers are `PeriodicTimer`
|
||||
with a cancellation source cancelled on leave.
|
||||
33. n/a.
|
||||
34. **DTO naming by direction:** `View*` (response), `Upsert*` (create/update), `*Form` (any other input). Contracts
|
||||
34. **DTO naming by direction:** `View*` (response), `*Request` (create/update), `*Form` (any other input). Contracts
|
||||
grouped by area in `MeWeb.Shared/<Area>Contracts.cs`.
|
||||
35. n/a. 36. **Path safety** for anything served from disk by name. 37. **Layered gating: the UI hides; the server
|
||||
refuses.** 57. n/a.
|
||||
|
||||
Reference in new issue
Block a user