Module: Causalontology::Semantics

Defined in:
lib/causalontology/semantics.rb

Constant Summary collapse

UNIT_SECONDS =

Rule 4: the fixed unit-conversion constants (average Gregorian values).

{
  "instant" => 0,
  "seconds" => 1,
  "minutes" => 60,
  "hours"   => 3600,
  "days"    => 86400,
  "weeks"   => 604800,
  "months"  => 2629746,
  "years"   => 31556952,
}.freeze
ORDINAL_UNITS =

3.0.0: the ordinal (dimensionless) temporal units. A tick is a discrete step with NO wall-clock mapping; a tick window is ordered by integer comparison, and an ordinal window and a wall-clock window are DIFFERENT DIMENSIONS that do not compare (mixing them is never within-window and never overlapping).

Set.new(["ticks"]).freeze
ENRICHMENT_FIELDS =

Rule 12: enrichment field-to-kind validity and entry shapes. Two occurrent forms added in 2.0.0.

{
  "aliases"            => [["occurrent", "continuant"], "alias"],
  "participants"       => [["occurrent"],               "continuant"],
  "subsumes"           => [["continuant"],              "continuant"],
  "part_of"            => [["continuant"],              "continuant"],
  "realized_in"        => [["realizable"],              "occurrent"],
  "occurrent_subsumes" => [["occurrent"],               "occurrent"],
  "occurrent_part_of"  => [["occurrent"],               "occurrent"],
}.freeze
CRO_OPTIONAL_FIELDS =
["mechanism", "temporal", "modality", "context"].freeze
POSITIVE =

Rule 6 (amended): necessary, sufficient, contributory, enabling are mutually compatible; preventive opposes all four.

Set.new(["necessary", "sufficient", "contributory",
"enabling"]).freeze

Class Method Summary collapse

Class Method Details

.admissible(cro, elapsed) ⇒ Object

Rule 4: temporal admissibility. For a wall-clock window elapsed is in seconds; for an ordinal ('ticks') window elapsed is a tick count. Ordering is by magnitude WITHIN the window's own dimension (3.0.0).



166
167
168
169
170
171
172
# File 'lib/causalontology/semantics.rb', line 166

def admissible(cro, elapsed)
  t = cro["temporal"]
  return true if t.nil? # no window imposes no constraint
  lo = magnitude(t["minimum_delay"], t["unit"])
  hi = magnitude(t["maximum_delay"], t["unit"])
  lo <= elapsed && elapsed <= hi
end

.bridge_closure(occurrent_id, bridges) ⇒ Object

ALGORITHM A. Every finer occurrent an occurrent resolves to, following Bridges downward, transitively. Includes the starting occurrent (N12.1.1). bridges is any iterable of bridge objects. The visited guard (N12.1.2) prevents an infinite loop on malformed cyclic data.



242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
# File 'lib/causalontology/semantics.rb', line 242

def bridge_closure(occurrent_id, bridges)
  result = Set.new([occurrent_id])
  frontier = [occurrent_id]
  visited = Set.new
  coarse_index = {}
  bridges.each { |b| (coarse_index[b["coarse"]] ||= []) << b }
  until frontier.empty?
    current = frontier.pop
    next if visited.include?(current)
    visited << current
    (coarse_index[current] || []).each do |b|
      b["fine"].each do |f|
        result << f
        frontier << f
      end
    end
  end
  result
end

.bridge_wellformed(bridge, occ_map, stratum_map) ⇒ Object

Rule 14 / N3.2.1: Bridge well-formedness. [ok, reason]. All of (a)-(e) of N3.2.1 must hold, else malformed_bridge.



398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
# File 'lib/causalontology/semantics.rb', line 398

