Class: FactDb::Models::Fact
- Inherits:
-
ActiveRecord::Base
- Object
- ActiveRecord::Base
- FactDb::Models::Fact
- Defined in:
- lib/fact_db/models/fact.rb
Overview
Represents a temporal fact in the database
Facts are the core data structure in FactDb, representing statements with temporal validity (valid_at/invalid_at), entity mentions, and source provenance. Facts can be canonical, superseded, or synthesized from other facts.
Constant Summary collapse
- STATUSES =
Returns valid fact statuses.
%w[canonical superseded corroborated synthesized].freeze
- EXTRACTION_METHODS =
Returns valid extraction methods.
%w[manual llm rule_based].freeze
Scopes collapse
-
#became_invalid_between(from, to) ⇒ ActiveRecord::Relation
Returns facts that became invalid within a date range.
-
#became_valid_between(from, to) ⇒ ActiveRecord::Relation
Returns facts that became valid within a date range.
-
#by_extraction_method(method) ⇒ ActiveRecord::Relation
Alias for extracted_by.
-
#canonical ⇒ ActiveRecord::Relation
Returns facts with canonical status.
-
#currently_valid ⇒ ActiveRecord::Relation
Returns facts that are currently valid (no invalid_at date).
-
#extracted_by(method) ⇒ ActiveRecord::Relation
Returns facts extracted by a specific method.
-
#high_confidence ⇒ ActiveRecord::Relation
Returns facts with confidence >= 0.9.
-
#historical ⇒ ActiveRecord::Relation
Returns facts that have been invalidated.
-
#low_confidence ⇒ ActiveRecord::Relation
Returns facts with confidence < 0.5.
-
#mentioning_entity(entity_id) ⇒ ActiveRecord::Relation
Returns facts that mention a specific entity.
-
#search_text(query) ⇒ ActiveRecord::Relation
Full-text search on fact text using PostgreSQL tsvector.
-
#superseded ⇒ ActiveRecord::Relation
Returns facts that have been superseded.
-
#synthesized ⇒ ActiveRecord::Relation
Returns facts that were synthesized from other facts.
-
#valid_at(date) ⇒ ActiveRecord::Relation
Returns facts valid at a specific point in time.
-
#valid_between(from, to) ⇒ ActiveRecord::Relation
Returns facts valid during a date range.
-
#with_role(entity_id, role) ⇒ ActiveRecord::Relation
Returns facts where an entity has a specific role.
Class Method Summary collapse
-
.nearest_neighbors(embedding, limit: 10) ⇒ ActiveRecord::Relation
Finds facts by vector similarity using pgvector.
Instance Method Summary collapse
-
#add_mention(entity:, text:, role: nil, confidence: 1.0) ⇒ FactDb::Models::EntityMention
Adds an entity mention to this fact.
-
#add_source(source:, kind: "primary", excerpt: nil, confidence: 1.0) ⇒ FactDb::Models::FactSource
Adds a source document to this fact.
-
#corroborating_facts ⇒ ActiveRecord::Relation
Returns facts that corroborate this one.
-
#currently_valid? ⇒ Boolean
Checks if the fact is currently valid.
-
#duration ⇒ ActiveSupport::Duration?
Returns the duration the fact was valid.
-
#duration_days ⇒ Integer?
Returns the duration in days the fact was valid.
-
#evidence_chain ⇒ Array<FactDb::Models::Source>
Returns the complete evidence chain back to original sources.
-
#invalidate!(at: Time.current) ⇒ Boolean
Invalidates this fact at a specific time.
-
#prove_it ⇒ Hash?
Returns the original source lines from which this fact was derived.
-
#source_facts ⇒ ActiveRecord::Relation
Returns the source facts for synthesized facts.
-
#supersede_with!(new_text, valid_at:) ⇒ FactDb::Models::Fact
Supersedes this fact with new information.
-
#superseded? ⇒ Boolean
Checks if this fact has been superseded.
-
#synthesized? ⇒ Boolean
Checks if this fact was synthesized from other facts.
-
#valid_at?(date) ⇒ Boolean
Checks if the fact was valid at a specific date.
Class Method Details
.nearest_neighbors(embedding, limit: 10) ⇒ ActiveRecord::Relation
Finds facts by vector similarity using pgvector
410 411 412 413 414 |
# File 'lib/fact_db/models/fact.rb', line 410 def self.nearest_neighbors(, limit: 10) return none unless order(Arel.sql("embedding <=> '#{}'")).limit(limit) end |
Instance Method Details
#add_mention(entity:, text:, role: nil, confidence: 1.0) ⇒ FactDb::Models::EntityMention
Adds an entity mention to this fact
255 256 257 258 259 260 |
# File 'lib/fact_db/models/fact.rb', line 255 def add_mention(entity:, text:, role: nil, confidence: 1.0) entity_mentions.find_or_create_by!(entity: entity, mention_text: text) do |m| m.mention_role = role m.confidence = confidence end end |
#add_source(source:, kind: "primary", excerpt: nil, confidence: 1.0) ⇒ FactDb::Models::FactSource
Adds a source document to this fact
269 270 271 272 273 274 275 |
# File 'lib/fact_db/models/fact.rb', line 269 def add_source(source:, kind: "primary", excerpt: nil, confidence: 1.0) fact_sources.find_or_create_by!(source: source) do |s| s.kind = kind s.excerpt = excerpt s.confidence = confidence end end |
#became_invalid_between(from, to) ⇒ ActiveRecord::Relation
Returns facts that became invalid within a date range
112 113 114 |
# File 'lib/fact_db/models/fact.rb', line 112 scope :became_invalid_between, lambda { |from, to| where(invalid_at: from..to) } |
#became_valid_between(from, to) ⇒ ActiveRecord::Relation
Returns facts that became valid within a date range
103 104 105 |
# File 'lib/fact_db/models/fact.rb', line 103 scope :became_valid_between, lambda { |from, to| where(valid_at: from..to) } |
#by_extraction_method(method) ⇒ ActiveRecord::Relation
Alias for extracted_by
153 |
# File 'lib/fact_db/models/fact.rb', line 153 scope :by_extraction_method, ->(method) { where(extraction_method: method) } |
#canonical ⇒ ActiveRecord::Relation
Returns facts with canonical status
58 |
# File 'lib/fact_db/models/fact.rb', line 58 scope :canonical, -> { where(status: "canonical") } |
#corroborating_facts ⇒ ActiveRecord::Relation
Returns facts that corroborate this one
289 290 291 292 293 |
# File 'lib/fact_db/models/fact.rb', line 289 def return Fact.none unless corroborated_by_ids.any? Fact.where(id: corroborated_by_ids) end |
#currently_valid ⇒ ActiveRecord::Relation
Returns facts that are currently valid (no invalid_at date)
73 |
# File 'lib/fact_db/models/fact.rb', line 73 scope :currently_valid, -> { where(invalid_at: nil) } |
#currently_valid? ⇒ Boolean
Checks if the fact is currently valid
170 171 172 |
# File 'lib/fact_db/models/fact.rb', line 170 def currently_valid? invalid_at.nil? end |
#duration ⇒ ActiveSupport::Duration?
Returns the duration the fact was valid
185 186 187 188 189 |
# File 'lib/fact_db/models/fact.rb', line 185 def duration return nil if invalid_at.nil? invalid_at - valid_at end |
#duration_days ⇒ Integer?
Returns the duration in days the fact was valid
194 195 196 197 198 |
# File 'lib/fact_db/models/fact.rb', line 194 def duration_days return nil if invalid_at.nil? (invalid_at.to_date - valid_at.to_date).to_i end |
#evidence_chain ⇒ Array<FactDb::Models::Source>
Returns the complete evidence chain back to original sources
Recursively traces through synthesized facts to find all original sources.
300 301 302 303 304 305 306 307 308 309 310 |
# File 'lib/fact_db/models/fact.rb', line 300 def evidence_chain evidence = sources.to_a if synthesized? && derived_from_ids.any? source_facts.each do |source_fact| evidence.concat(source_fact.evidence_chain) end end evidence.uniq end |
#extracted_by(method) ⇒ ActiveRecord::Relation
Returns facts extracted by a specific method
147 |
# File 'lib/fact_db/models/fact.rb', line 147 scope :extracted_by, ->(method) { where(extraction_method: method) } |
#high_confidence ⇒ ActiveRecord::Relation
Returns facts with confidence >= 0.9
158 |
# File 'lib/fact_db/models/fact.rb', line 158 scope :high_confidence, -> { where("confidence >= ?", 0.9) } |
#historical ⇒ ActiveRecord::Relation
Returns facts that have been invalidated
78 |
# File 'lib/fact_db/models/fact.rb', line 78 scope :historical, -> { where.not(invalid_at: nil) } |
#invalidate!(at: Time.current) ⇒ Boolean
Invalidates this fact at a specific time
218 219 220 |
# File 'lib/fact_db/models/fact.rb', line 218 def invalidate!(at: Time.current) update!(invalid_at: at) end |
#low_confidence ⇒ ActiveRecord::Relation
Returns facts with confidence < 0.5
163 |
# File 'lib/fact_db/models/fact.rb', line 163 scope :low_confidence, -> { where("confidence < ?", 0.5) } |
#mentioning_entity(entity_id) ⇒ ActiveRecord::Relation
Returns facts that mention a specific entity
120 121 122 |
# File 'lib/fact_db/models/fact.rb', line 120 scope :mentioning_entity, lambda { |entity_id| joins(:entity_mentions).where(fact_db_entity_mentions: { entity_id: entity_id }).distinct } |
#prove_it ⇒ Hash?
Returns the original source lines from which this fact was derived
Uses line metadata to extract the relevant section from the source document and highlights lines containing key terms from the fact.
328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 |
# File 'lib/fact_db/models/fact.rb', line 328 def prove_it source = fact_sources.first&.source return nil unless source&.content line_start = &.dig("line_start") line_end = &.dig("line_end") return nil unless line_start && line_end lines = source.content.lines start_idx = line_start.to_i - 1 end_idx = line_end.to_i - 1 return nil if start_idx < 0 || end_idx >= lines.length section_lines = lines[start_idx..end_idx] full_section = section_lines.join # Find focused lines by matching key terms from fact key_terms = extract_key_terms scored_lines = score_lines_by_relevance(section_lines, key_terms, start_idx) # Return lines that have at least one match, sorted by line number relevant = scored_lines.select { |l| l[:score] > 0 } .sort_by { |l| l[:line_number] } { full_section: full_section, focused_lines: relevant.map { |l| l[:text] }.join, focused_line_numbers: relevant.map { |l| l[:line_number] }, key_terms: key_terms } end |
#search_text(query) ⇒ ActiveRecord::Relation
Full-text search on fact text using PostgreSQL tsvector
139 140 141 |
# File 'lib/fact_db/models/fact.rb', line 139 scope :search_text, lambda { |query| where("to_tsvector('english', text) @@ plainto_tsquery('english', ?)", query) } |
#source_facts ⇒ ActiveRecord::Relation
Returns the source facts for synthesized facts
280 281 282 283 284 |
# File 'lib/fact_db/models/fact.rb', line 280 def source_facts return Fact.none unless derived_from_ids.any? Fact.where(id: derived_from_ids) end |
#supersede_with!(new_text, valid_at:) ⇒ FactDb::Models::Fact
Supersedes this fact with new information
Creates a new canonical fact and marks this one as superseded.
229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 |
# File 'lib/fact_db/models/fact.rb', line 229 def supersede_with!(new_text, valid_at:) transaction do new_fact = self.class.create!( text: new_text, valid_at: valid_at, status: "canonical", extraction_method: extraction_method ) update!( status: "superseded", superseded_by_id: new_fact.id, invalid_at: valid_at ) new_fact end end |
#superseded ⇒ ActiveRecord::Relation
Returns facts that have been superseded
63 |
# File 'lib/fact_db/models/fact.rb', line 63 scope :superseded, -> { where(status: "superseded") } |
#superseded? ⇒ Boolean
Checks if this fact has been superseded
203 204 205 |
# File 'lib/fact_db/models/fact.rb', line 203 def superseded? status == "superseded" end |
#synthesized ⇒ ActiveRecord::Relation
Returns facts that were synthesized from other facts
68 |
# File 'lib/fact_db/models/fact.rb', line 68 scope :synthesized, -> { where(status: "synthesized") } |
#synthesized? ⇒ Boolean
Checks if this fact was synthesized from other facts
210 211 212 |
# File 'lib/fact_db/models/fact.rb', line 210 def synthesized? status == "synthesized" end |
#valid_at(date) ⇒ ActiveRecord::Relation
Returns facts valid at a specific point in time
84 85 86 87 |
# File 'lib/fact_db/models/fact.rb', line 84 scope :valid_at, lambda { |date| where("valid_at <= ?", date) .where("invalid_at > ? OR invalid_at IS NULL", date) } |
#valid_at?(date) ⇒ Boolean
Checks if the fact was valid at a specific date
178 179 180 |
# File 'lib/fact_db/models/fact.rb', line 178 def valid_at?(date) valid_at <= date && (invalid_at.nil? || invalid_at > date) end |
#valid_between(from, to) ⇒ ActiveRecord::Relation
Returns facts valid during a date range
94 95 96 |
# File 'lib/fact_db/models/fact.rb', line 94 scope :valid_between, lambda { |from, to| where("valid_at <= ? AND (invalid_at > ? OR invalid_at IS NULL)", to, from) } |
#with_role(entity_id, role) ⇒ ActiveRecord::Relation
Returns facts where an entity has a specific role
129 130 131 132 133 |
# File 'lib/fact_db/models/fact.rb', line 129 scope :with_role, lambda { |entity_id, role| joins(:entity_mentions).where( fact_db_entity_mentions: { entity_id: entity_id, mention_role: role } ).distinct } |