Architecture Spine: ruleprobe
Every rule below is tagged. [ADOPTED] means the 0.1.0 code already does it (implemented, 0.1.0).
[PROPOSED] means the rule binds v0.2.0 work that does not exist yet (planned, v0.2.0). Inferences carry
[ASSUMPTION: ...]. Rationale is in .memlog.md beside this file.
Design Paradigm
Pipes and filters, batch per session. iter_sessions streams sessions. Inside a session the event
list is materialised, parsed once, and handed whole to every detector, because session matchers
(order, absent, change) read the whole list. A detector is a filter from that list to hits. A row is
a session’s hits reduced to counts. The report and the validity scorer are two sinks over the same
filters.
flowchart LR
T[(transcripts on disk)] --> R[readers<br/>one per runtime]
R -->|Session with event list| A[shell.analyse<br/>once per session]
A -->|events, Context| D[detectors<br/>Python or compiled data]
D -->|Hit list| M[report.measure]
M -->|row| P[report / report_data]
C[(corpus: sessions + labels)] --> R2[same readers]
R2 --> A2[same analyse] --> D2[same detectors] --> V[validity: precision, recall, floor]
| Layer | Modules |
|---|---|
| Schema (leaf) | ruleprobe/events.py |
| Format (leaf) | ruleprobe/declarative.py |
| Parse | ruleprobe/shell.py |
| Engine | ruleprobe/registry.py, ruleprobe/detectors/ |
| Compilers | ruleprobe/matchers.py |
| Binding | ruleprobe/rules.py |
| Sources | ruleprobe/readers/ |
| Sinks | ruleprobe/report.py, ruleprobe/validity.py |
| Shell | ruleprobe/cli.py, ruleprobe/__main__.py, ruleprobe/__init__.py (re-exports) |
Inherited Invariants
| Inherited | From parent | Binds here |
|---|---|---|
AD-13: detector logic lives in ruleprobe | agent-harness spine | The harness vendors a pinned wheel and never forks detector logic. A ruleprobe release means a harness bump. Declarative detectors are read through this engine. Here: AD-9 declares what the harness imports; no ruleprobe change may require the harness to copy detector code. |
| AD-21: dependencies | same | Standard library first, on a Python 3.9 floor. Third-party code enters the harness only as a pinned wheel loaded by path. Here: AD-10; the wheel stays pure Python and importable from a zip. |
Read-only. A local AD that weakens either is a conflict to surface, not an override.
Invariants & Rules
AD-1: Dependency direction [ADOPTED]
- Binds: all modules.
- Prevents: a reader that imports a detector, a detector that imports a reader, or an import cycle that breaks the zip import the harness relies on.
- Rule:
- Imports point down the graph below. Nothing imports
cli. - Five upward imports are sanctioned, each inside a function or at the module’s tail, and no
others:
registry.from_specimportsmatchers;report._validity_noteimportsvalidity;registryimportsdetectors.commonat its tail to populateDEFAULT; and explain’s two helpers inreport,report._hit_keyimportingvalidityforhit_key(AD-6) andreport._redactimportingdetectors.commonforredact(AD-13). The explain pair is implemented under #51 and ships in v0.2.0. - A new module takes a place in this graph in the change that adds it, and the spine is updated.
detectors.catalog(v0.2.0, story 3.5) is a Python literal module under AD-9’s package data rule, and imports nothing.rulesimports it and compiles its entries withmatchers. Every other package module,cliincluded, reads catalog entries throughrules.catalog_detectors(), never by importingdetectors.catalog.
- Imports point down the graph below. Nothing imports
flowchart TD
cli --> report
cli --> registry
cli --> rules
cli --> validity
cli --> readers
validity --> readers
validity --> registry
validity --> declarative
validity --> events
report --> registry
rules --> matchers
rules --> registry
rules --> declarative
rules --> detectors_catalog[detectors.catalog]
matchers --> registry
matchers --> shell
matchers --> declarative
matchers --> events
detectors_common[detectors.common] --> registry
detectors_common --> shell
detectors_common --> events
registry --> shell
registry --> events
registry --> contract_data
shell --> events
readers --> events
Amended 2026-09-23: contract_data joins the graph as a leaf imported only by registry, which
exposes its fold map to report and validity through fold_map (#34)
Amended 2026-09-23: detectors.catalog is a leaf holding only literals, as its literals-only test
requires; rules, which already imports matchers, compiles the entries, so the planned
detectors.catalog --> matchers edge is dropped; cli lists catalog entries through
rules.catalog_detectors(), over its existing cli --> rules edge (#49)
Amended 2026-09-24: readers.gemini joins as the third module under readers, importing
events only, so the readers --> events edge covers it and the graph is unchanged (#55)
AD-2: The event schema is the one contract between readers and detectors [ADOPTED]
- Binds: FR-1, FR-2, FR-3, FR-4, FR-32; every detector and matcher.
- Prevents: a reader adding a field a detector then depends on for one runtime only, or a detector that works on one runtime and silently not on another.
- Rule:
ruleprobe/events.py’s module docstring is the schema: five kinds (assistant_text,tool_use,tool_result,user_prompt,compact), each withturnand the fields listed there. A reader emits nothing else.turnincrements on everyuser_prompt.finalis derived by the reader: the lastassistant_textbefore the nextuser_promptor the end. Every reader derives both the same way.- A detector reads fields through
input_of,text_ofand.get, never by index, and never branches on the runtime.Session.runtimeexists for rows, not for detectors. - A new kind or field is added in
events.pyfirst, in the same change as the reader that emits it. Adding one is a minor change; renaming, removing or changing the meaning of one is a breaking change under AD-9.
AD-3: One tool vocabulary across runtimes [ADOPTED for Bash and Agent; PROPOSED for file tools]
- Binds: FR-2, FR-3, FR-32.
- Prevents: two readers mapping the same action to different names, so one detector counts it on one runtime and misses it on another.
- Rule:
- Claude Code’s tool names and input keys are canonical:
Bashwithcommand,AgentandTask,Writewithfile_pathandcontent,Editwithfile_pathandnew_string. The list grows only with a shipped detector that reads the new name or key. - A reader translates at read time, in one module-level table in its own module.
ruleprobe/readers/codex.pymapsexec_commandtoBashandspawn_agenttoAgent. A translated input keeps the native key beside the canonical one, ascmdsits besidecommand. - A native tool with no exact canonical twin keeps its native name. It then matches no canonical detector, which under-counts (AD-4). A lossy mapping is never made to gain a hit.
- The third reader to map is Gemini CLI (PRD Q1, decided 2026-09-23). Spike RP-SP003 lists each of its file tools as exact or native before its reader is built; a native one stays native.
- [ASSUMPTION: the canonical file-tool list above is what
ruleprobe/detectors/common.pyreads today; Gemini CLI’s file tools are mapped to it or left native, decided in its story]
- Claude Code’s tool names and input keys are canonical:
Amended 2026-09-24: the Gemini CLI reader maps write_file to Write and replace to Edit
exactly, and run_shell_command to Bash only when the session’s .project_root is a POSIX
path; a Windows or unknown root keeps it native. invoke_agent and every other Gemini tool stay
native (#55)
AD-4: Detectors under-count rather than over-count [ADOPTED; negation implemented under #19, ships in v0.2.0]
- Binds: NFR-4, FR-5, FR-12, FR-16, FR-21; every detector and matcher.
- Prevents: a new matcher or detector that guesses on input it cannot read, so a report shows a hit that did not happen.
- Rule:
- Undecidable input yields no hit: a missing or wrongly typed field, a command the shell parse
skipped, or a command inside a substitution.
Parsed.skippedmarks a skipped command as unparsed, empty or overMAX_COMMAND. - Negation must not turn an undecidable input into a hit. In 0.1.0 it does
(#19): a segment matcher returns false for a
skipped parse, so both
notover it andabsentof it fire (historical, 0.1.0; implemented under #19, unreleased, shipping in v0.2.0). - The segment matchers are every
commandkey exceptregexandunparsed, everygitkey and everyenvkey. Over a command the parse skipped, a segment matcher returns undecided, and so does atextread ofsource: heredocs, because the parse is what finds heredoc bodies.ruleprobe/matchers.py’s module docstring states the same list. any,allandnotpass undecided through:notof undecided is undecided;anyis true on any true, else undecided on any undecided;allis false on any false, else undecided on any undecided. Awhenthat is undecided produces no hit.not,absentandordertreat undecided as no hit. Anabsentscope holding any undecided candidate and no true one yields no hit; anorderhit needsfirstandthenboth true.- The undecided rule changes counts for user detectors that use
notorabsent, so it ships as part of the 0.2 break (AD-9). - A new matcher lands with
fireandskipexamples, at least oneskipa deliberate near-miss, and its known misses listed in theruleprobe/matchers.pydocstring. - A new Python detector lists its known misses in its docstring, as
ruleprobe/shell.pydoes.
- Undecidable input yields no hit: a missing or wrongly typed field, a command the shell parse
skipped, or a command inside a substitution.
AD-5: The shared shell parse is the only Bash decomposition [ADOPTED]
- Binds: FR-5, FR-6, FR-12, FR-30.
- Prevents: two detectors splitting the same command differently, so
commandmatchers and Python detectors disagree about which segment a word is in. - Rule:
run()callsshell.analyse(events)once per session. Detectors readctx.bash, a list ofParsed. Matchers read the sameParsedthrough their environment.- No detector, matcher or reader tokenises a command itself. A
regexover the raw command text is a text read, not a decomposition, and is allowed. - The parse never runs, resolves or opens anything. A new shell capability lands in
ruleprobe/shell.pywith its test intests/test_shell.py, and its misses in the module docstring.
AD-6: The labelled corpus is the validity contract, and the floor is 0.9 [ADOPTED; third-runtime sessions PROPOSED]
- Binds: FR-23, FR-24, FR-26, FR-33, NFR-6, FR-16.
- Prevents: a label keyed differently from a hit, a detector shipped unscored, or a floor lowered to let a detector through.
- Rule:
- A label is keyed exactly as a
Hit:"<turn>:<tool_use_id>", or"<turn>:-"for a session hit.ruleprobe/validity.py’shit_keyandevent_keyare the only functions that build it; explain and label use them too. - The labels for a session are the whole truth for every detector they name.
- Shipped corpus sessions are synthetic, hand-authored native-runtime transcripts read through the real readers, so the corpus tests readers as well as detectors. No real transcript content.
- A session written by the FR-26 label command is an event-schema file,
<name>.events.jsonl, one event dict per line, whichvalidityloads without a runtime reader. It is the only other corpus session format. The loader takes each event’sturnandfinalas written and never re-derives them, so a label keyed to turn 37 still matches. [ASSUMPTION: a native transcript cannot be rebuilt from one event] - Every detector the package ships, catalog entries included, is scored by labels or
examples:and meets the floor in CI. An unscored third-party detector is shown as unscored and passes. - The floor stays 0.9. A detector under it is fixed or removed, with a changelog entry.
- Only the FR-26 label command writes into a corpus, and only under a directory the user names.
- A label is keyed exactly as a
AD-7: The declarative format is a closed YAML subset with JSON as its equal [ADOPTED]
- Binds: FR-8, FR-10, FR-11, FR-12, FR-16, FR-27.
- Prevents: a detector file that two parsers read differently, a YAML dependency, or a matcher key that loads and never fires.
- Rule:
ruleprobe/declarative.pyis the only parser. The subset in its docstring is closed: anchors, aliases, tags, block scalars, directives, merge keys, multi-document files,yes/no/on/offand leading-zero numbers are refused with a line number.- A document parses to the same objects from YAML or JSON.
- The matcher vocabulary in
ruleprobe/matchers.pyis closed: an unknown key is aDeclarativeErrorwith a line number. - Widening the subset or the vocabulary is a contract change under AD-9, with tests in
tests/test_declarative.pyortests/test_matchers.py. - A shipped Python detector that the matcher set can express keeps a declarative twin in
ruleprobe/detectors/common.yaml, andtests/test_equivalence.pyasserts hit-for-hit equality. Catalog detectors are declarative only.
AD-8: The package envelope: no dependency, no network, no write, no model, deterministic [ADOPTED; enforcing tests implemented under #37, ships in v0.2.0]
- Binds: NFR-1, NFR-2, NFR-3, NFR-5, NFR-7, NFR-8; all modules.
- Prevents: one feature quietly adding a dependency, a network call, a file write or an order-dependent count.
- Rule:
dependencies = []. Standard library only, no syntax or call newer than Python 3.9.- No module in the package imports
socket,urllib,http,subprocessor a model client. No command anywhere in ruleprobe calls a model. Drafting a detector from prose (FR-34) was withdrawn on 2026-09-23 and moved to the judge library tracked in #21, so ruleprobe has no drafting command and no boundary for one (PRD Q4). open()is for reading. The one write in the package is the FR-26 label command, under a directory the user names.report,detectors,corpusand the explain path write nothing.- The clock is read only to resolve
--since, inruleprobe/readers/__init__.py. - Every ordering in output comes from an explicit sort. Path lists are sorted before reading, as the readers do today. No count depends on dict, set or filesystem order.
- 0.2 adds tests that fail on a new dependency and that run
reportwith network and writes denied.
AD-9: The contract is versioned, once, in 0.2.0 [PROPOSED; fold on read ADOPTED]
- Binds: FR-27, FR-28, FR-29, FR-30, FR-31; agent-harness AD-13.
- Prevents: a stored row or a detector file read under the wrong schema, a renamed detector dropping its history, and a silent break in the harness.
- Rule:
- Schema version. The key is
schema_version; absent means 1. It sits on each detector entry and on each row andreport_dataresult. A detector file’s top-levelversionsets the default for its entries, and an entry’s own key wins. 0.1.0 ignores the top-levelversionthatcommon.yamlalready carries; from 0.2 it is read, and a value that is not a known schema version is a finding. An entry above the highest version the package knows is a finding; a row above it is excluded from counts and reported, as FR-20 excludes a row with norulesmap. The value is an integer, and 0.2.0 writes2(PRD Q9, decided 2026-09-23). [ASSUMPTION: key name and file-level default] - The 0.2 break carries the schema version, the fold map, the declared API below, and AD-4’s
undecided rule for
notandabsent, which changes counts for existing user detector files. - Fold map. One function resolves renamed ids for
report,report_dataand validity. It reads the union of the shipped map and the consumer’sRegistry(renamed=)map; the consumer’s entry wins on a clash. Chains resolve to their end; a cycle is an error when the registry is built.report_dataemits the effective map beside its rows, so stored JSON folds without the registry. The same function folds a row’scompliancemap (AD-11): a retired id’sopportunities,followedandundecidedsum under the current id, as its hits do. [ASSUMPTION: this is the persistence FR-29 asks for] - Shipped ids. A list of every detector id any release has shipped. A test fails when a listed id is neither registered nor folded.
- Package data. The shipped fold map and the shipped-id list are Python literals in one module,
as
SECRET_PATTERNSis, never a data file read beside__file__. A module literal loads through the import system the same way from a directory, a wheel or a zip onsys.pathon 3.9, and the import system loads it once per process, so building aRegistryadds no file I/O. A test asserts the module holds only literals. [ASSUMPTION: the module isruleprobe/contract_data.py] - Catalog. The shipped catalog’s entries are Python literals in
ruleprobe/detectors/catalog.pyunder the same rule, compiled withcompile_detector, never a data file read when a bundle builds aRegistry(implemented under #49, unreleased, shipping in v0.2.0). - Corpus. The corpus stays a directory, read only by validity and
ruleprobe corpus, never at import or registry build. A caller that imports from a zip setsRULEPROBE_CORPUSor unpacks it, as the harness does. [ADOPTED] - Declared API. One contract test module is the list: every name and call shape in PRD FR-30,
including
fn(events, ctx),Context,Parsed,hit,git_calls3-tuples, positionalDetector(id, rule, event, fn, gate),Registry(list), the validity helpers anddeclarative.loadreturning(document, lines). The README renders from it. A name joins only with its test. Root__all__names outside FR-30’s list stay importable and undeclared (PRD Q8, decided 2026-09-23). The list is re-derived from agent-harnessmainbefore each release. - Non-name dependencies.
SECRET_PATTERNSstays a plain module-level assignment of a literal list inruleprobe/detectors/common.py. The wheel is pure Python, namedruleprobe-<version>-py3-none-any.whl, carriescorpus/, and imports from a zip onsys.path: no import-time file read, and no file read when aRegistryis built orreport_dataruns. - Within a minor series none of the above changes incompatibly (FR-31).
- Later breaks. A later 0.x minor may break the declared surface, with notice: its changelog section opens with a Breaking heading naming the migration, and the contract test is updated in the same change (PRD Q12, decided 2026-09-23).
- Schema version. The key is
AD-10: How a reader is added [ADOPTED]
- Binds: FR-4, FR-32, FR-33.
- Prevents: a third reader that differs in turn, final, error or ordering behaviour from the first two.
- Rule:
- One module in
ruleprobe/readers/withROOT,transcripts(root)returning sorted paths, andread(path)returning aSession; one entry inRUNTIMES. It importsruleprobe.eventsonly. - It owns its tool-name table (AD-3) and derives
turnandfinalas AD-2 says. - A malformed line is skipped and the file still reads; an unreadable file lands in
errorswith its path; a missingROOTyields nothing and no error. - It lands with a labelled corpus session from that runtime, near-misses included, and a README note of what that runtime’s transcripts do not record.
- One module in
AD-11: Opportunity and compliance travel beside hits, never inside them [ADOPTED; implemented under #41, #42 and #43, ships in v0.2.0]
- Binds: FR-9, FR-17, FR-18, FR-19, FR-20, FR-21, FR-22, FR-28, FR-30.
- Prevents: matchers, Python detectors and the report each inventing their own opportunity shape,
a compliance rate that comes out inverted, or the harness’s
fn(events, ctx)changing to carry one. - Rule:
Detectorgains one optional attribute,opportunities, set by keyword only: a callable over(events, ctx)returning(turn, tool_use_id, followed)triples, wherefollowedisTrue,FalseorNonefor undecided.fn, its return shape and the positional five-argument constructor do not change.- Polarity comes from the matchers’ code.
absent’sofis the action the rule asks for, andabsenthas no trigger key. absentwithscope: turncompilesopportunities. Each turn present in the event list is one opportunity; it is followed whenofmatched in that turn. A hit is an opportunity not followed.ordercompilesopportunities. Each eventfirstmatches opens one opportunity; it is followed whenthenmatched withinwithin. A hit is a followed opportunity.absentwithscope: sessiondefines no opportunity; its hit per session is already the rate.- The compiler derives
followedfrom the same evaluation that yields the hit. A test asserts, for every compiled detector on the corpus, hits = opportunities - followed forabsentand hits = followed fororder. - Under AD-4, an opportunity whose
followedis undecided is left out of both counts and counted asundecided. It is never recorded as not followed. [ASSUMPTION: an event whosefirstis undecided is counted asundecidedtoo] - A Python detector may set
opportunitiesby hand. - A row carries
compliance: detector id to{"opportunities": N, "followed": M, "undecided": U}, only for detectors that define one.reportandreport_datashowundecidedbeside the other two, apply the minimum of 20opportunitiesto each printed group, asmin_sessionsis applied (PRD Q7, decided 2026-09-23), and never replace hits per session. run()and its return are unchanged.measure()calls each enabled detector’sopportunities, behind the sameenabled(stances)gate and the same isolation asrun(): a raise is recorded inrules_errorsagainst that detector and costs only its own figures.- A Python detector declares opportunities through this keyword-only callable, not a second return value (PRD Q10, decided 2026-09-23).
AD-12: Rule binding and rule ids [ADOPTED; per section implemented under #48 and the catalog under #49, ship in v0.2.0]
- Binds: FR-13, FR-14, FR-15, FR-16.
- Prevents: two parts of the package computing a rule’s id differently, or a catalog entry that binds loosely.
- Rule:
ruleprobe/rules.pyowns binding and the rule id. The report reads itsBundleand never binds.- A rule is
measured,darkorunmeasured(STATES), and a binding records its source: own or catalog.report --json’scoveragecarriescatalog, the count of measured rules whose source is still catalog once the registry is built; it is not a state and stays out of the share’s total. - A file bound in its front matter (FR-13) keeps one rule, named by its front-matter
rule:key, else by its file name without extension. Arule:with no value is ignored; any other value that is not a non-empty, slash-free string is a finding, and the file name stands. On an unbound file split at its headings, anyrule:key is a finding that it does not apply, since section rules are named by path and heading. A section rule’s id is the file’s path relative to--rules,#, and a slug of its heading text, so it survives an edit above it. A slug repeated in one file takes an ordinal suffix in document order (CLAUDE.md#testing,CLAUDE.md#testing-2); a collision that remains is a finding. The split unit is the heading only, for 0.2 (PRD Q2, decided 2026-09-23); list items are not split, so a section of bullet rules is one rule. A file its front matter does not bind that has no heading, or whose sections include no rule, stays one rule, and its whole text binds the catalog as one unit. - Detector precedence is fixed: the base registry (shipped, plus plugins when asked), then the
catalog entries a rule bound, then the three discovery places in
load_bundle’s order; a later id replaces an earlier one (Registry.add), except that a catalog entry never replaces an id already held. An entry restating a shipped detector (verification/no-verify,transcript-hygiene/whole-file-cat) leaves the shipped one in place, and its rule stays catalog-bound. A plugin or base registry already holding a catalog id wins, and the rule is reported as bound to that detector, source own. A user’s detector with a catalog id replaces the entry, and the rule is reported as bound to the user’s own. - A catalog entry carries one anchored pattern. A rule binds an entry only when exactly one entry matches; none or several leaves it unmeasured. Binding reads text; no model.
- The 0.2 catalog is a small set of six to eight shapes, each with
examples:at the floor, listed with their detector kinds in PRD FR-16 (PRD Q3, decided 2026-09-23).
AD-13: Explain, label and redaction [ADOPTED; explain implemented under #51 and label under #52, ship in v0.2.0]
- Binds: FR-25, FR-26, NFR-5.
- Prevents: a secret printed by explain or written into a corpus, or the row widened to carry hit keys.
- Rule:
ruleprobe explainreruns detectors over transcripts and prints fromHitkeys: session, turn, tool-use id, detector id, matched field. It changes no report number. Over a stored row it prints that the row holds counts only, and the rerun that would explain it.ruleprobe labelwrites one.events.jsonlsession (AD-6) and onenearlabel under a directory the user names. It refuses a session hit ("<turn>:-") and says why, because one event cannot reproduce anabsentororderhit. [ASSUMPTION: refusing, rather than writing the whole turn window]labelrefuses to write, and says why, when redaction changes the field the detector matched, because the written negative would then pass trivially.- One
redactfunction, besideSECRET_PATTERNSinruleprobe/detectors/common.py, is the only path for text that explain prints or label writes. Each has a test that a known secret shape never appears. - [ASSUMPTION: the subcommand names; the PRD left the CLI form to the spine]
AD-14: Shared registry state is never mutated after import [ADOPTED]
- Binds: FR-6, FR-7, FR-11, FR-16, FR-30.
- Prevents: one caller’s loaded detectors leaking into another caller’s
DEFAULTin the same process, as when the harness and the CLI share an interpreter. - Rule:
DEFAULTholds the shipped detectors, registered once at import byregistry.py’s tail. Package code never adds to it afterwards; a bundle, the catalog or entry points build a new registry fromDEFAULT.copy(), asBundle.registryinruleprobe/rules.pydoes throughbase.copy(), whosebaseisDEFAULTunless a caller passes one built from it.COMPILERSis written only byregister_compilerat import time.
Consistency Conventions
| Concern | Convention |
|---|---|
| Detector id | rule/observable, lower-case, hyphenated. A rule name holds no slash. |
| Hit and label key | "<turn>:<tool_use_id>", or "<turn>:-" for a session hit. |
| Row | A dict with session_id, repo, runtime, started, ended, stances, rules (id to count); 0.2 adds schema_version and compliance (id to opportunities, followed, undecided). |
| Errors | FR-9. Detector failures {"detector": id, "error": ExceptionName} in rules_errors; reader failures {"path": ..., "error": ...} in errors; spec failures a DeclarativeError or finding with file, line and reason. Nothing fails a whole run for one bad input. |
| Contracts | Each module’s docstring states its contract and known misses; it is the reference a reviewer checks. |
| Spelling | British in identifiers already shipped (analyse, normalise); new names follow. |
| Tests | tests/test_<module>.py, unittest, run on 3.9 and 3.x. |
| Output | Tables for people; --json and report_data carry the same numbers. |
Stack
Verified against the repository files at ae84ac3, not the web.
| Name | Version |
|---|---|
| Python (floor) | >=3.9 (pyproject.toml); CI runs 3.9 and 3.x |
| setuptools (build backend) | >=61 |
| build (CI and release) | 1.6.1 |
| actions/checkout | v7.0.1, SHA-pinned |
| actions/setup-python | v7.0.0, SHA-pinned |
| actions/upload-artifact | v7.0.1, SHA-pinned |
| actions/download-artifact | v8.0.1, SHA-pinned |
| pypa/gh-action-pypi-publish | v1.14.2, SHA-pinned |
| Runtime dependencies | none |
| Licence | MIT |
Amended 2026-09-23: the build backend floor is setuptools>=77, for the SPDX licence expression (#6)
Structural Seed
ruleprobe/
events.py declarative.py shell.py registry.py matchers.py rules.py report.py validity.py cli.py
contract_data.py # 0.2: shipped fold map and shipped ids, as literals (AD-9)
readers/ # one module per runtime, plus RUNTIMES and iter_sessions
detectors/ # common.py (reference), common.yaml (twin); 0.2: catalog data
corpus/ # labels.yaml and synthetic sessions/, shipped in the wheel
tests/ # test_<module>.py; 0.2: the contract test (AD-9)
flowchart LR
src[ruleprobe source] -->|CI: compileall, unittest 3.9 and 3.x, corpus floor, wheel install| gh[GitHub Actions]
gh -->|tag: preflight, build, publish| pypi[(PyPI wheel)]
pypi --> uvx[uvx / pip: CLI on a developer machine]
pypi -->|pinned wheel, vendored| harness[agent-harness lib/vendor, imported from zip]
uvx -->|reads| local[(local transcripts)]
No service, no hosted component, no environment beyond a developer machine and CI.
Capability → Architecture Map
| Capability | Lives in | Governed by |
|---|---|---|
| FR-1 to FR-4, FR-32 readers and schema | readers/, events.py | AD-2, AD-3, AD-10 |
| FR-5 shell parse | shell.py | AD-5, AD-4 |
| FR-6 to FR-9 registry, entry points, six detectors, failure isolation | registry.py, detectors/ | AD-1, AD-4, AD-7 |
| FR-10 to FR-12 declarative format and matchers | declarative.py, matchers.py | AD-7, AD-4 |
| FR-13 to FR-16 binding, share, sections, catalog | rules.py, detectors/ | AD-12, AD-6 |
| FR-17 to FR-22 report and compliance | report.py | AD-11, AD-8, AD-9 |
| FR-23, FR-24, FR-33 corpus and floor | validity.py, corpus/ | AD-6 |
| FR-25, FR-26 explain and label | cli.py, report.py, validity.py | AD-13, AD-8 |
| FR-27 to FR-31 versioned contract | registry.py, report.py, declarative.py, contract test | AD-9 |
| FR-34 drafting (withdrawn 2026-09-23; moved to #21) | not in ruleprobe | AD-8 |
| NFR-1 to NFR-8 | all | AD-8, AD-4, AD-6 |
Deferred
- Compliance by position (PRD Q5). Out of 0.2.0 (decided 2026-09-23); a candidate beyond 0.2. Needs AD-11 first.
- A trigger key for
absent. Without one, every turn is an opportunity forscope: turn(AD-11). Adding one is an AD-7 vocabulary change. Revisit when the catalog’s firstabsentrule shows a rate diluted by turns where the rule could not apply. - Performance (NFR-9). No timing exists; the batch-per-session shape bounds memory per session. A measured timing decides whether anything needs fixing here.
- Concurrency. Single process, one session at a time. Parallel reading is not planned.
- Release operations. Owned by
scripts/release_preflight.pyand.github/workflows/release.yml; nothing here a unit could diverge on.
Reviews, digests and validation reports for this document stay on GitHub, in
_bmad-output/planning-artifacts/architecture-spines/architecture-ruleprobe-2026-09-23 at v0.2.0.