def bridge_wellformed(bridge, occ_map, stratum_map)
  coarse = occ_map[bridge["coarse"]] || {}
  cs = coarse["stratum"]
  return [false, "malformed_bridge: coarse has no stratum (a)"] if cs.nil?
  fine_strata = bridge["fine"].map { |f| (occ_map[f] || {})["stratum"] }
  if fine_strata.any?(&:nil?)
    return [false, "malformed_bridge: a fine member has no stratum (b)"]
  end
  if Set.new(fine_strata).length != 1
    return [false, "malformed_bridge: fine members span >1 stratum (c)"]
  end
  fs = fine_strata[0]
  if stratum_map[cs]["scheme"] != stratum_map[fs]["scheme"]
    return [false, "malformed_bridge: coarse and fine differ in scheme (d)"]
  end
  unless stratum_map[cs]["ordinal"] > stratum_map[fs]["ordinal"]
    return [false, "malformed_bridge: coarse ordinal not > fine ordinal (e)"]
  end
  [true, "well-formed bridge"]
end

.classify_cro(cro, occ_map, stratum_map) ⇒ Object

ALGORITHM C (Rule 15): "intra_stratal" | "adjacent_stratal" | "skipping" | "mixed" | "unclassifiable" | "scheme_mismatch". Derived, never asserted; recompute on ingest (N12.3.1).



310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
# File 'lib/causalontology/semantics.rb', line 310

def classify_cro(cro, occ_map, stratum_map)
  stratum_of = lambda { |occ_id| (occ_map[occ_id] || {})["stratum"] }
  cause_strata = cro["causes"].map { |c| stratum_of.call(c) }
  effect_strata = cro["effects"].map { |e| stratum_of.call(e) }
  if (cause_strata + effect_strata).any?(&:nil?)
    return "unclassifiable" # surface unstratified_occurrent (invitation)
  end
  all_strata = Set.new(cause_strata) | Set.new(effect_strata)
  schemes = Set.new(all_strata.map { |s| stratum_map[s]["scheme"] })
  return "scheme_mismatch" if schemes.length > 1 # HARD
  c_ord = cause_strata.map { |s| stratum_map[s]["ordinal"] }
  e_ord = effect_strata.map { |s| stratum_map[s]["ordinal"] }
  if c_ord.max == c_ord.min && c_ord.min == e_ord.max && e_ord.max == e_ord.min
    return "intra_stratal"
  end
  pairs = c_ord.product(e_ord).map { |i, j| (i - j).abs }
  gap = pairs.min
  span = pairs.max
  return "adjacent_stratal" if span == 1
  return "skipping" if gap > 1
  "mixed" # some pairs adjacent, some skipping
end

.conduit_wellformed(conduit, port_map, cro_map = nil) ⇒ Object

Rule 17 / N4.2.1-2: Conduit well-formedness. [ok, reason]. N4.2.1 with the transform exception of N4.2.2.



488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
# File 'lib/causalontology/semantics.rb', line 488

def conduit_wellformed(conduit, port_map, cro_map = nil)
  frm = port_map[conduit["from"]]
  to = port_map[conduit["to"]]
  if frm.nil? || to.nil?
    return [false, "malformed_conduit: dangling port reference"]
  end
  unless ["out", "bidirectional"].include?(frm["direction"])
    return [false, "malformed_conduit: from port is not out/bidirectional (a)"]
  end
  unless ["in", "bidirectional"].include?(to["direction"])
    return [false, "malformed_conduit: to port is not in/bidirectional (b)"]
  end
  carries = conduit["carries"]
  unless carries.all? { |o| frm["accepts"].include?(o) }
    return [false, "malformed_conduit: carries not accepted by from (c)"]
  end
  transform = conduit["transform"]
  if transform.nil?
    unless carries.all? { |o| to["accepts"].include?(o) }
      return [false, "malformed_conduit: carries not accepted by to (d)"]
    end
  else
    law = (cro_map || {})[transform]
    unless law.nil?
      unless law["effects"].all? { |o| to["accepts"].include?(o) }
        return [false, "malformed_conduit: transform effects not " \
                       "accepted by to (d, relaxed per N4.2.2)"]
      end
    end
  end
  [true, "well-formed conduit"]
end

.conflicts(a, b) ⇒ Object

Rule 6: the formal conflict test.



