activefacts 0.6.0 → 0.7.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 (35) hide show
  1. data/Manifest.txt +7 -2
  2. data/examples/CQL/Address.cql +0 -2
  3. data/examples/CQL/Blog.cql +2 -2
  4. data/examples/CQL/CompanyDirectorEmployee.cql +1 -1
  5. data/examples/CQL/Death.cql +1 -1
  6. data/examples/CQL/Metamodel.cql +5 -5
  7. data/examples/CQL/MultiInheritance.cql +2 -0
  8. data/examples/CQL/PersonPlaysGame.cql +1 -1
  9. data/lib/activefacts/cql/Concepts.treetop +17 -8
  10. data/lib/activefacts/cql/Language/English.treetop +1 -2
  11. data/lib/activefacts/generate/absorption.rb +1 -1
  12. data/lib/activefacts/generate/null.rb +8 -1
  13. data/lib/activefacts/generate/oo.rb +174 -0
  14. data/lib/activefacts/generate/ruby.rb +49 -208
  15. data/lib/activefacts/generate/sql/server.rb +137 -72
  16. data/lib/activefacts/generate/text.rb +1 -1
  17. data/lib/activefacts/input/orm.rb +12 -2
  18. data/lib/activefacts/persistence.rb +5 -1
  19. data/lib/activefacts/persistence/columns.rb +324 -0
  20. data/lib/activefacts/persistence/foreignkey.rb +87 -0
  21. data/lib/activefacts/persistence/index.rb +171 -0
  22. data/lib/activefacts/persistence/reference.rb +326 -0
  23. data/lib/activefacts/persistence/tables.rb +307 -0
  24. data/lib/activefacts/support.rb +1 -1
  25. data/lib/activefacts/version.rb +1 -1
  26. data/lib/activefacts/vocabulary/extensions.rb +42 -5
  27. data/spec/absorption_spec.rb +8 -6
  28. data/spec/cql_cql_spec.rb +1 -0
  29. data/spec/cql_sql_spec.rb +2 -1
  30. data/spec/cql_unit_spec.rb +0 -6
  31. data/spec/norma_cql_spec.rb +1 -0
  32. data/spec/norma_sql_spec.rb +1 -1
  33. data/spec/norma_tables_spec.rb +41 -43
  34. metadata +9 -4
  35. data/lib/activefacts/persistence/composition.rb +0 -653
