Changelog
All notable changes to this project are documented here. The format follows Keep a Changelog, and the project follows semantic versioning.
0.1.0 (2026-09-22)
Added
- The measurement engine:
run()over a session’s events, aRegistryofDetectorobjects, andreport()over the rows it produces. - Readers for Claude Code transcripts and Codex rollouts, behind one event schema and one
iter_sessions(root, runtime, since). - The Bash decomposition every shell detector shares: compounds, pipelines, heredocs, substitutions and continuations, parsed once per command.
- Six generic detectors: whole-file cat, unfiltered find, no-verify, secret in a write, compaction, and a model switch mid-session.
- A
ruleprobe reportcommand, grouping by detector, by repository or by stance. - An extension point for declarative detectors:
register_compilerandfrom_spec. - A declarative detector format, so a rule can be measured without writing Python: one entry
of
id,rule,event,whenand an optionalgate, compiled byruleprobe/matchers.pyinto the sameDetectora Python detector builds. - Matchers for a tool name, an argument, a Bash command through the shared parse, a
gitcall, an environment assignment, a text body, an assistant message and a raw event kind, composed withany,allandnot; and three that read a whole session,order,absentandchange. ruleprobe/declarative.py: a strict, minimal YAML subset with a line number and a reason on every refusal, and the same objects from a.jsonfile. No third-party dependency.- Detector files found without a flag:
.ruleprobe/detectors.yamlat the root of the repository you are in, and~/.config/ruleprobe/detectors.yamlfor the user.--detectorsnames one,--no-configskips discovery. ruleprobe report --rules <dir>: markdown rule files bind to the detectors in their front matter, and the report says which rules are measured, which are dark byopt_out, and which are unmeasured - the gap an author cannot otherwise see.ruleprobe/detectors/common.yaml: the six shipped detectors written as data, with a test asserting they produce identical hits to the Python reference over the corpus and the fixture transcripts.- A worked example that runs from a clone:
docs/rules/anddocs/example-session.jsonl. - A labelled corpus, shipped as package data at
ruleprobe/corpus/: six synthetic sessions in both transcript shapes, every interesting event labelled by hand inlabels.yamlwith the detectors that should fire on it, and a deliberate near-miss beside each - acatof a range, a filteredfind, a push after the gate ran, a heredoc withrm -rfin its body as text. Every shipped detector carries at least five positives and five negatives. ruleprobe corpus, andruleprobe.validity()behind it: per-detector precision, recall and F1 with their counts, a total,--json,--corpus DIRfor a corpus of your own, and a non-zero exit under--floor 0.9. The floor is this repository’s CI gate, not a runtime failure: nothing inruleprobe reportreads it.ruleprobe report --validity: each detector’s corpus precision and recall beside its row. Off by default, because the two numbers belong to the detector rather than to the run.- An optional
examples:block on a declarative detector -fire:andskip:lists of minimal cases, eachbash:,event:orevents:- so a detector of your own can be scored without a corpus. A detector with neither saysno examplesrather than a number, and the floor steps over it. .github/workflows/ci.yml: the unit tests on 3.9 and the newest 3.x,compileall, andruleprobe corpus --floor 0.9.
Changed
- A malformed detector entry is a finding with a file, a line and a reason, printed under the report; the entry is skipped and every other detector still runs.
- The README no longer says detector validity is unmeasured; it prints the corpus table instead, and a test asserts the README quotes it byte for byte.
Fixed
Every finding of the pre-release review, #2,
each with the reviewer’s own input as a regression test in tests/test_findings.py.
- The shell parse.
cat <<\EOFis a heredoc header, so its body is no longer parsed as commands and no longer yields a falseunfiltered-findhit; a heredoc terminator must be the delimiter alone, as bash has it, and only<<-may have it indented; a command that does not tokenize isunparsedrather than invisible to every matcher. - The denominator. A detector that raises leaves the denominator of that detector only, instead of taking the whole row out of every other detector’s evidence, and a row is never counted twice in the preamble.
- Errors are counted.
iter_sessions(errors=[...])collects every transcript that raised or held no session, andruleprobe reportprints the count. The directory walks follow symlinks. - Matchers.
arg: {equals: ...}no longer raises on a list-valued field;absentover a session with no events is not a hit;command: {contains: ...}is a substring; a list ofcommand: {regex: ...}patterns is alternatives;order: {within: N}does not spend its budget ontool_resultevents;path_globis matched as a path, with*stopping at a/,**crossing one, and a relative pattern matching an absolutefile_path. - The parser. An escaped quote inside a double-quoted string no longer truncates it at a
#;[a: b]is a one-key mapping and not a tuple;yes,no,on,offand a leading-zero number are refused with a line and a reason rather than read one way here and another by YAML. - The readers. A runtime is chosen by parsing the first line, not by searching it for
"session_meta"; a streamed partial text block is one message that grows, not two; a Codex user message is auser_promptandfinalis derived there as it is on Claude Code, so a detector means the same thing on both.
Changed
iter_sessionstakes anerrorslist;measure,run,report,Registryanditer_sessionsare otherwise unchanged in signature.report_data(rows, ...)is new and is whatreport()renders andruleprobe report --jsonprints, so the table and the JSON cannot disagree about a denominator, a fold or a note.--jsonnow prints that object with the rows underrows, and exits non-zero on an empty root as the table does.ruleprobe report --stance dimension=variant, repeatable: agate:block was unreachable from the command line, so a gated detector never fired and--by stancecould only print(no stances).ruleprobe detectorsnames the stance each gate is waiting for.--since 2024is refused rather than silently read as 2024 days back.Registry.renameremoves the detector it renamed.- A
user_promptevent carries the prompt’stexton both runtimes. README.mdandruleprobe/detectors/common.yamlno longer claim the shipped detector file uses every matcher; they name the ten it uses and the four it does not.