197
198
199
200
201
202
203
204
205
206
# File 'lib/causalontology/semantics.rb', line 197

def conflicts(a, b)
  return false if Set.new(a["causes"]) != Set.new(b["causes"])
  return false if Set.new(a["effects"]) != Set.new(b["effects"])
  return false unless contexts_compatible(a, b)
  return false unless window_overlap(a, b)
  ma = a["modality"]
  mb = b["modality"]
  (ma == "preventive" && POSITIVE.include?(mb)) ||
    (mb == "preventive" && POSITIVE.include?(ma))
end

.contexts_compatible(a, b) ⇒ Object



187
188
189
190
191
192
193
194
# File 'lib/causalontology/semantics.rb', line 187

def contexts_compatible(a, b)
  ca = a["context"]
  cb = b["context"]
  return true if ca.nil? || ca.empty? || cb.nil? || cb.empty?
  sa = Set.new(ca)
  sb = Set.new(cb)
  sa == sb || sa.subset?(sb) || sb.subset?(sa)
end

.covering_law_mismatch(tcc, token_map, law) ⇒ Object

Rule 20: true iff the token claim's cause/effect tokens do not instantiate the covering law's causes/effects (surfaces covering_law_mismatch).



542
543
544
545
546
547
548
549
550
551
552
553
# File 'lib/causalontology/semantics.rb', line 542

def covering_law_mismatch(tcc, token_map, law)
  return false if law.nil?
  law_causes = Set.new(law["causes"])
  law_effects = Set.new(law["effects"])
  tcc["causes"].each do |c|
    return true unless law_causes.include?(token_map[c]["instantiates"])
  end
  tcc["effects"].each do |e|
    return true unless law_effects.include?(token_map[e]["instantiates"])
  end
  false
end

.delay_within_window(actual_delay, temporal) ⇒ Object

ALGORITHM E (Rule 20): does an observed delay fall within a covering law's temporal window? Inclusive at both ends (N12.5.2). 3.0.0: an ordinal delay compares to an ordinal window by integer tick count; an ordinal delay and a wall-clock window (or vice versa) are different dimensions and never fall within one another.



385
386
387
388
389
390
391
392
393
394
# File 'lib/causalontology/semantics.rb', line 385

def delay_within_window(actual_delay, temporal)
  return true if actual_delay.nil? || actual_delay.empty? ||
                 temporal.nil? || temporal.empty?
  # dimension mismatch: a tick delay is not within a wall-clock window
  return false if dimension(actual_delay["unit"]) != dimension(temporal["unit"])
  observed = magnitude(actual_delay["duration"], actual_delay["unit"])
  lo = magnitude(temporal["minimum_delay"], temporal["unit"])
  hi = magnitude(temporal["maximum_delay"], temporal["unit"])
  lo <= observed && observed <= hi
end

.dimension(unit) ⇒ Object

3.0.0: "ordinal" for a tick-like unit, else "wallclock".



58
59
60
# File 'lib/causalontology/semantics.rb', line 58

def dimension(unit)
  ORDINAL_UNITS.include?(unit) ? "ordinal" : "wallclock"
end

.endpoints_mixed(cro, occ_map) ⇒ Object

True iff causes or effects span more than one distinct stratum (surfaces mixed_stratal_endpoints, an invitation; N12.3.2).



335
336
337
338
339
340
341
# File 'lib/causalontology/semantics.rb', line 335

def endpoints_mixed(cro, occ_map)
  stratum_of = lambda { |occ_id| (occ_map[occ_id] || {})["stratum"] }
  cs = Set.new(cro["causes"].map { |c| stratum_of.call(c) })
  es = Set.new(cro["effects"].map { |e| stratum_of.call(e) })
  return false if cs.include?(nil) || es.include?(nil)
  cs.length > 1 || es.length > 1
end

.has_cycle(edges) ⇒ Object

Rules 4 / 6.1: true iff a directed graph (node -> iterable of successors) has a cycle. Used for bridge graph, occurrent_subsumes, occurrent_part_of, and token mereology (part_of).