@@ -0,0 +1,87 @@
1
+ module ActiveFacts
2
+ module Metamodel
3
+
4
+ class ForeignKey
5
+ attr_reader :from, :to, :reference, :from_columns, :to_columns
6
+ def initialize(from, to, fk_ref, from_columns, to_columns)
7
+ @from, @to, @fk_ref, @from_columns, @to_columns =
8
+ from, to, fk_ref, from_columns, to_columns
9
+ end
10
+ end
11
+
12
+ class Concept
13
+ def all_absorbed_foreign_key_reference_path
14
+ references_from.inject([]) do |array, ref|
15
+ if ref.is_simple_reference
16
+ array << [ref]
17
+ elsif ref.is_absorbing
18
+ ref.to.all_absorbed_foreign_key_reference_path.each{|aref|
19
+ array << aref.insert(0, ref)
20
+ }
21
+ end
22
+ array
23
+ end
24
+ end
25
+
26
+ def foreign_keys
27
+ fk_ref_paths = all_absorbed_foreign_key_reference_path
28
+
29
+ # Get the ForeignKey object for each absorbed reference path
30
+ fk_ref_paths.map do |fk_ref_path|
31
+ debug :fk, "\nFK: " + fk_ref_path.map{|fk_ref| fk_ref.reading }*" and " do
32
+
33
+ from_columns = columns.select{|column|
34
+ column.references[0...fk_ref_path.size] == fk_ref_path
35
+ }
36
+ debug :fk, "from_columns = #{from_columns.map { |column| column.name }*", "}"
37
+
38
+ absorption_path = []
39
+ to = fk_ref_path.last.to
40
+ # REVISIT: There should be a better way to find where it's absorbed (especially since this fails for absorbed subtypes having their own identification!)
41
+ while (r = to.absorbed_via)
42
+ absorption_path << r
43
+ to = r.to == to ? r.from : r.to
44
+ end
45
+ raise "REVISIT: #{fk_ref_path.inspect} is bad" unless to and to.columns
46
+
47
+ unless absorption_path.empty?
48
+ debug :fk, "Reference target #{fk_ref_path.last.to.name} is absorbed into #{to.name} via:" do
49
+ debug :fk, "#{absorption_path.map(&:reading)*" and "}"
50
+ end
51
+ end
52
+
53
+ debug :fk, "Looking at absorption depth of #{absorption_path.size} in #{to.name} for to_columns for #{from_columns.map(&:name)*", "}:"
54
+ to_supertypes = to.supertypes_transitive
55
+ to_columns = from_columns.map do |from_column|
56
+ debug :fk, "\tLooking for counterpart of #{from_column.name}: #{from_column.comment}" do
57
+ target_path = absorption_path + from_column.references[fk_ref_path.size..-1]
58
+ debug :fk, "\tcounterpart MUST MATCH #{target_path.map(&:reading)*" and "}"
59
+ c = to.columns.detect do |column|
60
+ debug :fk, "Considering #{column.references.map(&:reading) * " and "}"
61
+ debug :fk, "exact match: #{column.name}: #{column.comment}" if column.references == target_path
62
+ # Column may be inherited into "to", in which case target_path is too long.
63
+ cr = column.references
64
+ allowed_type = fk_ref_path.last.to
65
+ #debug :fk, "Check for absorption, need #{allowed_type.name}" if cr != target_path
66
+ cr == target_path or
67
+ cr == target_path[-cr.size..-1] &&
68
+ !target_path[0...-cr.size].detect do |ref|
69
+ ft = ref.fact_type
70
+ next true if allowed_type.absorbed_via != ref # Problems if it doesn't match
71
+ allowed_type = ref.from
72
+ false
73
+ end
74
+ end
75
+ raise "REVISIT: Failed to find conterpart column for #{from_column.name}" unless c
76
+ c
77
+ end
78
+ end
79
+ debug :fk, "to_columns in #{to.name}: #{to_columns.map { |column| column ? column.name : "OOPS!" }*", "}"
80
+
81
+ ForeignKey.new(self, to, fk_ref_path[-1], from_columns, to_columns)
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,171 @@
1
+ #
2
+ # An Index on a Concept is used to represent a unique constraint across roles absorbed
3
+ # into that concept's table.
4
+ #
5
+ # Reference objects update each concept's list of the references *to* and *from* that concept.
6
+ #
7
+ # Copyright (c) 2008 Clifford Heath. Read the LICENSE file.
8
+ #
9
+
10
+ module ActiveFacts
11
+ module Metamodel
12
+ class Index
13
+ attr_reader :uniqueness_constraint, :on, :over, :columns, :is_primary, :is_unique
14
+
15
+ # An Index arises from a uniqueness constraint and applies to a table,
16
+ # but because the UC may actually be over an object absorbed into the table,
17
+ # we must record that object also.
18
+ # We record the columns it's over, whether it's primary (for 'over'),
19
+ # and whether it's unique (always, at present)
20
+ def initialize(uc, on, over, columns, is_primary, is_unique = true)
21
+ @uniqueness_constraint, @on, @over, @columns, @is_primary, @is_unique =
22
+ uc, on, over, columns, is_primary, is_unique
23
+ end
24
+
25
+ def real_name
26
+ @uniqueness_constraint.name && @uniqueness_constraint.name != '' ? @uniqueness_constraint.name : nil
27
+ end
28
+
29
+ def name
30
+ uc = @uniqueness_constraint
31
+ r = real_name
32
+ return r if r && r !~ /^(Ex|In)ternalUniquenessConstraint[0-9]+$/
33
+ (uc.is_preferred_identifier ? "PK_" : "IX_") +
34
+ view_name +
35
+ (uc.is_preferred_identifier ? "" : "By"+column_names*"")
36
+ end
37
+
38
+ def abbreviated_column_names
39
+ columns.map{|column| column.name.sub(/^#{over.name}/,'')}
40
+ end
41
+
42
+ def column_names
43
+ columns.map{|column| column.name}
44
+ end
45
+
46
+ def view_name
47
+ "#{over.name}#{on == over ? "" : "In"+on.name}"
48
+ end
49
+
50
+ def to_s
51
+ name = @uniqueness_constraint.name
52
+ colnames = @columns.map(&:name)*", "
53
+ preferred = @uniqueness_constraint.is_preferred_identifier ? " (preferred)" : ""
54
+ "Index #{name} on #{@on.name} over #{@over.name}(#{colnames})#{preferred}"
55
+ end
56
+ end
57
+
58
+ class Concept
59
+ attr_reader :indices
60
+
61
+ def clear_indices
62
+ # Clear any previous indices
63
+ @indices = nil
64
+ end
65
+
66
+ def populate_indices
67
+ # The absorption path of a column indicates how it came to be in this table.
68
+ # It might be a direct many:one valuetype relationship, or it might be in such
69
+ # a relationship to an entity that was absorbed into this table (and so on).
70
+ # The reference path is the set of absorption references and one past it.
71
+ # Stopping here means we don't dig into the definitions of FK column counterparts.
72
+ # Note that many columns of an object may have the same ref_path.
73
+ all_column_by_ref_path =
74
+ debug :index2, "Indexing columns by ref_path" do
75
+ columns.inject({}) do |hash, column|
76
+ debug :index2, "References in column #{name}#{column.name}" do
77
+ ref_path = column.absorption_references
78
+ raise "No absorption_references for #{column.name} from #{column.references.map(&:to_s)*" and "}" if !ref_path || ref_path.empty?
79
+ (hash[ref_path] ||= []) << column
80
+ debug :index2, "#{column.name} involves #{ref_path.map(&:to_s)*" and "}"
81
+ end
82
+ hash
83
+ end
84
+ end
85
+
86
+ columns_by_unique_constraint = {}
87
+ all_column_by_role_ref =
88
+ all_column_by_ref_path.
89
+ keys. # Go through all refpaths and find uniqueness constraints
90
+ inject({}) do |hash, ref_path|
91
+ ref_path.each do |ref|
92
+ next unless ref.to_role
93
+ ref.to_role.all_role_ref.each do |role_ref|
94
+ pcs = role_ref.role_sequence.all_presence_constraint.
95
+ reject do |pc|
96
+ !pc.max_frequency or # No maximum freq; cannot be a uniqueness constraint
97
+ pc.max_frequency != 1 or # maximum is not 1
98
+ pc.role_sequence.all_role_ref.size == 1 && # UniquenessConstraint is over one role
99
+ (pc.role_sequence.all_role_ref[0].role.fact_type.is_a?(TypeInheritance) || # Inheritance
100
+ pc.role_sequence.all_role_ref[0].role.fact_type.all_role.size == 1) # Unary
101
+ # The preceeeding two restrictions exclude the internal UCs created within NORMA.
102
+ end
103
+ next unless pcs.size > 0
104
+ # The columns for this ref_path support the UCs in "pcs".
105
+ pcs.each do |pc|
106
+ (columns_by_unique_constraint[pc] ||= []).concat(all_column_by_ref_path[ref_path])
107
+ end
108
+ hash[role_ref] = all_column_by_ref_path[ref_path]
109
+ end
110
+ end
111
+ hash
112
+ end
113
+
114
+ debug :index, "All Indices in #{name}:" do
115
+ @indices = columns_by_unique_constraint.map do |uc, columns|
116
+ absorption_level = columns.map(&:absorption_level).min
117
+ over = columns[0].references[absorption_level].from
118
+
119
+ # Absorption through a one-to-one forms a UC that we don't need to enforce using an index:
120
+ next if over != self and
121
+ over.absorbed_via == columns[0].references[absorption_level-1] and
122
+ (rrs = uc.role_sequence.all_role_ref).size == 1 and
123
+ over.absorbed_via.fact_type.all_role.include?(rrs[0].role)
124
+
125
+ index = Index.new(
126
+ uc,
127
+ self,
128
+ over,
129
+ columns,
130
+ uc.is_preferred_identifier
131
+ )
132
+ debug :index, index
133
+ index
134
+ end
135
+ end
136
+ end
137
+
138
+ end
139
+
140
+ class Vocabulary
141
+ def populate_all_indices
142
+ debug :index, "Populating all concept indices" do
143
+ all_feature.each do |feature|
144
+ next unless feature.is_a? Concept
145
+ feature.clear_indices
146
+ end
147
+ all_feature.each do |feature|
148
+ next unless feature.is_a? Concept
149
+ next unless feature.is_table
150
+ debug :index, "Populating indices for #{feature.name}" do
151
+ feature.populate_indices
152
+ end
153
+ end
154
+ end
155
+ debug :index, "Finished concept indices" do
156
+ all_feature.each do |feature|
157
+ next unless feature.is_a? Concept
158
+ next unless feature.is_table
159
+ next unless feature.indices.size > 0
160
+ debug :index, "#{feature.name}:" do
161
+ feature.indices.each do |index|
162
+ debug :index, index
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
169
+
170
+ end
171
+ end
@@ -0,0 +1,326 @@
1
+ #
2
+ # A Reference from one Concept to another is created for each many-1 or 1-1 relationship
3
+ # (including subtyping), and also for a unary role (implicitly to Boolean concept).
4
+ # A 1-1 or subtyping reference should be created in only one direction, and may be flipped
5
+ # if needed.
6
+ #
7
+ # A reference to a concept that's a table or is fully absorbed into a table will
8
+ # become a foreign key, otherwise it will absorb all that concept's references.
9
+ #
10
+ # Reference objects update each concept's list of the references *to* and *from* that concept.
11
+ #
12
+ # Copyright (c) 2008 Clifford Heath. Read the LICENSE file.
13
+ #
14
+ # REVISIT: References need is_mandatory
15
+ # REVISIT: Need to index References by to_role, to help in finding PK references etc.
16
+
17
+ module ActiveFacts
18
+ module Metamodel
19
+
20
+ class Reference
21
+ attr_reader :from, :to # A "from" instance is related to one "to" instance
22
+ attr_reader :from_role, :to_role # For objectified facts, one role will be nil (a phantom)
23
+ attr_reader :fact_type
24
+
25
+ # A Reference is created from a concept in regard to a role it plays
26
+ def initialize(from, role)
27
+ @from = from
28
+ return unless role # All done if it's a self-value reference for a ValueType
29
+ @fact_type = role.fact_type
30
+ if @fact_type.all_role.size == 1
31
+ # @from_role is nil for a unary
32
+ @to_role = role
33
+ @to = role.fact_type.entity_type # nil unless the unary is objectified
34
+ elsif (role.fact_type.entity_type == @from) # role is in "from", an objectified fact type
35
+ @from_role = nil # Phantom role
36
+ @to_role = role
37
+ @to = @to_role.concept
38
+ else
39
+ @from_role = role
40
+ @to = role.fact_type.entity_type # If set, to_role is a phantom
41
+ unless @to
42
+ raise "Illegal reference through >binary fact type" if @fact_type.all_role.size >2
43
+ @to_role = (role.fact_type.all_role-[role])[0]
44
+ @to = @to_role.concept
45
+ end
46
+ end
47
+ end
48
+
49
+ def role_type
50
+ role = @from_role||@to_role
51
+ role && role.role_type
52
+ end
53
+
54
+ def is_mandatory
55
+ !@from_role || # All phantom roles of fact types are mandatory
56
+ is_unary || # Unary fact types become booleans, which must be true or false
57
+ @from_role.is_mandatory
58
+ end
59
+
60
+ def is_unary
61
+ !@to && @to_role && @to_role.fact_type.all_role.size == 1
62
+ end
63
+
64
+ # This case is the only one that cannot be used in the preferred identifier of @from
65
+ def is_to_objectified_fact
66
+ @to && !@to_role && @from_role
67
+ end
68
+
69
+ def is_from_objectified_fact
70
+ @to && @to_role && !@from_role
71
+ end
72
+
73
+ def is_self_value
74
+ !@to && !@to_role
75
+ end
76
+
77
+ def is_absorbing
78
+ @to && @to.absorbed_via == self
79
+ end
80
+
81
+ def is_simple_reference
82
+ # It's a simple reference to a thing if that thing is a table,
83
+ # or is fully absorbed into another table but not via this reference.
84
+ @to && (@to.is_table or @to.absorbed_via && !is_absorbing)
85
+ end
86
+
87
+ def to_names
88
+ case
89
+ when is_unary
90
+ @to_role.fact_type.preferred_reading.reading_text.gsub(/\{[0-9]\}/,'').strip.split(/\s/)
91
+ when @to && !@to_role # @to is an objectified fact type so @to_role is a phantom
92
+ [@to.name]
93
+ when !@to_role # Self-value role of an independent ValueType
94
+ ["#{@from.name}Value"]
95
+ when @to_role.role_name # Named role
96
+ [@to_role.role_name]
97
+ else # Use the name from the preferred reading
98
+ role_ref = @to_role.preferred_reference
99
+ [role_ref.leading_adjective, @to_role.concept.name, role_ref.trailing_adjective].compact.map{|w| w.split(/\s/)}.flatten.reject{|s| s == ''}
100
+ end
101
+ end
102
+
103
+ # For a one-to-one (or a subtyping fact type), reverse the direction:
104
+ def flip
105
+ raise "Illegal flip of #{self}" unless @to and [:one_one, :subtype, :supertype].include?(role_type)
106
+
107
+ detabulate
108
+
109
+ if @to.absorbed_via == self
110
+ @to.absorbed_via = nil
111
+ @from.absorbed_via = self
112
+ end
113
+
114
+ # Flip the reference
115
+ @to, @from = @from, @to
116
+ @to_role, @from_role = @from_role, @to_role
117
+
118
+ tabulate
119
+ end
120
+
121
+ def tabulate
122
+ # Add to @to and @from's reference lists
123
+ @from.references_from << self
124
+ @to.references_to << self if @to # Guard against self-values
125
+
126
+ debug :references, "Adding #{to_s}"
127
+ self
128
+ end
129
+
130
+ def detabulate
131
+ # Remove from @to and @from's reference lists if present
132
+ return unless @from.references_from.delete(self)
133
+ @to.references_to.delete self if @to # Guard against self-values
134
+ debug :references, "Dropping #{to_s}"
135
+ self
136
+ end
137
+
138
+ def to_s
139
+ "reference from #{@from.name}#{@to ? " to #{@to.name}" : ""}" + (@fact_type ? " in '#{@fact_type.default_reading}'" : "")
140
+ end
141
+
142
+ def reading
143
+ is_self_value ? "#{from.name} has value" : @fact_type.default_reading
144
+ end
145
+
146
+ def inspect; to_s; end
147
+ end
148
+
149
+ class Concept
150
+ # Say whether the independence of this object is still under consideration
151
+ # This is used in detecting dependency cycles, such as occurs in the Metamodel
152
+ attr_accessor :tentative
153
+ attr_writer :is_table # The two Concept subclasses provide the reader
154
+
155
+ def show_tabular
156
+ (tentative ? "tentatively " : "") +
157
+ (is_table ? "" : "not ")+"a table"
158
+ end
159
+
160
+ def definitely_table
161
+ @is_table = true
162
+ @tentative = false
163
+ end
164
+
165
+ def definitely_not_table
166
+ @is_table = false
167
+ @tentative = false
168
+ end
169
+
170
+ def probably_table
171
+ @is_table = true
172
+ @tentative = true
173
+ end
174
+
175
+ def probably_not_table
176
+ @is_table = false
177
+ @tentative = true
178
+ end
179
+
180
+ def references_from
181
+ @references_from ||= []
182
+ end
183
+
184
+ def references_to
185
+ @references_to ||= []
186
+ end
187
+
188
+ def has_references
189
+ @references_from || @references_to
190
+ end
191
+
192
+ def clear_references
193
+ # Clear any previous references:
194
+ @references_to = nil
195
+ @references_from = nil
196
+ end
197
+
198
+ def populate_references
199
+ all_role.each do |role|
200
+ populate_reference role
201
+ end
202
+ end
203
+
204
+ def populate_reference role
205
+ role_type = role.role_type
206
+ debug :references, "#{name} has #{role_type} role in '#{role.fact_type.describe}'"
207
+ case role_type
208
+ when :many_one
209
+ Reference.new(self, role).tabulate # A simple reference
210
+
211
+ when :one_many
212
+ if role.fact_type.entity_type == self # A Role of this objectified FactType
213
+ Reference.new(self, role).tabulate # A simple reference; check that
214
+ else
215
+ # Can't absorb many of these into one of those
216
+ #debug :references, "Ignoring #{role_type} reference from #{name} to #{Reference.new(self, role).to.name}"
217
+ end
218
+
219
+ when :unary
220
+ Reference.new(self, role).tabulate # A simple reference
221
+
222
+ when :supertype # A subtype absorbs a reference to its supertype when separate, or all when partitioned
223
+ # REVISIT: Or when partitioned
224
+ if role.fact_type.subtype.is_independent
225
+ debug :references, "supertype #{name} doesn't absorb a reference to separate subtype #{role.fact_type.subtype.name}"
226
+ else
227
+ r = Reference.new(self, role)
228
+ r.to.absorbed_via = r
229
+ debug :references, "supertype #{name} absorbs subtype #{r.to.name}"
230
+ r.tabulate
231
+ end
232
+
233
+ when :subtype # This object is a supertype, which can absorb the subtype unless that's independent
234
+ if is_independent # REVISIT: Or when partitioned
235
+ Reference.new(self, role).tabulate
236
+ # If partitioned, the supertype is absorbed into *each* subtype; a reference to the supertype needs to know which
237
+ else
238
+ # debug :references, "subtype #{name} is absorbed into #{role.fact_type.supertype.name}"
239
+ end
240
+
241
+ when :one_one
242
+ r = Reference.new(self, role)
243
+
244
+ # Decide which way the one-to-one is likely to go; it will be flipped later if necessary.
245
+ # Force the decision if just one is independent:
246
+ r.tabulate and return if is_independent and !r.to.is_independent
247
+ return if !is_independent and r.to.is_independent
248
+
249
+ if is_a?(ValueType)
250
+ # Never absorb an entity type into a value type
251
+ return if r.to.is_a?(EntityType) # Don't tabulate it
252
+ else
253
+ if r.to.is_a?(ValueType)
254
+ r.tabulate # Always absorb a value type into an entity type
255
+ return
256
+ end
257
+
258
+ # Force the decision if one EntityType identifies another:
259
+ if preferred_identifier.role_sequence.all_role_ref.detect{|rr| rr.role == r.to_role}
260
+ debug :references, "EntityType #{name} is identified by EntityType #{r.to.name}, so gets absorbed elsewhere"
261
+ return
262
+ end
263
+ if r.to.preferred_identifier.role_sequence.all_role_ref.detect{|rr| rr.role == role}
264
+ debug :references, "EntityType #{name} identifies EntityType #{r.to.name}, so absorbs it"
265
+ r.to.absorbed_via = r
266
+ r.tabulate
267
+ return
268
+ end
269
+ end
270
+
271
+ # Either both EntityTypes, or both ValueTypes.
272
+ # Make an arbitrary (but stable) decision which way to go. We might flip it later.
273
+ unless r.from.name < r.to.name or
274
+ (r.from == r.to && references_to.detect{|ref| ref.to_role == role}) # one-to-one self reference, done already
275
+ r.tabulate
276
+ end
277
+ else
278
+ raise "Illegal role type, #{role.fact_type.describe(role)} no uniqueness constraint"
279
+ end
280
+ end
281
+ end
282
+
283
+ class EntityType
284
+ def populate_references
285
+ if fact_type && fact_type.all_role.size > 1
286
+ # NOT: fact_type.all_role.each do |role| # Place roles in the preferred order instead:
287
+ fact_type.preferred_reading.role_sequence.all_role_ref.map(&:role).each do |role|
288
+ populate_reference role # Objectified fact role, handled specially
289
+ end
290
+ end
291
+ super
292
+ end
293
+ end
294
+
295
+ class Vocabulary
296
+ def populate_all_references
297
+ debug :references, "Populating all concept references" do
298
+ all_feature.each do |feature|
299
+ next unless feature.is_a? Concept
300
+ feature.clear_references
301
+ feature.is_table = nil # Undecided; force an attempt to decide
302
+ feature.tentative = true # Uncertain
303
+ end
304
+ all_feature.each do |feature|
305
+ next unless feature.is_a? Concept
306
+ debug :references, "Populating references for #{feature.name}" do
307
+ feature.populate_references
308
+ end
309
+ end
310
+ end
311
+ debug :references, "Finished concept references" do
312
+ all_feature.each do |feature|
313
+ next unless feature.is_a? Concept
314
+ next unless feature.references_from.size > 0
315
+ debug :references, "#{feature.name}:" do
316
+ feature.references_from.each do |ref|
317
+ debug :references, "#{ref}"
318
+ end
319
+ end
320
+ end
321
+ end
322
+ end
323
+ end
324
+
325
+ end
326
+ end