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