581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
# File 'lib/causalontology/semantics.rb', line 581

def has_cycle(edges)
  white = 0
  grey = 1
  black = 2
  state = {}
  visit = nil
  visit = lambda do |node|
    state[node] = grey
    (edges[node] || []).each do |nxt|
      s = state.fetch(nxt, white)
      return true if s == grey
      return true if s == white && visit.call(nxt)
    end
    state[node] = black
    false
  end
  edges.keys.any? { |n| state.fetch(n, white) == white && visit.call(n) }
end

.hierarchy_consistent(parent, members, bridges = []) ⇒ Object

ALGORITHM B (amended Rule 7): "consistent" | "inconsistent" | "indeterminate", ACROSS STRATA via bridged reachability.

members: mapping from CRO identifier to CRO object for the mechanism entries. bridges: the store's bridges (empty -> 1.0.0 literal reachability, the degenerate case, N12.2.3).



281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
# File 'lib/causalontology/semantics.rb', line 281

def hierarchy_consistent(parent, members, bridges = [])
  mechanism = parent["mechanism"] || []
  return "consistent" if mechanism.empty? # nothing claimed (N12.2.1)
  edges = {}
  mechanism.each do |mid|
    m = members[mid]
    return "indeterminate" if m.nil? # dangling; ignorance, not refutation
    m["causes"].each do |c|
      (edges[c] ||= Set.new).merge(m["effects"])
    end
  end
  b_cause = {}
  parent["causes"].each { |c| b_cause[c] = bridge_closure(c, bridges) }
  b_effect = {}
  parent["effects"].each { |e| b_effect[e] = bridge_closure(e, bridges) }
  parent["causes"].each do |c|
    parent["effects"].each do |e|
      connected = b_cause[c].any? do |cp|
        b_effect[e].any? { |ep| path_exists(edges, cp, ep) }
      end
      return "inconsistent" unless connected
    end
  end
  "consistent"
end

.is_partial(cro) ⇒ Object

[partial, missing] - which optional CRO fields are unspecified.



158
159
160
161
# File 'lib/causalontology/semantics.rb', line 158

def is_partial(cro)
  missing = CRO_OPTIONAL_FIELDS.reject { |f| cro.key?(f) }
  [!missing.empty?, missing]
end

.kind_of_id(identifier) ⇒ Object



53
54
55
# File 'lib/causalontology/semantics.rb', line 53

def kind_of_id(identifier)
  Canonical::KIND_OF_PREFIX[identifier.split(":", 2)[0]]
end

.magnitude(value, unit) ⇒ Object

3.0.0: a comparable magnitude within ONE dimension: raw tick count for an ordinal unit, seconds for a wall-clock unit. Never mix dimensions.



64
65
66
67
68
# File 'lib/causalontology/semantics.rb', line 64

def magnitude(value, unit)
  return value if ORDINAL_UNITS.include?(unit) # a dimensionless tick count
  return 0 if unit == "instant"
  value * UNIT_SECONDS.fetch(unit)
end

.path_exists(edges, src, dst) ⇒ Object



262
263
264
265
266
267
268
269
270
271
272
273
# File 'lib/causalontology/semantics.rb', line 262

def path_exists(edges, src, dst)
  seen = Set.new
  stack = [src]
  until stack.empty?
    node = stack.pop
    return true if node == dst
    next if seen.include?(node)
    seen << node
    stack.concat((edges[node] || []).to_a)
  end
  false
end

.prediction_pairing_mismatch(error, predicted, observed) ⇒ Object

4.0.0 Rule 24: prediction-to-observation pairing. True iff the prediction error's observed token does not instantiate the occurrent its predicted_occurrence instantiates (surfaces pairing_mismatch). An ABSENT observed is never a mismatch - it means the predicted occurrence was not fulfilled by any recorded occurrence.



560
561
562
563
# File 'lib/causalontology/semantics.rb', line 560

