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:
Naming conventions¶
PascalCase for all variables, no exceptions. $script: scope for module-level
data shared across functions in a file/module:
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 pairStart-Nerthus/Stop-Nerthushas an empty noun. The.psm1loader exports only names matching this surface (see Module manifest & loader). - Daemon-internal functions also carry the
Nerthusinfix (Build-NerthusModel,Set-NerthusEntityTagInFile). A service whose natural name is taken by a client cmdlet appendsService(Resolve-NerthusNameService,Get-NerthusEntityPathService) so the two never collide when both are loaded in one session. HTTP glue functions areHandle-<Name>(Handle-GetEntity) —Handleis 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:
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@prfwebhooktag in the committed index. Everything else stays in gitignored config, never in source (policy in Configuration).
See also¶
- Architecture — the daemon/client spine, the C# substrate, hooks
- API reference — routes, envelopes, middleware,
-WhatIf/?dryRun, capabilities - Tag schema · Session model — the data grammar
- Run tests — Pester conventions and fixtures
- Write documentation — documentation-authoring rules