Project 3: RuleForge Event-Condition-Action Core

Build a pure Kotlin rule language and deterministic planner that can explain an automation before any Android side effect occurs.

Quick Reference

Attribute Value
Difficulty Level 3 — Advanced
Time Estimate 22-30 hours
Language Kotlin; Java or a separate Rust engine experiment are alternatives
Prerequisites Projects 1-2, immutable data, tests, basic state-machine reasoning
Key Topics ECA modeling, interpreters, condition algebra, planning, idempotency, causation, conflicts

1. Learning Objectives

By completing this project, you will:

  1. Model events, triggers, conditions, actions, rules, plans, runs, attempts, and results as distinct concepts.
  2. Evaluate condition trees as pure functions over an event and immutable context snapshot.
  3. Represent true, false, unknown, and unavailable outcomes with evidence.
  4. Separate deterministic planning from effectful adapter execution.
  5. Resolve priority, conflict, cooldown, concurrency, and causation policy explicitly.
  6. Explain at-least-once attempts, idempotency, and ambiguous outcomes honestly.
  7. Version rule-language nodes without persisting implementation class names.
  8. Simulate and replay planning in JVM tests with no Android framework.

2. Theoretical Foundation

2.1 Core Concepts

Event-condition-action grammar

An event records that something happened. A trigger decides which rules are candidates for that event type and payload. Conditions evaluate current context. Actions describe desired effects. The grammar is intentionally small, but its runtime needs precise semantics when information is missing, rules conflict, effects fail, or events loop.

Rule as data

A rule is a versioned graph of stable type identifiers and configuration data. It is not a serialized Kotlin object, lambda, Android Intent, or plugin instance. Treating the rule as data allows migration, validation, editor rendering, import, simulation, and unavailable-node preservation.

Pure planning

Planning transforms event plus context plus rule set into an immutable execution plan. It performs no Android, network, notification, file, plugin, or accessibility effect. This boundary makes simulation safe: the same planner can run in the app, tests, migration validation, and a future observability replay tool.

Condition algebra

Conditions form a tree: all, any, not, comparison, set membership, temporal window, capability state, and prior-run facts. A Boolean alone is insufficient. Unknown means data is absent or stale; unavailable means the required source or authority cannot currently provide it. The rule owns the policy to skip, wait, ask, or degrade.

Causation and cycles

Actions may emit derived events. A local notification can produce a notification event; a plugin can callback into the host. Causation IDs, origin metadata, depth limits, self-origin suppression, and cooldowns prevent accidental loops. Cycle handling is runtime policy, not a hidden adapter trick.

Delivery and idempotency

Exactly-once external effects cannot be guaranteed across process death. The runtime may die after an effect succeeds but before success is persisted. Actions declare an idempotency strategy, retry class, and ambiguity behavior. Planning prepares this metadata; later projects persist and execute it.

2.2 Why This Matters

A pile of callback handlers can produce simple automations, but it cannot support reliable authoring, simulation, migration, plugins, or diagnostics. Tasker-like power comes from a language whose semantics remain stable while adapters change.

Pure planning contains authority. The planner does not gain Android Context or arbitrary execution. It produces a proposal that later capability and policy gates can inspect. That proposal can be shown to the user before enabling a rule.

Evidence-bearing conditions make support possible. A user sees battery below threshold evaluated false with observed value 67, or unavailable because the source is disabled. Both prevent action, but they require different remedies.

2.3 Historical Context / Background

ECA systems appear in active databases, complex event processing, home automation, workflow engines, and monitoring platforms. Design patterns such as Command, Composite, Strategy, and State provide implementation vocabulary. Integration patterns add message identity, correlation, routing, and idempotent receiver concepts.

Android does not change these fundamentals. It makes lifecycle and authority failures more frequent, increasing the value of a platform-independent core. Modern accessibility distribution policy also favors deterministic, human-defined rule behavior over autonomous action planning.