def prediction_pairing_mismatch(error, predicted, observed)
  return false if error["observed"].nil? || observed.nil?
  observed["instantiates"] != predicted["instantiates"]
end

.refinement_valid(child, parent) ⇒ Object

Rule 3: [ok, reason] - is child a valid refinement of parent?



209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
# File 'lib/causalontology/semantics.rb', line 209

def refinement_valid(child, parent)
  if child["refines"] != parent["id"]
    return [false, "child does not name the parent in refines"]
  end
  if Set.new(child["causes"]) != Set.new(parent["causes"]) ||
     Set.new(child["effects"]) != Set.new(parent["effects"])
    return [false, "a refinement must keep the parent's causes and effects"]
  end
  added = 0
  CRO_OPTIONAL_FIELDS.each do |field|
    if parent.key?(field)
      if child[field] != parent[field]
        return [false, "a refinement may not change a field the " \
                       "parent specified; this is a rival claim"]
      end
    elsif child.key?(field)
      added += 1
    end
  end
  if added == 0
    return [false, "a refinement must add at least one unspecified field"]
  end
  [true, "valid refinement"]
end

.retrocausal(tcc, token_map) ⇒ Object

Rule 21: true iff any cause token starts after any effect token (HARD; retrocausal_claim). RFC 3339 UTC "Z" strings compare lexicographically.



567
568
569
570
571
572
573
574
575
576
# File 'lib/causalontology/semantics.rb', line 567

def retrocausal(tcc, token_map)
  tcc["causes"].each do |c|
    cstart = token_map[c]["interval"]["start"]
    tcc["effects"].each do |e|
      estart = token_map[e]["interval"]["start"]
      return true if cstart > estart
    end
  end
  false
end

.seam_home(seam, occ_map, stratum_map) ⇒ Object

THE HOME RULE (3.0.0): a Cross Stratal Seam belongs to the COARSEST stratum it touches - the endpoint of the greater ordinal. Returns that stratum's identifier (nil if an endpoint is unstratified). A layer-to-stratum binding places and checks the seam by this rule.



475
476
477
478
479
480
481
482
483
484
# File 'lib/causalontology/semantics.rb', line 475

def seam_home(seam, occ_map, stratum_map)
  src_s = (occ_map[seam["source"]] || {})["stratum"]
  tgt_s = (occ_map[seam["target"]] || {})["stratum"]
  return nil if src_s.nil? || tgt_s.nil?
  if stratum_map[src_s]["ordinal"] >= stratum_map[tgt_s]["ordinal"]
    src_s
  else
    tgt_s
  end
end

.seam_wellformed(seam, occ_map, stratum_map) ⇒ Object

3.0.0 Rule 22 / Algorithm F: Cross Stratal Seam well-formedness. [ok, reason]. All of (a)-(g) must hold, else malformed_seam. A seam is a MANAGED jump across NON-ADJACENT strata; when it DRAWS a chain, the chain must be an adjacent-stratum path spanning the two endpoints' strata.



423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
# File 'lib/causalontology/semantics.rb', line 423

