F4 Phase B5: horizontal conflict precedence#40
Conversation
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>
| #{pair => Pair, | ||
| ref => content_avp_value(RuleNode, ChildAttr), | ||
| char => undefined, | ||
| mode => maps:get(mode, Deploy, mandatory), |
There was a problem hiding this comment.
why is the default mandatory?
There was a problem hiding this comment.
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.
| #{pair => Pair, | ||
| ref => maps:get(target_class, Spec), | ||
| char => maps:get(characterization, Spec), | ||
| mode => maps:get(mode, Deploy, mandatory), |
There was a problem hiding this comment.
same question, why defaulting mandatory?
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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>
|
Addressed in 205e7fa:
Full suite green: 418 CT + 105 EUnit = 523, clean compile. |
Summary
Implements F4 Phase B5 — horizontal conflict precedence, the last core
division of the
graphdb_rulesfiring engine (resolves OI-2). When a class andits 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.
graphdb_instance:create_instance/5(mirroring B4's connection resolver)./3and/4inject the built-ingraphdb_rules:default_conflict_resolver/0.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 survivingMinto thewinner's and
Maxto the greatest across winner + dropped losers, and demotesa loser to
proposeonly when it and the winner both carry a non-defaulttemplate (otherwise the loser is dropped).
graphdb_rulesprocess(
plan_composition_firing/3); connection resolution runs in thegraphdb_instanceprocess. The resolver closure is deadlock-safe — it touchesonly in-memory node AVPs, dirty
relationshipsreads, andgraphdb_class(a different gen_server), never calling back into
graphdb_rules.plan_composition_firing/2,effective_rules_for_class/2) is preserved as additive — the/2path isrouted through a standalone identity resolver and resolves nothing.
Known deferred (documented in
TASKS.md+ a code comment): equidistant-diamondprecedence resolves by
graphdb_class:ancestors/1BFS order rather thanmode-priority arbitration across equidistant parents.
Docs updated:
docs/Architecture.md, root +apps/graphdbCLAUDE.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 warningsunrelated, 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