2.4 Common Misconceptions

  • A rule is executable code: durable rules should be constrained, versioned data interpreted by known implementations.
  • Simulation can call fake adapters: safest simulation ends before any adapter invocation.
  • Unknown equals false: missing evidence needs explicit rule policy.
  • Exactly once is a database flag: external effects and local commits cannot generally be atomic.
  • Retries are always safe: taps, messages, and delegated actions can be ambiguous.
  • Priority solves every conflict: resource, concurrency, and user policy still matter.
  • A depth limit finds all cycles: it bounds harm; static graph checks and origin suppression add defense.

3. Project Specification

3.1 What You Will Build

Build a pure Kotlin RuleForge module and Compose Rule Lab. The module accepts canonical events from Project 2, immutable context snapshots, and versioned rule graphs. It produces plans and detailed traces. The UI supports one demonstration rule:

When power connects, if battery is below 40 percent and local time is before 23:00, plan a demo notification.

The project does not execute the notification. An adapter spy must prove zero side effects during planning and simulation.

3.2 Functional Requirements

  1. Versioned event envelope: Consume domain events without Android objects.
  2. Versioned rule graph: Stable trigger, condition, and action type IDs.
  3. Condition tree: Support all, any, not, comparison, set membership, and temporal conditions.
  4. Four-state evidence: True, false, unknown, and unavailable with reasons.
  5. Immutable context: Every plan references one snapshot identity and freshness evidence.
  6. Pure planner: No adapter, Android, network, clock, or mutable global side effect.
  7. Plan validation: Validate schema, action capability declarations, and parameter bounds.
  8. Conflict policy: Handle multiple actions targeting the same resource key.
  9. Causation policy: Track origin, chain, depth, and cooldown.
  10. Trace: Record every candidate, condition, decision, conflict, and planned action.
  11. Simulation UI: Edit event/context fixtures and inspect the plan.
  12. Unknown node handling: Disable or reject safely with preserved raw configuration.

3.3 Non-Functional Requirements

  • Determinism: Same normalized inputs and engine version produce the same plan and trace.
  • Isolation: The engine module has no Android framework dependency.
  • Safety: Planning never performs an effect, even through a test double.
  • Performance: Rule selection indexes by event type rather than scanning irrelevant graphs at scale.
  • Explainability: Every skip and block has typed evidence.
  • Evolvability: Node schemas include stable identifiers and versions.

3.4 Example Usage / Output

SIMULATION run-sim-0042
event: power.connection.changed connected=true
context: snapshot-19 battery=32 time=22:10

candidate rule: low-battery-evening-reminder
trigger: MATCH
condition all:
  battery below 40 -> TRUE observed=32
  local time before 23:00 -> TRUE observed=22:10
decision: ELIGIBLE
conflict policy: no conflict
planned action: local.notification.demo
required capability: notifications.post
effects performed: 0

3.5 Real World Outcome

Open Rule Lab and choose the Low Battery Evening fixture. The screen has Event, Context, Rule Graph, and Trace panels. The graph visually groups one trigger, an All condition with two children, and one action descriptor.

Press Simulate. The trace identifies the event and context snapshot, then expands each condition with expected value, observed value, freshness, and result. It selects a rule priority, checks cooldown and conflict policy, validates required capabilities descriptively, and produces one immutable action plan. A prominent badge states No effects performed.

Change battery to unknown. Select Skip when unknown and rerun; the trace stops with an explicit unknown condition. Select Wait for context and rerun; the plan becomes deferred with a bounded context requirement. Change time beyond the threshold; the decision becomes false rather than unavailable.

Load a conflict fixture with two rules targeting the same demo resource. The trace applies the chosen policy: highest priority, serialize, or block all. Load a causation fixture whose action emits the same event type. The engine blocks the chain at the configured depth and records why.

Run the same fixtures as JVM tests. They finish without an emulator, Android Context, or adapter implementation.


4. Solution Architecture

4.1 High-Level Design

 Canonical EventEnvelope      Context providers
          |                         |
          |                  immutable snapshot
          |                         |
          +------------+------------+
                       v
               +---------------+
               | Rule selector |
               +-------+-------+
                       |
                candidate rules
                       |
                       v
             +-------------------+
             | Condition         |
             | interpreter       |
             | pure + evidence   |
             +---------+---------+
                       |
                 eligible rules
                       |
                       v
        +--------------------------------+
        | Policy resolution              |
        | priority / conflict / cooldown |
        | causation / schema validation  |
        +----------------+---------------+
                         |
                         v
             Immutable ExecutionPlan
                  + PlanningTrace
                         |
              future executor boundary
              not invoked in this project

