causalontology 2.0.0 → 4.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.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +56 -14
  3. data/conformance.rb +503 -11
  4. data/lib/causalontology/canonical.rb +14 -4
  5. data/lib/causalontology/schema.rb +14 -1
  6. data/lib/causalontology/semantics.rb +161 -16
  7. data/lib/causalontology/spec/schema/assertion.schema.json +75 -0
  8. data/lib/causalontology/spec/schema/attitude.schema.json +45 -0
  9. data/lib/causalontology/spec/schema/bridge.schema.json +46 -0
  10. data/lib/causalontology/spec/schema/causal_relation_object.schema.json +111 -0
  11. data/lib/causalontology/spec/schema/conduit.schema.json +56 -0
  12. data/lib/causalontology/spec/schema/continuant.schema.json +38 -0
  13. data/lib/causalontology/spec/schema/cross_stratal_seam.schema.json +50 -0
  14. data/lib/causalontology/spec/schema/enrichment.schema.json +80 -0
  15. data/lib/causalontology/spec/schema/individual.schema.json +36 -0
  16. data/lib/causalontology/spec/schema/occurrent.schema.json +42 -0
  17. data/lib/causalontology/spec/schema/port.schema.json +56 -0
  18. data/lib/causalontology/spec/schema/predicted_occurrence.schema.json +68 -0
  19. data/lib/causalontology/spec/schema/prediction_error.schema.json +37 -0
  20. data/lib/causalontology/spec/schema/quality.schema.json +44 -0
  21. data/lib/causalontology/spec/schema/realizable.schema.json +38 -0
  22. data/lib/causalontology/spec/schema/retraction.schema.json +43 -0
  23. data/lib/causalontology/spec/schema/state.schema.json +89 -0
  24. data/lib/causalontology/spec/schema/stratum.schema.json +52 -0
  25. data/lib/causalontology/spec/schema/succession.schema.json +39 -0
  26. data/lib/causalontology/spec/schema/token.schema.json +96 -0
  27. data/lib/causalontology/spec/schema/token_causal_claim.schema.json +76 -0
  28. data/lib/causalontology/store.rb +5 -2
  29. data/lib/causalontology.rb +17 -1
  30. metadata +23 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 45698b76b404d70f72038135518d44cce3ae20352371528ace7763d99a5bd85b
4
- data.tar.gz: fe232d7bfd1c97fb2259f2c4eedcc09e6d3b65e67528a7cd2032372ec8643858
3
+ metadata.gz: efd3a466f37b7098982f2829bdf01629a684b637dbe562cd2dbf461be644879d
4
+ data.tar.gz: 2e88e13a3f8c0eaf9f00fec6921834c7860640fbfcb90e6e757715a7a7833059
5
5
  SHA512:
6
- metadata.gz: 7e979fe48bc823b6c1c428cfc4a5531326a8f00b1af4e3cc1f5eff38e65714c145f15a88f652505d104ac270bf26d121995179a089d99b83b69b86fe96522eda
7
- data.tar.gz: 35fd2f951d9564a44138512be8d376fd88951d215f11ea8f264dfb8f8e0345025210ac2e189f154c61257195b65d2162d3e09bed1c58f7d9950c743b437c1d16
6
+ metadata.gz: 6835f2290f7131f151221dbbaeec1c9ce3ee34b5505914af24f09f3d1f56646cac366f6e6b3b600a6eba735bc3228643a951bca77c41c7eb8bbbbd331ad8aeb7
7
+ data.tar.gz: c603eba74103082be26eda44bbb98beeafe1f4bd08e159a736a497a8d7d5cf7df7034588074b69de62f0f862439ce373c64f98a90d45912294edcfc60f698867
data/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
  **Zero gems.** The Ruby standard library carries everything the standard
7
7
  needs: `json` (whose parser keeps the integer-versus-decimal source
8
8
  distinction — `1` parses to `Integer`, `1.0` to `Float` — so it survives to
9
- the canonicalizer), `digest` (SHA-256 and SHA-512), and Ruby's native
9
+ the canonicalizer), `digest` (Secure Hash Algorithm 256-bit (SHA-256) and SHA-512), and Ruby's native
10
10
  bignums with three-argument `Integer#pow` for the pure-Ruby Ed25519.
11
11
  Requires **Ruby 3.0 or newer** (CI runs 3.3).
12
12
 
@@ -16,29 +16,71 @@ Requires **Ruby 3.0 or newer** (CI runs 3.3).
16
16
  | `lib/causalontology/canonical.rb` | identity-bearing field filtering per kind and SHA-256 content-addressed `identify` (spec/identity.md) |
17
17
  | `lib/causalontology/ed25519.rb` | pure-Ruby Ed25519 (RFC 8032) over native bignums, verified against the RFC's TEST 1 known answer before any vector runs |
18
18
  | `lib/causalontology/signing.rb` | record-level `sign_record` / `verify_record` over canonical identity-bearing bytes (spec/provenance.md); a succession verifies against its predecessor key |
