• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

orneryd / NornicDB / 26455168367

26 May 2026 02:39PM UTC coverage: 85.059% (-0.06%) from 85.123%
26455168367

push

github

orneryd
fix(cypher,storage): resolve mcp-neo4j-memory regressions; full Lucene wildcard parity

  The mcp-neo4j-memory tool surfaced four independently reproducible
  defects against v1.1.0. All four are fixed here with deeply-asserted
  regression tests; Neo4j 5.x DDL and Lucene wildcard semantics are
  preserved end-to-end.

  Bug 1 — map-param property access stored as literal text
  The WITH-binding substitution treated `m.<key>` as a standalone
  identifier and replaced just `m`, producing `{name:'hello'}.name`
  literal text in the downstream clause. New expandMapMemberAccess
  runs first for map-typed bindings, expanding `<ident>.<key>` into
  the property's Cypher literal value. Token boundary checks (word /
  underscore / dot) keep unrelated identifiers untouched. Fixes
  WITH $entity AS entity MERGE (e:Memory {name: entity.name}) and
  the UNWIND-WHERE shape that mcp's create_entities depends on.

  Bug 2 — fulltext index ignored declared label scope; no wildcard
  queryNodes had no wildcard handler so '*' silently produced 0 rows
  through the BM25 term path; queryRelationships had no type-scope
  filter at all and accepted every edge. Three Lucene wildcard
  shapes are now first-class on both procedures: '*' (MatchAllDocs),
  '*:*' (Solr-style equivalent), and '<prop>:*' (field-presence).
  Each shape honors the index's declared scope (Labels for nodes,
  RelationshipTypes for edges), property allowlist, and returns
  empty for undeclared fields — matching Neo4j-Lucene posting-list
  semantics. The schema gained a RelationshipTypes []string field
  with `omitempty` JSON tag so old binaries reading new files see
  no extra key (forward compat) and new binaries reading old files
  see an empty slice (backward compat); no on-disk schema-version
  bump. AddFulltextRelationshipIndex helper plus parser support for
  the Neo4j 5.x form `CREATE FULLTEXT INDEX … FOR ()-[r:Type]-()`
  round out the surface.

  Bug 3 — WHERE / WITH-WHERE after CALL…YIELD ret... (continued)

305 of 459 new or added lines in 9 files covered. (66.45%)

52 existing lines in 11 files now uncovered.

130569 of 153504 relevant lines covered (85.06%)

0.99 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.7
/pkg/knowledgepolicy/binding_builder.go
1
package knowledgepolicy
2

3
import (
4
        "fmt"
5
        "math"
6
        "sort"
7
        "strings"
8
)
9

10
var ln2 = math.Log(2)
11

12
func bindingLabelKey(labels []string) string {
1✔
13
        sorted := make([]string, len(labels))
1✔
14
        copy(sorted, labels)
1✔
15
        sort.Strings(sorted)
1✔
16
        return strings.Join(sorted, "\x00")
1✔
17
}
1✔
18

19
func policyTargetKey(p *PromotionPolicyDef) string {
1✔
20
        if p.IsEdge {
2✔
21
                return "edge:" + p.TargetEdgeType
1✔
22
        }
1✔
23
        if p.IsWildcard {
1✔
24
                return "wild:node"
×
25
        }
×
26
        sorted := make([]string, len(p.TargetLabels))
1✔
27
        copy(sorted, p.TargetLabels)
1✔
28
        sort.Strings(sorted)
1✔
29
        return "node:" + strings.Join(sorted, "\x00")
1✔
30
}
31

32
func computeThresholdAgeNanos(fn DecayFunction, halfLifeNanos int64, threshold float64) int64 {
1✔
33
        if threshold <= 0 || halfLifeNanos <= 0 {
2✔
34
                return math.MaxInt64
1✔
35
        }
1✔
36
        switch fn {
1✔
37
        case DecayFunctionExponential:
1✔
38
                return int64(-float64(halfLifeNanos) * math.Log(threshold) / ln2)
1✔
39
        case DecayFunctionLinear:
1✔
40
                return int64((1.0 - threshold) * float64(halfLifeNanos) * 2.0)
1✔
41
        case DecayFunctionStep:
1✔
42
                return halfLifeNanos
1✔
43
        default:
×
44
                return math.MaxInt64
×
45
        }
46
}
47