def seam_wellformed(seam, occ_map, stratum_map)
  src_s = (occ_map[seam["source"]] || {})["stratum"]
  tgt_s = (occ_map[seam["target"]] || {})["stratum"]
  if src_s.nil? || tgt_s.nil?
    return [false, "malformed_seam: an endpoint has no stratum (a)"]
  end
  if stratum_map[src_s]["scheme"] != stratum_map[tgt_s]["scheme"]
    return [false, "malformed_seam: endpoints differ in scheme (b)"]
  end
  so = stratum_map[src_s]["ordinal"]
  to_ = stratum_map[tgt_s]["ordinal"]
  if (so - to_).abs <= 1
    return [false, "malformed_seam: endpoints are adjacent or co-stratal; " \
                   "a seam is for NON-adjacent strata (c)"]
  end
  chain = seam["chain"]
  unless chain.nil?
    if seam["mechanism_status"] == "absent"
      return [false, "malformed_seam: a drawn chain contradicts " \
                     "mechanism_status 'absent' (d)"]
    end
    lo = [so, to_].min
    hi = [so, to_].max
    ords = []
    chain.each do |oid|
      st = (occ_map[oid] || {})["stratum"]
      if st.nil?
        return [false, "malformed_seam: a chain member has no stratum (e)"]
      end
      if stratum_map[st]["scheme"] != stratum_map[src_s]["scheme"]
        return [false, "malformed_seam: a chain member differs in scheme (e)"]
      end
      ords << stratum_map[st]["ordinal"]
    end
    unless ords.all? { |o| lo < o && o < hi }
      return [false, "malformed_seam: a chain member is not at an " \
                     "INTERVENING stratum, strictly between the endpoints (f)"]
    end
    diffs = (0...(ords.length - 1)).map { |i| ords[i + 1] - ords[i] }
    if !diffs.empty? &&
       !(diffs.all? { |d| d > 0 } || diffs.all? { |d| d < 0 })
      return [false, "malformed_seam: chain is not strictly monotone from " \
                     "one endpoint toward the other (g)"]
    end
  end
  [true, "well-formed cross_stratal_seam"]
end

.skip_gaps(cro, classification) ⇒ Object

ALGORITHM D (Rule 16): the gaps a Causal Relation Object surfaces for the skip decision. THE ASYMMETRY (clause 3) is the whole point of the field and is implemented exactly.



346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
# File 'lib/causalontology/semantics.rb', line 346

def skip_gaps(cro, classification)
  gaps = []
  has_mech = !(cro["mechanism"] || []).empty?
  if cro["skips"] == true && has_mech
    gaps << "contradictory_skip" # HARD
    return gaps
  end
  if cro["skips"] == true &&
     !["skipping", "unclassifiable"].include?(classification)
    gaps << "vacuous_skip" # invitation
  end
  if classification == "skipping" && !has_mech
    if cro["skips"] == true
      # NOTHING: absence is a finding
    else
      gaps << "incomplete_mechanism" # invitation
    end
  end
  gaps
end

.state_gaps(state, quality) ⇒ Object

Rule 19 / N5.3.1-2: the HARD gaps a state assertion surfaces against its quality: value_type_mismatch and/or unit_mismatch.



523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
# File 'lib/causalontology/semantics.rb', line 523

def state_gaps(state, quality)
  gaps = []
  dt = quality["datatype"]
  v = state["value"] || {}
  shape = if v.key?("quantity") then "quantity"
          elsif v.key?("categorical") then "categorical"
          elsif v.key?("boolean") then "boolean"
          end
  if shape != dt
    gaps << "value_type_mismatch"
  elsif dt == "quantity" && v["unit"] != quality["unit"]
    gaps << "unit_mismatch"
  end
  gaps
end

.to_seconds(duration, unit) ⇒ Object

ALGORITHM E helper: normalize a delay to seconds by the fixed table. 3.0.0: an ordinal ('ticks') unit is dimensionless and has NO wall-clock mapping - converting one to seconds is a category error and is refused.



370
371
372
373
374
375
376
377
378
# File 'lib/causalontology/semantics.rb', line 370

def to_seconds(duration, unit)
  if ORDINAL_UNITS.include?(unit)
    raise ArgumentError,
          "'#{unit}' is an ordinal (dimensionless) unit and has no " \
          "wall-clock seconds mapping"
  end
  return 0 if unit == "instant"
  duration * UNIT_SECONDS.fetch(unit)
end

.validate_semantics(obj, kind = nil) ⇒ Object

[ok, reasons] - the locally checkable semantic rules.



71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/causalontology/semantics.rb', line 71

