Build / Desktop, server and tools (push) Successful in 1m7s
The first screen (online or not, a Check button, every language with a draft notice), the desktop and Android heads, the server's /api/info and /api/ip, a release tool that builds deb, rpm, a Windows exe, macOS zips, a signed APK and the server, and the Gitea workflows that run it. Rules adapted from passolo; AGPL-3.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
12 KiB
12 KiB
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
- 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. - Registration only through extension methods, one group per area:
AddMeWebApp(App),AddMeWebHosting,AddMeWebData(server).Program.csreads as a list of calls. - Lifetime discipline. Server: stateless per-request →
AddScoped; process-global state, caches, rate counters →AddSingleton. App: services singleton, view models transient. - Interface fan-in via factory re-registration when one instance serves two contracts; never register it twice.
- A service never takes
IHttpContextAccessor— the endpoint reads the caller's address and passes it in.
B. Web boundary
WebResultenvelope + early return on every server endpoint that can refuse; the happy path stays flat.- Pure or internal code may return tuples (
(value, error)), never across the web boundary. try/catchplacement: 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.- 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.) - Routes centralized in
src/MeWeb.Shared/Endpoints.cs(GET_Name,GET_M_Name(args)), shared by server and App. No"/api/literal anywhere else. - Prefer the
WebResultpath over inlinetry/catchin endpoints. - n/a — SSE.
- Subprocesses: argv only, never a shell string.
ProcessStartInfo.ArgumentList; nosh -c, nocmd /c(tracepath,ip,netshare run this way, if at all). - Long-running server work is registered and stoppable (probe runs, the per-address limiter).
- n/a — no request-bound streams.
C. Verification (there is no test project)
- 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.
- n/a — xUnit theories. No test project by owner decision.
- Hand-written fakes, no mocking framework.
- Server endpoints are verified by the curl ladder against a scratch database (
/verify). - 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. - n/a — no model output.
- An inventory that lives in a check rots; keep one map and assert it against reality.
- A sweep asserts what must hold and records what is a judgement call.
- Prefer a source lint to a runtime check when the bug is visible in the source (
tools/lint.cs). - A check that has always failed is a decision nobody made — fix it or delete it.
- 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).
- Seed data before judging a surface — the screenshot tool runs every language and two sizes.
- 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:
AutomationIds are unique). 79. n/a. - 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
recordtypes only for local, performance-oriented data. Wire DTOs, options classes and everything inMeWeb.SharedandMeWeb.Appare classes.- Config layering:
appsettings.json→ environment variables (Section__Key) from/etc/meweb/meweb.envon the box. This repository is public: no secret is ever committed (the house carve-out of committing secrets does not apply here). - 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_TEMPby the release job. IOptionsMonitor<T>+.CurrentValue— neverIOptions<T>.
E. Lifecycle and concurrency
- Three lifecycle idioms:
IHostedServicefor start/stop hooks,BackgroundServicefor periodic work with a per-tick catch, explicitIAsyncDisposablefor sockets and listeners. - Disposable-lease mutex (
TryBegin → IDisposable) when an operation must not overlap (one probe run per address). - n/a. 24. n/a.
CancellationTokenis 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) passesCancellationToken.Noneon purpose.
F. Data
- One data accessor per store:
MongoContexton the server,KnowledgeStoreandHistoryStorein the App. - Entity → view is a hand-written
ToView(). No mapper library. - A multi-row mutation is one server call.
- No in-between DTOs — a response DTO is pure data;
WebResultcarries ok/error. - A list is
[]by construction end to end — nevernull, never?? []at a use site. - Never hand-roll a
JsonSerializerOptions. One source-generated context per assembly (MeWebJsonContext,AppJsonContext) with Web defaults. - n/a.
- No
= string.Emptyproperty defaults — anywhere.
G. App (Avalonia)
- 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.
- One screen =
XView.axaml+XViewModel, the view model resolved from DI. - n/a.
- Resilience engages on connection failure only; a definite answer is never retried.
AutomationProperties.AutomationIdon every interactive element (<view>-<slug>).- Subscription discipline: every
+=has its-=in the matching unload/dispose; timers arePeriodicTimerwith a cancellation source cancelled on leave. - n/a.
- DTO naming by direction:
View*(response),Upsert*(create/update),*Form(any other input). Contracts grouped by area inMeWeb.Shared/<Area>Contracts.cs. - n/a. 36. Path safety for anything served from disk by name. 37. Layered gating: the UI hides; the server refuses. 57. n/a.
- Every submit-shaped screen is a real form with a
*Formobject and one submit command whoseCanExecutefollows validity and busy state. - No converters or inline logic for display values: the view model exposes
Show*booleans and*Formattedstrings; XAML binds to them. - No service locator in views: a view receives its view model;
App.Servicesis read only where Avalonia builds the root view. - n/a. 59. n/a. 60. n/a —
prefers-reduced-motionis not read (as on arasaka.software). 62. n/a. 76. n/a. - Editable rows bind straight to their item.
- No
recordtypes in the App — classes only. - n/a.
- Never write
.Valueon a localizer indexer.Localizer.Instance[key]returns a string. - Never
try/catcha client call — it already handed you the failure.
H. Logging, localization, packages, docs
- Structured logging with
{Placeholder}templates, never interpolated strings (Microsoft.Extensions.Logging, as passolo). - Localization-first: every user-facing string is a resx key in
src/MeWeb.App/Strings/Strings*.resx; no literals in XAML or view models (the product nameme-web?excepted). - Central Package Management (
Directory.Packages.props); Avalonia packages move together throughAvaloniaVersion. - Docs as convention:
AGENT.md(operational),CLAUDE.md(quick reference + pitfalls),PATTERNS.md(rulings),README.md(public, English). - 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. - Binaries only through
tools/setup.cs(versions and SHA-256 pinned intools/setup.json).
I. Version control
- Never create a branch. Commit on
master; never force-push, never rewrite pushed history.
J. Styling and UI text
- Colors only through tokens in
src/MeWeb.App/Resources/Colors.axaml; no hex literal in a view. Every text/background pair passes WCAG AA. - One icon source (
Resources/Icons.axaml, Lucide geometries, ISC); no inline path data in a view. - Action rows run
[destructive] … [Cancel] [Primary]— primary on the end side (mirrored in RTL). - All UI text is sentence case — never Title Case.
- A helper must earn its existence — 3+ call sites or extracted pure logic (#14); otherwise inline it.
- Zero-call-site code is deleted.
varfor every local; a local with no initializer isvar x = default(T).
K. MeWeb-only rulings
- 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.
- 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.
- 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.
- 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.
- 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.
- Every language ships; drafts say so.
data/languages.jsonmarks each languagereviewedordraft; a draft shows its notice and the "suggest a correction" link. The rootFlowDirectionfollows the language. ?appears only in display text. File names, package ids and paths useMeWeb,mewebandsoftware.arasaka.meweb.