48
func effectiveBundleFunction(bundle *DecayProfileBundle) DecayFunction {
1✔
49
        if bundle == nil || bundle.Function == "" {
2✔
50
                return DecayFunctionExponential
1✔
51
        }
1✔
52
        return bundle.Function
1✔
53
}
54

55
func effectiveBundleDecayEnabled(bundle *DecayProfileBundle) bool {
1✔
56
        if bundle == nil {
1✔
57
                return false
×
58
        }
×
59
        if bundle.DecayEnabled {
2✔
60
                return true
1✔
61
        }
1✔
62
        if bundle.Function == DecayFunctionNone {
2✔
63
                return false
1✔
64
        }
1✔
65
        // Legacy in-memory test fixtures often omit Enabled/DecayEnabled entirely.
66
        // Treat those unset fixtures as decay-enabled when they otherwise describe a
67
        // real decay profile, while preserving explicit schema bundles with
68
        // Enabled=true and DecayEnabled=false.
69
        if !bundle.Enabled && (bundle.HalfLifeSeconds > 0 || bundle.Function != "") {
2✔
70
                return true
1✔
71
        }
1✔
72
        return false
1✔
73
}
74

75
func compileBinding(binding *DecayProfileBinding, bundle *DecayProfileBundle, bundles map[string]*DecayProfileBundle) *CompiledBinding {
1✔
76
        if binding.NoDecay {
2✔
77
                return &CompiledBinding{
1✔
78
                        DecayBinding: binding,
1✔
79
                        NoDecay:      true,
1✔
80
                }
1✔
81
        }
1✔
82
        fn := effectiveBundleFunction(bundle)
1✔
83
        if bundle != nil && (!effectiveBundleDecayEnabled(bundle) || fn == DecayFunctionNone) {
2✔
84
                return &CompiledBinding{
1✔
85
                        DecayProfile:        bundle,
1✔
86
                        DecayBinding:        binding,
1✔
87
                        VisibilityThreshold: bundle.VisibilityThreshold,
1✔
88
                        ScoreFrom:           bundle.ScoreFrom,
1✔
89
                        ScoreFromProperty:   bundle.ScoreFromProperty,
1✔
90
                        Function:            fn,
1✔
91
                        ThresholdAgeNanos:   math.MaxInt64,
1✔
92
                        DecayFloor:          bundle.ScoreFloor,
1✔
93
                        NoDecay:             true,
1✔
94
                }
1✔
95
        }
1✔
96

97
        vis := bundle.VisibilityThreshold
1✔
98
        if binding.VisibilityThreshold != nil {
2✔
99
                vis = *binding.VisibilityThreshold
1✔
100
        }
1✔
101

102
        halfLifeNanos := bundle.HalfLifeSeconds * 1e9
1✔
103

1✔
104
        cb := &CompiledBinding{
1✔
105
                DecayProfile:        bundle,
1✔
106
                DecayBinding:        binding,
1✔
107
                VisibilityThreshold: vis,
1✔
108
                ScoreFrom:           bundle.ScoreFrom,
1✔
109
                ScoreFromProperty:   bundle.ScoreFromProperty,
1✔
110
                Function:            fn,
1✔
111
                HalfLifeNanos:       halfLifeNanos,
1✔
112
                ThresholdAgeNanos:   computeThresholdAgeNanos(fn, halfLifeNanos, vis),
1✔
113
                DecayFloor:          bundle.ScoreFloor,
1✔
114
        }
1✔
115

1✔
116
        if len(binding.PropertyRules) > 0 {
2✔
117
                cb.CompiledPropertyRules = make(map[string]*CompiledPropertyOverride, len(binding.PropertyRules))
1✔
118
                for i := range binding.PropertyRules {
2✔
119
                        rule := &binding.PropertyRules[i]
1✔
120
                        override := &CompiledPropertyOverride{NoDecay: rule.NoDecay}
1✔
121
                        if !rule.NoDecay {
2✔
122
                                ruleFn := fn
1✔
123
                                ruleHalfLife := halfLifeNanos
1✔
124
                                ruleFloor := cb.DecayFloor
1✔
125

1✔
126
                                if rule.ProfileRef != "" && bundles != nil {
2✔
127
                                        if refBundle, ok := bundles[rule.ProfileRef]; ok {
2✔
128
                                                ruleHalfLife = refBundle.HalfLifeSeconds * 1e9
1✔
129
                                                if refBundle.Function != "" {
2✔
130
                                                        ruleFn = refBundle.Function
1✔
131
                                                }
1✔
132
                                                if refBundle.ScoreFloor > 0 {
1✔
133
                                                        ruleFloor = refBundle.ScoreFloor
×
134
                                                }
×
135
                                        }
136
                                }
137
                                if rule.HalfLifeSeconds > 0 {
2✔
138
                                        ruleHalfLife = rule.HalfLifeSeconds * 1e9
1✔
139
                                }
1✔
140
                                if rule.ScoreFloor > 0 {
2✔
141
                                        ruleFloor = rule.ScoreFloor
1✔
142
                                }
1✔
143
                                override.Function = ruleFn
1✔
144
                                override.HalfLifeNanos = ruleHalfLife
1✔
145
                                override.ThresholdAgeNanos = computeThresholdAgeNanos(ruleFn, ruleHalfLife, vis)
1✔
146
                                override.DecayFloor = ruleFloor
1✔
147
                        }
148
                        cb.CompiledPropertyRules[rule.PropertyPath] = override
1✔
149
                }
150
                for _, override := range cb.CompiledPropertyRules {
2✔
151
                        if override.NoDecay {
2✔
152
                                cb.HasNoDecayProperty = true
1✔
153
                                break
1✔
154
                        }
155
                }
156
        }
157

158
        return cb
1✔
159
}
160