4.2 Key Components

Component Responsibility Key Decisions
RuleGraph Durable language structure Stable node IDs and versions
RuleIndex Finds candidates by trigger/event Deterministic ordering
ContextSnapshot Frozen values and freshness No live reads mid-evaluation
ConditionInterpreter Evaluates tree recursively Returns evidence algebra
PlanBuilder Converts eligible actions into descriptors Immutable output
ConflictResolver Applies resource and priority policy Typed blocked outcomes
CausationGuard Bounds self-origin and chain depth Preserves trace evidence
SchemaRegistry Validates node type and configuration Unknown nodes do not execute
TraceBuilder Captures decision transitions Structured, not log strings

4.3 Data Structures

Rule
  rule_id
  language_version
  enabled
  priority
  trigger_node
  condition_tree
  action_nodes
  unavailable_policy
  conflict_policy
  cooldown_policy

ConditionEvidence
  node_id
  outcome: TRUE | FALSE | UNKNOWN | UNAVAILABLE
  observed_summary
  freshness
  reason_code

ExecutionPlan
  plan_id
  engine_version
  event_id
  context_snapshot_id
  correlation_id
  causation_chain
  planned_actions
  decision_trace

4.4 Algorithm Overview

Key Algorithm: Deterministic Planning

  1. Validate event schema and select enabled trigger candidates.
  2. Sort candidates by stable priority and rule identity.
  3. Acquire or receive one immutable context snapshot.
  4. Evaluate each condition tree and collect evidence.
  5. Apply the rule’s unknown/unavailable policy.
  6. Reject cooldown and causation violations.
  7. Convert eligible action nodes into validated action descriptors.
  8. Group actions by resource key and apply conflict policy.
  9. Build one immutable plan and complete structured trace.
  10. Return without invoking any adapter.

Complexity Analysis:

  • Time: O(Rm + N + A log A), where Rm is matched rules, N condition nodes, and A planned actions sorted for deterministic resolution.
  • Space: O(N + A) for condition evidence, trace, and immutable plan.

5. Implementation Guide

5.1 Development Environment Setup

Use a pure Kotlin/JVM module, a separate Android Compose app module, coroutine test utilities where needed, and a property-testing library if desired. The domain module should compile and test without an Android SDK runtime.

$ ./gradlew :ruleforge-domain:test

BUILD SUCCESSFUL
Rule fixtures: 18 passed
Android adapter calls during simulation: 0

The transcript is a target outcome, not a provided implementation.

5.2 Project Structure

ruleforge/
├── domain/
│   ├── event/
│   ├── rule-language/
│   ├── context/
│   ├── conditions/
│   ├── planning/
│   ├── policy/
│   └── trace/
├── schema/
│   ├── node-registry/
│   └── fixtures/
├── app/ui/rule-lab/
└── tests/
    ├── examples/
    ├── properties/
    ├── conflicts/
    └── causation/

5.3 The Core Question You Are Answering

Can I explain exactly why a rule will act before I let any Android side effect happen?

If a simulation needs an Android adapter, current system clock, or mutable singleton, the planning boundary is incomplete.

5.4 Concepts You Must Understand First

Stop and research these before coding:

  1. Command pattern
    • Why is an action descriptor a command rather than an executed function?
    • Book Reference: Design Patterns — Command.
  2. Composite pattern
    • How do All, Any, and Not compose leaf conditions?
    • Book Reference: Design Patterns — Composite.
  3. Pure functions and immutable state
    • Why must context be frozen for one evaluation?
    • Book Reference: Effective Java, 3rd Edition — Items 15-17.
  4. Message identity and correlation
    • How do event, plan, run, and derived event relate?
    • Book Reference: Enterprise Integration Patterns — Correlation Identifier.
  5. Idempotency and ambiguity
    • Which effects can safely repeat?
    • Why is unknown outcome different from failure?
  6. Language versioning
    • Why are implementation class names unstable storage IDs?
    • How should unknown node data survive?

