<commit_before>#
#       ActiveFacts Generators.
#       Generate json output from a vocabulary, for loading into APRIMO
#
# Copyright (c) 2009 <NAME>. Read the LICENSE file.
#
require 'json'
require 'digest/sha1'
require 'activefacts/registry'

module ActiveFacts
  module Generators
    module Diagrams
      # Generate json output from a vocabulary, for loading into APRIMO.
      # Invoke as
      #   afgen --diagrams/json <file>.cql=diagrams
      class JSON
      private
	def initialize(vocabulary)
	  @vocabulary = vocabulary
	  @vocabulary = @vocabulary.Vocabulary.values[0] if ActiveFacts::API::Constellation === @vocabulary
	end

	def puts(*a)
	  @out.puts *a
	end

      public
	def generate(out = $>)
	  @out = out
	  uuids = {}

	  puts "{ model: '#{@vocabulary.name}',\n" +
	  "diagrams: [\n#{
	    @vocabulary.all_diagram.sort_by{|o| o.name.gsub(/ /,'')}.map do |d|
	      j = {:uuid => (uuids[d] ||= uuid_from_id(d)), :name => d.name}
	      "    #{j.to_json}"
	    end*",\n"
	  }\n  ],"

	  object_types = @vocabulary.all_object_type.sort_by{|o| o.name.gsub(/ /,'')}
	  puts "  object_types: [\n#{
	    object_types.sort_by{|o|o.identifying_role_values.inspect}.map do |o|
	      uuids[o] ||= uuid_from_id(o)
	      ref_mode = nil
	      if o.is_a?(ActiveFacts::Metamodel::EntityType) and
		p = o.preferred_identifier and
		(rrs = p.role_sequence.all_role_ref).size == 1 and
		(r = rrs.single.role).fact_type != o.fact_type and
		r.object_type.is_a?(ActiveFacts::Metamodel::ValueType) and
		!r.fact_type.is_a?(ActiveFacts::Metamodel::TypeInheritance)
		ref_mode = "#{r.object_type.name}"
		ref_mode.sub!(%r{#{o.name} *}, '.')
	      end
	      j = {
		:uuid => uuids[o],
		:name => o.name,
		:shapes => o.all_object_type_shape.sort_by{|s| [s.location.x, s.location.y]}.map do |shape|
		  x = { :diagram => uuids[shape.orm_diagram],
		    :is_expanded => shape.is_expanded,
		    :uuid => uuid_from_id(shape),
		    :x => shape.location.x,
		    :y => shape.location.y
		  }
		  x[:is_expanded] = true if ref_mode && shape.is_expanded  # Don't show the reference mode
		  x
		end
	      }
	      j[:ref_mode] = ref_mode if ref_mode
	      j[:independent] = true if o.is_independent

	      if o.is_a?(ActiveFacts::Metamodel::EntityType)
		# Entity Type may be objectified, and may have supertypes:
		if o.fact_type
		  uuid = (uuids[o.fact_type] ||= uuid_from_id(o.fact_type))
		  j[:objectifies] = uuid
		  j[:implicit] = true if o.concept.implication_rule
		end
		if o.all_type_inheritance_as_subtype.size > 0
		  j[:supertypes] = o.
		    all_type_inheritance_as_subtype.
		    sort_by{|ti| ti.provides_identification ? 0 : 1}.
		    map{|ti|
		      [ uuids[ti.supertype] ||= uuid_from_id(ti.supertype),
			uuids[ti.supertype_role] = uuid_from_id(ti.supertype_role)
		      ]
		    }
		end
	      else
		# ValueType usually has a supertype:
		if (o.supertype)
		  j[:supertype] = (uuids[o.supertype] ||= uuid_from_id(o.supertype))
		end
	      end
	      # REVISIT: Place a ValueConstraint and shape
	      "    #{j.to_json}"
	    end*",\n"
	  }\n  ],"

	  fact_types = @vocabulary.constellation.
	    FactType.values.
	    reject{|ft|
	      ActiveFacts::Metamodel::LinkFactType === ft || ActiveFacts::Metamodel::TypeInheritance === ft
	    }
	  puts "  fact_types: [\n#{
	    fact_types.sort_by{|f| f.identifying_role_values.inspect}.map do |f|
	      uuids[f] ||= uuid_from_id(f)
	      j = {:uuid => uuids[f]}

	      if f.entity_type
		j[:objectified_as] = uuids[f.entity_type]
	      end

	      # Emit roles
	      roles = f.all_role.sort_by{|r| r.ordinal }
	      j[:roles] = roles.map do |role|
		uuid = (uuids[role] ||= uuid_from_id(role))
		# REVISIT: Internal Mandatory Constraints
		# REVISIT: Place a ValueConstraint and shape
		# REVISIT: Place a RoleName shape
		{:uuid => uuid, :player => uuids[role.object_type]}
		# N.B. The object_type shape to which this role is attached is not in the meta-model
		# Attach to the closest instance on this diagram (if any)
	      end

	      # Emit readings. Each is a [role_order, text] pair
	      j[:readings] = f.all_reading.map do |r|
		role_refs = r.role_sequence.all_role_ref_in_order
		[
		  role_order(uuids, role_refs.map{|rr| rr.role}, roles),
		  r.text.gsub(/\{([0-9])\}/) do |insert|
		    role_ref = role_refs[$1.to_i]
		    la = role_ref.leading_adjective
		    la = nil if la == ''
		    ta = role_ref.trailing_adjective
		    ta = nil if ta == ''
		    (la ? la+'-' : '') +
		      (la && la.index(' ') ? ' ' : '') +
		      insert +
		      (ta && ta.index(' ') ? ' ' : '') +
		      (ta ? '-'+ta : '')
		  end
		]
	      end.sort_by{|(ro,text)| ro }.map do |(ro,text)|
		[ ro, text ]
	      end

	      # Emit shapes
	      j[:shapes] = f.all_fact_type_shape.sort_by{|s| [s.location.x, s.location.y]}.map do |shape|
		sj = {
		  :diagram => uuids[shape.orm_diagram],
		  :uuid => uuid_from_id(shape),
		  :x => shape.location.x,
		  :y => shape.location.y
		}

		# Add the role_order, if specified
		if shape.all_role_display.size > 0
		  if shape.all_role_display.size != roles.size
		    raise "Invalid RoleDisplay for #{f.default_reading} in #{shape.orm_diagram.name} diagram"
		  end
		  ro = role_order(
		    uuids,
		    shape.all_role_display.sort_by{|rd| rd.ordinal }.map{|rd| rd.role },
		    roles
		  )
		  sj[:role_order] = ro if ro
		end

		# REVISIT: Place the ReadingShape

		# Emit the location of the name, if objectified
		if n = shape.objectified_fact_type_name_shape
		  sj[:name_shape] = {:x => n.location.x, :y => n.location.y}
		end
		sj
	      end

	      # Emit Internal Presence Constraints
	      f.internal_presence_constraints.to_a.sort_by{|ipc, z|
		    [ipc.is_preferred_identifier ? 0 : 1, ipc.is_mandatory ? 0 : 1, ipc.min_frequency || 0, ipc.max_frequency || 1_000]
		  }.each do |ipc|
		uuid = (uuids[ipc] ||= uuid_from_id(ipc))

		constraint = {
		  :uuid => uuid,
		  :min => ipc.min_frequency,
		  :max => ipc.max_frequency,
		  :is_preferred => ipc.is_preferred_identifier,
		  :mandatory => ipc.is_mandatory
		}

		# Get the role (or excluded role, for a UC)
		roles = ipc.role_sequence.all_role_ref_in_order.map{|r| r.role}
		if roles.size > 1 || (!ipc.is_mandatory && ipc.max_frequency == 1)
		  # This can be only a uniqueness constraint. Record the missing role, if any
		  role = (f.all_role.to_a - roles)[0]
		  constraint[:uniqueExcept] = uuids[role]
		else
		  # An internal mandatory or frequency constraint applies to only one role.
		  # If it's also unique (max == 1), that applies on the counterpart role.
		  # You can also have a mandatory frequency constraint, but that applies on this role.
		  constraint[:role] = uuids[roles[0]]
		end
		(j[:constraints] ||= []) << constraint
	      end

	      # Add ring constraints
	      f.all_role_in_order.
		map{|r| r.all_ring_constraint.to_a+r.all_ring_constraint_as_other_role.to_a }.
		flatten.uniq.each do |ring|
		  (j[:constraints] ||= []) << {
		      :uuid => (uuids[ring] ||= uuid_from_id(ring)),
		      :shapes => ring.all_constraint_shape.sort_by{|s| [s.location.x, s.location.y]}.map do |shape|
			{ :diagram => uuids[shape.orm_diagram],
			  :uuid => uuid_from_id(shape),
			  :x => shape.location.x,
			  :y => shape.location.y
			}
		      end,
		      :ringKind => ring.ring_type,
		      :roles => [uuids[ring.role], uuids[ring.other_role]]
		      # REVISIT: Deontic, enforcement
		    }
		end

	      # REVISIT: RotationSetting

	      "    #{j.to_json}"
	    end*",\n"
	  }\n  ],"

	  constraints = @vocabulary.constellation.
	    Constraint.values
	  puts "  constraints: [\n#{
	    constraints.sort_by{|c|c.identifying_role_values.inspect}.select{|c| !uuids[c]}.map do |c|
	      uuid = uuids[c] ||= uuid_from_id(c)
	      j = {
		:uuid => uuid,
		:type => c.class.basename,
		:shapes => c.all_constraint_shape.sort_by{|s| [s.location.x, s.location.y]}.map do |shape|
		  { :diagram => uuids[shape.orm_diagram],
		    :uuid => uuid_from_id(shape),
		    :x => shape.location.x,
		    :y => shape.location.y
		  }
		end
	      }

	      if (c.enforcement)
		# REVISIT: Deontic constraint
	      end
	      if (c.concept.all_context_note_as_relevant_concept.size > 0)
		# REVISIT: Context Notes
	      end

	      case c
	      when ActiveFacts::Metamodel::PresenceConstraint
		j[:min_frequency] = c.min_frequency
		j[:max_frequency] = c.max_frequency
		j[:is_mandatory] = c.is_mandatory
		j[:is_preferred_identifier] = c.is_preferred_identifier
		rss = [c.role_sequence.all_role_ref_in_order.map(&:role)]

		# Ignore internal presence constraints on TypeInheritance fact types
		next nil if !c.role_sequence.all_role_ref.
		  detect{|rr|
		    !rr.role.fact_type.is_a?(ActiveFacts::Metamodel::TypeInheritance)
		  }

	      when ActiveFacts::Metamodel::RingConstraint
		next nil  # These are emitted with the corresponding fact type

	      when ActiveFacts::Metamodel::SetComparisonConstraint
		rss = c.
		  all_set_comparison_roles.sort_by{|scr| scr.ordinal}.
		  map{|scr| scr.role_sequence.all_role_ref_in_order.map(&:role) }
		if (ActiveFacts::Metamodel::SetExclusionConstraint === c)
		  j[:is_mandatory] = c.is_mandatory
		end

	      when ActiveFacts::Metamodel::SubsetConstraint
		rss = [c.subset_role_sequence, c.superset_role_sequence].
		  map{|rs| rs.all_role_ref_in_order.map(&:role) }

	      when ActiveFacts::Metamodel::ValueConstraint
		next nil  # REVISIT: Should have been handled elsewhere
		if (c.role)
		  # Should have been handled as role.role_value_constraint
		elsif (c.value_type)
		  # Should have been handled as object_type.value_constraint
		end
		j[:allowed_ranges] = c.all_allowed_range.map{|ar|
		  [ ar.value_range.minimum_bound, ar.value_range.maximum_bound ].
		    map{|b| [b.value.literal, b.value.unit.name, b.is_inclusive] }
		}

	      else
		raise "REVISIT: Constraint type not yet dumped to JSON"
	      end

	      # rss contains the constrained role sequences; map to uuids
	      j[:role_sequences] = rss.map{|rs|
		rs.map do |role|
		  uuids[role]
		end
	      }

	      "    #{j.to_json}"
	    end.compact*",\n"
	  }\n  ]"

	  puts "}"
	end

	def role_order(uuids, roles, order)
	  if (roles.size > 9)
	    roles.map{|r| uuids[r] }
	  else
	    roles.map{|r| order.index(r).to_s }*''
	  end
	end

	def uuid_from_id o
	  irvs = o.identifying_role_values.inspect
	  d = Digest::SHA1.digest irvs
	  # $stderr.puts "#{o.class.basename}: #{irvs}"
	  d[0,4].unpack("H8")[0]+'-'+
	    d[4,2].unpack("H4")[0]+'-'+
	    d[6,2].unpack("H4")[0]+'-'+
	    d[8,2].unpack("H4")[0]+'-'+
	    d[10,6].unpack("H6")[0]
	end

      end
    end
  end