19
- | `lib/causalontology/schema.rb` | validation against the seventeen JSON Schemas in `spec/schema/` (a small interpreter for exactly the keywords those schemas use) |
20
- | `lib/causalontology/semantics.rb` | the 21 semantic rules: temporal admissibility with the fixed unit constants, the formal conflict test, refinement validity, bridged reachability, stratal classification, the skip decision, enrichment field/shape rules, and the token-tier coherence checks |
19
+ | `lib/causalontology/schema.rb` | validation against the twenty-one JSON Schemas vendored at `lib/causalontology/spec/schema/`, a byte-for-byte copy of `spec/schema/` that ships inside the gem (a small interpreter for exactly the keywords those schemas use) |
20
+ | `lib/causalontology/semantics.rb` | the semantic rules: temporal admissibility with the fixed unit constants (months 2629746 s, years 31556952 s) and the ordinal `ticks` dimension, the formal conflict test, refinement validity, bridged reachability, stratal classification, the skip decision, cross-stratal-seam well-formedness and the home rule, enrichment field/shape rules, the token-tier coherence checks, the predicted-interval dimension check, and the prediction-to-observation pairing |
21
21
  | `lib/causalontology/store.rb` | an in-memory conformant store: idempotent immutable puts, signed add-only records with quarantine, materialized enrichment views with contributors, retraction and succession lineage, the resolve minimum, the deterministic cycle-breaking view rule, and the stigmergy `gaps` read — Ruby Hashes preserve insertion order, and the iteration order deliberately mirrors the reference store's |
22
- | `conformance.rb` | the conformance runner: internal known-answer checks (RFC 8032 TEST 1, RFC 8785 basics), then all 107 vectors, mirroring `bindings/python/tests/run_conformance.py` exactly |
22
+ | `conformance.rb` | the conformance runner: internal known-answer checks (RFC 8032 TEST 1, RFC 8785 basics), then all 137 vectors, mirroring `bindings/python/tests/run_conformance.py` exactly |
23
23
 
24
24
  ## Conformance
25
25
 
26
26
  ```
27
27
  $ ruby bindings/ruby/conformance.rb
28
28
  ...
29
- 107/107 vectors passed
30
- causalontology-ruby is CONFORMANT to the suite (vectors frozen at specification 2.0.0).
29
+ 137/137 vectors passed
30
+ causalontology-ruby is CONFORMANT to the suite (vectors frozen at specification 4.0.0).
31
31
  ```
32
32
 
33
- The runner locates the repository root from the `CAUSALONTOLOGY_ROOT`
34
- environment variable when set, otherwise from its own location inside
35
- `bindings/ruby/`; the schemas are read from `spec/schema` under the same
36
- root (overridable with `CAUSALONTOLOGY_SPEC`, which names the `spec/`
37
- directory).
33
+ The runner locates the vectors from the `CAUSALONTOLOGY_ROOT` environment
34
+ variable when set, otherwise from its own location inside `bindings/ruby/`.
38
35
 
39
- The vectors are frozen at specification 2.0.0 (2026-07-13): they carry
40
- concrete identifiers, real keys, and a real verifying signature. The
41
- harness's old normalization now simply passes frozen values through.
36
+ The schemas are resolved in strict precedence:
37
+
38
+ 1. `$CAUSALONTOLOGY_SPEC/schema`, when that variable is set (it names the
39
+ `spec/` directory, not the `schema/` directory);
40
+ 2. the copy vendored at `lib/causalontology/spec/schema`, which travels
41
+ inside the gem and is what an installed consumer validates against;
42
+ 3. the repository-relative `spec/schema`, a last resort for a checkout in
43
+ which the vendored copy has not been made yet.
44
+
45
+ Before running the vectors, the runner checks the vendored copy byte-for-byte
46
+ against `spec/schema` and aborts on any drift, so the published gem can never
47
+ quietly enforce a different standard than the repository states.
48
+
49
+ ### Testing the installed gem rather than the working tree
50
+
51
+ By default the runner loads the working tree next to it. Set
52
+ `CAUSALONTOLOGY_TEST_INSTALLED=1` and it instead loads the binding the way a
53
+ real consumer does, through a plain `require "causalontology"` resolved by
54
+ RubyGems; it prints the path it actually loaded and aborts if that path lies
55
+ inside the repository. Without that refusal a "fresh install" check silently
56
+ exercises repository source through a relative path and reports a false pass.
57
+
58
+ ```
59
+ $ cd bindings/ruby # RubyGems resolves manifest paths
60
+ $ gem build causalontology.gemspec -o /tmp/causalontology-4.0.0.gem
61
+ # against the working directory
62
+ $ gem install --install-dir /tmp/gemhome --no-document --local \
63
+ /tmp/causalontology-4.0.0.gem
64
+ $ cd /tmp && env -u CAUSALONTOLOGY_SPEC GEM_HOME=/tmp/gemhome GEM_PATH=/tmp/gemhome \
65
+ CAUSALONTOLOGY_TEST_INSTALLED=1 CAUSALONTOLOGY_ROOT=/path/to/causalontology \
66
+ ruby /tmp/gemhome/gems/causalontology-4.0.0/conformance.rb
67
+ binding under test: /tmp/gemhome/gems/causalontology-4.0.0/lib/causalontology.rb
68
+ ...
69
+ 137/137 vectors passed
70
+ ```
71
+
72
+ The gemspec itself refuses to build if the vendored schemas are absent,
73
+ incomplete, or byte-different from `spec/schema`, so a gem that cannot
74
+ validate can never be produced in the first place.
75
+
76
+ The V01–V107 vectors are the whole-word 2.0.0 baseline (2026-07-13): they
77
+ carry concrete identifiers, real keys, and a real verifying signature, and
78
+ the harness's normalization now simply passes those frozen values through.
79
+ The V108–V119 (3.0.0: the `ticks` unit, the cross_stratal_seam, the conduit
80
+ `realized_by`) and V120–V137 (4.0.0: the attitude, the predicted_occurrence,
81
+ the prediction_error) fixtures are built in the runner, mirroring the Python
82
+ reference exactly. Twenty-one object kinds; 137 vectors, frozen at
83
+ specification 4.0.0 (2026-07-22).
42
84
 
