Build / Desktop, server and tools (push) Successful in 33s
The language list comes from CLDR (official in a UN member state, plus the six UN languages), with autonyms and text direction supplied for the 21 that ICU does not know. Strings move from resx to one JSON file per language, looked up by code, so a language unknown to ICU or MSBuild still loads; tools/strings.cs checks keys and placeholders in CI. Arabic, Hindi and Chinese drafts prove right-to-left and system-font fallback. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
185 lines
12 KiB
Markdown
185 lines
12 KiB
Markdown
# 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
|
||
|
||
25. **One data accessor per store**: `MongoContext` on the server, `KnowledgeStore` and `HistoryStore` in the App.
|
||
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;
|
||
transport failures become a state ("can't tell from here"), never an exception in a page.
|
||
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).
|
||
39. **Localization-first:** every user-facing string is a key in `src/MeWeb.App/Strings/<code>.json`, one flat
|
||
file per language, English the fallback; no literals in XAML or view models (the product name `me-web?`
|
||
excepted). JSON rather than resx on purpose: 21 of the 113 official languages are unknown to ICU and to MSBuild's
|
||
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.
|
||
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.
|
||
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`.
|
||
126. **`?` appears only in display text.** File names, package ids and paths use `MeWeb`, `meweb` and
|
||
`software.arasaka.meweb`.
|