end

ActiveFacts::Registry.generator('diagrams/json', ActiveFacts::Generators::Diagrams::JSON)
<commit_msg>Unary fact types are always identified by their player, not by its identifier<commit_after>#
#       ActiveFacts Generators.
#       Generate json output from a vocabulary, for loading into APRIMO
#
# Copyright (c) 2009 <NAME>. Read the LICENSE file.
#
require 'json'
require 'digest/sha1'
require 'activefacts/registry'

module ActiveFacts
  module Generators
    module Diagrams
      # Generate json output from a vocabulary, for loading into APRIMO.
      # Invoke as
      #   afgen --diagrams/json <file>.cql=diagrams
      class JSON
      private
	def initialize(vocabulary)
	  @vocabulary = vocabulary
	  @vocabulary = @vocabulary.Vocabulary.values[0] if ActiveFacts::API::Constellation === @vocabulary
	end

	def puts(*a)
	  @out.puts *a
	end

      public
	def generate(out = $>)
	  @out = out
	  uuids = {}

	  puts "{ model: '#{@vocabulary.name}',\n" +
	  "diagrams: [\n#{
	    @vocabulary.all_diagram.sort_by{|o| o.name.gsub(/ /,'')}.map do |d|
	      j = {:uuid => (uuids[d] ||= uuid_from_id(d)), :name => d.name}
	      "    #{j.to_json}"
	    end*",\n"
	  }\n  ],"

	  object_types = @vocabulary.all_object_type.sort_by{|o| o.name.gsub(/ /,'')}
	  puts "  object_types: [\n#{
	    object_types.sort_by{|o|o.identifying_role_values.inspect}.map do |o|
	      uuids[o] ||= uuid_from_id(o)
	      ref_mode = nil
	      if o.is_a?(ActiveFacts::Metamodel::EntityType) and
		p = o.preferred_identifier and
		(rrs = p.role_sequence.all_role_ref).size == 1 and
		!(o.fact_type && o.fact_type.all_role.size == 1) and
		(r = rrs.single.role).fact_type != o.fact_type and
		r.object_type.is_a?(ActiveFacts::Metamodel::ValueType) and
		!r.fact_type.is_a?(ActiveFacts::Metamodel::TypeInheritance)
		ref_mode = "#{r.object_type.name}"
		ref_mode.sub!(%r{#{o.name} *}, '.')
	      end
	      j = {
		:uuid => uuids[o],
		:name => o.name,
		:shapes => o.all_object_type_shape.sort_by{|s| [s.location.x, s.location.y]}.map do |shape|
		  x = { :diagram => uuids[shape.orm_diagram],
		    :is_expanded => shape.is_expanded,
		    :uuid => uuid_from_id(shape),
		    :x => shape.location.x,
		    :y => shape.location.y
		  }
		  x[:is_expanded] = true if ref_mode && shape.is_expanded  # Don't show the reference mode
		  x
		end
	      }
	      j[:ref_mode] = ref_mode if ref_mode
	      j[:independent] = true if o.is_independent

	      if o.is_a?(ActiveFacts::Metamodel::EntityType)
		# Entity Type may be objectified, and may have supertypes:
		if o.fact_type
		  uuid = (uuids[o.fact_type] ||= uuid_from_id(o.fact_type))
		  j[:objectifies] = uuid
		  j[:implicit] = true if o.concept.implication_rule
		end
		if o.all_type_inheritance_as_subtype.size > 0
		  j[:supertypes] = o.
		    all_type_inheritance_as_subtype.
		    sort_by{|ti| ti.provides_identification ? 0 : 1}.
		    map{|ti|
		      [ uuids[ti.supertype] ||= uuid_from_id(ti.supertype),
			uuids[ti.supertype_role] = uuid_from_id(ti.supertype_role)
		      ]
		    }
		end
	      else
		# ValueType usually has a supertype:
		if (o.supertype)
		  j[:supertype] = (uuids[o.supertype] ||= uuid_from_id(o.supertype))
		end
	      end
	      # REVISIT: Place a ValueConstraint and shape
	      "    #{j.to_json}"
	    end*",\n"
	  }\n  ],"

	  fact_types = @vocabulary.constellation.
	    FactType.values.
	    reject{|ft|
	      ActiveFacts::Metamodel::LinkFactType === ft || ActiveFacts::Metamodel::TypeInheritance === ft
	    }
	  puts "  fact_types: [\n#{
	    fact_types.sort_by{|f| f.identifying_role_values.inspect}.map do |f|
	      uuids[f] ||= uuid_from_id(f)
	      j = {:uuid => uuids[f]}

	      if f.entity_type
		j[:objectified_as] = uuids[f.entity_type]
	      end

	      # Emit roles
	      roles = f.all_role.sort_by{|r| r.ordinal }
	      j[:roles] = roles.map do |role|
		uuid = (uuids[role] ||= uuid_from_id(role))
		# REVISIT: Internal Mandatory Constraints
		# REVISIT: Place a ValueConstraint and shape
		# REVISIT: Place a RoleName shape
		{:uuid => uuid, :player => uuids[role.object_type]}
		# N.B. The object_type shape to which this role is attached is not in the meta-model
		# Attach to the closest instance on this diagram (if any)
	      end

	      # Emit readings. Each is a [role_order, text] pair
	      j[:readings] = f.all_reading.map do |r|
		role_refs = r.role_sequence.all_role_ref_in_order
		[
		  role_order(uuids, role_refs.map{|rr| rr.role}, roles),
		  r.text.gsub(/\{([0-9])\}/) do |insert|
		    role_ref = role_refs[$1.to_i]
		    la = role_ref.leading_adjective
		    la = nil if la == ''
		    ta = role_ref.trailing_adjective
		    ta = nil if ta == ''
		    (la ? la+'-' : '') +
		      (la && la.index(' ') ? ' ' : '') +
		      insert +
		      (ta && ta.index(' ') ? ' ' : '') +
		      (ta ? '-'+ta : '')
		  end
		]
	      end.sort_by{|(ro,text)| ro }.map do |(ro,text)|
		[ ro, text ]
	      end

	      # Emit shapes
	      j[:shapes] = f.all_fact_type_shape.sort_by{|s| [s.location.x, s.location.y]}.map do |shape|
		sj = {
		  :diagram => uuids[shape.orm_diagram],
		  :uuid => uuid_from_id(shape),
		  :x => shape.location.x,
		  :y => shape.location.y
		}

		# Add the role_order, if specified
		if shape.all_role_display.size > 0
		  if shape.all_role_display.size != roles.size
		    raise "Invalid RoleDisplay for #{f.default_reading} in #{shape.orm_diagram.name} diagram"
		  end
		  ro = role_order(
		    uuids,
		    shape.all_role_display.sort_by{|rd| rd.ordinal }.map{|rd| rd.role },
		    roles
		  )
		  sj[:role_order] = ro if ro
		end

		# REVISIT: Place the ReadingShape

		# Emit the location of the name, if objectified
		if n = shape.objectified_fact_type_name_shape
		  sj[:name_shape] = {:x => n.location.x, :y => n.location.y}
		end
		sj
	      end

	      # Emit Internal Presence Constraints
	      f.internal_presence_constraints.to_a.sort_by{|ipc, z|
		    [ipc.is_preferred_identifier ? 0 : 1, ipc.is_mandatory ? 0 : 1, ipc.min_frequency || 0, ipc.max_frequency || 1_000]
		  }.each do |ipc|
		uuid = (uuids[ipc] ||= uuid_from_id(ipc))

		constraint = {
		  :uuid => uuid,
		  :min => ipc.min_frequency,
		  :max => ipc.max_frequency,
		  :is_preferred => ipc.is_preferred_identifier,
		  :mandatory => ipc.is_mandatory
		}

		# Get the role (or excluded role, for a UC)
		roles = ipc.role_sequence.all_role_ref_in_order.map{|r| r.role}
		if roles.size > 1 || (!ipc.is_mandatory && ipc.max_frequency == 1)
		  # This can be only a uniqueness constraint. Record the missing role, if any
		  role = (f.all_role.to_a - roles)[0]
		  constraint[:uniqueExcept] = uuids[role]
		else
		  # An internal mandatory or frequency constraint applies to only one role.
		  # If it's also unique (max == 1), that applies on the counterpart role.
		  # You can also have a mandatory frequency constraint, but that applies on this role.
		  constraint[:role] = uuids[roles[0]]
		end
		(j[:constraints] ||= []) << constraint
	      end

	      # Add ring constraints
	      f.all_role_in_order.
		map{|r| r.all_ring_constraint.to_a+r.all_ring_constraint_as_other_role.to_a }.
		flatten.uniq.each do |ring|
		  (j[:constraints] ||= []) << {
		      :uuid => (uuids[ring] ||= uuid_from_id(ring)),
		      :shapes => ring.all_constraint_shape.sort_by{|s| [s.location.x, s.location.y]}.map do |shape|
			{ :diagram => uuids[shape.orm_diagram],
			  :uuid => uuid_from_id(shape),
			  :x => shape.location.x,
			  :y => shape.location.y
			}
		      end,
		      :ringKind => ring.ring_type,
		      :roles => [uuids[ring.role], uuids[ring.other_role]]
		      # REVISIT: Deontic, enforcement
		    }
		end

	      # REVISIT: RotationSetting

	      "    #{j.to_json}"
	    end*",\n"
	  }\n  ],"

	  constraints = @vocabulary.constellation.
	    Constraint.values
	  puts "  constraints: [\n#{
	    constraints.sort_by{|c|c.identifying_role_values.inspect}.select{|c| !uuids[c]}.map do |c|
	      uuid = uuids[c] ||= uuid_from_id(c)
	      j = {
		:uuid => uuid,
		:type => c.class.basename,
		:shapes => c.all_constraint_shape.sort_by{|s| [s.location.x, s.location.y]}.map do |shape|
		  { :diagram => uuids[shape.orm_diagram],
		    :uuid => uuid_from_id(shape),
		    :x => shape.location.x,
		    :y => shape.location.y
		  }
		end
	      }

	      if (c.enforcement)
		# REVISIT: Deontic constraint
	      end
	      if (c.concept.all_context_note_as_relevant_concept.size > 0)
		# REVISIT: Context Notes
	      end

	      case c
	      when ActiveFacts::Metamodel::PresenceConstraint
		j[:min_frequency] = c.min_frequency
		j[:max_frequency] = c.max_frequency
		j[:is_mandatory] = c.is_mandatory
		j[:is_preferred_identifier] = c.is_preferred_identifier
		rss = [c.role_sequence.all_role_ref_in_order.map(&:role)]

		# Ignore internal presence constraints on TypeInheritance fact types
		next nil if !c.role_sequence.all_role_ref.
		  detect{|rr|
		    !rr.role.fact_type.is_a?(ActiveFacts::Metamodel::TypeInheritance)
		  }

	      when ActiveFacts::Metamodel::RingConstraint
		next nil  # These are emitted with the corresponding fact type

	      when ActiveFacts::Metamodel::SetComparisonConstraint
		rss = c.
		  all_set_comparison_roles.sort_by{|scr| scr.ordinal}.
		  map{|scr| scr.role_sequence.all_role_ref_in_order.map(&:role) }
		if (ActiveFacts::Metamodel::SetExclusionConstraint === c)
		  j[:is_mandatory] = c.is_mandatory
		end

	      when ActiveFacts::Metamodel::SubsetConstraint
		rss = [c.subset_role_sequence, c.superset_role_sequence].
		  map{|rs| rs.all_role_ref_in_order.map(&:role) }

	      when ActiveFacts::Metamodel::ValueConstraint
		next nil  # REVISIT: Should have been handled elsewhere
		if (c.role)
		  # Should have been handled as role.role_value_constraint
		elsif (c.value_type)
		  # Should have been handled as object_type.value_constraint
		end
		j[:allowed_ranges] = c.all_allowed_range.map{|ar|
		  [ ar.value_range.minimum_bound, ar.value_range.maximum_bound ].
		    map{|b| [b.value.literal, b.value.unit.name, b.is_inclusive] }
		}

	      else
		raise "REVISIT: Constraint type not yet dumped to JSON"
	      end

	      # rss contains the constrained role sequences; map to uuids
	      j[:role_sequences] = rss.map{|rs|
		rs.map do |role|
		  uuids[role]
		end
	      }

	      "    #{j.to_json}"
	    end.compact*",\n"
	  }\n  ]"

	  puts "}"
	end

	def role_order(uuids, roles, order)
	  if (roles.size > 9)
	    roles.map{|r| uuids[r] }
	  else
	    roles.map{|r| order.index(r).to_s }*''
	  end
	end

	def uuid_from_id o
	  irvs = o.identifying_role_values.inspect
	  d = Digest::SHA1.digest irvs
	  # $stderr.puts "#{o.class.basename}: #{irvs}"
	  d[0,4].unpack("H8")[0]+'-'+
	    d[4,2].unpack("H4")[0]+'-'+
	    d[6,2].unpack("H4")[0]+'-'+
	    d[8,2].unpack("H4")[0]+'-'+
	    d[10,6].unpack("H6")[0]
	end

      end
    end
  end
end

ActiveFacts::Registry.generator('diagrams/json', ActiveFacts::Generators::Diagrams::JSON)

{'fraction_non_alphanumeric': 0.14294338921655134, 'fraction_numerical': 0.004876236474248828, 'mean_word_length': 2.9519177830794643, 'pattern_counts': {'":': 0, '<': 15, '<?xml version=': 0, '>': 97, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4812988', 'n_tokens_mistral': 8985, 'n_tokens_neox': 8010, 'n_words': 1620}