43
85
  ## Thirty-second taste
44
86
 
data/conformance.rb CHANGED
@@ -1,14 +1,17 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: false
3
3
 
4
- # The Causalontology conformance runner for causalontology-ruby (spec 2.0.0).
4
+ # The Causalontology conformance runner for causalontology-ruby (spec 4.0.0).
5
5
  #
6
6
  # Runs every vector in conformance/vectors/ against the Ruby binding. An
7
7
  # implementation is conformant if and only if it passes every vector; this
8
8
  # runner exits nonzero on any failure. It mirrors
9
9
  # bindings/python/tests/run_conformance.py exactly: same fixtures, same
10
- # expected results. Vectors are the whole-word 2.0.0 baseline (Principle P7):
11
- # V01-V38 re-frozen unaltered in meaning, V39-V107 new.
10
+ # expected results. Vectors V01-V107 are the whole-word 2.0.0 baseline
11
+ # (Principle P7): V01-V38 re-frozen unaltered in meaning, V39-V107 new.
12
+ # V108-V119 are the 3.0.0 additions (the ticks unit, the cross_stratal_seam,
13
+ # the conduit realized_by); V120-V137 are the 4.0.0 additions (the attitude,
14
+ # the predicted_occurrence, the prediction_error).
12
15
  #
13
16
  # Symbolic names still used inside this harness's own behavioral constructions
14
17
  # ("occurrent:A", key "alice") are normalized deterministically - symbolic
@@ -18,12 +21,52 @@
18
21
  require "json"
19
22
  require "digest"
20
23
  require "set"
21
- require_relative "lib/causalontology"
22
24
 
23
- C = Causalontology
24
25
  ROOT = ENV["CAUSALONTOLOGY_ROOT"] || File.expand_path("../..", __dir__)
25
26
  VECDIR = File.join(ROOT, "conformance", "vectors")
26
27
 
28
+ # How the binding under test is loaded.
29
+ #
30
+ # The default (no environment variable) is unchanged: load the working tree
31
+ # sitting next to this file, which is what repo-mode development wants.
32
+ #
33
+ # With CAUSALONTOLOGY_TEST_INSTALLED set, load it the way a real consumer
34
+ # does - a plain "require" resolved through RubyGems against the installed
35
+ # gem - and refuse to run if what actually got loaded turns out to live
36
+ # inside the repository. Without that refusal a "fresh install" test happily
37
+ # exercises the repository source through a relative path and reports a false
38
+ # pass, which is precisely how the missing-schema defect reached production.
39
+ if ENV["CAUSALONTOLOGY_TEST_INSTALLED"]
40
+ require "causalontology"
41
+ entry = $LOADED_FEATURES.find { |f| f.end_with?(File::SEPARATOR + "causalontology.rb") }
42
+ abort "CAUSALONTOLOGY_TEST_INSTALLED is set but no causalontology.rb was loaded" unless entry
43
+ entry = File.realpath(entry)
44
+ if entry.start_with?(File.realpath(ROOT) + File::SEPARATOR)
45
+ abort "CAUSALONTOLOGY_TEST_INSTALLED is set but the repository copy was loaded: #{entry}"
46
+ end
47
+ puts "binding under test: #{entry}"
48
+ else
49
+ require_relative "lib/causalontology"
50
+ end
51
+
52
+ C = Causalontology
53
+
54
+ # Drift guard. The schemas vendored into the gem are the ones an installed
55
+ # consumer validates against, so they must stay byte-for-byte identical to
56
+ # the spec. If they silently go stale the published artifact enforces a
57
+ # different standard than the repository claims.
58
+ BUNDLED_SCHEMAS = File.expand_path("lib/causalontology/spec/schema", __dir__)
59
+ SPEC_SCHEMAS = File.join(ROOT, "spec", "schema")
60
+ if File.directory?(BUNDLED_SCHEMAS) && File.directory?(SPEC_SCHEMAS)
61
+ Dir.glob(File.join(SPEC_SCHEMAS, "*.schema.json")).sort.each do |src|
62
+ dst = File.join(BUNDLED_SCHEMAS, File.basename(src))
63
+ unless File.exist?(dst) && File.binread(dst) == File.binread(src)
64
+ abort "bundled schema drift: #{File.basename(src)} differs from " \
65
+ "spec/schema - re-copy before packing"
66
+ end
67
+ end
68
+ end
69
+
27
70
  # ---------------------------------------------------------------------------
28
71
  # assertion helper
29
72
  # ---------------------------------------------------------------------------
@@ -38,9 +81,10 @@ end
38
81
  # ---------------------------------------------------------------------------
39
82
  SCHEMES = ["occurrent", "causal_relation_object", "continuant", "realizable",
40
83
  "assertion", "enrichment", "retraction", "succession",
41
- "stratum", "bridge", "port", "conduit", "quality",
42
- "token_individual", "token_occurrence", "state_assertion",
43
- "token_causal_claim"].freeze
84
+ "stratum", "bridge", "cross_stratal_seam", "port", "conduit",
85
+ "quality", "token_individual", "token_occurrence", "state_assertion",
86
+ "token_causal_claim",
87
+ "attitude", "predicted_occurrence", "prediction_error"].freeze
44
88
  WHOLE_WORD = Set.new(SCHEMES) | Set.new(["ed25519"])
45
89
  SCHEME_PREFIX = /\A(#{(SCHEMES + ["ed25519"]).join("|")}):/
46
90
  HEX64 = /\A[0-9a-f]{64}\z/
@@ -1151,14 +1195,462 @@ def v107
1151
1195
  assert ok, why
1152
1196
  end
1153
1197
 
1198
+ # ---------------------------------------------------------------------------
1199
+ # V108 - V119: the 3.0.0 additions (tick unit, cross_stratal_seam, realized_by)
1200
+ # ---------------------------------------------------------------------------
1201
+ def seam(source, target, mechanism_status, chain = nil)
1202
+ o = { "type" => "cross_stratal_seam", "source" => source,
1203
+ "target" => target, "mechanism_status" => mechanism_status }
1204
+ o["chain"] = chain unless chain.nil? || chain.empty?
1205
+ mk(o)
1206
+ end
1207
+
1208
+ # Build a seam over the neuro fixture: [seam, occ_map, stratum_map].
1209
+ def seam_fixture(src_ord, tgt_ord, mechanism_status, chain_ords = nil)
1210
+ s = neuro
1211
+ src = occ("source_event", s[src_ord]["id"])
1212
+ tgt = occ("target_event", s[tgt_ord]["id"])
1213
+ omap = { src["id"] => src, tgt["id"] => tgt }
1214
+ smap = { s[src_ord]["id"] => s[src_ord], s[tgt_ord]["id"] => s[tgt_ord] }
1215
+ chain = nil
1216
+ unless chain_ords.nil?
1217
+ chain = []
1218
+ chain_ords.each_with_index do |o, i|
1219
+ c = occ("chain_#{i}", s[o]["id"])
1220
+ omap[c["id"]] = c
1221
+ smap[s[o]["id"]] = s[o]
1222
+ chain << c["id"]
1223
+ end
1224
+ end
1225
+ [seam(src["id"], tgt["id"], mechanism_status, chain), omap, smap]
1226
+ end
1227
+
1228
+ # A conduit with an optional realized_by reference, completed with its id.
1229
+ def conduit_realized(realized_by = nil)
1230
+ frm = "port:" + "1" * 64
1231
+ to = "port:" + "2" * 64
1232
+ x = "occurrent:" + "3" * 64
1233
+ o = { "type" => "conduit", "label" => "conn", "from" => frm, "to" => to,
1234
+ "carries" => [x] }
1235
+ o["realized_by"] = realized_by if realized_by
1236
+ mk(o)
1237
+ end
1238
+
1239
+ # -- Change One: the ordinal (tick) temporal unit --
1240
+ def v108
1241
+ p = cro([sym("occurrent:a")], [sym("occurrent:b")],
1242
+ temporal: { "minimum_delay" => 0, "maximum_delay" => 5,
1243
+ "unit" => "ticks" },
1244
+ modality: "sufficient")
1245
+ ok, why = C.validate_schema(p)
1246
+ assert ok, why
1247
+ ok, why = C.validate_semantics(p)
1248
+ assert ok, why
1249
+ end
1250
+
1251
+ def v109
1252
+ p = cro([sym("occurrent:a")], [sym("occurrent:b")],
1253
+ temporal: { "minimum_delay" => 2, "maximum_delay" => 5,
1254
+ "unit" => "ticks" })
1255
+ assert C.admissible(p, 3) == true # 3 ticks inside [2, 5]
1256
+ assert C.admissible(p, 2) == true && C.admissible(p, 5) == true
1257
+ assert C.admissible(p, 6) == false && C.admissible(p, 1) == false
1258
+ end
1259
+
1260
+ def v110
1261
+ tick_window = { "minimum_delay" => 0, "maximum_delay" => 5, "unit" => "ticks" }
1262
+ wall_window = { "minimum_delay" => 0, "maximum_delay" => 5, "unit" => "seconds" }
1263
+ assert C.delay_within_window({ "duration" => 3, "unit" => "ticks" },
1264
+ tick_window) == true
1265
+ assert C.delay_within_window({ "duration" => 1, "unit" => "ticks" },
1266
+ wall_window) == false
1267
+ assert C.delay_within_window({ "duration" => 1, "unit" => "seconds" },
1268
+ tick_window) == false
1269
+ a = { "causes" => [sym("occurrent:a")], "effects" => [sym("occurrent:b")],
1270
+ "temporal" => tick_window, "modality" => "sufficient" }
1271
+ b = { "causes" => [sym("occurrent:a")], "effects" => [sym("occurrent:b")],
1272
+ "temporal" => wall_window, "modality" => "preventive" }
1273
+ assert C.conflicts(a, b) == false # disjoint dimensions
1274
+ begin
1275
+ C.to_seconds(1, "ticks")
1276
+ raise VectorFailure, "to_seconds accepted ticks"
1277
+ rescue ArgumentError
1278
+ # expected: an ordinal unit has no wall-clock seconds mapping
1279
+ end
1280
+ end
1281
+
1282
+ def v111
1283
+ base = { "type" => "causal_relation_object", "causes" => [sym("occurrent:a")],
1284
+ "effects" => [sym("occurrent:b")], "modality" => "sufficient" }
1285
+ tick = base.merge("temporal" => { "minimum_delay" => 0, "maximum_delay" => 1,
1286
+ "unit" => "ticks" })
1287
+ secs = base.merge("temporal" => { "minimum_delay" => 0, "maximum_delay" => 1,
1288
+ "unit" => "seconds" })
1289
+ assert C.identify(tick) != C.identify(secs) # the unit is identity-bearing
1290
+ # a wall-clock record's identity is UNCHANGED under 3.0.0 (pinned 2.0.0 value)
1291
+ assert C.identify(secs) == "causal_relation_object:" \
1292
+ "d8daf899daa3ee03caa6b1425cc6d4d33cef20d951e1203ffd35df29857aa43c"
1293
+ end
1294
+
1295
+ # -- Change Two: the managed cross-stratal seam (eighteenth kind) --
1296
+ def v112
1297
+ sm, omap, smap = seam_fixture(14, 4, "unmodeled")
1298
+ ok, why = C.validate_schema(sm)
1299
+ assert ok, why
1300
+ ok, why = C.validate_semantics(sm)
1301
+ assert ok, why
1302
+ ok, why = C.seam_wellformed(sm, omap, smap)
1303
+ assert ok, why
1304
+ end
1305
+
1306
+ def v113
1307
+ a, = seam_fixture(14, 4, "unmodeled")
1308
+ b, omap, smap = seam_fixture(14, 4, "absent")
1309
+ ok, why = C.validate_schema(b)
1310
+ assert ok, why
1311
+ ok, why = C.seam_wellformed(b, omap, smap)
1312
+ assert ok, why
1313
+ assert a["id"] != b["id"] # mechanism_status is identity-bearing
1314
+ end
1315
+
1316
+ def v114
1317
+ drawn, omap, smap = seam_fixture(14, 4, "unmodeled", [9, 7, 6, 5])
1318
+ ok, why = C.validate_schema(drawn)
1319
+ assert ok, why
1320
+ ok, why = C.seam_wellformed(drawn, omap, smap)
1321
+ assert ok, why
1322
+ bad, omap2, smap2 = seam_fixture(14, 4, "absent", [9, 7, 6, 5])
1323
+ ok, why = C.validate_semantics(bad)
1324
+ assert !ok && why.any? { |w| w.include?("contradictory_seam") }, why
1325
+ ok2, _ = C.seam_wellformed(bad, omap2, smap2)
1326
+ assert !ok2
1327
+ end
1328
+
1329
+ def v115
1330
+ sm, omap, smap = seam_fixture(14, 4, "unmodeled")
1331
+ s = neuro
1332
+ assert C.seam_home(sm, omap, smap) == s[14]["id"] # coarsest (max ordinal) stratum
1333
+ end
1334
+
1335
+ def v116
1336
+ adj, o1, s1 = seam_fixture(6, 5, "unmodeled") # adjacent (gap 1)
1337
+ ok, _ = C.seam_wellformed(adj, o1, s1)
1338
+ assert !ok
1339
+ co, o2, s2 = seam_fixture(6, 6, "unmodeled") # co-stratal (gap 0)
1340
+ ok, _ = C.seam_wellformed(co, o2, s2)
1341
+ assert !ok
1342
+ sm, = seam_fixture(14, 4, "unmodeled")
1343
+ assert sm["id"].start_with?("cross_stratal_seam:") # a new identity scheme
1344
+ end
1345
+
1346
+ # -- Change Three: the realized_by reference --
1347
+ def v117
1348
+ c = conduit_realized("causal_relation_object:" + "a" * 64)
1349
+ ok, why = C.validate_schema(c)
1350
+ assert ok, why
1351
+ c2 = conduit_realized("native:region_stratum_predict")
1352
+ ok, why = C.validate_schema(c2)
1353
+ assert ok, why # a native scheme reference is legal
1354
+ end
1355
+
1356
+ def v118
1357
+ bound = conduit_realized("native:region_stratum_predict")
1358
+ unbound = conduit_realized
1359
+ assert bound["id"] != unbound["id"] # realized_by is identity-bearing
1360
+ # an unbound conduit's identity is UNCHANGED under 3.0.0 (pinned 2.0.0 value)
1361
+ assert unbound["id"] == "conduit:" \
1362
+ "dc4af3b1a24f0560d5ebcee488779f06ab3c78301cfb9d0c7edff80bc62e27a6"
1363
+ end
1364
+
1365
+ def v119
1366
+ unbound = conduit_realized
1367
+ ok, why = C.validate_schema(unbound)
1368
+ assert ok, why # unbound is legal
1369
+ bad = unbound.dup
1370
+ bad["realized_by"] = "not-a-scheme-qualified-reference"
1371
+ ok, _ = C.validate_schema(bad, "conduit")
1372
+ assert !ok
1373
+ end
1374
+
1375
+ # ---------------------------------------------------------------------------
1376
+ # V120 - V137: the 4.0.0 additions (attitude, predicted_occurrence,
1377
+ # prediction_error)
1378
+ # ---------------------------------------------------------------------------
1379
+ def attitude(holder, attitude_type, content)
1380
+ mk({ "type" => "attitude", "holder" => holder,
1381
+ "attitude_type" => attitude_type, "content" => content })
1382
+ end
1383
+
1384
+ def predicted(instantiates, interval, predictor, strength: nil)
1385
+ o = { "type" => "predicted_occurrence", "instantiates" => instantiates,
1386
+ "interval" => interval, "predictor" => predictor }
1387
+ o["strength"] = strength unless strength.nil?
1388
+ mk(o)
1389
+ end
1390
+
1391
+ def prediction_error(predicted_id, discrepancy, observed: nil)
1392
+ o = { "type" => "prediction_error", "predicted" => predicted_id,
1393
+ "discrepancy" => discrepancy }
1394
+ o["observed"] = observed if observed
1395
+ mk(o)
1396
+ end
1397
+
1398
+ # A modeled predicting agent (a token individual), by identity.
1399
+ def predictor_id
1400
+ c = cnt("forecasting_mind")
1401
+ individual(c["id"], designator: "predictor_p")["id"]
1402
+ end
1403
+
1404
+ # A modeled believing agent (a token individual), by identity.
1405
+ def believer(designator = "holder_h")
1406
+ c = cnt("believing_mind")
1407
+ individual(c["id"], designator: designator)["id"]
1408
+ end
1409
+
1410
+ # -- Group X: prediction and prediction error (Section A) --
1411
+ def v120
1412
+ o = occ("rainfall_begins")
1413
+ p = predicted(o["id"], { "start_tick" => 3, "end_tick" => 8 }, predictor_id)
1414
+ ok, why = C.validate_schema(p)
1415
+ assert ok, why
1416
+ ok, why = C.validate_semantics(p)
1417
+ assert ok, why
1418
+ assert p["id"].start_with?("predicted_occurrence:")
1419
+ report = C.identify({ "type" => "token_occurrence", "instantiates" => o["id"],
1420
+ "interval" => { "start_tick" => 3, "end_tick" => 8 } },
1421
+ "token_occurrence")
1422
+ assert p["id"] != report # a forecast is not a report
1423
+ assert report.start_with?("token_occurrence:")
1424
+ end
1425
+
1426
+ def v121
1427
+ o = occ("rainfall_begins")
1428
+ wall = { "start" => "2026-07-23T00:00:00Z", "end" => "2026-07-24T00:00:00Z" }
1429
+ who = predictor_id
1430
+ with_strength = predicted(o["id"], wall, who, strength: 0.8)
1431
+ without = predicted(o["id"], wall, who)
1432
+ [with_strength, without].each do |p|
1433
+ ok, why = C.validate_schema(p)
1434
+ assert ok, why
1435
+ ok, why = C.validate_semantics(p)
1436
+ assert ok, why
1437
+ end
1438
+ assert with_strength["id"] != without["id"] # strength is identity-bearing
1439
+ end
1440
+
1441
+ def v122
1442
+ o = occ("rainfall_begins")
1443
+ bad = mk({ "type" => "predicted_occurrence", "instantiates" => o["id"],
1444
+ "interval" => { "start_tick" => 3 } })
1445
+ ok, why = C.validate_schema(bad, "predicted_occurrence")
1446
+ assert !ok && why.any? { |w| w.include?("predictor") }, why
1447
+ end
1448
+
1449
+ def v123
1450
+ o = occ("rainfall_begins")
1451
+ both = predicted(o["id"],
1452
+ { "start" => "2026-07-23T00:00:00Z", "start_tick" => 3 },
1453
+ predictor_id)
1454
+ ok, why = C.validate_schema(both)
1455
+ assert ok, why
1456
+ ok, why = C.validate_semantics(both)
1457
+ assert !ok && why.any? { |w| w.include?("dimension_conflict") }, why
1458
+ end
1459
+
1460
+ def v124
1461
+ o = occ("rainfall_begins")
1462
+ p = predicted(o["id"], { "start" => "2026-07-23T00:00:00Z" }, predictor_id)
1463
+ t = token(o["id"], { "start" => "2026-07-23T06:00:00Z" })
1464
+ err = prediction_error(p["id"], 0.0, observed: t["id"])
1465
+ ok, why = C.validate_schema(err)
1466
+ assert ok, why
1467
+ ok, why = C.validate_semantics(err)
1468
+ assert ok, why
1469
+ assert C.prediction_pairing_mismatch(err, p, t) == false
1470
+ end
1471
+
1472
+ def v125
1473
+ o = occ("rainfall_begins")
1474
+ p = predicted(o["id"], { "start" => "2026-07-23T00:00:00Z" }, predictor_id)
1475
+ err = prediction_error(p["id"], -1.0)
1476
+ ok, why = C.validate_schema(err)
1477
+ assert ok, why
1478
+ ok, why = C.validate_semantics(err)
1479
+ assert ok, why
1480
+ assert !err.key?("observed")
1481
+ assert C.prediction_pairing_mismatch(err, p, nil) == false
1482
+ end
1483
+
1484
+ def v126
1485
+ o = occ("rainfall_begins")
1486
+ p = predicted(o["id"], { "start_tick" => 0 }, predictor_id)
1487
+ bad = mk({ "type" => "prediction_error", "predicted" => p["id"] })
1488
+ ok, why = C.validate_schema(bad, "prediction_error")
1489
+ assert !ok && why.any? { |w| w.include?("discrepancy") }, why
1490
+ end
1491
+
1492
+ def v127
1493
+ o = occ("rainfall_begins")
1494
+ other = occ("snowfall_begins")
1495
+ p = predicted(o["id"], { "start" => "2026-07-23T00:00:00Z" }, predictor_id)
1496
+ t = token(other["id"], { "start" => "2026-07-23T06:00:00Z" })
1497
+ err = prediction_error(p["id"], 1.0, observed: t["id"])
1498
+ ok, why = C.validate_schema(err)
1499
+ assert ok, why
1500
+ assert C.prediction_pairing_mismatch(err, p, t) == true
1501
+ end
1502
+
1503
+ # -- Group Y: attitude and theory of mind (Section B) --
1504
+ def v128
1505
+ st, = state_fixture("quantity", { "quantity" => 15.0, "unit" => "ug/dL" },
1506
+ "ug/dL")
1507
+ att = attitude(believer, "believes", st["id"])
1508
+ ok, why = C.validate_schema(att)
1509
+ assert ok, why
1510
+ ok, why = C.validate_semantics(att)
1511
+ assert ok, why
1512
+ end
1513
+
1514
+ def v129
1515
+ a = occ("switch_pressed")
1516
+ b = occ("light_on")
1517
+ actual = cro([a["id"]], [b["id"]], modality: "sufficient")
1518
+ believed = cro([a["id"]], [b["id"]], modality: "preventive")
1519
+ assert C.conflicts(believed, actual) == true # the CLAIMS contradict
1520
+ att = attitude(believer, "believes", believed["id"])
1521
+ ok, why = C.validate_schema(att)
1522
+ assert ok, why
1523
+ ok, why = C.validate_semantics(att)
1524
+ assert ok, why # validity unaffected
1525
+ s = C::InMemoryStore.new
1526
+ s.put(a)
1527
+ s.put(b)
1528
+ s.put(actual)
1529
+ s.put(att)
1530
+ assert s.gaps("conflict") == [] # Rule 25: NO conflict raised
1531
+ end
1532
+
1533
+ def v130
1534
+ o = occ("rainfall_begins")
1535
+ att = attitude(believer, "desires", o["id"])
1536
+ ok, why = C.validate_schema(att)
1537
+ assert ok, why
1538
+ ok, why = C.validate_semantics(att)
1539
+ assert ok, why
1540
+ end
1541
+
1542
+ def v131
1543
+ o = occ("press_button")
1544
+ att = attitude(believer, "intends", o["id"])
1545
+ ok, why = C.validate_schema(att)
1546
+ assert ok, why
1547
+ ok, why = C.validate_semantics(att)
1548
+ assert ok, why
1549
+ end
1550
+
1551
+ def v132
1552
+ st, = state_fixture("boolean", { "boolean" => true })
1553
+ inner = attitude(believer("holder_b"), "believes", st["id"])
1554
+ outer = attitude(believer("holder_a"), "believes", inner["id"])
1555
+ [inner, outer].each do |att|
1556
+ ok, why = C.validate_schema(att)
1557
+ assert ok, why
1558
+ ok, why = C.validate_semantics(att)
1559
+ assert ok, why
1560
+ end
1561
+ assert outer["id"] != inner["id"]
1562
+ assert outer["content"] == inner["id"] # nested content
1563
+ end
1564
+
1565
+ def v133
1566
+ o = occ("rainfall_begins")
1567
+ bad = mk({ "type" => "attitude", "holder" => believer,
1568
+ "attitude_type" => "suspects", "content" => o["id"] })
1569
+ ok, why = C.validate_schema(bad, "attitude")
1570
+ assert !ok && why.any? { |w| w.include?("attitude_type") }, why
1571
+ end
1572
+
1573
+ def v134
1574
+ o = occ("rainfall_begins")
1575
+ bad = mk({ "type" => "attitude", "holder" => believer,
1576
+ "attitude_type" => "believes", "content" => o["id"],
1577
+ "strength" => 0.9 })
1578
+ ok, why = C.validate_schema(bad, "attitude")
1579
+ assert !ok && why.any? { |w| w.include?("strength") }, why
1580
+ end
1581
+
1582
+ def v135
1583
+ o = occ("rainfall_begins")
1584
+ att = attitude(believer, "expects", o["id"])
1585
+ a = signed("assertion", { "about" => att["id"],
1586
+ "evidence_type" => "observation",
1587
+ "confidence" => 0.9 }, "signer")
1588
+ ok, why = C.validate_schema(a)
1589
+ assert ok, why
1590
+ assert C.verify_record(a) == true
1591
+ # the HOLDER (a modeled agent) and the SOURCE (a signing key) differ
1592
+ assert att["holder"].split(":", 2)[0] == "token_individual"
1593
+ assert a["source"].split(":", 2)[0] == "ed25519"
1594
+ assert att["holder"] != a["source"]
1595
+ end
1596
+
1597
+ def v136
1598
+ # the V111 wall-clock Causal Relation Object, re-pinned under 4.0.0
1599
+ secs = { "type" => "causal_relation_object", "causes" => [sym("occurrent:a")],
1600
+ "effects" => [sym("occurrent:b")], "modality" => "sufficient",
1601
+ "temporal" => { "minimum_delay" => 0, "maximum_delay" => 1,
1602
+ "unit" => "seconds" } }
1603
+ assert C.identify(secs) == "causal_relation_object:" \
1604
+ "d8daf899daa3ee03caa6b1425cc6d4d33cef20d951e1203ffd35df29857aa43c"
1605
+ # the V118 unbound conduit, re-pinned under 4.0.0
1606
+ unbound = conduit_realized
1607
+ assert unbound["id"] == "conduit:" \
1608
+ "dc4af3b1a24f0560d5ebcee488779f06ab3c78301cfb9d0c7edff80bc62e27a6"
1609
+ end
1610
+
1611
+ def v137
1612
+ hexid = "0" * 64
1613
+ # NOTE: the abbreviated prefixes below are intentional (the negative test);
1614
+ # they must NOT be re-minted. Each is assembled to survive re-mint tools.
1615
+ att_abbr = "a" + "t" + "t"
1616
+ prd_abbr = "p" + "r" + "d"
1617
+ err_abbr = "e" + "r" + "r"
1618
+ bad_att = { "type" => "attitude", "id" => att_abbr + ":" + hexid,
1619
+ "holder" => "token_individual:" + hexid,
1620
+ "attitude_type" => "believes",
1621
+ "content" => "state_assertion:" + hexid }
1622
+ ok, _ = C.validate_schema(bad_att, "attitude")
1623
+ assert !ok
1624
+ bad_prd = { "type" => "predicted_occurrence", "id" => prd_abbr + ":" + hexid,
1625
+ "instantiates" => "occurrent:" + hexid,
1626
+ "interval" => { "start_tick" => 0 },
1627
+ "predictor" => "token_individual:" + hexid }
1628
+ ok, _ = C.validate_schema(bad_prd, "predicted_occurrence")
1629
+ assert !ok
1630
+ bad_err = { "type" => "prediction_error", "id" => err_abbr + ":" + hexid,
1631
+ "predicted" => "predicted_occurrence:" + hexid,
1632
+ "discrepancy" => 0.0 }
1633
+ ok, _ = C.validate_schema(bad_err, "prediction_error")
1634
+ assert !ok
1635
+ whole_att = bad_att.merge("id" => "attitude:" + hexid)
1636
+ ok, why = C.validate_schema(whole_att, "attitude")
1637
+ assert ok, why
1638
+ whole_prd = bad_prd.merge("id" => "predicted_occurrence:" + hexid)
1639
+ ok, why = C.validate_schema(whole_prd, "predicted_occurrence")
1640
+ assert ok, why
1641
+ whole_err = bad_err.merge("id" => "prediction_error:" + hexid)
1642
+ ok, why = C.validate_schema(whole_err, "prediction_error")
1643
+ assert ok, why
1644
+ end
1645
+
1154
1646
  # ---------------------------------------------------------------------------
1155
1647
  def main
1156
- puts "causalontology-ruby conformance run (specification 2.0.0)"
1648
+ puts "causalontology-ruby conformance run (specification 4.0.0)"
1157
1649
  print "internal checks (RFC 8032, RFC 8785, fixed constants) ... "
1158
1650
  internal_checks
1159
1651
  puts "ok"
1160
1652
  failures = 0
1161
- total = 107
1653
+ total = 137
1162
1654
  (1..total).each do |n|
1163
1655
  name = vec_name(n)
1164
1656
  begin
@@ -1173,7 +1665,7 @@ def main
1173
1665
  puts "#{total - failures}/#{total} vectors passed"
1174
1666
  exit 1 if failures > 0
1175
1667
  puts "causalontology-ruby is CONFORMANT to the suite " \
1176
- "(vectors frozen at specification 2.0.0)."
1668
+ "(vectors frozen at specification 4.0.0)."
1177
1669
  end
1178
1670
 
1179
1671
  main if __FILE__ == $PROGRAM_NAME