Current section
Files
Jump to
Current section
Files
lib/cairnloop/knowledge_automation/stale_article_signal.ex
defmodule Cairnloop.KnowledgeAutomation.StaleArticleSignal do
alias Cairnloop.KnowledgeAutomation.ArticleSuggestionEvidence
defstruct ready?: false,
reason: :insufficient_signals,
signal_count: 0,
fresh_canonical_snapshot?: false,
matching_events: []
@window_days 30
@minimum_signals 2
@eligible_reasons [:canonical_insufficient_detail, :clarification_limit_reached]
@eligible_outcomes [:weak_grounding, :policy_limit]
def build_revision_gate(article_id, base_revision_id, opts \\ []) do
now = Keyword.get(opts, :now_fn, &DateTime.utc_now/0).()
gap_events = Keyword.get(opts, :gap_events, [])
grounding_bundle = Keyword.get(opts, :grounding_bundle, %{})
matching_events =
gap_events
|> List.wrap()
|> Enum.filter(&recent_enough?(&1, now))
|> Enum.filter(&eligible_failure_signal?/1)
|> Enum.filter(&article_linked?(&1, article_id, base_revision_id))
fresh_canonical_snapshot? =
grounding_bundle
|> canonical_results()
|> Enum.any?(&article_linked_result?(&1, article_id, base_revision_id))
signal_count = length(matching_events)
{ready?, reason} =
cond do
signal_count < @minimum_signals -> {false, :insufficient_signals}
not fresh_canonical_snapshot? -> {false, :missing_fresh_canonical_snapshot}
true -> {true, :ready}
end
%__MODULE__{
ready?: ready?,
reason: reason,
signal_count: signal_count,
fresh_canonical_snapshot?: fresh_canonical_snapshot?,
matching_events: matching_events
}
end
defp recent_enough?(event, now) do
occurred_at = map_value(event, :occurred_at)
case occurred_at do
%DateTime{} = value ->
DateTime.diff(now, value, :day) <= @window_days
%NaiveDateTime{} = value ->
DateTime.diff(now, DateTime.from_naive!(value, "Etc/UTC"), :day) <= @window_days
_ ->
false
end
end
defp eligible_failure_signal?(event) do
reason = map_value(event, :reason)
outcome_class = map_value(event, :outcome_class)
clarification_attempts = map_value(event, :clarification_attempts) || 0
outcome_class in @eligible_outcomes or reason in @eligible_reasons or
clarification_attempts > 0
end
defp article_linked?(event, article_id, base_revision_id) do
event
|> map_value(:attempted_evidence_snapshots)
|> List.wrap()
|> Enum.any?(fn snapshot ->
canonical_anchor_snapshot?(snapshot) and
article_linked_result?(snapshot, article_id, base_revision_id)
end)
end
defp canonical_results(nil), do: []
defp canonical_results(bundle) do
Map.get(bundle, :canonical_results) || Map.get(bundle, "canonical_results") || []
end
defp article_linked_result?(result, article_id, base_revision_id) do
citation_target =
map_value(result, :citation_target) ||
%{
article_id: map_value(result, :article_id),
revision_id: map_value(result, :revision_id)
}
map_value(citation_target, :article_id) == article_id and
map_value(citation_target, :revision_id) == base_revision_id
end
defp canonical_anchor_snapshot?(%ArticleSuggestionEvidence{} = evidence) do
evidence.source_type == :knowledge_base and evidence.trust_level == :canonical and
article_linked_result?(
evidence,
map_value(evidence.citation_target, :article_id),
map_value(evidence.citation_target, :revision_id)
)
end
defp canonical_anchor_snapshot?(snapshot) do
source_type = map_value(snapshot, :source_type)
trust_level = map_value(snapshot, :trust_level)
citation_target = map_value(snapshot, :citation_target)
source_type in [:knowledge_base, "knowledge_base"] and
trust_level in [:canonical, "canonical"] and is_map(citation_target) and
not is_nil(map_value(citation_target, :article_id)) and
not is_nil(map_value(citation_target, :revision_id))
end
defp map_value(map, key) when is_map(map) do
Map.get(map, key) || Map.get(map, Atom.to_string(key))
end
defp map_value(_, _), do: nil
end