Novolis.Commands v0 library and repo
Policies that keep the org coherent
name: Novolis Commands v0
overview: Scaffold a new novolis-commands repo on Novolis-Platform (default branch main) from novolis-template-dotnet, implementing five NuGet packages that parse text into CommandEnvelope, queue via a wrapped channel, and run with interrupt-aware cancellation—without executing domain behavior in the parser.
todos:
- id: scaffold-repo
content: Create Novolis-Platform/novolis-commands from novolis-template-dotnet (main, CI, .novolis/packages.json, slnx) status: completed
- id: abstractions
content: Implement Novolis.Commands.Abstractions with full spec types and interfaces status: completed
- id: engine
content: Implement CommandEngine, registry, built-in matcher, context resolver hook; no domain execution status: completed
- id: queueing
content: Implement ChannelCommandQueue and CommandQueueRunner<TContext> status: completed
- id: di-testing
content: Add DependencyInjection extensions and Novolis.Commands.Testing fakes/builders status: completed
- id: tests
content: "TUnit tests: helm parse, belay interrupt envelope, unknown command, queue runner cancel, ambiguity" status: completed
- id: docs-verify
content: Write design.md boundary doc; dotnet build/test; ready for first preview release status: completed isProject: false
Goal
Deliver milestone v0:
string prompt → ParseResult → CommandEnvelope on ICommandQueueParser describes intent only (envelopes + flags). ICommandProcessor<TContext> and CommandQueueRunner<TContext> own runtime behavior later.
Repo bootstrap
| Item | Value |
|---|---|
| GitHub org/repo | `Novolis-Platform/novolis-commands` |
| Default branch | `main` (matches [novolis-template-dotnet CI](d:\novolis\novolis-template-dotnet\.github\workflows\ci.yml)) |
| Scaffold | GitHub **Use this template** on [novolis-template-dotnet](https://github.com/Novolis-Platform/novolis-template-dotnet) |
| Local path | `d:\novolis\novolis-commands\` (alongside [novolis-math](d:\novolis\novolis-math)) |
| Solution | `Novolis.Commands.slnx` with `/src` and `/tests` folders per [frank-naming-and-structure.md](d:\novolis\novolis-governance\docs\frank-naming-and-structure.md) |
Copy/adapt from template + novolis-math:
Directory.Build.props,Directory.Packages.props,global.json(SDK10.0.100, MTP test runner).github/workflows/ci.yml+release.yml→ reusablenovolis-workflows(dotnet-ci,dotnet-publish-nuget).novolis/packages.json— one entry per packable project (same pattern as novolis-math/.novolis/packages.json)docs/{getting-started,design,release}.md— design doc describes parse/queue/interrupt boundary- Initial versions:
0.1.0-preview.1per package (matches novolis-messaging)
Post-scaffold (manual/org): set default branch main, enable OIDC publish, add novolis-registry entries after first release.
Package layout and dependencies
flowchart BT
Abstractions[Novolis.Commands.Abstractions]
Engine[Novolis.Commands.Engine]
Queueing[Novolis.Commands.Queueing]
Testing[Novolis.Commands.Testing]
DI[Novolis.Commands.DependencyInjection]
Engine --> Abstractions
Queueing --> Abstractions
Testing --> Abstractions
DI --> Engine
DI --> Queueing
DI --> Abstractions| Package | Responsibility |
|---|---|
| **Abstractions** | All public contracts from your spec: `ICommandEngine<TContext>`, `ParseResult`, `CommandEnvelope`, `CommandId`, `CommandPriority`, failures, candidates, `BuiltInCommands`, `ICommandQueue`, `ICommandProcessor<TContext>` |
| **Engine** | Parsing only — registry lookup, tokenization, built-in phrase detection, ambiguity detection |
| **Queueing** | `ChannelCommandQueue`, `CommandQueueRunner<TContext>` (channel hidden behind `ICommandQueue`) |
| **DependencyInjection** | `ServiceCollectionExtensions` — register queue, engine, optional runner factory |
| **Testing** | Fakes/builders for consumers and this repo’s tests: in-memory queue, `CommandRegistryBuilder`, sample `TContext` helpers |
Project references (no circular refs): Engine and Queueing never reference each other; DI wires both.
Abstractions (your API verbatim)
Implement types exactly as specified in Novolis.Commands.Abstractions (new):
ParseResultwithSucceeded/FailedfactoriesCommandIdusingGuid.CreateVersion7()(.NET 10)CommandEnvelopewith interrupt/cancel flags andIReadOnlyDictionary<string, object?> ArgumentsParseFailure/ParseFailureCode/CommandCandidateBuiltInCommands:system.belay-that,system.clear-queue,system.repeat-last
Keep Abstractions free of Microsoft.Extensions.* and System.Threading.Channels so it stays a thin contract package.
Engine design (parse only, no domain execution)
Registry model (data, not behavior)
// Engine — illustrative
public sealed record CommandDefinition(
string Name,
string? ContextWord,
IReadOnlyList<string> Verbs,
IReadOnlyList<CommandArgumentDefinition> Arguments);
public interface ICommandRegistry
{
IReadOnlyList<CommandDefinition> GetAll();
}- Built-ins live in Engine as phrase → envelope mapping (no processor calls):
"belay that"→BuiltInCommands.BelayThat,Emergency,InterruptsCurrentCommand = true"clear queue"/"repeat last"→ correspondingBuiltInCommands.*with appropriate flags (document in design.md)- Domain commands (e.g.
helm.set-heading) are registered by the host, not hardcoded in Engine — keeps Engine free of game/sim domain.
Context hook
public interface ICommandContextResolver<TContext>
{
string? GetActiveContextWord(TContext context);
// v0: optional alias map for future; can return empty dictionary
IReadOnlyDictionary<string, string> GetAliases(TContext context);
}CommandEngine<TContext> : ICommandEngine<TContext> depends on ICommandRegistry + ICommandContextResolver<TContext>.
Parse pipeline (deterministic, synchronous internally)
- Normalize: trim;
EmptyPromptif whitespace-only - Built-in phrase table (case-insensitive, whole prompt) → success envelope
- Tokenize on whitespace
- Resolve optional leading context word (explicit token or
GetActiveContextWordwhen omitted) - Match verb token(s) against registry for that context → 0 =
UnknownCommand, 1 = continue, 2+ =AmbiguousCommand+Candidates(confidence/reason from simple heuristics in v0) - Parse trailing tokens into arguments (
CommandArgumentDefinition: name, kindInteger/String, required flag) - Return
ParseResult.Succeeded(envelope)— never callICommandProcessoror mutate world state
v0 argument parsing (minimal)
For helm heading 270:
- Registry entry:
Name = "helm.set-heading",ContextWord = "helm",Verbs = ["heading"], one int argheading - Parser reads last token as int →
Arguments["heading"] = 270
Invalid/missing args → MissingArgument / InvalidArgument.
Ambiguity (test-ready, not required for helm tests)
Support AmbiguousCommand when multiple definitions share a verb across contexts or alias collision. v0 heuristic: fixed confidence scores from match quality (exact verb > alias > active context boost). Include one Engine test with two overlapping definitions (your "fire" example) to lock behavior.
Queueing
Implement in Novolis.Commands.Queueing:
ChannelCommandQueue— exactly yourChannel.CreateUnboundedsettings; only type that touchesChannel<CommandEnvelope>CommandQueueRunner<TContext>— your sketch; v0 additions:- Dispose linked
CancellationTokenSourceafter each command completes - On
command.CancelsQueuedCommands, document as processor/queue policy in v0 (runner does not drain channel yet unless you want a follow-up hook — keep v0 aligned with your snippet)
DependencyInjection
Novolis.Commands.DependencyInjection/ServiceCollectionExtensions.cs:
public static IServiceCollection AddNovolisCommands<TContext>(
this IServiceCollection services,
Action<CommandRegistryBuilder>? configureRegistry = null,
ServiceLifetime lifetime = ServiceLifetime.Singleton)Registers:
ICommandQueue→ChannelCommandQueueICommandRegistry(built fromCommandRegistryBuilder+ built-ins merged in Engine)ICommandContextResolver<TContext>(must be supplied by host — no guess default)ICommandEngine<TContext>→CommandEngine<TContext>
Optional: AddCommandQueueRunner<TContext>() registering a factory/hosted-service wrapper — defer if not needed for v0 milestone.
Package refs: Microsoft.Extensions.DependencyInjection.Abstractions only.
Testing package and test projects
Governance: TUnit only (naming.md) — adapt your examples from FluentAssertions to TUnit:
await Assert.That(result.Success).IsTrue();
await Assert.That(result.Command!.Name).IsEqualTo("helm.set-heading");| Test project | Covers |
|---|---|
| `tests/Novolis.Commands.Engine.Tests` | Your three parse tests + built-in belay + empty prompt + ambiguity |
| `tests/Novolis.Commands.Queueing.Tests` | Enqueue/read order; belay cancels in-flight via runner + test processor that honors CT |
Engine test setup (domain definitions in test, not product code):
var registry = new CommandRegistryBuilder()
.Add("helm.set-heading", context: "helm", verbs: ["heading"],
CommandArgumentDefinition.Integer("heading", required: true))
.Build();
var engine = new CommandEngine<TestContext>(registry, resolver, builtInMatcher);Novolis.Commands.Testing exports CommandRegistryBuilder, RecordingCommandQueue (implements ICommandQueue, no channel leak), TestCommandContext with ActiveContextWord.
End-to-end flow (v0 milestone)
sequenceDiagram
participant App
participant Engine as CommandEngine
participant Queue as ICommandQueue
participant Runner as CommandQueueRunner
participant Proc as ICommandProcessor
App->>Engine: ParseCommandAsync(prompt, context)
Engine-->>App: ParseResult
App->>Queue: EnqueueAsync(envelope)
Runner->>Queue: ReadAllAsync
Queue-->>Runner: CommandEnvelope
Runner->>Proc: ProcessAsync(command, context, ct)
Note over Runner,Proc: Interrupt cancels linked CTS onlyFiles to create (high level)
novolis-commands/
Novolis.Commands.slnx
Directory.Build.props
Directory.Packages.props
global.json
.novolis/packages.json
.github/workflows/{ci,release}.yml
docs/{getting-started,design,release}.md
src/
Novolis.Commands.Abstractions/*.cs
Novolis.Commands.Engine/{CommandEngine,CommandRegistry,CommandRegistryBuilder,BuiltInCommandMatcher,CommandParser}.cs
Novolis.Commands.Queueing/{ChannelCommandQueue,CommandQueueRunner}.cs
Novolis.Commands.DependencyInjection/ServiceCollectionExtensions.cs
Novolis.Commands.Testing/{CommandRegistryBuilder,RecordingCommandQueue,TestCommandContext}.cs
tests/
Novolis.Commands.Engine.Tests/
Novolis.Commands.Queueing.Tests/Verification
cd d:\novolis\novolis-commands
dotnet build
dotnet testCI on push/PR to main should pass via reusable workflow (no local secrets required for build/test).
Out of scope (explicit v0 boundaries)
- No
ICommandProcessorimplementations for helm/tactical/admin (apps bring those) - No NLP, fuzzy matching beyond simple alias table
- No confirmation flow execution (
RequiresConfirmation— parse-only failure code is fine) - No integration with novolis-messaging channels (separate bounded command queue abstraction)
- No changes to simulation/physics stack (library-boundaries)
Optional follow-ups (after v0 merges)
novolis-registrypackage manifest for all five packages- Governance table row for
novolis-commandsinfrank-naming-and-structure.md - Hosted-service wrapper for
CommandQueueRunner+ sample insamples/