2026-10-15 01:14:44 +02:00
|
|
|
|
# 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.
|
|
|
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
|
A number whose subject does not exist here is kept as a one-line **n/a** so the numbering never drifts. Rulings
|
|
|
|
|
|
only MeWeb needs start at **#120**.
|
|
|
|
|
|
|
|
|
|
|
|
Scope: `src/`, `tools/`. Out of scope: generated files (`obj/`, `*.g.cs`), third-party trees, the licence text.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## A. Dependency injection and services
|
|
|
|
|
|
|
|
|
|
|
|
1. **Primary-constructor DI is the default.** `class ServerClient(IHttpClientFactory factory)`. Constructors with
|
|
|
|
|
|
logic stay explicit; a constructor that only assigns fields is a primary constructor. Applies to server services,
|
|
|
|
|
|
App services and view models alike.
|
|
|
|
|
|
2. **Registration only through extension methods**, one group per area: `AddMeWebApp` (App), `AddMeWebHosting`,
|
|
|
|
|
|
`AddMeWebData` (server). `Program.cs` reads as a list of calls.
|
|
|
|
|
|
3. **Lifetime discipline.** Server: stateless per-request → `AddScoped`; process-global state, caches, rate counters →
|
|
|
|
|
|
`AddSingleton`. App: services singleton, view models transient.
|
|
|
|
|
|
4. **Interface fan-in via factory re-registration** when one instance serves two contracts; never register it twice.
|
|
|
|
|
|
86. **A service never takes `IHttpContextAccessor`** — the endpoint reads the caller's address and passes it in.
|
|
|
|
|
|
|
|
|
|
|
|
## B. Web boundary
|
|
|
|
|
|
|
|
|
|
|
|
5. **`WebResult` envelope + early return** on every server endpoint that can refuse; the happy path stays flat.
|
|
|
|
|
|
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.
|
|
|
|
|
|
8. **Endpoints are minimal-API handlers**, static methods mapped in one `Map*` extension per area, thin bodies that
|
|
|
|
|
|
call a service. (passolo's controller shape: n/a.)
|
|
|
|
|
|
9. **Routes centralized in `src/MeWeb.Shared/Endpoints.cs`** (`GET_Name`, `GET_M_Name(args)`), shared by server and
|
|
|
|
|
|
App. No `"/api/` literal anywhere else.
|
|
|
|
|
|
10. **Prefer the `WebResult` path over inline `try/catch`** in endpoints.
|
|
|
|
|
|
11. **n/a — SSE.**
|
|
|
|
|
|
12. **Subprocesses: argv only, never a shell string.** `ProcessStartInfo.ArgumentList`; no `sh -c`, no `cmd /c`
|
|
|
|
|
|
(`tracepath`, `ip`, `netsh` are run this way, if at all).
|
|
|
|
|
|
13. **Long-running server work is registered and stoppable** (probe runs, the per-address limiter).
|
|
|
|
|
|
53. n/a — no request-bound streams.
|
|
|
|
|
|
|
|
|
|
|
|
## C. Verification (there is no test project)
|
|
|
|
|
|
|
|
|
|
|
|
14. **Pure static helpers for IO-free logic** (verdicts, address classification, PTR patterns). They are exercised
|
|
|
|
|
|
by the CLI mode and the screenshot tool, not by a test project.
|
|
|
|
|
|
15. **n/a — xUnit theories.** No test project by owner decision.
|
|
|
|
|
|
16. **Hand-written fakes, no mocking framework.**
|
|
|
|
|
|
17. **Server endpoints are verified by the curl ladder** against a scratch database (`/verify`).
|
|
|
|
|
|
54. **UI work is verified by running the app and looking at it** — desktop, Android device, or the headless
|
|
|
|
|
|
screenshots from `tools/Screenshots` (evidence E2), a real device for E3. A compile is not a UI verification.
|
|
|
|
|
|
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`).
|
|
|
|
|
|
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).
|
|
|
|
|
|
68. **Seed data before judging a surface** — the screenshot tool runs every language and two sizes.
|
|
|
|
|
|
69. n/a. 70. **Intermittent failures are usually an assertion on something transient** (network timing): fix the
|
|
|
|
|
|
assertion, not the retry count. 71. **A filter check asserts both directions.** 72. **Verify tooling against
|
|
|
|
|
|
every case it claims to cover.** 73. n/a — CSS selectors (corollary: `AutomationId`s are unique). 79. n/a.
|
|
|
|
|
|
80. n/a. 81. **Shared Mongo is not scratch space**: verification uses `meweb_<scratch>` and drops it. 82. **A new
|
|
|
|
|
|
verification mode is a rung of `/verify`.** 83. **A bulk change is bracketed by a full verification before and
|
|
|
|
|
|
after.**
|
|
|
|
|
|
|
|
|
|
|
|
## D. Configuration
|
|
|
|
|
|
|
|
|
|
|
|
18. **`record` types only for local, performance-oriented data.** Wire DTOs, options classes and everything in
|
|
|
|
|
|
`MeWeb.Shared` and `MeWeb.App` are classes.
|
|
|
|
|
|
19. **Config layering:** `appsettings.json` → environment variables (`Section__Key`) from
|
|
|
|
|
|
`/etc/meweb/meweb.env` on the box. **This repository is public: no secret is ever committed** (the house carve-out
|
|
|
|
|
|
of committing secrets does not apply here).
|
|
|
|
|
|
20. **Signing keys never touch git**: the Android keystore, the update-feed key and any packaging key live in Gitea
|
|
|
|
|
|
Actions secrets and are decoded into `$RUNNER_TEMP` by the release job.
|
|
|
|
|
|
87. **`IOptionsMonitor<T>` + `.CurrentValue` — never `IOptions<T>`.**
|
|
|
|
|
|
|
|
|
|
|
|
## E. Lifecycle and concurrency
|
|
|
|
|
|
|
|
|
|
|
|
21. **Three lifecycle idioms:** `IHostedService` for start/stop hooks, `BackgroundService` for periodic work with a
|
|
|
|
|
|
per-tick catch, explicit `IAsyncDisposable` for sockets and listeners.
|
|
|
|
|
|
22. **Disposable-lease mutex** (`TryBegin → IDisposable`) when an operation must not overlap (one probe run per
|
|
|
|
|
|
address).
|
|
|
|
|
|
23. n/a. 24. n/a.
|
|
|
|
|
|
88. **`CancellationToken` is the last parameter of every async method, and it is forwarded.** Relay commands take
|
|
|
|
|
|
one (the toolkit cancels them). Best-effort cleanup (removing a port mapping on exit) passes `CancellationToken.None`
|
|
|
|
|
|
on purpose.
|
|
|
|
|
|
|
|
|
|
|
|
## F. Data
|
|
|
|
|
|
|
2026-10-15 05:41:44 +02:00
|
|
|
|
25. **One data accessor per store**: `ShareService` on the server (one JSON file per submission), `KnowledgeBase`,
|
|
|
|
|
|
`HistoryStore`, `Leftovers` and `Submissions` in `MeWeb.Core`.
|
2026-10-15 04:39:03 +02:00
|
|
|
|
No SQLite: the provider map is a sorted brotli table read into arrays, the history a small JSON file, so no
|
|
|
|
|
|
native library has to ship on four platforms.
|
2026-10-15 01:14:44 +02:00
|
|
|
|
26. **Entity → view is a hand-written `ToView()`.** No mapper library.
|
|
|
|
|
|
56. **A multi-row mutation is one server call.**
|
|
|
|
|
|
74. **No in-between DTOs — a response DTO is pure data; `WebResult` carries ok/error.**
|
|
|
|
|
|
75. **A list is `[]` by construction end to end** — never `null`, never `?? []` at a use site.
|
|
|
|
|
|
84. **Never hand-roll a `JsonSerializerOptions`.** One source-generated context per assembly
|
|
|
|
|
|
(`MeWebJsonContext`, `AppJsonContext`) with Web defaults.
|
|
|
|
|
|
85. n/a.
|
|
|
|
|
|
89. **No `= string.Empty` property defaults — anywhere.**
|
|
|
|
|
|
|
|
|
|
|
|
## G. App (Avalonia)
|
|
|
|
|
|
|
|
|
|
|
|
27. **Client calls never throw** across the view-model boundary: a probe returns a result the view model can show;
|
2026-10-15 09:10:37 +02:00
|
|
|
|
transport failures become a state ("can't tell from here"), never an exception in a page. At the network edge
|
|
|
|
|
|
that means `catch (Exception) when (!ct.IsCancellationRequested)`, not a list of expected types: on Android the
|
|
|
|
|
|
HTTPS client is the platform's own stack, and a network switch throws exceptions no list foresees. Behind it,
|
|
|
|
|
|
`ErrorLog.Watch` is the last guard (the UI dispatcher, unobserved tasks, Android's `UnhandledExceptionRaiser`):
|
|
|
|
|
|
it writes `errors.log` in the app folder, sends nothing, and keeps the app running.
|
2026-10-15 01:14:44 +02:00
|
|
|
|
28. **One screen = `XView.axaml` + `XViewModel`**, the view model resolved from DI.
|
|
|
|
|
|
29. n/a.
|
|
|
|
|
|
30. **Resilience engages on connection failure only**; a definite answer is never retried.
|
|
|
|
|
|
31. **`AutomationProperties.AutomationId` on every interactive element** (`<view>-<slug>`).
|
|
|
|
|
|
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
|
|
|
|
|
|
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.
|
|
|
|
|
|
43. **Every submit-shaped screen is a real form** with a `*Form` object and one submit command whose `CanExecute`
|
|
|
|
|
|
follows validity and busy state.
|
|
|
|
|
|
44. **No converters or inline logic for display values:** the view model exposes `Show*` booleans and `*Formatted`
|
|
|
|
|
|
strings; XAML binds to them.
|
|
|
|
|
|
45. **No service locator in views**: a view receives its view model; `App.Services` is read only where Avalonia
|
|
|
|
|
|
builds the root view.
|
|
|
|
|
|
58. n/a. 59. n/a. 60. n/a — `prefers-reduced-motion` is not read (as on arasaka.software). 62. n/a. 76. n/a.
|
|
|
|
|
|
77. **Editable rows bind straight to their item.**
|
|
|
|
|
|
78. **No `record` types in the App** — classes only.
|
|
|
|
|
|
90. n/a.
|
|
|
|
|
|
91. **Never write `.Value` on a localizer indexer.** `Localizer.Instance[key]` returns a string.
|
|
|
|
|
|
93. **Never `try/catch` a client call — it already handed you the failure.**
|
|
|
|
|
|
|
|
|
|
|
|
## H. Logging, localization, packages, docs
|
|
|
|
|
|
|
|
|
|
|
|
38. **Structured logging with `{Placeholder}` templates**, never interpolated strings (Microsoft.Extensions.Logging,
|
|
|
|
|
|
as passolo).
|
2026-10-15 01:18:13 +02:00
|
|
|
|
39. **Localization-first:** every user-facing string is a key in `src/MeWeb.App/Strings/<code>.json`, one flat
|
2026-10-15 22:49:40 +02:00
|
|
|
|
file per language, English the fallback; no literals in XAML or view models (the product name too is a key,
|
|
|
|
|
|
`AppName`). JSON rather than resx on purpose: 21 of the 113 official languages are unknown to ICU and to MSBuild's
|
2026-10-15 01:18:13 +02:00
|
|
|
|
satellite-culture list, and a file looked up by code needs neither. Keys change only through
|
|
|
|
|
|
`tools/strings.cs set`, and `tools/strings.cs check` (keys and `{n}` placeholders against English) runs in CI.
|
2026-10-15 01:14:44 +02:00
|
|
|
|
40. **Central Package Management** (`Directory.Packages.props`); Avalonia packages move together through
|
|
|
|
|
|
`AvaloniaVersion`.
|
|
|
|
|
|
41. **Docs as convention:** `AGENT.md` (operational), `CLAUDE.md` (quick reference + pitfalls), `PATTERNS.md`
|
|
|
|
|
|
(rulings), `README.md` (public, English).
|
|
|
|
|
|
42. **Code comments — strict ban.** No `//`, `/* */`, `///`, `<!-- -->` in owned source (C#, AXAML, csproj, props,
|
|
|
|
|
|
manifests, plist, scripts, workflows). Intent goes in names, structure and the markdown docs.
|
|
|
|
|
|
61. **Binaries only through `tools/setup.cs`** (versions and SHA-256 pinned in `tools/setup.json`).
|
|
|
|
|
|
|
|
|
|
|
|
## I. Version control
|
|
|
|
|
|
|
|
|
|
|
|
46. **Never create a branch.** Commit on `master`; never force-push, never rewrite pushed history.
|
|
|
|
|
|
|
|
|
|
|
|
## J. Styling and UI text
|
|
|
|
|
|
|
|
|
|
|
|
47. **Colors only through tokens** in `src/MeWeb.App/Resources/Colors.axaml`; no hex literal in a view. Every
|
|
|
|
|
|
text/background pair passes WCAG AA.
|
|
|
|
|
|
48. **One icon source** (`Resources/Icons.axaml`, Lucide geometries, ISC); no inline path data in a view.
|
|
|
|
|
|
49. **Action rows run `[destructive] … [Cancel] [Primary]`** — primary on the end side (mirrored in RTL).
|
|
|
|
|
|
50. **All UI text is sentence case — never Title Case.**
|
|
|
|
|
|
51. **A helper must earn its existence** — 3+ call sites or extracted pure logic (#14); otherwise inline it.
|
|
|
|
|
|
52. **Zero-call-site code is deleted.**
|
|
|
|
|
|
92. **`var` for every local**; a local with no initializer is `var x = default(T)`.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## K. MeWeb-only rulings
|
|
|
|
|
|
|
|
|
|
|
|
120. **Every outbound contact is declared.** The app talks only to the hosts listed in "What this app contacts"
|
|
|
|
|
|
(Arasaka's server, the STUN servers, the MX hosts tested on port 25, blocklists through the user's own
|
|
|
|
|
|
resolver). A new host is a change to that screen in the same commit. No telemetry, analytics or crash
|
|
|
|
|
|
reporting.
|
|
|
|
|
|
121. **A verdict states what was measured, not what is hoped.** Each card says what it tried, what answered, and
|
|
|
|
|
|
what that means; "can't tell from here" is a valid result and never rendered as success or failure.
|
|
|
|
|
|
122. **Nothing is changed on the user's network without consent**: a port mapping is asked for first, leased for
|
|
|
|
|
|
at most ten minutes, removed after the run and recorded so a leftover is removed at the next start.
|
|
|
|
|
|
123. **The probe only ever connects back to the caller's own address**, bound to a single-use ticket; it refuses
|
|
|
|
|
|
private, loopback, link-local, CGNAT, ULA and the server's own prefixes.
|
|
|
|
|
|
124. **Shared findings leave only after the person has read exactly what is sent**, and every submission can be
|
|
|
|
|
|
deleted with the id and token the app keeps.
|
2026-10-15 01:18:13 +02:00
|
|
|
|
125. **Every language ships; drafts say so.** `data/languages.json` (written by `tools/languages.cs` from CLDR:
|
|
|
|
|
|
every language official or de facto official in a UN member state, plus the six UN languages) marks each
|
|
|
|
|
|
`reviewed` or `draft` and carries its own `rtl` flag, because ICU does not know every script's direction. The
|
|
|
|
|
|
picker lists a language once its strings file exists; a draft shows its notice and the "suggest a correction"
|
|
|
|
|
|
link; the root `FlowDirection` follows `rtl`.
|
2026-10-15 22:49:40 +02:00
|
|
|
|
126. **`?` appears only in display text, and there always quoted** (“me-web?”, each language with its own marks from
|
|
|
|
|
|
`data/languages.json`; `tools/strings.cs check` enforces it). File names, package ids and paths use `MeWeb`, `meweb` and
|
2026-10-15 01:14:44 +02:00
|
|
|
|
`software.arasaka.meweb`.
|
2026-10-15 04:39:03 +02:00
|
|
|
|
127. **A provider fact carries who said it, where and when.** Every value in `data/isps/*.json` has `basis`
|
|
|
|
|
|
(`operator`, `users`, `one-user`, `press`, `research`), `source` (a URL) and `date` (`yyyy-MM` or `yyyy`), and
|
|
|
|
|
|
the card shows all three. A fact nobody published is left out, not guessed; the values describe what is usual
|
|
|
|
|
|
for the provider, never the person's contract.
|