Packages
fnord
0.9.32
0.9.40
0.9.39
0.9.38
0.9.37
0.9.36
0.9.35
0.9.34
0.9.33
0.9.32
0.9.31
0.9.30
0.9.29
0.9.28
0.9.27
0.9.26
0.9.25
0.9.24
0.9.23
0.9.22
0.9.21
0.9.20
0.9.19
0.9.18
0.9.17
0.9.16
0.9.15
0.9.14
0.9.13
0.9.12
0.9.11
0.9.10
0.9.9
0.9.8
0.9.7
0.9.6
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.99
0.8.98
0.8.97
0.8.96
0.8.95
0.8.94
0.8.93
0.8.92
0.8.91
0.8.90
0.8.89
0.8.88
0.8.87
0.8.86
0.8.85
0.8.84
0.8.83
0.8.82
0.8.81
0.8.80
0.8.79
0.8.78
0.8.77
0.8.76
0.8.75
0.8.74
0.8.73
0.8.72
0.8.71
0.8.70
0.8.69
0.8.68
0.8.67
0.8.66
0.8.65
0.8.64
0.8.63
0.8.62
0.8.61
0.8.60
0.8.59
0.8.58
0.8.57
0.8.56
0.8.55
0.8.54
0.8.53
0.8.52
0.8.51
0.8.50
0.8.49
0.8.48
0.8.47
0.8.46
0.8.45
0.8.44
0.8.43
0.8.42
0.8.41
0.8.40
0.8.39
0.8.38
0.8.37
0.8.36
0.8.35
0.8.34
0.8.33
0.8.32
0.8.31
0.8.30
0.8.29
0.8.27
0.8.26
0.8.25
0.8.24
0.8.23
0.8.22
0.8.21
0.8.20
0.8.19
0.8.18
0.8.17
0.8.16
0.8.15
0.8.14
0.8.13
0.8.12
0.8.11
0.8.1
0.8.0
0.7.24
0.7.23
0.7.22
0.7.21
0.7.20
0.7.19
0.7.18
0.7.17
0.7.16
0.7.15
0.7.14
0.7.13
0.7.12
0.7.11
0.7.10
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.3
0.7.2
0.7.1
0.7.0
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.1
0.6.0
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.44
0.4.43
0.4.42
0.4.41
0.4.40
0.4.39
0.4.38
0.4.37
0.4.36
0.4.35
0.4.34
0.4.33
0.4.32
0.4.30
0.4.29
0.4.28
0.4.27
0.4.26
0.4.25
0.4.24
0.4.23
0.4.22
0.4.21
0.4.20
0.4.19
0.4.18
0.4.17
0.4.16
0.4.15
0.4.14
0.4.13
0.4.12
0.4.11
0.4.10
0.4.9
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.0
0.2.0
0.1.0
AI code archaeology
Current section
Files
Jump to
Current section
Files
lib/memory/scope_policy.ex
defmodule Memory.ScopePolicy do
@moduledoc """
Defines long-term scope rules for memories.
This module decides whether a memory may live in a given long-term scope and
whether automatic scope transitions are allowed. The indexer and consolidator
both consult these rules so that scope decisions follow the same policy.
"""
@reserved_global_titles ["Me"]
@long_term_scopes [:global, :project]
@automatic_move_min_signal_count 2
@doc """
Returns true when the title identifies a reserved long-term memory.
"""
@spec reserved_title?(String.t()) :: boolean()
def reserved_title?(title) when is_binary(title) do
title in @reserved_global_titles
end
@doc """
Returns true when the memory is reserved from automatic scope moves.
"""
@spec reserved_from_automatic_move?(Memory.t()) :: boolean()
def reserved_from_automatic_move?(%Memory{title: title}) do
reserved_title?(title)
end
@doc """
Returns the allowed long-term scopes for the given title.
Reserved titles may be pinned to a single scope, while ordinary memories may
live in either long-term scope.
"""
@spec allowed_scopes_for_title(String.t()) :: [Memory.scope()]
def allowed_scopes_for_title(title) when is_binary(title) do
case title do
"Me" -> [:global]
_ -> @long_term_scopes
end
end
@doc """
Returns true when the memory is allowed to live in the target scope.
"""
@spec valid_target_scope?(Memory.t(), Memory.scope()) :: boolean()
def valid_target_scope?(%Memory{title: title}, target_scope) do
target_scope in allowed_scopes_for_title(title)
end
@doc """
Returns true when the memory has enough project-specific evidence to be
considered for an automatic move from global to project scope.
"""
@spec automatic_move_candidate?(Memory.t()) :: boolean()
def automatic_move_candidate?(%Memory{} = memory) do
case {global_scope?(memory), reserved_from_automatic_move?(memory)} do
{true, false} ->
count_project_signals(memory) >= @automatic_move_min_signal_count
_ ->
false
end
end
@doc """
Returns the number of project-specific signals present across title, content,
and topics.
"""
@spec count_project_signals(Memory.t()) :: non_neg_integer()
def count_project_signals(%Memory{} = memory) do
[
project_signal_in_text?(memory.title),
project_signal_in_text?(memory.content),
project_signal_in_topics?(memory.topics)
]
|> Enum.count(& &1)
end
@doc """
Returns true when the memory may be moved automatically from one scope to
another.
Automatic scope moves are narrower than general scope validity. A memory may
be valid in a scope without being eligible for an unattended move.
"""
@spec allow_automatic_move?(Memory.t(), Memory.scope()) :: boolean()
def allow_automatic_move?(%Memory{} = memory, target_scope) do
case {memory.scope, target_scope, valid_target_scope?(memory, target_scope)} do
{:global, :project, true} -> automatic_move_candidate?(memory)
_ -> false
end
end
@doc """
Returns :ok when the memory is allowed in the target long-term scope.
Returns {:error, :project_scope_not_allowed} when the memory is not allowed
in the requested long-term scope and {:error, :invalid_scope} when the scope
value cannot be normalized.
"""
@spec validate_scope(Memory.t(), String.t() | atom()) ::
:ok | {:error, :project_scope_not_allowed} | {:error, :invalid_scope}
def validate_scope(%Memory{title: title}, scope) do
case normalize_scope(scope) do
{:ok, scope_atom} ->
allowed_scopes = allowed_scopes_for_title(title)
case Enum.member?(allowed_scopes, scope_atom) do
true -> :ok
false -> {:error, :project_scope_not_allowed}
end
:error ->
{:error, :invalid_scope}
end
end
@doc """
Returns true when the target title and scope form a valid long-term target.
Non-binary titles return false.
"""
@spec valid_long_term_target?(String.t(), String.t() | atom()) :: boolean()
def valid_long_term_target?(title, scope) when is_binary(title) do
case normalize_scope(scope) do
{:ok, scope_atom} -> scope_atom in allowed_scopes_for_title(title)
:error -> false
end
end
def valid_long_term_target?(_, _), do: false
defp global_scope?(%Memory{scope: :global}), do: true
defp global_scope?(%Memory{}), do: false
defp project_signal_in_topics?(topics) when is_list(topics) do
Enum.any?(topics, &project_signal_in_text?/1)
end
defp project_signal_in_topics?(_), do: false
defp project_signal_in_text?(text) when is_binary(text) do
downcased_text = String.downcase(text)
path_signal?(downcased_text) or
workflow_term_signal?(downcased_text) or
code_identifier_signal?(text, downcased_text) or
scope_token_signal?(downcased_text)
end
defp project_signal_in_text?(_), do: false
defp path_signal?(text) do
Enum.any?(["lib/", "test/", "config/", "docs/", "scratch/"], &String.contains?(text, &1))
end
defp workflow_term_signal?(text) do
Regex.match?(~r/\bmix\s+/i, text) or
Regex.match?(~r/\bfnord\s+/i, text) or
Regex.match?(~r/\b(branch|ticket|deploy|workflow|module|component)\b/i, text)
end
defp code_identifier_signal?(original_text, downcased_text) do
String.contains?(original_text, "`") or
Regex.match?(
~r/\b[A-Z][A-Za-z0-9_]*(?:\.[A-Z][A-Za-z0-9_]*)+(?:\.[a-z_][A-Za-z0-9_]*[!?]?)?\b/,
original_text
) or
Regex.match?(~r/\b[a-z_][a-z0-9_]*\.[a-z_][a-z0-9_]*[!?]?\b/, downcased_text)
end
defp scope_token_signal?(text) do
Regex.match?(~r/\b(consolidator|project|global|session)\b/i, text) or
Regex.match?(
~r/\bmemory\b\s*(?:scope|policy|store|index(?:er)?|consolidator|topic|topics|title|content)s?\b/i,
text
) or
Regex.match?(~r/\bcmd\b\s*[:\/]/i, text) or
Regex.match?(~r/\bfile_store\b/i, text)
end
@doc """
Normalizes a long-term scope value into an atom.
"""
@spec normalize_scope(String.t() | atom()) :: {:ok, Memory.scope()} | :error
def normalize_scope(scope) do
case scope do
:global -> {:ok, :global}
:project -> {:ok, :project}
"global" -> {:ok, :global}
"project" -> {:ok, :project}
_ -> :error
end
end
end