Class: Causalontology::InMemoryStore
- Inherits:
-
Object
- Object
- Causalontology::InMemoryStore
- Defined in:
- lib/causalontology/store.rb
Instance Attribute Summary collapse
-
#enforcing ⇒ Object
readonly
Returns the value of attribute enforcing.
-
#objects ⇒ Object
readonly
Returns the value of attribute objects.
-
#quarantine ⇒ Object
readonly
Returns the value of attribute quarantine.
-
#records ⇒ Object
readonly
Returns the value of attribute records.
Class Method Summary collapse
-
.canon_label(text) ⇒ Object
-------------------------------------------------------------- resolve.
-
.find_cycle_records(recs) ⇒ Object
The records forming one directed cycle among the given enrichment records, or an empty array when the graph they draw is acyclic.
- .norm_alias(text) ⇒ Object
Instance Method Summary collapse
-
#active_taxonomy_edges(field) ⇒ Object
[edges, excluded] for subsumes/part_of after rule 13 cycle-breaking.
- #assertions_about(identifier, include_retracted: false) ⇒ Object
- #enrichments_about(identifier, include_retracted: false) ⇒ Object
-
#force_merge_record(record, kind = nil) ⇒ Object
Simulate a decentralized replica merge (no enforcement gate).
-
#gaps(kind = nil) ⇒ Object
The stigmergy read.
-
#get(identifier, view: "default") ⇒ Object
The object with its materialized enrichment sets and contributors.
-
#initialize(enforcing: true) ⇒ InMemoryStore
constructor
A new instance of InMemoryStore.
-
#lineage(key) ⇒ Object
The succession chain closure containing key (includes key).
-
#put(obj, kind = nil) ⇒ Object
Write a content object; idempotent; returns the identifier.
-
#put_record(record, kind = nil, force: false) ⇒ Object
Write a signed provenance record; returns the identifier.
-
#records_of(kind) ⇒ Object
----------------------------------------------------- record queries.
-
#resolve(text, lang = nil) ⇒ Object
The conformance minimum: exact label, then alias, then nothing.
- #retracted_ids ⇒ Object
- #retraction_source_ok(retraction) ⇒ Object
- #would_cycle(record) ⇒ Object
Constructor Details
#initialize(enforcing: true) ⇒ InMemoryStore
Returns a new instance of InMemoryStore.
30 31 32 33 34 35 |
# File 'lib/causalontology/store.rb', line 30 def initialize(enforcing: true) @enforcing = enforcing @objects = {} # id -> content object @records = {} # id -> provenance record @quarantine = {} # id -> record (unsigned / unverifiable) end |
Instance Attribute Details
#enforcing ⇒ Object (readonly)
Returns the value of attribute enforcing.
28 29 30 |
# File 'lib/causalontology/store.rb', line 28 def enforcing @enforcing end |
#objects ⇒ Object (readonly)
Returns the value of attribute objects.
28 29 30 |
# File 'lib/causalontology/store.rb', line 28 def objects @objects end |
#quarantine ⇒ Object (readonly)
Returns the value of attribute quarantine.
28 29 30 |
# File 'lib/causalontology/store.rb', line 28 def quarantine @quarantine end |
#records ⇒ Object (readonly)
Returns the value of attribute records.
28 29 30 |
# File 'lib/causalontology/store.rb', line 28 def records @records end |
Class Method Details
.canon_label(text) ⇒ Object
-------------------------------------------------------------- resolve
250 251 252 |
# File 'lib/causalontology/store.rb', line 250 def self.canon_label(text) text.strip.downcase.split.join("_") end |
.find_cycle_records(recs) ⇒ Object
The records forming one directed cycle among the given enrichment records, or an empty array when the graph they draw is acyclic.
185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 |
# File 'lib/causalontology/store.rb', line 185 def self.find_cycle_records(recs) edges = {} recs.each { |r| (edges[r["about"]] ||= []) << [r["entry"], r] } state = Hash.new(0) cycle = [] dfs = nil dfs = lambda do |node, path_records| state[node] = 1 (edges[node] || []).each do |nxt, rec| if state[nxt] == 1 cycle.concat(path_records + [rec]) return true end if state[nxt] == 0 return true if dfs.call(nxt, path_records + [rec]) end end state[node] = 2 false end edges.keys.each do |start| return cycle if state[start] == 0 && dfs.call(start, []) end [] end |
.norm_alias(text) ⇒ Object
254 255 256 |
# File 'lib/causalontology/store.rb', line 254 def self.norm_alias(text) text.split.join(" ").downcase(:fold) end |
Instance Method Details
#active_taxonomy_edges(field) ⇒ Object
[edges, excluded] for subsumes/part_of after rule 13 cycle-breaking.
164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 |
# File 'lib/causalontology/store.rb', line 164 def active_taxonomy_edges(field) retracted = retracted_ids recs = records_of("enrichment").select do |r| r["field"] == field && !retracted.include?(r["id"]) end active = recs.dup excluded = [] loop do cyc = self.class.find_cycle_records(active) break if cyc.empty? # exclude the cycle-completing record with the LATEST timestamp, # ties broken by lexicographic record identifier (deterministic) loser = cyc.max_by { |r| [r["timestamp"], r["id"]] } active.delete_at(active.index(loser)) excluded << loser end [active, excluded] end |
#assertions_about(identifier, include_retracted: false) ⇒ Object
136 137 138 139 140 141 142 143 144 145 146 147 148 |
# File 'lib/causalontology/store.rb', line 136 def assertions_about(identifier, include_retracted: false) retracted = retracted_ids out = [] records_of("assertion").each do |r| next unless r["about"] == identifier if retracted.include?(r["id"]) out << r.merge("retracted" => true) if include_retracted next end out << r end out end |
#enrichments_about(identifier, include_retracted: false) ⇒ Object
150 151 152 153 154 155 156 157 158 159 |
# File 'lib/causalontology/store.rb', line 150 def enrichments_about(identifier, include_retracted: false) retracted = retracted_ids out = [] records_of("enrichment").each do |r| next unless r["about"] == identifier next if retracted.include?(r["id"]) && !include_retracted out << r end out end |
#force_merge_record(record, kind = nil) ⇒ Object
Simulate a decentralized replica merge (no enforcement gate).
92 93 94 |
# File 'lib/causalontology/store.rb', line 92 def force_merge_record(record, kind = nil) put_record(record, kind, force: true) end |
#gaps(kind = nil) ⇒ Object
The stigmergy read. Gap kinds per spec/store.md.
288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 |
# File 'lib/causalontology/store.rb', line 288 def gaps(kind = nil) out = [] refined = Set.new @objects.each_value do |obj| next unless obj["type"] == "cro" && obj["refines"] parent = @objects[obj["refines"]] next if parent.nil? ok, _reason = Semantics.refinement_valid(obj, parent) refined << parent["id"] if ok end @objects.each do |oid, obj| next unless obj["type"] == "cro" # missing_field: lacking the temporal window or the modality - # mechanism and context may legitimately stay unspecified forever # (empty_mechanism is its own kind; absent context = context-free). if (!obj.key?("temporal") || !obj.key?("modality")) && !refined.include?(oid) out << { "id" => oid, "kind" => "missing_field", "missing" => Semantics.is_partial(obj)[1] } end if !obj.key?("mechanism") || obj["mechanism"] == [] unless refined.include?(oid) out << { "id" => oid, "kind" => "empty_mechanism" } end end end ["subsumes", "part_of"].each do |field| _, excluded = active_taxonomy_edges(field) excluded.each do |rec| out << { "id" => rec["id"], "kind" => "inconsistent_hierarchy", "note" => "excluded by the deterministic " \ "cycle-breaking view rule" } end end # dangling_reference: a reference to an object absent from the store - # the red link that says "this page is wanted". @objects.each do |oid, obj| refs = [] if obj["type"] == "cro" refs = (obj["causes"] || []).to_a + (obj["effects"] || []).to_a + (obj["context"] || []).to_a + (obj["mechanism"] || []).to_a refs << obj["refines"] if obj["refines"] elsif obj["type"] == "realizable" refs = [obj["bearer"]] end refs.each do |ref| if ref && !@objects.key?(ref) out << { "id" => oid, "kind" => "dangling_reference", "ref" => ref } end end end # conflict: pairs of claims satisfying the formal test (rule 6). cros = @objects.values.select { |o| o["type"] == "cro" } (0...cros.length).each do |i| ((i + 1)...cros.length).each do |j| if Semantics.conflicts(cros[i], cros[j]) out << { "kind" => "conflict", "a" => cros[i]["id"], "b" => cros[j]["id"] } end end end out = out.select { |g| g["kind"] == kind } unless kind.nil? out end |
#get(identifier, view: "default") ⇒ Object
The object with its materialized enrichment sets and contributors.
222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 |
# File 'lib/causalontology/store.rb', line 222 def get(identifier, view: "default") obj = @objects[identifier] return nil if obj.nil? include_retracted = (view == "history") excluded_ids = Set.new ["subsumes", "part_of"].each do |field| _, excluded = active_taxonomy_edges(field) excluded.each { |r| excluded_ids << r["id"] } end fields = {} enrichments_about(identifier, include_retracted: include_retracted).each do |rec| next if excluded_ids.include?(rec["id"]) && view != "history" entry = rec["entry"] entry_key = [rec["field"], entry.is_a?(Hash) ? entry.to_a.sort : entry] slot = (fields[rec["field"]] ||= {}) bucket = (slot[entry_key] ||= { "entry" => entry, "contributors" => [] }) bucket["contributors"] << { "source" => rec["source"], "timestamp" => rec["timestamp"] } end enrichments = fields.transform_values(&:values) return { "object" => obj } if view == "raw" { "object" => obj, "enrichments" => enrichments } end |
#lineage(key) ⇒ Object
The succession chain closure containing key (includes key).
115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 |
# File 'lib/causalontology/store.rb', line 115 def lineage(key) succ = {} pred = {} records_of("succession").each do |s| succ[s["predecessor"]] = s["successor"] pred[s["successor"]] = s["predecessor"] end chain = Set.new([key]) cursor = key while pred.key?(cursor) cursor = pred[cursor] chain << cursor end cursor = key while succ.key?(cursor) cursor = succ[cursor] chain << cursor end chain end |
#put(obj, kind = nil) ⇒ Object
Write a content object; idempotent; returns the identifier.
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
# File 'lib/causalontology/store.rb', line 40 def put(obj, kind = nil) kind ||= Canonical.infer_kind(obj) unless CONTENT_KINDS.include?(kind) raise ArgumentError, "put() takes content objects; use put_record()" end obj = obj.dup obj["type"] = kind unless obj.key?("type") obj["id"] = Canonical.identify(obj, kind) unless obj.key?("id") return obj["id"] if @objects.key?(obj["id"]) # immutable: identical identity is a no-op ok, why = Schema.validate_schema(obj, kind) raise RejectedWrite, why.join("; ") unless ok ok, why = Semantics.validate_semantics(obj, kind) raise RejectedWrite, why.join("; ") unless ok @objects[obj["id"]] = obj obj["id"] end |
#put_record(record, kind = nil, force: false) ⇒ Object
Write a signed provenance record; returns the identifier.
58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
# File 'lib/causalontology/store.rb', line 58 def put_record(record, kind = nil, force: false) kind ||= Canonical.infer_kind(record) unless RECORD_KINDS.include?(kind) raise ArgumentError, "put_record() takes provenance records" end record = record.dup record["type"] = kind unless record.key?("type") rid = record["id"] rid = Canonical.identify(record, kind) if rid.nil? || rid.empty? record["id"] = rid return rid if @records.key?(rid) # add-only and idempotent unless Signing.verify_record(record, kind) @quarantine[rid] = record raise RejectedWrite, "unsigned or unverifiable record: quarantined" end ok, why = Semantics.validate_semantics(record, kind) raise RejectedWrite, why.join("; ") unless ok if kind == "retraction" && !retraction_source_ok(record) raise RejectedWrite, "a retraction is valid only from the retracted record's " \ "source or its succession lineage" end if kind == "enrichment" && @enforcing && !force if ["subsumes", "part_of"].include?(record["field"]) && would_cycle(record) raise RejectedWrite, "would create a cycle in the materialized " \ "#{record["field"]} graph" end end @records[rid] = record rid end |
#records_of(kind) ⇒ Object
----------------------------------------------------- record queries
98 99 100 |
# File 'lib/causalontology/store.rb', line 98 def records_of(kind) @records.values.select { |r| r["type"] == kind } end |
#resolve(text, lang = nil) ⇒ Object
The conformance minimum: exact label, then alias, then nothing.
259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 |
# File 'lib/causalontology/store.rb', line 259 def resolve(text, lang = nil) label_hits = [] alias_hits = [] wanted_label = self.class.canon_label(text) wanted_alias = self.class.norm_alias(text) retracted = retracted_ids @objects.each do |oid, obj| next unless ["occurrent", "continuant"].include?(obj["type"]) if obj["label"] == wanted_label label_hits << oid next end records_of("enrichment").each do |rec| next unless rec["about"] == oid && rec["field"] == "aliases" next if retracted.include?(rec["id"]) entry = rec["entry"] next if !lang.nil? && entry["lang"] != lang if self.class.norm_alias(entry["text"] || "") == wanted_alias alias_hits << oid break end end end label_hits + alias_hits end |
#retracted_ids ⇒ Object
102 103 104 105 106 |
# File 'lib/causalontology/store.rb', line 102 def retracted_ids out = Set.new records_of("retraction").each { |r| out << r["retracts"] } out end |
#retraction_source_ok(retraction) ⇒ Object
108 109 110 111 112 |
# File 'lib/causalontology/store.rb', line 108 def retraction_source_ok(retraction) target = @records[retraction["retracts"]] return true if target.nil? # open world: the target may arrive later lineage(target["source"]).include?(retraction["source"]) end |
#would_cycle(record) ⇒ Object
213 214 215 216 217 218 219 |
# File 'lib/causalontology/store.rb', line 213 def would_cycle(record) retracted = retracted_ids recs = records_of("enrichment").select do |r| r["field"] == record["field"] && !retracted.include?(r["id"]) end !self.class.find_cycle_records(recs + [record]).empty? end |