161
// BuildBindingTable compiles all decay bindings and promotion policies into a
162
// BindingTable ready for the resolver. Returns an error if cross-references are
163
// invalid or if two bindings conflict (same target key and Order).
164
func BuildBindingTable(
165
        bundles map[string]*DecayProfileBundle,
166
        bindings map[string]*DecayProfileBinding,
167
        profiles map[string]*PromotionProfileDef,
168
        policies map[string]*PromotionPolicyDef,
169
) (*BindingTable, error) {
1✔
170
        bt := NewBindingTable()
1✔
171

1✔
172
        type seen struct {
1✔
173
                name  string
1✔
174
                order int
1✔
175
        }
1✔
176
        nodeConflicts := map[string]seen{}
1✔
177

1✔
178
        for _, binding := range bindings {
2✔
179
                if !binding.NoDecay && binding.ProfileRef != "" {
2✔
180
                        if bundles == nil {
2✔
181
                                return nil, fmt.Errorf("binding %q references profile %q but no profiles exist", binding.Name, binding.ProfileRef)
1✔
182
                        }
1✔
183
                        if _, ok := bundles[binding.ProfileRef]; !ok {
1✔
184
                                return nil, fmt.Errorf("binding %q references unknown profile %q", binding.Name, binding.ProfileRef)
×
185
                        }
×
186
                }
187

188
                for _, rule := range binding.PropertyRules {
2✔
189
                        if rule.ProfileRef != "" {
2✔
190
                                if bundles == nil {
2✔
191
                                        return nil, fmt.Errorf("property rule on binding %q references profile %q but no profiles exist", binding.Name, rule.ProfileRef)
1✔
192
                                }
1✔
193
                                if _, ok := bundles[rule.ProfileRef]; !ok {
1✔
194
                                        return nil, fmt.Errorf("property rule on binding %q references unknown profile %q", binding.Name, rule.ProfileRef)
×
195
                                }
×
196
                        }
197
                }
198
        }
199

200
        for _, policy := range policies {
2✔
201
                for _, wc := range policy.WhenClauses {
2✔
202
                        if wc.ProfileRef != "" {
2✔
203
                                if profiles == nil {
2✔
204
                                        return nil, fmt.Errorf("policy %q WHEN clause references profile %q but no promotion profiles exist", policy.Name, wc.ProfileRef)
1✔
205
                                }
1✔
206
                                if _, ok := profiles[wc.ProfileRef]; !ok {
1✔
207
                                        return nil, fmt.Errorf("policy %q WHEN clause references unknown promotion profile %q", policy.Name, wc.ProfileRef)
×
208
                                }
×
209
                        }
210
                }
211
        }
212

213
        for _, binding := range bindings {
2✔
214
                var bundle *DecayProfileBundle
1✔
215
                if !binding.NoDecay && binding.ProfileRef != "" {
2✔
216
                        bundle = bundles[binding.ProfileRef]
1✔
217
                }
1✔
218

219
                var cb *CompiledBinding
1✔
220
                if binding.NoDecay || bundle != nil {
2✔
221
                        cb = compileBinding(binding, bundle, bundles)
1✔
222
                } else {
1✔
223
                        cb = &CompiledBinding{DecayBinding: binding, NoDecay: true}
×
224
                }
×
225

226
                if binding.IsWildcard {
2✔
227
                        if binding.IsEdge {
2✔
228
                                bt.SetWildEdge(cb)
1✔
229
                        } else {
2✔
230
                                bt.SetWildNode(cb)
1✔
231
                        }
1✔
232
                        continue
1✔
233
                }
234

235
                if binding.IsEdge {
2✔
236
                        bt.SetEdge(binding.TargetEdgeType, cb)
1✔
237
                        continue
1✔
238
                }
239

240
                key := bindingLabelKey(binding.TargetLabels)
1✔
241
                if prev, ok := nodeConflicts[key]; ok {
2✔
242
                        if prev.order == binding.Order {
2✔
243
                                return nil, fmt.Errorf("conflict: bindings %q and %q both target label set %v with Order %d",
1✔
244
                                        prev.name, binding.Name, binding.TargetLabels, binding.Order)
1✔
245
                        }
1✔
246
                        if binding.Order < prev.order {
1✔
UNCOV
247
                                bt.SetNode(key, cb)
×
UNCOV
248
                                nodeConflicts[key] = seen{name: binding.Name, order: binding.Order}
×
UNCOV
249
                        }
×
250
                } else {
1✔
251
                        bt.SetNode(key, cb)
1✔
252
                        nodeConflicts[key] = seen{name: binding.Name, order: binding.Order}
1✔
253
                }
1✔
254
        }
255

256
        policyByKey := map[string]*PromotionPolicyDef{}
1✔
257
        for _, policy := range policies {
2✔
258
                if !policy.Enabled {
2✔
259
                        continue
1✔
260
                }
261
                policyByKey[policyTargetKey(policy)] = policy
1✔
262
        }
263

264
        bt.mu.Lock()
1✔
265
        for key, cb := range bt.nodes {
2✔
266
                if p, ok := policyByKey["node:"+key]; ok {
2✔
267
                        cb.PromotionPolicy = p
1✔
268
                        cb.CompiledPromotionRules = compilePromotionRules(p, profiles)
1✔
269
                }
1✔
270
        }
271
        for key, cb := range bt.edges {
2✔
272
                if p, ok := policyByKey["edge:"+key]; ok {
2✔
273
                        cb.PromotionPolicy = p
1✔
274
                        cb.CompiledPromotionRules = compilePromotionRules(p, profiles)
1✔
275
                }
1✔
276
        }
277
        if bt.wildNode != nil {
2✔
278
                if p, ok := policyByKey["wild:node"]; ok {
1✔
279
                        bt.wildNode.PromotionPolicy = p
×
280
                        bt.wildNode.CompiledPromotionRules = compilePromotionRules(p, profiles)
×
281
                }
×
282
        }
283
        if bt.wildEdge != nil {
2✔
284
                if p, ok := policyByKey["wild:edge"]; ok {
1✔
285
                        bt.wildEdge.PromotionPolicy = p
×
286
                        bt.wildEdge.CompiledPromotionRules = compilePromotionRules(p, profiles)
×
287
                }
×
288
        }
289
        bt.mu.Unlock()
1✔
290

1✔
291
        return bt, nil
1✔
292
}
293

294
func compilePromotionRules(policy *PromotionPolicyDef, profiles map[string]*PromotionProfileDef) []CompiledPromotionRule {
1✔
295
        if policy == nil || len(policy.WhenClauses) == 0 {
2✔
296
                return nil
1✔
297
        }
1✔
298
        rules := make([]CompiledPromotionRule, 0, len(policy.WhenClauses))
1✔
299
        for _, clause := range policy.WhenClauses {
2✔
300
                if profiles == nil || clause.ProfileRef == "" {
1✔
301
                        continue
×
302
                }
303
                profile := profiles[clause.ProfileRef]
1✔
304
                if profile == nil || !profile.Enabled {
1✔
305
                        continue
×
306
                }
307
                rules = append(rules, CompiledPromotionRule{
1✔
308
                        Predicate: clause.Predicate,
1✔
309
                        Profile:   profile,
1✔
310
                        Order:     clause.Order,
1✔
311
                })
1✔
312
        }
313
        sort.Slice(rules, func(i, j int) bool { return rules[i].Order < rules[j].Order })
2✔
314
        return rules
1✔
315
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc