feat(rules): add rules test --json machine-readable result contract - #52
Open
ronheichman wants to merge 1 commit into
Open
feat(rules): add rules test --json machine-readable result contract#52ronheichman wants to merge 1 commit into
rules test --json machine-readable result contract#52ronheichman wants to merge 1 commit into
Conversation
ronheichman
force-pushed
the
feat/rules-test-json-result
branch
2 times, most recently
from
September 7, 2026 16:27
a19c4a3 to
1b88204
Compare
Add an optional --json mode to `numbat rules test` that emits a versioned NDJSON result stream on stdout: one event_result object per successfully evaluated fixture line plus one terminal summary object. The stream distinguishes findings (with enforcement eligibility and rule version), malformed input, evaluator failure, and shell-parse coverage as separate result classes, so a downstream consumer never has to parse human CLI text or infer a clean no-match from an empty stdout. The change extends the existing rules test seam and its evalFixture sibling; no parallel evaluator is introduced. rule.Engine gains an EvalDetailed companion to Eval that returns per-rule errors and shell-analysis diagnostics without changing Eval's error text or the existing tab-separated output. The legacy stdout format is byte-identical when --json is absent. The contract is documented in docs/schema/rules-test-result.v1.md and the CLI reference. Behavioral checks in rules_test_json_test.go exercise all five result classes end-to-end through runCLI. Built with Codex
ronheichman
force-pushed
the
feat/rules-test-json-result
branch
from
September 7, 2026 16:38
1b88204 to
8cb3f4f
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds an optional
--jsonmode tonumbat rules testthat emits a versioned NDJSON result stream on stdout — oneevent_resultper evaluated fixture line plus one terminalsummaryobject. The stream distinguishes the result classes a downstream consumer needs so it never has to parse human CLI text or infer a clean no-match from empty stdout:enforcement_eligible(mirroring the live-enforce gate including shell-safety), andvia(enginevssequence)findings: [](or field omitted)error.kind ∈ {decode, validate, scan}error.kindissequencewhen the tracker failed, otherwiseevaluation.shell_parse ∈ {ok, degraded, unusable},sequence_tracker_activeThe result-envelope schema versions independently of the wire schema (
rules-test-result.v1vsmodel.SchemaVersion="0.3.0"). Consumers must ignore unknown keys and treat unknown enum values as unrecognized rather thancompleted.Exit codes
0:summary.status == "completed"with a passed or unchecked assertion, delivered as a well-formed NDJSON stream terminated by exactly onesummaryobject.1:summary.status == "partial"OR a failed assertion, delivered as a well-formed NDJSON stream terminated by exactly onesummaryobject. This is a successfully delivered result, not a delivery failure.2: one of two cases that share this exit code and are distinguished by inspecting stdout and stderr:event_resultor the terminalsummaryreturned an OS write error (ENOSPC, read-only descriptor); the process caught it, wrote awrite rules-test-result:diagnostic to stderr, and short-circuited the remainder of the stream. Any partial stream on stdout is not terminated by asummary.rules test --jsonwithout--fixtureprints a usage diagnostic to stderr and exits 2 with empty stdout. This is existing CLI behavior, not new to--json.Exit code alone is not sufficient to identify delivery failure. A usage/setup failure exits 2 before any stream, and abnormal process termination (SIGPIPE on a real broken pipe, SIGKILL, panic, host death) may end the process before the handled exit-2 path runs and before a terminal
summaryis written — the observed exit status in that case is signal-encoded (for example a Python subprocessreturncodeof-13for SIGPIPE), not2. This PR does not add a signal handler, and no envelope is emitted after an abnormal termination.Consumer rule. Only a well-formed NDJSON stream terminated by exactly one
summaryobject AND an exit status compatible with that summary proves the documented outcome. A missing terminalsummary, a malformed or truncated final JSON line, or an abnormal/signal-encoded termination cannot prove a clean no-match and must be treated as an indeterminate delivery outcome — not silently reclassified as delivery failure, and not silently accepted as success.Why
Guardian and other downstream consumers of
numbat rules testtoday have to parse tab-separated stdout and infer a clean no-match from empty output; there is no way to distinguish a clean no-match from an unusable shell parse, from a per-rule CEL runtime error, from malformed input, from a scan-time truncation, or from a broken output channel. This blocks Guardian's ability to make an action-level enforcement decision from the direct evaluator without a parallel evaluator or fragile text parsing.How
Extends the existing
rules testseam.rule.Enginegains an additiveEvalDetailedcompanion toEvalthat returns per-rule errors and shell-analysis diagnostics without changingEval's error text or the existing legacy tab-separated output. Every JSON write goes through a singleemitclosure that flips adeliveryFailedsentinel on any encoder error and short-circuits further stdout writes, so a stdout failure at either anevent_resultor the terminalsummarypropagates to a distinct exit code (2) rather than a silent success.Legacy stdout is byte-identical when
--jsonis absent — verified against all five shipped fixtures (secrets,sequence,custom,benign,exfil).No new subsystem, no parallel evaluator, no wire-schema bump, no new dependencies.
Contract & docs
Authoritative machine-readable contract at
docs/schema/rules-test-result.v1.md. CLI reference updated atdocs/cli.md. The schema doc pins exact counter semantics (matchesmay exceedevents_evaluatedwhen several direct rules match one event, and each sequence finding adds tomatcheswithout adding toevents_evaluated), delivery-failure semantics, and the exit-code table.Tests
cmd/numbat/rules_test_json_test.goexercises the result classes end-to-end throughrunCLIplus adversarial cases:tags[N]-out-of-bounds inputs), asserting both errors ANDerror.kind: "sequence"classification ANDsummary.status: "partial"AND exit 1matches=2, events_evaluated=1; partial final event still counts inevents_evaluated; sequence findings add tomatcheswithout adding toevents_evaluatedevent_resultwrite returns exit 2 with a stderr diagnosticsummarywrite (after event_results succeeded) also returns exit 2numbat rules test --jsonwith stdout redirected to/dev/full— exits nonzero (Linux only; skipped when/dev/fullunavailable)uncheckedIndependent review
This PR was reviewed by two independent Perplexity Computer subagents (adversarial, evidence-only, verifying every claim against the exact head). A first-pass review found three concrete defects at head
5b3df36:enc.Encodeerrors — writing to a broken descriptor returned exit 0 with no stderr, while the legacy path returned exit 1 with a diagnostic.matches <= events_evaluated.All three were reproduced against the compiled CLI with real OS file descriptors and synthetic read-only inputs, then fixed as one package on this branch. A second-pass independent review of the fix head confirmed PASS with no blocking findings, plus a small set of non-blocking polish items that were applied in a follow-up amendment; a third review confirmed the polish delta introduces no behavior change and all gates remain green.
No upstream GitHub reviewer has approved this PR yet.
Compatibility
--jsonis absentrule.Engine.Evaland its error text unchanged; existing callers unaffectedmodel.SchemaVersionstays0.3.0)rules-test-result.v1; future breaking changes will bump tov2Gates
Local:
go build ./...,go test ./... -count=1,go test -race ./cmd/numbat/... ./internal/rule/... -count=1,go vet ./...,gofmt -l .empty,go mod tidyclean,golangci-lint run0 issues,go generate ./rulesno drift. Compiled-CLI write-failure to/dev/fullreturns exit 2 as designed; legacy mode continues to return exit 1 on the same failure.Not in this PR
Guidance
Please skim
docs/schema/rules-test-result.v1.mdfirst — that is the load-bearing artifact. The Go code is the smallest supported implementation that satisfies the contract.Built with Codex