causalontology 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,358 @@
1
+ # frozen_string_literal: false
2
+
3
+ # An in-memory conformant store.
4
+ #
5
+ # Implements the store side of the abstract operation set (spec/store.md):
6
+ # immutable content objects with idempotent put; signed, add-only provenance
7
+ # records; materialized enrichment views with contributors; retraction handling
8
+ # in default views; succession lineage; the resolve minimum; the deterministic
9
+ # cycle-breaking view rule; and the stigmergy gap read.
10
+ #
11
+ # Ruby Hashes preserve insertion order, exactly like Python dicts, so every
12
+ # iteration below deliberately mirrors the reference store's iteration order.
13
+
14
+ require "set"
15
+ require_relative "canonical"
16
+ require_relative "schema"
17
+ require_relative "semantics"
18
+ require_relative "signing"
19
+
20
+ module Causalontology
21
+ CONTENT_KINDS = Set.new(["occurrent", "causal_relation_object", "continuant",
22
+ "realizable", "stratum", "bridge", "port", "conduit",
23
+ "quality", "token_individual", "token_occurrence",
24
+ "state_assertion", "token_causal_claim"]).freeze
25
+ RECORD_KINDS = Set.new(["assertion", "enrichment", "retraction", "succession"]).freeze
26
+
27
+ # An enforcing store refused a write, with the reason as the message.
28
+ class RejectedWrite < StandardError; end
29
+
30
+ class InMemoryStore
31
+ attr_reader :enforcing, :objects, :records, :quarantine
32
+
33
+ def initialize(enforcing: true)
34
+ @enforcing = enforcing
35
+ @objects = {} # id -> content object
36
+ @records = {} # id -> provenance record
37
+ @quarantine = {} # id -> record (unsigned / unverifiable)
38
+ end
39
+
40
+ # ---------------------------------------------------------------- put
41
+
42
+ # Write a content object; idempotent; returns the identifier.
43
+ def put(obj, kind = nil)
44
+ kind ||= Canonical.infer_kind(obj)
45
+ unless CONTENT_KINDS.include?(kind)
46
+ raise ArgumentError, "put() takes content objects; use put_record()"
47
+ end
48
+ obj = obj.dup
49
+ obj["type"] = kind unless obj.key?("type")
50
+ obj["id"] = Canonical.identify(obj, kind) unless obj.key?("id")
51
+ return obj["id"] if @objects.key?(obj["id"]) # immutable: identical identity is a no-op
52
+ ok, why = Schema.validate_schema(obj, kind)
53
+ raise RejectedWrite, why.join("; ") unless ok
54
+ ok, why = Semantics.validate_semantics(obj, kind)
55
+ raise RejectedWrite, why.join("; ") unless ok
56
+ @objects[obj["id"]] = obj
57
+ obj["id"]
58
+ end
59
+
60
+ # Write a signed provenance record; returns the identifier.
61
+ def put_record(record, kind = nil, force: false)
62
+ kind ||= Canonical.infer_kind(record)
63
+ unless RECORD_KINDS.include?(kind)
64
+ raise ArgumentError, "put_record() takes provenance records"
65
+ end
66
+ record = record.dup
67
+ record["type"] = kind unless record.key?("type")
68
+ rid = record["id"]
69
+ rid = Canonical.identify(record, kind) if rid.nil? || rid.empty?
70
+ record["id"] = rid
71
+ return rid if @records.key?(rid) # add-only and idempotent
72
+ unless Signing.verify_record(record, kind)
73
+ @quarantine[rid] = record
74
+ raise RejectedWrite, "unsigned or unverifiable record: quarantined"
75
+ end
76
+ ok, why = Semantics.validate_semantics(record, kind)
77
+ raise RejectedWrite, why.join("; ") unless ok
78
+ if kind == "retraction" && !retraction_source_ok(record)
79
+ raise RejectedWrite,
80
+ "a retraction is valid only from the retracted record's " \
81
+ "source or its succession lineage"
82
+ end
83
+ if kind == "enrichment" && @enforcing && !force
84
+ if ["subsumes", "part_of"].include?(record["field"]) && would_cycle(record)
85
+ raise RejectedWrite,
86
+ "would create a cycle in the materialized " \
87
+ "#{record["field"]} graph"
88
+ end
89
+ end
90
+ @records[rid] = record
91
+ rid
92
+ end
93
+
94
+ # Simulate a decentralized replica merge (no enforcement gate).
95
+ def force_merge_record(record, kind = nil)
96
+ put_record(record, kind, force: true)
97
+ end
98
+
99
+ # ----------------------------------------------------- record queries
100
+
101
+ def records_of(kind)
102
+ @records.values.select { |r| r["type"] == kind }
103
+ end
104
+
105
+ def retracted_ids
106
+ out = Set.new
107
+ records_of("retraction").each { |r| out << r["retracts"] }
108
+ out
109
+ end
110
+
111
+ def retraction_source_ok(retraction)
112
+ target = @records[retraction["retracts"]]
113
+ return true if target.nil? # open world: the target may arrive later
114
+ lineage(target["source"]).include?(retraction["source"])
115
+ end
116
+
117
+ # The succession chain closure containing key (includes key).
118
+ def lineage(key)
119
+ succ = {}
120
+ pred = {}
121
+ records_of("succession").each do |s|
122
+ succ[s["predecessor"]] = s["successor"]
123
+ pred[s["successor"]] = s["predecessor"]
124
+ end
125
+ chain = Set.new([key])
126
+ cursor = key
127
+ while pred.key?(cursor)
128
+ cursor = pred[cursor]
129
+ chain << cursor
130
+ end
131
+ cursor = key
132
+ while succ.key?(cursor)
133
+ cursor = succ[cursor]
134
+ chain << cursor
135
+ end
136
+ chain
137
+ end
138
+
139
+ def assertions_about(identifier, include_retracted: false)
140
+ retracted = retracted_ids
141
+ out = []
142
+ records_of("assertion").each do |r|
143
+ next unless r["about"] == identifier
144
+ if retracted.include?(r["id"])
145
+ out << r.merge("retracted" => true) if include_retracted
146
+ next
147
+ end
148
+ out << r
149
+ end
150
+ out
151
+ end
152
+
153
+ def enrichments_about(identifier, include_retracted: false)
154
+ retracted = retracted_ids
155
+ out = []
156
+ records_of("enrichment").each do |r|
157
+ next unless r["about"] == identifier
158
+ next if retracted.include?(r["id"]) && !include_retracted
159
+ out << r
160
+ end
161
+ out
162
+ end
163
+
164
+ # --------------------------------------------------- materialized views
165
+
166
+ # [edges, excluded] for subsumes/part_of after rule 13 cycle-breaking.
167
+ def active_taxonomy_edges(field)
168
+ retracted = retracted_ids
169
+ recs = records_of("enrichment").select do |r|
170
+ r["field"] == field && !retracted.include?(r["id"])
171
+ end
172
+ active = recs.dup
173
+ excluded = []
174
+ loop do
175
+ cyc = self.class.find_cycle_records(active)
176
+ break if cyc.empty?
177
+ # exclude the cycle-completing record with the LATEST timestamp,
178
+ # ties broken by lexicographic record identifier (deterministic)
179
+ loser = cyc.max_by { |r| [r["timestamp"], r["id"]] }
180
+ active.delete_at(active.index(loser))
181
+ excluded << loser
182
+ end
183
+ [active, excluded]
184
+ end
185
+
186
+ # The records forming one directed cycle among the given enrichment
187
+ # records, or an empty array when the graph they draw is acyclic.
188
+ def self.find_cycle_records(recs)
189
+ edges = {}
190
+ recs.each { |r| (edges[r["about"]] ||= []) << [r["entry"], r] }
191
+ state = Hash.new(0)
192
+ cycle = []
193
+
194
+ dfs = nil
195
+ dfs = lambda do |node, path_records|
196
+ state[node] = 1
197
+ (edges[node] || []).each do |nxt, rec|
198
+ if state[nxt] == 1
199
+ cycle.concat(path_records + [rec])
200
+ return true
201
+ end
202
+ if state[nxt] == 0
203
+ return true if dfs.call(nxt, path_records + [rec])
204
+ end
205
+ end
206
+ state[node] = 2
207
+ false
208
+ end
209
+
210
+ edges.keys.each do |start|
211
+ return cycle if state[start] == 0 && dfs.call(start, [])
212
+ end
213
+ []
214
+ end
215
+
216
+ def would_cycle(record)
217
+ retracted = retracted_ids
218
+ recs = records_of("enrichment").select do |r|
219
+ r["field"] == record["field"] && !retracted.include?(r["id"])
220
+ end
221
+ !self.class.find_cycle_records(recs + [record]).empty?
222
+ end
223
+
224
+ # The object with its materialized enrichment sets and contributors.
225
+ def get(identifier, view: "default")
226
+ obj = @objects[identifier]
227
+ return nil if obj.nil?
228
+ include_retracted = (view == "history")
229
+ excluded_ids = Set.new
230
+ ["subsumes", "part_of"].each do |field|
231
+ _, excluded = active_taxonomy_edges(field)
232
+ excluded.each { |r| excluded_ids << r["id"] }
233
+ end
234
+ fields = {}
235
+ enrichments_about(identifier, include_retracted: include_retracted).each do |rec|
236
+ next if excluded_ids.include?(rec["id"]) && view != "history"
237
+ entry = rec["entry"]
238
+ entry_key = [rec["field"],
239
+ entry.is_a?(Hash) ? entry.to_a.sort : entry]
240
+ slot = (fields[rec["field"]] ||= {})
241
+ bucket = (slot[entry_key] ||= { "entry" => entry, "contributors" => [] })
242
+ bucket["contributors"] << {
243
+ "source" => rec["source"], "timestamp" => rec["timestamp"]
244
+ }
245
+ end
246
+ enrichments = fields.transform_values(&:values)
247
+ return { "object" => obj } if view == "raw"
248
+ { "object" => obj, "enrichments" => enrichments }
249
+ end
250
+
251
+ # -------------------------------------------------------------- resolve
252
+
253
+ def self.canon_label(text)
254
+ text.strip.downcase.split.join("_")
255
+ end
256
+
257
+ def self.norm_alias(text)
258
+ text.split.join(" ").downcase(:fold)
259
+ end
260
+
261
+ # The conformance minimum: exact label, then alias, then nothing.
262
+ def resolve(text, lang = nil)
263
+ label_hits = []
264
+ alias_hits = []
265
+ wanted_label = self.class.canon_label(text)
266
+ wanted_alias = self.class.norm_alias(text)
267
+ retracted = retracted_ids
268
+ @objects.each do |oid, obj|
269
+ next unless ["occurrent", "continuant"].include?(obj["type"])
270
+ if obj["label"] == wanted_label
271
+ label_hits << oid
272
+ next
273
+ end
274
+ records_of("enrichment").each do |rec|
275
+ next unless rec["about"] == oid && rec["field"] == "aliases"
276
+ next if retracted.include?(rec["id"])
277
+ entry = rec["entry"]
278
+ next if !lang.nil? && entry["lang"] != lang
279
+ if self.class.norm_alias(entry["text"] || "") == wanted_alias
280
+ alias_hits << oid
281
+ break
282
+ end
283
+ end
284
+ end
285
+ label_hits + alias_hits
286
+ end
287
+
288
+ # ---------------------------------------------------------------- gaps
289
+
290
+ # The stigmergy read. Gap kinds per spec/store.md.
291
+ def gaps(kind = nil)
292
+ out = []
293
+ refined = Set.new
294
+ @objects.each_value do |obj|
295
+ next unless obj["type"] == "causal_relation_object" && obj["refines"]
296
+ parent = @objects[obj["refines"]]
297
+ next if parent.nil?
298
+ ok, _reason = Semantics.refinement_valid(obj, parent)
299
+ refined << parent["id"] if ok
300
+ end
301
+ @objects.each do |oid, obj|
302
+ next unless obj["type"] == "causal_relation_object"
303
+ # missing_field: lacking the temporal window or the modality -
304
+ # mechanism and context may legitimately stay unspecified forever
305
+ # (empty_mechanism is its own kind; absent context = context-free).
306
+ if (!obj.key?("temporal") || !obj.key?("modality")) &&
307
+ !refined.include?(oid)
308
+ out << { "id" => oid, "kind" => "missing_field",
309
+ "missing" => Semantics.is_partial(obj)[1] }
310
+ end
311
+ if !obj.key?("mechanism") || obj["mechanism"] == []
312
+ unless refined.include?(oid)
313
+ out << { "id" => oid, "kind" => "empty_mechanism" }
314
+ end
315
+ end
316
+ end
317
+ ["subsumes", "part_of"].each do |field|
318
+ _, excluded = active_taxonomy_edges(field)
319
+ excluded.each do |rec|
320
+ out << { "id" => rec["id"], "kind" => "inconsistent_hierarchy",
321
+ "note" => "excluded by the deterministic " \
322
+ "cycle-breaking view rule" }
323
+ end
324
+ end
325
+ # dangling_reference: a reference to an object absent from the store -
326
+ # the red link that says "this page is wanted".
327
+ @objects.each do |oid, obj|
328
+ refs = []
329
+ if obj["type"] == "causal_relation_object"
330
+ refs = (obj["causes"] || []).to_a +
331
+ (obj["effects"] || []).to_a +
332
+ (obj["context"] || []).to_a +
333
+ (obj["mechanism"] || []).to_a
334
+ refs << obj["refines"] if obj["refines"]
335
+ elsif obj["type"] == "realizable"
336
+ refs = [obj["bearer"]]
337
+ end
338
+ refs.each do |ref|
339
+ if ref && !@objects.key?(ref)
340
+ out << { "id" => oid, "kind" => "dangling_reference", "ref" => ref }
341
+ end
342
+ end
343
+ end
344
+ # conflict: pairs of claims satisfying the formal test (rule 6).
345
+ cros = @objects.values.select { |o| o["type"] == "causal_relation_object" }
346
+ (0...cros.length).each do |i|
347
+ ((i + 1)...cros.length).each do |j|
348
+ if Semantics.conflicts(cros[i], cros[j])
349
+ out << { "kind" => "conflict",
350
+ "a" => cros[i]["id"], "b" => cros[j]["id"] }
351
+ end
352
+ end
353
+ end
354
+ out = out.select { |g| g["kind"] == kind } unless kind.nil?
355
+ out
356
+ end
357
+ end
358
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: false
2
+
3
+ # causalontology - the Ruby binding of the Causalontology standard.
4
+ #
5
+ # A faithful port of causalontology-py, proving language independence:
6
+ # standard library only (json, digest), conformant when it passes every
7
+ # vector in conformance/vectors/ (run bindings/ruby/conformance.rb).
8
+ #
9
+ # Causalontology is a verb-first noun-hosting ontology: reality is what
10
+ # happens, and things are its participants.
11
+
12
+ require "json"
13
+ require "digest"
14
+ require "set"
15
+
16
+ require_relative "causalontology/jcs"
17
+ require_relative "causalontology/canonical"
18
+ require_relative "causalontology/ed25519"
19
+ require_relative "causalontology/signing"
20
+ require_relative "causalontology/schema"
21
+ require_relative "causalontology/semantics"
22
+ require_relative "causalontology/store"
23
+
24
+ module Causalontology
25
+ VERSION = "2.0.0" # specification 2.0.0 (whole-word re-mint; vectors re-frozen)
26
+
27
+ # Rule 4's fixed unit-conversion constants, re-exported at the top level.
28
+ UNIT_SECONDS = Semantics::UNIT_SECONDS
29
+
30
+ module_function
31
+
32
+ # -- canonicalization and identity (spec/identity.md) ---------------------
33
+
34
+ def canonicalize(obj, kind = nil)
35
+ Canonical.canonicalize(obj, kind)
36
+ end
37
+
38
+ def identify(obj, kind = nil)
39
+ Canonical.identify(obj, kind)
40
+ end
41
+
42
+ def identity_bearing(obj, kind = nil)
43
+ Canonical.identity_bearing(obj, kind)
44
+ end
45
+
46
+ def infer_kind(obj)
47
+ Canonical.infer_kind(obj)
48
+ end
49
+
50
+ # -- validation (spec/schema/, spec/semantics.md) --------------------------
51
+
52
+ def validate_schema(obj, kind = nil)
53
+ Schema.validate_schema(obj, kind)
54
+ end
55
+
56
+ def validate_semantics(obj, kind = nil)
57
+ Semantics.validate_semantics(obj, kind)
58
+ end
59
+
60
+ def is_partial(cro)
61
+ Semantics.is_partial(cro)
62
+ end
63
+
64
+ def admissible(cro, elapsed_seconds)
65
+ Semantics.admissible(cro, elapsed_seconds)
66
+ end
67
+
68
+ def conflicts(a, b)
69
+ Semantics.conflicts(a, b)
70
+ end
71
+
72
+ def refinement_valid(child, parent)
73
+ Semantics.refinement_valid(child, parent)
74
+ end
75
+
76
+ def hierarchy_consistent(parent, members, bridges = [])
77
+ Semantics.hierarchy_consistent(parent, members, bridges)
78
+ end
79
+
80
+ # -- 2.0.0 normative algorithms and rules (spec/semantics.md, Section 12) --
81
+
82
+ def bridge_closure(occurrent_id, bridges)
83
+ Semantics.bridge_closure(occurrent_id, bridges)
84
+ end
85
+
86
+ def classify_cro(cro, occ_map, stratum_map)
87
+ Semantics.classify_cro(cro, occ_map, stratum_map)
88
+ end
89
+
90
+ def endpoints_mixed(cro, occ_map)
91
+ Semantics.endpoints_mixed(cro, occ_map)
92
+ end
93
+
94
+ def skip_gaps(cro, classification)
95
+ Semantics.skip_gaps(cro, classification)
96
+ end
97
+
98
+ def to_seconds(duration, unit)
99
+ Semantics.to_seconds(duration, unit)
100
+ end
101
+
102
+ def delay_within_window(actual_delay, temporal)
103
+ Semantics.delay_within_window(actual_delay, temporal)
104
+ end
105
+
106
+ def bridge_wellformed(bridge, occ_map, stratum_map)
107
+ Semantics.bridge_wellformed(bridge, occ_map, stratum_map)
108
+ end
109
+
110
+ def conduit_wellformed(conduit, port_map, cro_map = nil)
111
+ Semantics.conduit_wellformed(conduit, port_map, cro_map)
112
+ end
113
+
114
+ def state_gaps(state, quality)
115
+ Semantics.state_gaps(state, quality)
116
+ end
117
+
118
+ def covering_law_mismatch(tcc, token_map, law)
119
+ Semantics.covering_law_mismatch(tcc, token_map, law)
120
+ end
121
+
122
+ def retrocausal(tcc, token_map)
123
+ Semantics.retrocausal(tcc, token_map)
124
+ end
125
+
126
+ def has_cycle(edges)
127
+ Semantics.has_cycle(edges)
128
+ end
129
+
130
+ # -- provenance (spec/provenance.md) ---------------------------------------
131
+
132
+ def keypair_from_seed(seed32)
133
+ Signing.keypair_from_seed(seed32)
134
+ end
135
+
136
+ def sign_record(record, secret, kind = nil)
137
+ Signing.sign_record(record, secret, kind)
138
+ end
139
+
140
+ def verify_record(record, kind = nil)
141
+ Signing.verify_record(record, kind)
142
+ end
143
+ end
metadata ADDED
@@ -0,0 +1,63 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: causalontology
3
+ version: !ruby/object:Gem::Version
4
+ version: 2.0.0
5
+ platform: ruby
6
+ authors:
7
+ - AI University (AIU)
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-07-16 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: 'The Ruby binding of the Causalontology standard - a verb-first noun-hosting
14
+ ontology; a programming-language-neutral standard and shared commons for reified
15
+ causation. Zero dependencies: RFC 8785 canonicalization, SHA-256 identity, pure-Ruby
16
+ Ed25519 (RFC 8032), schema and semantics validation, and an in-memory conformant
17
+ store.'
18
+ email:
19
+ - ai.university.aiu@gmail.com
20
+ executables: []
21
+ extensions: []
22
+ extra_rdoc_files: []
23
+ files:
24
+ - LICENSE
25
+ - README.md
26
+ - conformance.rb
27
+ - lib/causalontology.rb
28
+ - lib/causalontology/canonical.rb
29
+ - lib/causalontology/ed25519.rb
30
+ - lib/causalontology/jcs.rb
31
+ - lib/causalontology/schema.rb
32
+ - lib/causalontology/semantics.rb
33
+ - lib/causalontology/signing.rb
34
+ - lib/causalontology/store.rb
35
+ homepage: https://github.com/ai-university-aiu/causalontology
36
+ licenses:
37
+ - Nonstandard
38
+ metadata:
39
+ homepage_uri: https://github.com/ai-university-aiu/causalontology
40
+ source_code_uri: https://github.com/ai-university-aiu/causalontology/tree/main/bindings/ruby
41
+ documentation_uri: https://github.com/ai-university-aiu/causalontology/tree/main/spec
42
+ license_note: The attribution always; no profit, no problem license. (Apache License
43
+ 2.0 text) - see LICENSE and NOTICE in the repository root.
44
+ post_install_message:
45
+ rdoc_options: []
46
+ require_paths:
47
+ - lib
48
+ required_ruby_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: '3.0'
53
+ required_rubygems_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: '0'
58
+ requirements: []
59
+ rubygems_version: 3.5.22
60
+ signing_key:
61
+ specification_version: 4
62
+ summary: The Ruby binding of the Causalontology standard
63
+ test_files: []