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:
- Model events, triggers, conditions, actions, rules, plans, runs, attempts, and results as distinct concepts.
- Evaluate condition trees as pure functions over an event and immutable context snapshot.
- Represent true, false, unknown, and unavailable outcomes with evidence.
- Separate deterministic planning from effectful adapter execution.
- Resolve priority, conflict, cooldown, concurrency, and causation policy explicitly.
- Explain at-least-once attempts, idempotency, and ambiguous outcomes honestly.
- Version rule-language nodes without persisting implementation class names.
- 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
- Versioned event envelope: Consume domain events without Android objects.
- Versioned rule graph: Stable trigger, condition, and action type IDs.
- Condition tree: Support all, any, not, comparison, set membership, and temporal conditions.
- Four-state evidence: True, false, unknown, and unavailable with reasons.
- Immutable context: Every plan references one snapshot identity and freshness evidence.
- Pure planner: No adapter, Android, network, clock, or mutable global side effect.
- Plan validation: Validate schema, action capability declarations, and parameter bounds.
- Conflict policy: Handle multiple actions targeting the same resource key.
- Causation policy: Track origin, chain, depth, and cooldown.
- Trace: Record every candidate, condition, decision, conflict, and planned action.
- Simulation UI: Edit event/context fixtures and inspect the plan.
- 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
- Validate event schema and select enabled trigger candidates.
- Sort candidates by stable priority and rule identity.
- Acquire or receive one immutable context snapshot.
- Evaluate each condition tree and collect evidence.
- Apply the rule’s unknown/unavailable policy.
- Reject cooldown and causation violations.
- Convert eligible action nodes into validated action descriptors.
- Group actions by resource key and apply conflict policy.
- Build one immutable plan and complete structured trace.
- 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:
- Command pattern
- Why is an action descriptor a command rather than an executed function?
- Book Reference: Design Patterns — Command.
- Composite pattern
- How do All, Any, and Not compose leaf conditions?
- Book Reference: Design Patterns — Composite.
- Pure functions and immutable state
- Why must context be frozen for one evaluation?
- Book Reference: Effective Java, 3rd Edition — Items 15-17.
- Message identity and correlation
- How do event, plan, run, and derived event relate?
- Book Reference: Enterprise Integration Patterns — Correlation Identifier.
- Idempotency and ambiguity
- Which effects can safely repeat?
- Why is unknown outcome different from failure?
- Language versioning
- Why are implementation class names unstable storage IDs?
- How should unknown node data survive?
5.5 Questions to Guide Your Design
- What is the boundary between trigger match and condition evaluation?
- Does All short-circuit, and if so, how much evidence remains?
- How does Not transform unknown or unavailable?
- Is context freshness a value, condition, or policy input?
- Which ordering is deterministic when priorities tie?
- What resource key identifies conflicting actions?
- Can an unavailable plugin node remain editable?
- Which policy prevents a derived event loop?
- What does simulation promise about capabilities?
- 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
- Why separate planning from execution?
- How do you model an unavailable condition?
- What delivery guarantee can you honestly provide across process death?
- How do you prevent automation cycles?
- What is idempotency for a UI action?
- 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:
- Define event, context, rule, condition evidence, and action descriptor.
- Implement comparison plus All/Any/Not semantics in pseudocode-first tests.
- 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:
- Define explicit truth and unavailable algebra.
- Add stable candidate ordering and resource conflicts.
- 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:
- Add schema registry and unknown-node preservation.
- Add property tests for ordering, replay, and causation bounds.
- 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
- Deterministic replay: Identical inputs yield structurally identical plan and trace except permitted generated identity.
- Unknown battery: Rule follows Skip or Wait policy, never coerces false silently.
- Unavailable capability context: Trace distinguishes source absence from comparison false.
- Conflict: Two opposite actions on one resource follow configured policy.
- Cooldown: Recent matching run prevents a new action with evidence.
- Cycle: Derived self-origin event stops at bounded depth.
- Unknown node: Rule is disabled safely and raw configuration remains inspectable.
- No effects: Every simulation adapter spy records zero calls.
- Stable tie: Same-priority rules sort by documented stable identity.
- 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.
9.2 Related Open Source Projects
- 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.
10.4 Related Projects in This Series
- 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.