Skip to content

[FEATURE] Add Rule Dependency Graph - #6

Merged
jmalovera10 merged 8 commits into
mainfrom
feature/rule-dependency-graph
Aug 7, 2026
Merged

jmalovera10 merged 8 commits into
mainfrom
feature/rule-dependency-graph

Conversation

@jmalovera10

@jmalovera10 jmalovera10 commented Aug 5, 2026 •

Copy link
Copy Markdown
Owner

Description

Adds rule dependency management to Excanon. Rules can now declare ordering and prerequisite
relationships to other rules by name, instead of relying solely on their position in the
loaded JSON array:

  • after — pure ordering. The referenced rule(s) run first; the dependent rule then
    evaluates its own conditions and runs (or doesn't) exactly as it would today, regardless of
    whether the referenced rule matched.
  • requires — ordering plus a gate. The referenced rule(s) run first, and the dependent
    is only evaluated if every rule it requires actually fired (matched) during the same
    evaluation. An unmet prerequisite skips the dependent, and that skip propagates
    transitively through chains of requires.

Rule sets with no after/requires edges anywhere continue to execute in exactly their
original JSON-array order (no observable behavior change for existing callers).

Cycles in the dependency graph — via after, requires, or a mix of both — are rejected at
load_rules/2 time with a descriptive error. Cyclic/iterative rule execution is intentionally
out of scope for this iteration.

Changes

  • lib/rule.ex: added after/requires fields to %Rule{} (default []), with validation
    in Rule.new!/1 (must be a list of strings if present).
  • lib/rule_graph.ex (new, internal — @moduledoc false): validates a loaded rule set
    (duplicate names, unknown after/requires references, dependency cycles via :digraph)
    and produces a topologically-sorted rule list. Uses a hand-rolled, tie-broken Kahn's
    algorithm rather than :digraph_utils.topsort/1 so that rules with no ordering constraint
    between them keep their original relative order — this is what makes the zero-edge case
    byte-identical to today's behavior.
  • lib/stateful_rule_engine.ex: load_rules/2 now runs loaded rules through
    RuleGraph.build/1 before storing them; execute_rules/2 was reworked to track which
    rules fired during an evaluation so requires gating can be checked. evaluate/2's public
    contract is unchanged.
  • README.md / CHANGELOG.md: documented the new fields and the load-time error cases.
    Breaking: rule name values must now be unique within a loaded set — previously
    duplicate names were silently accepted.
  • Design docs added at the repo root: RULE_DEPENDENCY_GRAPH_PLAN.md (the design spec) and
    RULE_DEPENDENCY_GRAPH_EXECUTION_PLAN.md (the work-package breakdown used to implement it).

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Refactoring (no functional changes)
  • Test updates

How Has This Been Tested?

  • mix test — 156 tests, 0 failures, including the full pre-existing suite unmodified (the
    backward-compatibility guarantee) plus new coverage in test/rule_test.exs,
    test/rule_graph_test.exs (new), and test/stateful_rule_engine_test.exs for: field
    validation, ordering via after and requires independently and mixed, diamond
    dependencies, duplicate-name rejection, unknown-reference rejection, cycle rejection
    (via after only, requires only, a mix, and self-loops), and prerequisite gating (met,
    unmet, and transitive).

  • mix credo, mix dialyzer, mix sobelow — all clean, no new findings.

  • mix docs — confirmed RuleGraph does not appear in generated documentation
    (@moduledoc false respected), while StatefulRuleEngine's docs reflect the new fields.

  • Manual exploratory checks (ordering, gating, duplicate-name and cycle error messages)
    against a running engine.

  • Unit tests pass

  • Integration tests pass

  • Manual testing completed

Checklist

  • My code follows the project's style guidelines
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

Additional Notes

  • RuleGraph is intentionally undocumented as public API (@moduledoc false) — it's an
    internal implementation detail. The feature itself is documented at the
    StatefulRuleEngine and README level instead.
  • The duplicate-name rejection is the one backward-incompatible behavior change in this PR;
    it's called out explicitly in CHANGELOG.md since existing callers with accidental name
    collisions across rules could start seeing {:error, "Duplicate rule names: ..."} from
    load_rules/2 where they previously didn't.

@jmalovera10
jmalovera10 merged commit 2de3fc9 into main Aug 7, 2026
16 checks passed
@jmalovera10
jmalovera10 deleted the feature/rule-dependency-graph branch August 7, 2026 14:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant