Rulealize.Cli 0.11.0

dotnet tool install --global Rulealize.Cli --version 0.11.0
                    
This package contains a .NET tool you can call from the shell/command line.
dotnet new tool-manifest
                    
if you are setting up this repo
dotnet tool install --local Rulealize.Cli --version 0.11.0
                    
This package contains a .NET tool you can call from the shell/command line.
#tool dotnet:?package=Rulealize.Cli&version=0.11.0
                    
nuke :add-package Rulealize.Cli --version 0.11.0
                    

Rulealize.Cli

Works with Rulealize rule sets and the vocabularies they draw on, from a command line.

dotnet tool install -g Rulealize.Cli
rulealize restore <rule set> fetch what uses and requires name, through the whole graph, into folders
rulealize plugins what a folder of vocabularies provides, or why it provides nothing
rulealize check <rule set> does this document compile against that folder
rulealize moves <rule set> what is legal from a position
rulealize apply <rule set> <input> apply one, and write the state it reached
rulealize play <rule set> walk it, holding the position in memory
--plugins <folder>   where the vocabularies are        (default 'plugin')
--rulesets <folder>  where fetched components go and are read from
                     (default 'component'; your own sit beside the document)
--state <file>       the position to start from        (default the rule set's own)
--limit <n>          candidates GetValidInputs may try (default 10000)
--outcomes <n>       outcomes GetOutcomes may return   (default 64)
--outcome <file>     what was drawn, for an input that resolves something nobody chose
--write              amend --state in place instead of writing to standard output
--json               moves, as the runtime writes them

--out named the plugin folder in 0.1.0 and is --plugins from 0.2.0. The old name is refused rather than ignored, so a command line that still carries it says so.

Requires net10.0 and the .NET SDK, which a dotnet tool implies.

Trying a rule set

$ rulealize restore reversi.json
10 plugins -> plugin
'reversi.json' compiles against it.

$ rulealize play reversi.json
reversi@1.0.0 from the initial state
Choose by number. 'state' prints the position, 'q' stops.

    1. place(at: e6)
    2. place(at: f5)
    3. place(at: c4)
    4. place(at: d3)
> 4

    1. place(at: c5)
    2. place(at: c3)
    3. place(at: e3)
>

Two commands, no host program, and no file written but the plugin folder.

Where the state lives

Nowhere, until you say. ApplyToState is a function of the state it was handed, and a RuleContext holds a rule set and no position at all, so every command here has to be told where a position comes from and told what to do with the one it produced. There is no hidden current game.

told nothing the rule set's own state.initial, which is the document's opening position and not this tool's invention. The line above the answer says which was used
--state <file> that document
the new state standard output, so that redirecting gives a state document and nothing else
--write amend --state in place instead, which is one file name for a sequence of moves rather than one per move
play memory, and then gone
rulealize apply reversi.json "place(at: d3)" > s1.json
rulealize moves reversi.json --state s1.json

Naming an input

An input is named the way moves prints it, and the round trip is the runtime's own: the legal inputs are enumerated and the one the text names is asked for its document, so what can be named is exactly what was offered. Nothing is built from the text — an input the rule set does not offer cannot be written into existence here.

The arguments are matched by name, so they may be written in any order and spaced however you like. place(at: d3), place(at:d3) and, where an input takes two, assign(shift: mon-am, who: ann) and assign(who: ann, shift: mon-am) are one move named four ways rather than four names, only one of which happens to be the runtime's.

$ rulealize apply reversi.json "place(at: a1)"
'place(at: a1)' does not name an input the initial state offers. 'place' is, but not with
those arguments.
'place' is offered as:
  place(at: e6)
  place(at: f5)
  place(at: c4)
  place(at: d3)

Which leaves the one thing this cannot express — an input the rule set ought to refuse, and checking that it does — to --input <file> and a written document.

When nobody chooses the next state

Not every rule set settles where it lands from the input alone. A card comes off a deck and the input has more than one state it could arrive at, so apply will not pick one — doing that would be this tool inventing an answer nobody enumerated, and it is the one thing the runtime is built not to do. It says what could have happened instead:

$ rulealize apply blackjack.json "dealSeat"
'dealSeat' resolves something nobody chose, so applying it takes an outcome as well.

What could happen, most likely first:
  A (0.077)
  2 (0.077)
  …

Write the one that happened as a rulealize/outcome/v1 document and pass it
with --outcome <file>. 'play' asks rather than stopping.
// what.json
{ "$schema": "rulealize/outcome/v1", "ruleSet": "blackjack@1.0.0",
  "input": "dealSeat", "draws": ["9"] }
rulealize apply blackjack.json "dealSeat" --outcome what.json > s1.json

An input and an outcome settle a transition exactly, so that pair reproduces s1.json however long afterwards — which is what makes a redirected sequence of them a record rather than a re-run.

play asks, because it has somebody there to ask:

    1. dealSeat
>1

  Nobody chooses what happens next. Which did?
    1. A (0.077)
    2. 2 (0.077)
    …
>9

Nothing here rolls anything. The alternatives come from GetOutcomes, and choosing one is the caller's — a file in apply, a person in play, a random number generator in a host that wants one. That is the same arrangement either way, which is why neither command has a --seed.

A rule set with no chance in it notices none of this. An input that draws nothing has exactly one outcome, of probability one, so apply needs no --outcome and play never asks. Both commands call GetOutcomes regardless; there is no branch here for chance, because there is none in the runtime either.

moves

The moves go to standard output, one per line, and everything about them to standard error. A count is not a move.

$ rulealize moves reversi.json
reversi@1.0.0 from the initial state (ongoing)        <- standard error
place(at: e6)
place(at: f5)
place(at: c4)
place(at: d3)
4 legal inputs, 65 candidates evaluated               <- standard error

GetValidInputs stops at a limit rather than searching for as long as it takes, so a short list can mean the position or it can mean the limit. It always says which, because the worst failure this command has is a truncated answer that looks complete.

truncated at 2 candidates -- pass --limit to raise it

The folder is the seam

Every command works against a folder of assemblies, because that is the arrangement a deployed application has: they are swept, instantiated and registered, and nothing else about a vocabulary is consulted. Two things follow, and both are the point.

A vocabulary nobody publishes joins in by being copied there. restore fills the folder from nuget.org; a plugin still being written fills it with dotnet build and a copy. From that moment the two are indistinguishable, which is what makes the folder worth being the seam.

What goes wrong here goes wrong in production too. A class left internal will not load either place. The difference is that here something is willing to say so.

A rule set that holds other rule sets

uses names the rule sets a document holds, by identifier, and the runtime is handed their documents. They come from two folders, and the distinction is the point:

beside the document that holds it one you wrote. Source, in your repository, and nothing here writes to it
component/, or --rulesets <folder> one somebody published. Fetched, in a folder this tool owns and your history ignores

Yours wins. A document beside the one that holds it shadows a published one of the same identifier, because a component you are writing is the one you meant — and it is not fetched over, or fetched at all.

That split is why restore does not write next to your sources. No package manager puts a dependency in src, and for a rule set published from an ordinary repository the folder next to the document is also the folder being packed — so fetching there meant the next dotnet pack shipped somebody else's rule sets inside yours. Until 0.9.0 it did.

$ rulealize check process.json
'process.json' compiles against 'plugin' (5 vocabularies, 33 operations).
holding 1 rule set:
  counter@1.0.0 ('counter.json')

Which document answers to an identifier is read out of the document and not off its file name. id is what the document declares and what uses names; a file's name is nobody's business but the author's. Files in the folder that are not rule sets — the states and the outcomes written beside them — carry no id and are passed over, the way a plugin sweep passes over an assembly it cannot use.

An identifier has one document in a folder. Two of them declaring it is a refusal naming both, because taking whichever the file system listed first would settle something no document said, and settle it quietly.

No version is chosen while reading a folder. With one document per identifier there is nothing to choose between. Choosing happens where a version could be had, which is restore, and whether what is here satisfies what asked for it is Rulealize's reading either way:

'process.json' does not compile against 'plugin':
  /uses[0]/version: this rule set needs counter ^1.0 as c, but 2.0.0 was supplied.

Holding a rule set you did not write

A published rule set is a package whose ruleset folder holds one document, and its identifier is the identifier of that package. So uses names a package, and nothing has to be looked up anywhere to find it:

"uses": [
  { "ruleSet": "Rulealize.RuleSet.Request", "version": "^1.0", "as": "req" }
]

restore fetches what that names, through the whole graph, into component/:

$ rulealize restore roster.json
  Rulealize.RuleSet.Request@1.0.0 -> component\Rulealize.RuleSet.Request.json
1 rule set -> component
holding 1 rule set:
  Rulealize.RuleSet.Request@1.0.0 ('component\Rulealize.RuleSet.Request.json')
  ...
7 plugins -> plugin
'roster.json' compiles against it.

component/ and plugin/ are both this tool's and belong in your ignore file. Neither holds anything you wrote.

as is not optional for one of these. An alias defaults to the identifier and may not contain a ., so an entry naming a package-shaped identifier without as is refused, with a message about a key you did not write.

Which version is Rulealize's answer, not this tool's. RuleSetRequirement.Choose takes the lowest published version satisfying every constraint that named it — the same rule a requires is resolved by, and for the same reason: the document resolves to the same folder next year, when three more versions have shipped. Moving to a newer one means changing what the document asks for, and restoring it is not the moment to do that.

What is fetched is checked as it arrives, against both halves of what was asked:

'roster.json' holds Rulealize.RuleSet.Request ^2.0 as req,
and 'Rulealize.RuleSet.Request.json' is Rulealize.RuleSet.Request@1.0.0.

A document already there is never written over, in either folder. If one that restore put in component/ is now the wrong version, delete it and run again — or delete the folder, which is the whole of what it means for it to be derived.

A rule set nobody publishes is not a mistake. A component still being written is a file beside the document that holds it, and it is found without being fetched, mentioned or configured. That is the case the two folders exist to keep separate: the plugin folder can be one folder because an unpublished vocabulary arrives there as a build output, and an unpublished rule set is source.

restore fetches what the whole graph requires. requires is the whole of what one document may draw on, so a folder assembled from the holder's alone is one its components do not compile against:

$ rulealize restore process.json
holding 1 rule set:
  counter@1.0.0 ('counter.json')
  Rulealize.Plugin.Arithmetic 1.0.0
  ...
5 plugins -> plugin
'process.json' compiles against it.

Nothing else here changes. A held rule set's inputs are offered as alias.input, so moves prints them, apply takes them and play walks them like anything else; a composite's case is still one state document; and a document that holds nothing reads no folder at all.

rulealize plugins

plugin
  13 assemblies, 13 vocabularies

  My.Vocabulary 1.0.0  (my)
      my.length                    expression

  Rulealize.Plugin.Binding 1.0.0  (bind)  @
      bind.let                     expression
      bind.local                   expression
  …
69 operations in total.

The operations are read back off the loaded runtime — RuleRuntime.Operations — and not off anybody's source. Nothing declares what a vocabulary provides: the operations exist only as the calls it made while registering, so this list is the sole account of them, and it is the one a rule set will be compiled against. An operation that was written but never registered is missing from it, which is the cheapest way there is to find a forgotten AddExpression.

When a folder holds assemblies and yields no vocabulary, the runtime does not say why — a sweep is speculative, since pointing one at an application's own output must not be an error, so a type it cannot use is passed over in silence. That is right for the runtime and useless here, where somebody asked on purpose. So this asks again, itself:

notpublic
  1 assembly, 0 vocabularies

Nothing loaded, and the runtime does not say why: a folder sweep passes
over what it cannot use. What is in there:

  MyVocabulary.Vocabulary
      is not public. A sweep only takes public types.

The conditions are public, not nested, not abstract, and a parameterless constructor.

rulealize check

The same compile restore ends with, on its own and without the network.

'probe.json' compiles against 'plugin' (13 vocabularies, 69 operations).
'typo.json' does not compile against 'plugin':
  /inputs/measure/effects[0]/value: 'my.lenght' is not an operation any loaded plugin
  provides. Check the rule set's 'requires'.

Restoring is something a document needs once. This is the question asked after every edit, and a loop that reached nuget.org to answer it is a loop nobody would run.

rulealize restore

rulealize restore reversi.json
  Rulealize.Plugin.Arithmetic 1.0.0
  Rulealize.Plugin.Binding 1.0.0
  …
10 plugins -> plugin
'reversi.json' compiles against it.

Why

A rule set already says what it needs. requires names every vocabulary the document draws on and which versions of each will do, in a form the runtime reads to refuse a document it cannot run — so transcribing that list into dotnet add package lines by hand is copying a dependency list that already exists, in a place where it can drift from the original.

What decides what

Rulealize decides which versions. PluginRequirement.ReadFrom reads the constraints and PluginResolution.Resolve chooses; both are pure, and both are the code CreateContext checks a loaded plugin against. A second reading of ^1.0 living in this tool would eventually differ from the runtime's, and it would show up as a folder this assembled and the runtime refused — a fault with no symptom until the moment it is too late to be useful.

Two rules come with that, and neither is this tool's to change:

the lowest satisfying version wins so that the same document restores to the same folder after three more releases. Moving to a newer one means changing what the document asks for, and restoring a document should not rewrite it
constraints on one plugin are met together a folder cannot hold two versions of one assembly

nuget.org supplies the packages, and this does not talk to the registry at all. Restoring needs the published versions of a plugin the document already named, and the feed answers that; the registry's catalogue is for finding a plugin you could not already name.

The SDK does the fetching, with the versions pinned exactly so that delegating decides nothing. NuGet then gets target frameworks and a plugin's own dependencies right — which this could only approximate — and what lands in the folder is what dotnet publish would have put there, which is the arrangement a deployed application actually has.

What it will not do

Fetch a vocabulary nobody publishes. A rule set may name one, and it is a supported arrangement. There is nothing on the feed to fetch, so put the assembly in the folder yourself and restoring credits it and fetches the rest:

  My.Vocabulary (already in plugin)
  Rulealize.Plugin.State 1.0.0
  Rulealize.Plugin.TypeSchema 1.1.0
2 plugins -> plugin, 1 already there
'probe.json' compiles against it.

Whether the version there answers the constraint is Rulealize's reading — the same PluginRequirement.IsSatisfiedBy the runtime checks a loaded plugin against — for the reason no version choice is made here either.

A requirement neither the feed nor the folder can meet is still a refusal, naming what is missing and how many would have resolved.

Rewrite your document. Restoring means fetching what it asks for. Asking for something else is an edit, and an edit is yours.

The one assumption

A plugin identifier and its NuGet package identifier are the same string. That is a convention rather than something the runtime enforces — requires names a PluginManifest.Id, and where the package with that name lives is nobody's business but the publisher's — and it is the only thing this command assumes about where a plugin comes from.

Verification

The folder is only right if it runs the document, so the last thing restore does is load the folder and compile the rule set against it, the way an application will. It costs one compile and turns the packages downloaded into this rule set loads against what is now on disk.

If that fails, the folder is left where it was written and the compiler's own message is printed. A document that does not compile is still a document you may want the plugins for.

License

Apache-2.0.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

This package has no dependencies.

Version Downloads Last Updated
0.11.0 81 9/12/2026
0.10.0 97 9/1/2026
0.9.0 103 8/31/2026
0.8.0 103 8/30/2026
0.7.0 109 8/29/2026
0.6.0 106 8/29/2026
0.5.0 109 8/23/2026
0.4.0 108 8/22/2026
0.3.1 120 8/16/2026
0.3.0 131 8/16/2026
0.1.0 116 8/12/2026