5.5 Questions to Guide Your Design

  1. What is the boundary between trigger match and condition evaluation?
  2. Does All short-circuit, and if so, how much evidence remains?
  3. How does Not transform unknown or unavailable?
  4. Is context freshness a value, condition, or policy input?
  5. Which ordering is deterministic when priorities tie?
  6. What resource key identifies conflicting actions?
  7. Can an unavailable plugin node remain editable?
  8. Which policy prevents a derived event loop?
  9. What does simulation promise about capabilities?
  10. How will future persistence claim action attempts?

5.6 Thinking Exercise

Resolve Two Conflicting Focus Rules

Rule A enables demo Focus state when headphones connect. Rule B disables it when a calendar context becomes busy. Both receive events in one short interval.

Draw:

event -> candidates -> condition evidence -> resource key focus-state
                                     |              |
                                  Rule A         Rule B
                                     +------?-------+

Questions to answer:

  • Which event/context snapshot applies to each plan?
  • Is priority global or resource-specific?
  • Can actions serialize meaningfully, or should conflict block?
  • What evidence will explain the losing rule?
  • If action A emits a state event, how is causation tracked?

5.7 The Interview Questions They Will Ask

  1. Why separate planning from execution?
  2. How do you model an unavailable condition?
  3. What delivery guarantee can you honestly provide across process death?
  4. How do you prevent automation cycles?
  5. What is idempotency for a UI action?
  6. How do you version and migrate a stored rule language?

5.8 Hints in Layers

Hint 1: Ban Android imports in the domain

Make this a build rule, not a convention.

Hint 2: Make evidence a return value

Do not reconstruct explanations from log strings later.

Hint 3: Freeze context once

Two conditions should not observe different battery values in one plan.

Hint 4: Use adapter spies that fail

Simulation tests should fail immediately if any effect boundary is touched.

5.9 Books That Will Help

Topic Book Chapter
Engine patterns Design Patterns Command, Composite, State, Strategy
Domain boundaries Clean Architecture Entities and boundaries
Messaging Enterprise Integration Patterns Correlation and Idempotent Receiver
Immutability Effective Java, 3rd Edition Items 15-17, 50
Testing Test Driven Development: By Example Parts I-II

5.10 Implementation Phases

Phase 1: Language and Example Interpreter (6-8 hours)

Goals:

  • Define stable domain types and one end-to-end fixture.
  • Keep the module Android-free.

Tasks:

  1. Define event, context, rule, condition evidence, and action descriptor.
  2. Implement comparison plus All/Any/Not semantics in pseudocode-first tests.
  3. Produce a deterministic trace for the demo rule.

Checkpoint: The demo fixture produces a plan in a plain JVM test.

Phase 2: Policy and Simulation Lab (8-11 hours)

Goals:

  • Add unknown handling, conflict resolution, cooldown, and causation.
  • Visualize the complete trace.

Tasks:

  1. Define explicit truth and unavailable algebra.
  2. Add stable candidate ordering and resource conflicts.
  3. Build editable event/context fixtures in Compose.

Checkpoint: Unknown, conflict, cooldown, and cycle fixtures each have distinct trace outcomes.

Phase 3: Schema and Property Hardening (8-11 hours)

Goals:

  • Validate node versions and unknown data.
  • Prove determinism and bounded behavior.

Tasks:

  1. Add schema registry and unknown-node preservation.
  2. Add property tests for ordering, replay, and causation bounds.
  3. Add effect spies and architecture dependency tests.

Checkpoint: Repeating and reordering independent inputs preserves documented invariants.

5.11 Key Implementation Decisions

Decision Options Recommendation Rationale
Evaluation Execute inline; build plan Build immutable plan Enables simulation and gating
Context Live reads; snapshot Immutable snapshot Deterministic evidence
Truth Boolean; nullable; four-state evidence Four-state evidence Honest unavailable handling
Rule storage shape Class names; versioned type IDs Type IDs Stable migration contract
Conflict Last writer; explicit policy Explicit policy Predictable user meaning
Cycle control None; depth only; layered guard Layered guard Bounds and explains loops

6. Testing Strategy

6.1 Test Categories

