Skip to content

Code style

The code-style contract for implementing Nerthus.Core: comment style, naming, .NET patterns, the compiled C# substrate, error handling, and the project-specific code shapes (thin-client wrappers, daemon handlers, the dry-run plumbing). This page covers code — the data grammar (the Polish @tag schema, entity headings, session headers) is owned by the entity model, the tag schema, and the session model; documentation authoring is covered in Write documentation.

Where these conventions apply

Tree Role How much logic
client/*.ps1 thin REST wrappers (Verb-Nerthus*) minimal — discover + one HTTP call
daemon/**/*.ps1 the server (owns all data) the bulk; full conventions apply
lib/*.cs compiled C# (namespace Nerthus) performance-critical hot paths

All PowerShell targets pwsh 7+. All files are UTF-8 no BOM.

Comment styles

Every daemon .ps1 opens with a <# ... #> block containing .SYNOPSIS and .DESCRIPTION — the file's purpose, its helpers, module-level data, and the design rationale, before any code:

<#
    .SYNOPSIS
    Shared daemon plumbing: paths, config, the C# substrate loader, structured
    logging, JSON helpers, temporal-value resolution, and atomic writes.

    .DESCRIPTION
    Every other daemon file builds on this one. ...

    Helpers:
    - Initialize-NerthusCSharp:  Add-Type the lib/*.cs substrate (using-hoisted), once
    - Write-NerthusFileUtf8:     UTF-8 no BOM, newline-style preserving file write

    Module-level data:
    - $script:NerthusCSharpLoaded: guard so the substrate is compiled once per process
#>

Client cmdlet files are grouped per subsystem (client/Cmdlets-Entities.ps1) and open with a brief .SYNOPSIS-only block plus a pointer to the relevant documentation page.

Every function — exported cmdlet, service, or helper — carries its own minimal <# .SYNOPSIS #> immediately after the opening brace:

function Get-NerthusEntity {
    <# .SYNOPSIS Fetch one entity by name, temporal values resolved as of an optional date. #>
    [CmdletBinding()] param( ... )
    ...
}

Single-line # comments precede logical blocks and explain why, not what. End-of-line # comments clarify non-obvious values or decisions.

# Hoist all `using` directives above every file body (Add-Type compiles one unit).
$Usings = [System.Collections.Generic.SortedSet[string]]::new()

$HasRange = $M.Groups['from'].Success   # the optional "(from:to)" suffix matched

Operational warnings to stderr use a [WARN FunctionName] prefix:

[System.Console]::Error.WriteLine("[WARN Read-NerthusConfig] Could not parse config.json: $_")

Naming conventions

PascalCase for all variables, no exceptions. $script: scope for module-level data shared across functions in a file/module:

$RepoRoot
$script:ModuleRoot
$script:NerthusTemporalScalarTags
$script:NerthusDefaults

Functions follow Verb-Noun with approved verbs only:

Get Set New Remove Resolve Test Invoke Find Add Open Close Start Stop Initialize Send Compare Connect Grant Revoke
  • Public client cmdlets are Verb-Nerthus<Noun> (e.g. Get-NerthusEntity, Invoke-NerthusPUAssignment); the lifecycle pair Start-Nerthus/Stop-Nerthus has an empty noun. The .psm1 loader exports only names matching this surface (see Module manifest & loader).
  • Daemon-internal functions also carry the Nerthus infix (Build-NerthusModel, Set-NerthusEntityTagInFile). A service whose natural name is taken by a client cmdlet appends Service (Resolve-NerthusNameService, Get-NerthusEntityPathService) so the two never collide when both are loaded in one session. HTTP glue functions are Handle-<Name> (Handle-GetEntity) — Handle is a deliberate exemption from the approved-verb gate, which protects only the exported module surface. A handful of pure-local helpers omit the infix (Test-EntitySchema). Nothing daemon-side is ever exported — the daemon is dot-sourced by its boot script, not imported as a module.

.NET over cmdlets

Prefer .NET static methods over PowerShell cmdlets for performance and cross-platform consistency:

# File I/O
[System.IO.File]::ReadAllLines($FilePath)
[System.IO.File]::Exists($Path)
[System.IO.Directory]::GetFiles($ClientDir, '_*.ps1')
[System.IO.Path]::Combine($RepoRoot, '.nerthus')

# Strings
[string]::IsNullOrWhiteSpace($Value)

# Collections
[System.Collections.Generic.List[string]]::new()
[System.Collections.Generic.Dictionary[string, hashtable]]::new()
[System.Collections.Generic.HashSet[string]]::new()

# StringBuilder / Regex / Dates
[System.Text.StringBuilder]::new()
[regex]::new($Pattern, 'Compiled')
[datetime]::DaysInMonth($Y, $Mo)

Compiled C# types (lib/*.cs, namespace Nerthus)

Performance-critical code is C# and is called directly — the class inventory and each class's purpose are in Architecture. All of lib/*.cs compiles once, as a single Add-Type unit, via Initialize-NerthusCSharp (daemon/services/Common.ps1): using directives are hoisted above the concatenated file bodies, $PSHOME/ref assemblies are referenced, and the load is guarded by $script:NerthusCSharpLoaded plus a PSTypeName probe. Rules: all C# lives in lib/*.cs (no inline Add-Type heredocs); the namespace is Nerthus; Add-Type is a hard requirement — there is no PowerShell fallback shadowing these types.

Small conventions

One rule, one line each:

[void]$Tags.Add(@{ Key = 'typ'; Value = $B.type })       # [void] suppresses unwanted output
$Record = [PSCustomObject]@{ Name = $Name; Type = $Type } # structured output
$Server = [ordered]@{ bind = '127.0.0.1'; port = 0 }      # [ordered] where key order matters
return $Record                                            # always the explicit return keyword
return @{ StatusCode = 200; Body = $Record }              # handlers return a hashtable

String comparison

[System.StringComparer]::OrdinalIgnoreCase                                   # dictionary/hashset comparer
[string]::Equals($A, $B, [System.StringComparison]::OrdinalIgnoreCase)       # explicit comparison
[string]::Equals($Pattern, $Actual, 'OrdinalIgnoreCase')                     # string-literal shorthand
$Text.EndsWith($Suffix, [System.StringComparison]::OrdinalIgnoreCase)

Note

Polish text carries diacritics — compare canonical (diacritic) forms. The ASCII→diacritic rule is owned by the tag schema.

Parameter declarations

[CmdletBinding()] precedes param() on the same line. Parameters are PascalCase and typed; Mandatory, ValidateSet, and ValueFromPipelineByPropertyName are used where they apply. SupportsShouldProcess is declared by every mutating client cmdlet (it feeds the dry-run plumbing) and, daemon-side, only by the file-level write primitives in daemon/services/Write.ps1 and the importer (Import-NerthusRepo, which derives its dry-run from it) — other services take an -Apply switch instead.

function Set-NerthusEntity {
    <# .SYNOPSIS Append/update a (possibly dated) entity tag. #>
    [CmdletBinding(SupportsShouldProcess)] param(
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string]$Name,
        [Parameter(Mandatory)] [string]$Tag, [Parameter(Mandatory)] [string]$Value,
        [string]$Daemon, [string]$Token
    )
    ...
}

Every client cmdlet carries the -Daemon/-Token passthrough pair (convention specified in the API reference).

Error handling

throw for fatal/unrecoverable errors; try/catch for non-fatal per-item failures; [System.Console]::Error.WriteLine() for warnings that must not interrupt execution.

throw "Entity '$Name' not found in index"

try { Sync-NerthusModel -Daemon $Daemon }
catch { [System.Console]::Error.WriteLine("[WARN Sync-NerthusModel] $_") }

Fail-early validation throws a structured ErrorRecord whose TargetObject is the future HTTP error body. The exemplar is the monthly PU batch (semantics in the PU model) — any unresolved character name aborts the whole run with no partial writes:

$ErrObj = [PSCustomObject]@{ error = 'PUUnresolvedCharacters'; month = $Month
                             unresolved = @($Unresolved); applied = $false }
throw [System.Management.Automation.ErrorRecord]::new(
    [System.InvalidOperationException]::new("Unresolved character name(s) in PU for $Month"),
    'PUUnresolvedCharacters', [System.Management.Automation.ErrorCategory]::InvalidData, $ErrObj)

The dispatcher recognizes the error id and serializes the TargetObject verbatim as the response body. The error ids, their status mapping, and the wire shapes are an API contract, owned by the API reference — never invent an ad-hoc error body in a handler.

Precompiled regex

Patterns used across calls are compiled and stored at $script: scope, or as a local before a loop — e.g. the importer's legacy map-record matcher:

$LineRe = [regex]::new('^\s*-\s*Id:\s*(?<id>\d+)\s*;\s*Nazwa:\s*(?<name>.+?)\s*,\s*Url:\s*(?<url>https?://\S+)\s*$', 'Compiled')

Project-specific patterns

Thin-client wrapper

A client cmdlet does one thing: make one HTTP call via Invoke-NerthusApi (the thin-client rule is owned by Architecture). Path segments built from user input are always URL-escaped — Polish entity names contain spaces and diacritics (Gildia Teologów):

function Get-NerthusEntity {
    <# .SYNOPSIS Fetch one entity by name, temporal values resolved as of an optional date. #>
    [CmdletBinding()] param(
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string]$Name,
        [datetime]$ActiveOn, [string]$Daemon, [string]$Token
    )
    process {
        $Q = @{}; if ($PSBoundParameters.ContainsKey('ActiveOn')) { $Q['activeOn'] = $ActiveOn.ToString('yyyy-MM-dd') }
        return Invoke-NerthusApi -Method GET -Path "/entities/$([System.Uri]::EscapeDataString($Name))" -Query $Q -Daemon $Daemon -Token $Token
    }
}

The single write gate

Every route the closed table marks Write = $true funnels through Assert-NerthusWriteAllowed before its handler runs — never write around it (gate and exemptions specified in the API reference).

Daemon handler shape

A handler maps a request $Context to @{ StatusCode; Body } and contains no domain logic — it calls a services/ function:

function Handle-GetEntity { param($Context)
    $D = $Context.Daemon; $At = Get-NerthusActiveOn $Context
    $Res = Resolve-NerthusNameService -NameIndex $D.NameIndex -Name $Context.PathParams['name'] -ActiveOn $At
    if (-not $Res) { return @{ StatusCode = 404; Body = @{ error = "Entity not found: $($Context.PathParams['name'])" } } }
    return @{ StatusCode = 200; Body = $Res.Owner }
}

Dry-run plumbing (-WhatIf?dryRun-Apply)

The client owns the -WhatIf?dryRun translation. By default a mutating cmdlet does not abort on a declined ShouldProcess — it appends ?dryRun=true and still makes the call, so the daemon computes the real preview. (The cmdlets that instead short-circuit locally under -WhatIf are enumerated in the API reference.)

$Path = "/entities/$([System.Uri]::EscapeDataString($Name))"
if (-not $PSCmdlet.ShouldProcess($Name, "Set @$Tag")) { $Path += '?dryRun=true' }
return Invoke-NerthusApi -Method PATCH -Path $Path -Body @{ tag = $Tag; value = $Value } -Daemon $Daemon -Token $Token

The daemon never sees -WhatIf. The dispatcher parses the query into $Context.DryRun; a handler either short-circuits with a would* preview body:

if ($Context.DryRun) { return @{ StatusCode = 200; Body = @{ wouldSet = $Name; tag = $Key; value = $B.value } } }

or forwards the flag as a service -Apply switch — -Apply:(-not $Context.DryRun) — and the service returns Applied = $false plus a Preview instead of writing. The file-level write primitives in daemon/services/Write.ps1 and Import-NerthusRepo are the only daemon code declaring SupportsShouldProcess; handlers invoke them with -Confirm:$false. Which routes honor ?dryRun is specified in the API reference.

Logging

The daemon writes structured JSONL via Write-NerthusLog to .nerthus/log/{operational,request,audit}.jsonl (streams and retention owned by the logging model); the audit stream is the durable, never-rotated source of truth. Stderr [WARN FunctionName] lines are for boot-time and non-fatal in-flight warnings only — they are not a log stream.

Module manifest & loader

The manifest (Nerthus.Core.psd1) uses PowerShell data syntax with inline # field comments. The loader (Nerthus.Core.psm1) carries the same file-level <# .SYNOPSIS .DESCRIPTION #> block. It dot-sources the underscore-prefixed client/_*.ps1 shared helpers first (never exported), then every other client/*.ps1 (cmdlets grouped per subsystem). It exports by function name: only names matching the approved-verb Verb-Nerthus\w* pattern, empty noun allowed for Start-Nerthus/Stop-Nerthus — see $VerbNounPattern in Nerthus.Core.psm1.

Daemon .ps1 files are dot-sourced by the daemon boot (daemon/Start-NerthusDaemon.ps1), not at module import time.

Git commit messages

One logical change per commit. The message is a single line:

<type>: <summary>

The whole line stays under 30 characters. No scope, no body; machine trailers (Co-Authored-By:) are allowed after a blank line. The summary is lowercase, imperative, no trailing period; Polish domain nouns are fine. The type set is closed:

Type Use for
feat new behavior (route, cmdlet, service)
fix bug fix
docs documentation, README, comments only
test tests/fixtures only
refactor no behavior change
chore manifest, layout, housekeeping
ci pipeline config
feat: token sweep at boot
fix: sse replay bound
docs: commit syntax
test: contract layer
chore: initial import

Forbidden in code & comments

  • No implementation-plan identifiers — name the contract or behavior, not the plan that introduced it.
  • No plugin/agnostic machinery — Nerthus is the one and only system (non-goals in Architecture).
  • No client-side domain logic — the client never parses Markdown or computes PU.
  • No hardcoded secrets — tokens live in gitignored .nerthus/ state; the one deliberate exception is the per-player @prfwebhook tag in the committed index. Everything else stays in gitignored config, never in source (policy in Configuration).

See also