Files
MeWeb/PATTERNS.md
T
thepraandClaude Opus 5.5 320936c688
Build / Desktop, server and tools (push) Successful in 1m19s
Release / Every platform, the server, the release (push) Successful in 9m34s
"me-web?" 0.7.0: the name in quotes, copy buttons, and every translation rewritten in plain words
- The name is now always written in quotation marks, each language with its own: “me-web?”, «me-web?», „me-web?“, « me-web? », 「me-web?」. Without them the question mark read as the end of a sentence. In Arabic, Hebrew and the other right-to-left languages the question mark no longer jumps to the wrong side of the name.
- Your addresses, your router, your network, the reverse name, the route and the code of a submission you sent now sit in a small box with a copy button, ready to paste into a message to your provider or anywhere else.
- Every translation except English and Italian was rewritten for people who are not technical: everyday words first, the technical term in brackets, and one word for one thing across the whole app. They are still machine drafts; "Suggest a correction" is welcome in any language.
- A few lines are plainer in English and Italian too: the test servers for calls, and the server in the middle that carries a call when two homes can't connect directly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
2026-10-15 22:49:40 +02:00

13 KiB
Raw Blame History

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.
  5. A service never takes IHttpContextAccessor — the endpoint reads the caller's address and passes it in.

B. Web boundary

  1. WebResult envelope + early return on every server endpoint that can refuse; the happy path stays flat.
  2. Pure or internal code may return tuples ((value, error)), never across the web boundary.
  3. 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.
  4. 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.)
  5. Routes centralized in src/MeWeb.Shared/Endpoints.cs (GET_Name, GET_M_Name(args)), shared by server and App. No "/api/ literal anywhere else.
  6. Prefer the WebResult path over inline try/catch in endpoints.
  7. n/a — SSE.
  8. 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).
  9. Long-running server work is registered and stoppable (probe runs, the per-address limiter).
  10. n/a — no request-bound streams.

C. Verification (there is no test project)

  1. 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.
  2. n/a — xUnit theories. No test project by owner decision.
  3. Hand-written fakes, no mocking framework.
  4. Server endpoints are verified by the curl ladder against a scratch database (/verify).
  5. 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.
  6. n/a — no model output.
  7. An inventory that lives in a check rots; keep one map and assert it against reality.
  8. A sweep asserts what must hold and records what is a judgement call.
  9. Prefer a source lint to a runtime check when the bug is visible in the source (tools/lint.cs).
  10. A check that has always failed is a decision nobody made — fix it or delete it.
  11. 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).
  12. Seed data before judging a surface — the screenshot tool runs every language and two sizes.
  13. 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.
  14. 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

  1. record types only for local, performance-oriented data. Wire DTOs, options classes and everything in MeWeb.Shared and MeWeb.App are classes.
  2. 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).
  3. 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.
  4. IOptionsMonitor<T> + .CurrentValue — never IOptions<T>.

E. Lifecycle and concurrency

  1. Three lifecycle idioms: IHostedService for start/stop hooks, BackgroundService for periodic work with a per-tick catch, explicit IAsyncDisposable for sockets and listeners.
  2. Disposable-lease mutex (TryBegin → IDisposable) when an operation must not overlap (one probe run per address).
  3. n/a. 24. n/a.
  4. 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

  1. One data accessor per store: ShareService on the server (one JSON file per submission), KnowledgeBase, HistoryStore, Leftovers and Submissions in MeWeb.Core. 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.
  2. Entity → view is a hand-written ToView(). No mapper library.
  3. A multi-row mutation is one server call.
  4. No in-between DTOs — a response DTO is pure data; WebResult carries ok/error.
  5. A list is [] by construction end to end — never null, never ?? [] at a use site.
  6. Never hand-roll a JsonSerializerOptions. One source-generated context per assembly (MeWebJsonContext, AppJsonContext) with Web defaults.
  7. n/a.
  8. No = string.Empty property defaults — anywhere.

G. App (Avalonia)

  1. 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. 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.
  2. One screen = XView.axaml + XViewModel, the view model resolved from DI.
  3. n/a.
  4. Resilience engages on connection failure only; a definite answer is never retried.
  5. AutomationProperties.AutomationId on every interactive element (<view>-<slug>).
  6. Subscription discipline: every += has its -= in the matching unload/dispose; timers are PeriodicTimer with a cancellation source cancelled on leave.
  7. n/a.
  8. DTO naming by direction: View* (response), Upsert* (create/update), *Form (any other input). Contracts grouped by area in MeWeb.Shared/<Area>Contracts.cs.
  9. n/a. 36. Path safety for anything served from disk by name. 37. Layered gating: the UI hides; the server refuses. 57. n/a.
  10. Every submit-shaped screen is a real form with a *Form object and one submit command whose CanExecute follows validity and busy state.
  11. No converters or inline logic for display values: the view model exposes Show* booleans and *Formatted strings; XAML binds to them.
  12. No service locator in views: a view receives its view model; App.Services is read only where Avalonia builds the root view.
  13. n/a. 59. n/a. 60. n/a — prefers-reduced-motion is not read (as on arasaka.software). 62. n/a. 76. n/a.
  14. Editable rows bind straight to their item.
  15. No record types in the App — classes only.
  16. n/a.
  17. Never write .Value on a localizer indexer. Localizer.Instance[key] returns a string.
  18. Never try/catch a client call — it already handed you the failure.

H. Logging, localization, packages, docs

  1. Structured logging with {Placeholder} templates, never interpolated strings (Microsoft.Extensions.Logging, as passolo).
  2. 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 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 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.
  3. Central Package Management (Directory.Packages.props); Avalonia packages move together through AvaloniaVersion.
  4. Docs as convention: AGENT.md (operational), CLAUDE.md (quick reference + pitfalls), PATTERNS.md (rulings), README.md (public, English).
  5. 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.
  6. Binaries only through tools/setup.cs (versions and SHA-256 pinned in tools/setup.json).

I. Version control

  1. Never create a branch. Commit on master; never force-push, never rewrite pushed history.

J. Styling and UI text

  1. Colors only through tokens in src/MeWeb.App/Resources/Colors.axaml; no hex literal in a view. Every text/background pair passes WCAG AA.
  2. One icon source (Resources/Icons.axaml, Lucide geometries, ISC); no inline path data in a view.
  3. Action rows run [destructive] … [Cancel] [Primary] — primary on the end side (mirrored in RTL).
  4. All UI text is sentence case — never Title Case.
  5. A helper must earn its existence — 3+ call sites or extracted pure logic (#14); otherwise inline it.
  6. Zero-call-site code is deleted.
  7. var for every local; a local with no initializer is var x = default(T).

K. MeWeb-only rulings

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. ? 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 software.arasaka.meweb.
  8. 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.