Category Purpose Examples
Example tests Lock down intended semantics Low-battery demo rule
Algebra tests Prove composite truth behavior All/Any/Not with unknown
Property tests Prove invariants over generated graphs Determinism, bounded depth
Schema tests Prove node validation Unknown and version mismatch
Policy tests Prove conflict/cooldown choices Same resource key
Architecture tests Prove pure boundary No Android dependencies or adapter calls

6.2 Critical Test Cases

  1. Deterministic replay: Identical inputs yield structurally identical plan and trace except permitted generated identity.
  2. Unknown battery: Rule follows Skip or Wait policy, never coerces false silently.
  3. Unavailable capability context: Trace distinguishes source absence from comparison false.
  4. Conflict: Two opposite actions on one resource follow configured policy.
  5. Cooldown: Recent matching run prevents a new action with evidence.
  6. Cycle: Derived self-origin event stops at bounded depth.
  7. Unknown node: Rule is disabled safely and raw configuration remains inspectable.
  8. No effects: Every simulation adapter spy records zero calls.
  9. Stable tie: Same-priority rules sort by documented stable identity.
  10. Deep tree: Validation rejects or bounds pathological nesting.

6.3 Test Data

event:
  type: power.connection.changed
  payload: connected=true

context known:
  battery_percent: 32
  local_time: 22:10
expected: PLAN action local.notification.demo

context unknown:
  battery_percent: UNKNOWN
  unavailable_policy: SKIP
expected: SKIPPED reason CONTEXT_UNKNOWN

causation:
  origin_rule: loop-demo
  depth: 5
  maximum: 5
expected: BLOCKED reason CAUSATION_DEPTH

7. Common Pitfalls & Debugging

7.1 Frequent Mistakes

Pitfall Symptom Solution
Planner calls adapter Simulation posts a notification Return plan before effect boundary
Nullable Boolean Unknown behaves false Use explicit evidence algebra
Live context reads Conditions disagree within one run Freeze one snapshot
Class-name nodes Refactor breaks saved rules Stable IDs and versions
Hidden ordering Results change across runs Stable sort policy
No causation Notification rules loop Origin, depth, cooldown, self-suppression
Free-form logs UI cannot explain decisions Structured trace nodes

7.2 Debugging Strategies

  • Replay one fixture: Remove platform variability and compare trace transitions.
  • Inspect the first divergence: Candidate, condition, policy, or plan stage identifies the defect class.
  • Print canonical forms in tests: Compare normalized rule graphs rather than object identities.
  • Fail on effects: Keep a planner test environment with no executable adapters.
  • Shrink generated failures: Property testing should report the smallest rule graph that violates an invariant.
  • Check snapshot identity: Every condition row must reference the same context snapshot.

7.3 Performance Traps

Avoid scanning all rules for every event once the catalog grows; index trigger types. Bound rule depth, number of nodes, action count, and trace size. Short-circuiting may improve speed but should still record enough evidence to explain which branch determined the result. Do not use parallel evaluation until deterministic ordering and trace merging are explicit.


8. Extensions & Challenges

8.1 Beginner Extensions

  • Add Between, Contains, and Set Membership leaf conditions.
  • Add trace search by node and result.
  • Add a rule graph pretty-printer.

8.2 Intermediate Extensions

  • Add temporal conditions based on prior canonical events.
  • Add per-resource serialization policy.
  • Add a schema compatibility report for rule fixtures.

8.3 Advanced Extensions

  • Build a static cycle-risk analyzer for rule graphs.
  • Add property-based generation of condition trees and policies.
  • Define a constrained JSON rule interchange format with formal validation.
  • Benchmark indexed candidate selection with ten thousand disabled and enabled rules.

9. Real-World Connections

9.1 Industry Applications

  • Home automation: Event and state conditions produce device commands.
  • Fraud and monitoring: Evidence-bearing rules explain alerts.
  • Workflow engines: Immutable plans separate decisions from workers.
  • Integration platforms: Correlation and idempotency manage retries.
  • Feature policy engines: Pure evaluation supports simulation and audit.
  • Home Assistant: https://github.com/home-assistant/core — large event/state automation ecosystem for architectural comparison.
  • Node-RED: https://github.com/node-red/node-red — visual flow concepts and node versioning.
  • Kotlinx Serialization: https://github.com/Kotlin/kotlinx.serialization — versioned data-model tooling to evaluate later.

