Skip to content

F4 Phase B5: horizontal conflict precedence#40

Merged
david-w-t merged 8 commits into
davidwt-com:mainfrom
david-w-t:develop
Jun 17, 2026
Merged

F4 Phase B5: horizontal conflict precedence#40
david-w-t merged 8 commits into
davidwt-com:mainfrom
david-w-t:develop

Conversation

@david-w-t

Copy link
Copy Markdown
Contributor

Summary

Implements F4 Phase B5 — horizontal conflict precedence, the last core
division of the graphdb_rules firing engine (resolves OI-2). When a class and
its taxonomy ancestors carry rules that reference the same concept, a
conflict resolver picks one winner per group before firing, rather than
firing every rule additively.

  • A caller-overridable conflict-resolver fun is threaded through a new
    graphdb_instance:create_instance/5 (mirroring B4's connection resolver).
    /3 and /4 inject the built-in graphdb_rules:default_conflict_resolver/0.
  • The default resolver groups conflicting rules by referenced class (composition:
    same-or-descendant child class; connection: same characterization +
    same-or-descendant target class), picks the nearest-level member by mode
    priority (mandatory > auto > propose), sets surviving Min to the
    winner's and Max to the greatest across winner + dropped losers, and demotes
    a loser to propose only when it and the winner both carry a non-default
    template (otherwise the loser is dropped).
  • Composition resolution runs in the graphdb_rules process
    (plan_composition_firing/3); connection resolution runs in the
    graphdb_instance process. The resolver closure is deadlock-safe — it touches
    only in-memory node AVPs, dirty relationships reads, and graphdb_class
    (a different gen_server), never calling back into graphdb_rules.
  • The B1 read contract (plan_composition_firing/2,
    effective_rules_for_class/2) is preserved as additive — the /2 path is
    routed through a standalone identity resolver and resolves nothing.

Known deferred (documented in TASKS.md + a code comment): equidistant-diamond
precedence resolves by graphdb_class:ancestors/1 BFS order rather than
mode-priority arbitration across equidistant parents.

Docs updated: docs/Architecture.md, root + apps/graphdb CLAUDE.md,
README.md, TASKS.md, plus the B5 design and implementation plan.

Test Plan

  • make test-ct-parallel — 418 Common Test cases green (graphdb_rules 80,
    graphdb_instance 106)
  • ./rebar3 eunit — 105 EUnit tests green
  • ./rebar3 compile — clean, zero warnings
  • New CT coverage: composition cross-level/descendant shadow, additive
    unrelated, max-merge (unbounded), same-level mode priority, both-real
    template demote, mixed-template drop; connection target shadow + additive;
    end-to-end firing flip, cross-level shadow, and custom-resolver override
    through create_instance/5

🤖 Generated with Claude Code

david-w-t and others added 7 commits June 14, 2026 22:26
Design spec for the conflict-resolution division of the rule-firing
engine. Captures B5-D1..B5-D7: conflict grouping by referenced class
with descendant matching, nearest-rule precedence, nearest-Min/
greatest-Max multiplicity merge, real-template loser demotion to
propose, and resolver ownership in graphdb_instance threaded via
create_instance/5.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Five-task TDD plan implementing the B5 design. Threads a conflict-resolver
fun through create_instance/5 (mirroring B4); default built by
graphdb_rules:default_conflict_resolver/0 — a seed-baking closure applied
inside plan_node (composition) and resolve_nodes (connection), deadlock-safe
in both processes. Tasks: T1 behaviour-preserving plumbing (identity default),
T2 composition group/shadow/merge, T3 template demotion, T4 connection
resolution, T5 end-to-end firing proofs + custom-resolver override + docs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…default)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread apps/graphdb/src/graphdb_rules.erl Outdated
#{pair => Pair,
ref => content_avp_value(RuleNode, ChildAttr),
char => undefined,
mode => maps:get(mode, Deploy, mandatory),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is the default mandatory?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No good reason — fixed in 205e7fa (defaults to undefined now). This field feeds only pick_winner/1. A mandatory default meant a mode-less deployment would win its precedence group (priority 3) and suppress the legitimate sibling rules — then fire nothing anyway, since the resolved pair still carries no mode and plan_rules/5 skips it. undefined (priority 0) makes it yield, matching plan_rules/5's own absent-mode default at line 1217. It is a defensive path: rule creation always writes mode.

Comment thread apps/graphdb/src/graphdb_rules.erl Outdated
#{pair => Pair,
ref => maps:get(target_class, Spec),
char => maps:get(characterization, Spec),
mode => maps:get(mode, Deploy, mandatory),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same question, why defaulting mandatory?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same fix in 205e7fa — defaults to undefined. Identical reasoning to the composition item builder above: the field only drives pick_winner/1, and mandatory would let a mode-less rule win and suppress real siblings while firing nothing itself.

@david-w-t david-w-t left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code does not need to reference the specific F4 or B5, just the descriptive topic if not already present.

- Conflict-resolver item builders (comp_item/conn_item) defaulted an
  absent deployment mode to `mandatory`. That field feeds only
  pick_winner/1, so a mode-less rule would win its precedence group
  (priority 3) and suppress legitimate sibling rules, then fire nothing
  (the resolved pair still has no mode, so plan_rules/5 skips it).
  Default to `undefined` (priority 0) instead, matching plan_rules/5's
  own absent-mode default — a mode-less deployment now yields. Defensive
  path; rule creation always writes mode.
- Drop F4/Bn/OI phase labels from code comments, keeping the descriptive
  topic. Phase labels remain in the design/Architecture/README/TASKS docs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@david-w-t

Copy link
Copy Markdown
Contributor Author

Addressed in 205e7fa:

  • absent-mode default (both item builders) → undefined instead of mandatory; details in the two inline threads.
  • phase labels → dropped the F4/Bn/OI references from code comments, keeping the descriptive topic (12 references across graphdb_rules.erl and graphdb_instance.erl). Phase labels remain in the design/Architecture/README/TASKS docs, which use them as their organizing scheme.

Full suite green: 418 CT + 105 EUnit = 523, clean compile.

@david-w-t david-w-t merged commit 0e461b2 into davidwt-com:main Jun 17, 2026
1 check passed
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