def validate_semantics(obj, kind = nil)
  kind ||= Canonical.infer_kind(obj)
  errors = []

  if kind == "causal_relation_object"
    t = obj["temporal"]
    if !t.nil? && !t["minimum_delay"].nil? && !t["maximum_delay"].nil? && t["minimum_delay"] > t["maximum_delay"]
      errors << "minimum_delay must be <= maximum_delay"
    end
    oid = obj["id"]
    if oid && (obj["mechanism"] || []).include?(oid)
      errors << "mechanism must be acyclic " \
                "(a Causal Relation Object may not contain itself)"
    end
    if oid && obj["refines"] == oid
      errors << "refines must be acyclic"
    end
    # Rule 16, clause 1 (contradictory_skip): a HARD, locally-decidable
    # contradiction between skips:true and a non-empty mechanism.
    if obj["skips"] == true && obj["mechanism"] && !obj["mechanism"].empty?
      errors << "contradictory_skip: skips is true but a mechanism " \
                "is present"
    end
  end

  if kind == "enrichment"
    field = obj["field"]
    about = obj["about"] || ""
    entry = obj["entry"]
    spec = ENRICHMENT_FIELDS[field]
    if spec
      legal_kinds, shape = spec
      about_kind = kind_of_id(about)
      if about_kind && !legal_kinds.include?(about_kind)
        errors << "#{field} is not a legal field for a #{about_kind} (rule 12)"
      end
      if shape == "alias"
        unless entry.is_a?(Hash) && entry.key?("lang") && entry.key?("text")
          errors << "an aliases entry must be a language-tagged text object"
        end
      else
        unless entry.is_a?(String) && entry.start_with?(shape + ":")
          errors << "a #{field} entry must be a #{shape}: identifier"
        end
      end
    end
  end

  # 3.0.0 Rule 22, local clause: a Cross Stratal Seam that DRAWS a chain has,
  # by drawing it, a modelled intervening mechanism - so mechanism_status
  # 'absent' contradicts a present chain (the honest-ignorance distinction
  # must stay honest). The stratal well-formedness (non-adjacency, adjacency
  # of chain steps, scheme, the home rule) needs the strata map and lives in
  # seam_wellformed, exactly as bridge well-formedness does.
  if kind == "cross_stratal_seam"
    if !obj["chain"].nil? && obj["mechanism_status"] == "absent"
      errors << "contradictory_seam: a drawn chain cannot carry " \
                "mechanism_status 'absent' (a drawn mechanism is not absent)"
    end
  end

  # 4.0.0 Rule 24, local clause: a predicted_occurrence's interval carries
  # exactly ONE temporal dimension - a wall-clock start (optional end) or an
  # ordinal start_tick (optional end_tick), never both and never neither.
  # Per Rule 23 the two dimensions never compare. The pairing check of a
  # prediction_error against its predicted_occurrence and its observed
  # token_occurrence needs those objects and lives in
  # prediction_pairing_mismatch, exactly as covering_law_mismatch does.
  if kind == "predicted_occurrence"
    iv = obj["interval"] || {}
    wall = iv.key?("start")
    tick = iv.key?("start_tick")
    if wall && tick
      errors << "dimension_conflict: a predicted interval must " \
                "carry exactly one temporal dimension, not a " \
                "wall-clock start AND an ordinal start_tick"
    end
    if !wall && !tick
      errors << "missing_dimension: a predicted interval must " \
                "carry a wall-clock start or an ordinal start_tick"
    end
  end

  [errors.empty?, errors]
end

.window_overlap(a, b) ⇒ Object



174
175
176
177
178
179
180
181
182
183
184
185
# File 'lib/causalontology/semantics.rb', line 174

def window_overlap(a, b)
  ta = a["temporal"]
  tb = b["temporal"]
  return true if ta.nil? || tb.nil? # either absent counts as overlapping
  # 3.0.0: an ordinal window and a wall-clock window never overlap
  return false if dimension(ta["unit"]) != dimension(tb["unit"])
  lo_a = magnitude(ta["minimum_delay"], ta["unit"])
  hi_a = magnitude(ta["maximum_delay"], ta["unit"])
  lo_b = magnitude(tb["minimum_delay"], tb["unit"])
  hi_b = magnitude(tb["maximum_delay"], tb["unit"])
  lo_a <= hi_b && lo_b <= hi_a
end