9.3 Interview Relevance

  • Planning versus execution is a core architecture boundary.
  • Unknown-state algebra tests domain modeling depth.
  • Idempotency and ambiguity demonstrate distributed-systems maturity.
  • Conflict and cycle policy reveal runtime design skill.
  • Schema versioning shows awareness of long-lived user data.

10. Resources

10.1 Essential Reading

  • Android app architecture: https://developer.android.com/topic/architecture — recommended layered boundaries.
  • Guide to app architecture: https://developer.android.com/topic/architecture/recommendations — state and data-flow recommendations.
  • Kotlin sealed classes: https://kotlinlang.org/docs/sealed-classes.html — exhaustive domain alternatives.
  • Kotlin immutability guidance through API design: https://kotlinlang.org/docs/coding-conventions.html — consistent model practices.
  • Google Play AccessibilityService policy: https://support.google.com/googleplay/android-developer/answer/10964491 — deterministic human-defined automation boundary for later projects.

10.2 Video Resources

  • Android architecture sessions: https://www.youtube.com/@AndroidDevelopers
  • Kotlin language talks: https://www.youtube.com/@Kotlin

10.3 Tools & Documentation

  • JUnit on Android projects: https://developer.android.com/training/testing/local-tests — local JVM testing.
  • Test in isolation: https://developer.android.com/training/testing/fundamentals/what-to-test — layered test scope.
  • Compose testing: https://developer.android.com/develop/ui/compose/testing — Rule Lab UI evidence.
  • Kotlin coroutines test: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-test/ — deterministic asynchronous policies if used.
  • Previous — Project 2: Context Signal Monitor: Supplies canonical event envelopes.
  • Next — Project 4: Durable Automation Library: Persists the language, plans, attempts, and trace.
  • Project 5: Timekeeper Scheduler: Delivers scheduled events into RuleForge.
  • Project 14: Automation Observatory: Replays these pure planning traces.

11. Self-Assessment Checklist

Before considering this project complete, verify:

11.1 Understanding

  • I can distinguish Rule, Plan, Run, Attempt, and Result.
  • I can explain why planning must be pure.
  • I can evaluate All, Any, and Not with unknown/unavailable operands.
  • I understand why exactly-once external effects cannot be assumed.
  • I can explain causation, correlation, idempotency, and cooldown.
  • I can describe a stable rule-language versioning strategy.

11.2 Implementation

  • Domain compilation and tests need no Android runtime.
  • Events, conditions, actions, plans, and traces are versioned data.
  • Unknown and unavailable outcomes carry evidence.
  • Conflict, cooldown, causation, and schema policies are visible.
  • Simulation performs zero adapter calls.
  • Stable replay and bounded-depth tests pass.
  • Unknown nodes disable safely without data loss.

11.3 Growth

  • I wrote one property test beyond example fixtures.
  • I documented one action whose outcome can become ambiguous.
  • I can whiteboard the planner in an interview.
  • I identified one tempting Android dependency and kept it outside the domain.

12. Submission / Completion Criteria

Minimum Viable Completion:

  • One event, composite condition tree, action descriptor, and immutable plan work.
  • The demo rule produces a structured trace in JVM tests and Compose.
  • Unknown condition policy is explicit.
  • Simulation invokes no effects.
  • Domain module contains no Android framework dependency.

Full Completion:

  • All minimum criteria plus:
  • Conflict, cooldown, causation-depth, and unknown-node fixtures pass.
  • Stable ordering and schema validation are documented and tested.
  • Condition evidence includes observed summary, freshness, outcome, and reason.
  • Property tests cover determinism and bounded causation.

Excellence — Going Above & Beyond:

  • Publish a formal rule interchange schema and compatibility fixtures.
  • Add a static cycle-risk analyzer with explainable warnings.
  • Benchmark indexed selection at realistic catalog sizes.
  • Demonstrate a second interpreter experiment producing the same canonical plans for shared fixtures.

This guide was generated from ANDROID_AUTOMATION_APPS_MASTERY.md. For the complete learning path, see the parent guide and directory README.