Skip to content

bug: rule applications not being recorded #212

@Gradata

Description

@Gradata

Summary

brain.prove() / brain health reports show 0 rule applications even on brains with rich correction history (e.g. oliver-admin: 50 corrections in 30d, 13 graduated rules, 35 active lessons, 0 rule applications).

Root cause: the RULE_APPLICATION event emission plumbing is fully implemented (gradata.rules.rule_tracker.log_application, Brain.track_rule) but no production code path actually calls it. Every site that surfaces a rule (Brain.apply_brain_rules, the SessionStart inject_brain_rules hook, the daemon's /apply endpoint) injects rules into the prompt and returns — none of them log an application event.

The downstream consumers of RULE_APPLICATION (enhancements/scoring/success_conditions.py, enhancements/scoring/reports.py, the prove/manifest path) therefore see an empty event stream and report zero. This isn't a metric-display bug; it's a missing emission contract.

Reproduction

$ python3 -c "import sqlite3; c=sqlite3.connect('/home/olive/.gradata/brain/system.db'); \
    print(c.execute(\"SELECT type, COUNT(*) FROM events GROUP BY type ORDER BY 2 DESC\").fetchall())"
[('CORRECTION', 141), ('LESSON_CHANGE', 133), ('RULE_GRADUATED', 40),
 ('RULE_FAILURE', 20), ('HOOK_DEMOTED', 3), ('LESSON_ADDED', 3),
 ('RULE_PATCH_REVERTED', 3), ('RULE_TO_HOOK_INSTALLED', 2),
 ('RULE_TO_HOOK_FAILED', 1)]
# Note: no RULE_APPLICATION row at all.

Brain has 13 graduated rules and SessionStart hooks have been firing — but zero applications recorded.

Direct manual emission works, confirming the persistence layer is fine:

$ python -c "
from gradata.rules.rule_tracker import log_application
ev = log_application(rule_id='diag', session=99999, accepted=True, source='diag')
print(ev['type'])  # -> RULE_APPLICATION, row appears in events table
"

Investigation — code paths reviewed

$ grep -rn 'log_application\|track_rule' src/gradata/ --include='*.py'
src/gradata/_events.py:683:    """Public alias for session detection. Used by brain.track_rule()."""
src/gradata/brain.py:2119:    def track_rule(
src/gradata/brain.py:2128:        from gradata.rules.rule_tracker import log_application
src/gradata/brain.py:2138:        return log_application(...)
src/gradata/rules/rule_tracker.py:29:def log_application(
src/gradata/rules/__init__.py:14:from gradata.rules.rule_tracker import RuleApplication, log_application
src/gradata/rules/__init__.py:26:    "log_application",

Definition sites only — zero call sites outside Brain.track_rule, and Brain.track_rule itself has zero callers anywhere in src/. Callers reviewed and ruled out:

Code path What it does Logs RULE_APPLICATION?
Brain.apply_brain_rules (src/gradata/brain.py:1091) Ranks lessons, returns formatted prompt text. Emits rules.injected on the in-memory bus only. No
gradata.hooks.inject_brain_rules (src/gradata/hooks/inject_brain_rules.py) SessionStart hook: writes ranked rules into Claude Code system prompt and injection_log (sqlite, separate table). No
gradata.hooks.context_inject UserPromptSubmit hook: re-ranks rules per message. No
gradata.hooks.jit_inject Opt-in JIT injection scored against the draft. No
gradata.daemon /apply (src/gradata/daemon.py:376) HTTP apply_brain_rules endpoint for JS/TS clients. No
gradata.enhancements.rule_pipeline (src/gradata/enhancements/rule_pipeline.py:578) Calls verify_rules(...) for post-application checks. No

The injection_log sqlite table in inject_brain_rules._ensure_injection_log is a side-table for delta-injection deduping — it is not queryable by the prove/reports pipeline, which reads only the events table.

Root cause

Missing emission contract. The intended invariant — every rule surfaced through apply_brain_rules / SessionStart injection produces exactly one RULE_APPLICATION event with accepted=True (provisionally), to be downgraded/marked misfired=True if a subsequent CORRECTION event in the same session contradicts it — is unimplemented.

Suggested fix

Two-line emission at the bottom of Brain.apply_brain_rules, plus the same call at the bottom of inject_brain_rules.run_hook. Sketch in src/gradata/brain.py:

# in apply_brain_rules(), right after the `rules.injected` bus emit,
# before format_rules_for_prompt(applied):
if applied:
    from gradata.rules.rule_tracker import log_application
    from gradata._events import get_current_session
    sess = get_current_session() or 0
    for a in applied:
        try:
            log_application(
                rule_id=a.rule_id,
                session=sess,
                accepted=True,            # provisional; flipped by misfire detector
                source="apply_brain_rules",
                scope=scope,
            )
        except Exception as e:
            logger.debug("RULE_APPLICATION emit failed for %s: %s", a.rule_id, e)

And a symmetric call in src/gradata/hooks/inject_brain_rules.py after the injected rule IDs are finalised (use _session_id(data) for the session, fall back to 0).

The misfired=True retro-flip already has a home in enhancements/scoring/failure_detectors.py — once emissions exist, that detector will start producing meaningful misfire rates.

Regression test sketch

tests/test_rule_application_emission.py:

  1. Brain.init(tmp_path) + seed two RULE-state lessons via direct lessons.md write.
  2. Call brain.apply_brain_rules("write an email about budget").
  3. Assert len(query(event_type="RULE_APPLICATION")) >= 1.
  4. Repeat for the SessionStart hook by invoking gradata.hooks.inject_brain_rules.run_hook with a fake stdin payload.

Both assertions fail on main today.

Affected versions

  • SDK version: gradata 0.7.5 (from python -c "import gradata; print(gradata.__version__)")
  • Affected brain: /home/olive/.gradata/brain/system.db (oliver-admin), 141 CORRECTION events, 40 RULE_GRADUATED events, 0 RULE_APPLICATION events.
  • Likely all versions ≥ the introduction of rule_tracker.pygit log src/gradata/rules/rule_tracker.py will give the exact regression-window start.

Kanban / tracking

GRA-1240 (hermes kanban task t_2830c5b4).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions