18 · Coding Standards & Contributor Guide — Stella Ops

(v2.0 — 12 Jul 2025 · supersedes v1.0)

Audience — Anyone sending a pull‑request to the open‑source Core.
Goal — Keep the code‑base small‑filed, plug‑in‑friendly, DI‑consistent, and instantly readable.


0 Why read this?

  • Cuts review time → quicker merges.
  • Guarantees code is hot‑load‑safe for run‑time plug‑ins.
  • Prevents style churn and merge conflicts.

1 High‑level principles

  1. SOLID first – especially Interface & Dependency Inversion.
  2. 100‑line rule – any file > 100 physical lines must be split or refactored.
  3. Contract‑level ownership – public abstractions live in lightweight Contracts libraries; impl classes live in runtime projects.
  4. Single Composition Root – all DI wiring happens in StellaOps.Web/Program.cs and in each plug‑in’s IoCConfigurator; nothing else calls IServiceCollection.BuildServiceProvider.
  5. No Service Locator – constructor injection only; static ServiceProvider is banned.
  6. Fail‑fast startup – configuration validated before the web‑host listens.
  7. Hot‑load compatible – no static singletons that survive plug‑in unload; avoid Assembly.LoadFrom outside the built‑in plug‑in loader.

2 Repository layout (flat, July‑2025)**

src/
├─ backend/
│   ├─ StellaOps.Web/                # ASP.NET host + composition root
│   ├─ StellaOps.Common/             # Serilog, Result<T>, helpers
│   ├─ StellaOps.Contracts/          # DTO + interface contracts (no impl)
│   ├─ StellaOps.Configuration/      # Options + validation
│   ├─ StellaOps.Localization/
│   ├─ StellaOps.PluginLoader/       # Cosign verify, hot‑load
│   ├─ StellaOps.Scanners.Trivy/     # First‑party scanner
│   ├─ StellaOps.TlsProviders.OpenSsl/
│   └─ … (additional runtime projects)
├─ plugins-sdk/                      # Templated contracts & abstractions
└─ frontend/                         # Angular workspace
tests/                               # Mirrors src structure 1‑to‑1

There are no folders named “Module” and no nested solutions.

3 Naming & style conventions

ElementRuleExample
NamespacesFile‑scoped, StellaOps.namespace StellaOps.Scanners;
InterfacesI prefix, PascalCaseIScannerRunner
Classes / recordsPascalCaseScanRequest, TrivyRunner
Private fieldscamelCase (no leading underscore)redisCache, httpClient
ConstantsSCREAMING_SNAKE_CASEconst int MAX_RETRIES = 3;
Async methodsEnd with AsyncTaskScanAsync()
File length≤ 100 lines incl. using & bracesenforced by dotnet format check
Using directivesOutside namespace, sorted, no wildcards

Static analyzers (.editorconfig, StyleCop.Analyzers package) enforce the above.

4 Dependency‑injection policy

Composition root – exactly one per process:

builder.Services
       .AddStellaCore()          // extension methods from each runtime project
       .AddPluginLoader("/Plugins", cfg);   // hot‑load signed DLLs

Plug‑ins register additional services via the IoCConfigurator convention described in the Plug‑in SDK Guide, §5. Never resolve services manually (provider.GetService()) outside the composition root; tests may use WebApplicationFactory or ServiceProvider.New() helpers. Scoped lifetime is default; singletons only for stateless, thread‑safe helpers.

5 Project organisation rules

Contracts vs. Runtime – public DTO & interfaces live in.Contracts; implementation lives in sibling project. Feature folders – inside each runtime project group classes by use‑case, e.g.

├─ Scan/
│   ├─ ScanService.cs
│   └─ ScanController.cs
├─ Feed/
└─ Tls/

Tests – mirror the structure under tests/ one‑to‑one; no test code inside production projects.

6 C# language features

Nullable reference types enabled. record for immutable DTOs. Pattern matching encouraged; avoid long switch‑cascades. Span& MemoryOK when perf‑critical, but measure first. Use await foreach over manual paginator loops.

7 Error‑handling template

public async Task<IActionResult> PostScan([FromBody] ScanRequest req)
{
    if (!ModelState.IsValid) return BadRequest(ModelState);

    try
    {
        ScanResult result = await scanService.ScanAsync(req);
        if (result.Quota != null) 
        {
            Response.Headers.TryAdd("X-Stella-Quota-Remaining", result.Quota.Remaining.ToString());
            Response.Headers.TryAdd("X-Stella-Reset", result.Quota.ResetUtc.ToString("o"));
        }
        return Ok(result);
    }
}

RFC 7807 ProblemDetails for all non‑200s. Capture structured logs with Serilog’s message‑template syntax.

8 Async & threading

  • All I/O is async; no .Result / .Wait().
  • Library code: ConfigureAwait(false).
  • Limit concurrency via Channelor Parallel.ForEachAsync, never raw Task.Run loops.

9 Testing rules

LayerFrameworkCoverage gate
UnitxUnit + FluentAssertions≥ 80 % line, ≥ 60 % branch
IntegrationTestcontainersReal Redis & Trivy
Mutation (critical libs)Stryker.NET≥ 60 % score

One test project per runtime/contract project; naming.Tests.

10 Static analysis & formatting

  • dotnet format must exit clean (CI gate).
  • StyleCop.Analyzers + Roslyn‑Security‑Guard run on every PR.
  • CodeQL workflow runs nightly on main.

11 Commit & PR checklist

  • Conventional Commit prefix (feat:, fix:, etc.).
  • dotnet format & dotnet test both green.
  • Added or updated XML‑doc comments for public APIs.
  • File count & length comply with 100‑line rule.
  • If new public contract → update relevant markdown doc & JSON‑Schema.

12 Common pitfalls

SymptomRoot causeFix
InvalidOperationException: Cannot consume scoped service…Mis‑matched DI lifetimesUse scoped everywhere unless truly stateless
Hot‑reload plug‑in crashStatic singleton caching plugin typesStore nothing static; rely on DI scopes

100‑line style violation |Large handlers or utils |Split into private helpers or new class

13 Change log

VersionDateNotes
v2.02025‑07‑12Updated DI policy, 100‑line rule, new repo layout, camelCase fields, removed “Module” terminology.
1.02025‑07‑09Original standards.