mps 0.5.0 → 1.0.1

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.
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MPS
4
+ # Renders an elements hash to a string.
5
+ # Has no dependency on Thor; receives a colorize proc for terminal output.
6
+ #
7
+ # Usage (in CLI):
8
+ # p = Presenter.new(elements, color_fn: method(:set_color), resolver: resolver)
9
+ # puts p.render_tree
10
+ class Presenter
11
+ TYPE_COLORS = {
12
+ "task" => :green,
13
+ "note" => :cyan,
14
+ "reminder" => :magenta,
15
+ "log" => :yellow
16
+ }.freeze
17
+
18
+ ANSI_CODES = {
19
+ green: "\e[32m", cyan: "\e[36m", magenta: "\e[35m",
20
+ yellow: "\e[33m", white: "\e[37m", reset: "\e[0m"
21
+ }.freeze
22
+
23
+ # @param elements_hash [Hash] ref => element, as returned by Store#parse_date
24
+ # @param color_fn [Proc, nil] set_color(text, color) — nil disables color
25
+ # @param resolver [RefResolver, nil]
26
+ # @param with_refs [Boolean] prefix each line with human ref
27
+ def initialize(elements_hash, color_fn: nil, resolver: nil, with_refs: false)
28
+ @elements = elements_hash
29
+ @color_fn = color_fn || method(:_ansi_color)
30
+ @resolver = resolver
31
+ @with_refs = with_refs
32
+ end
33
+
34
+ # Renders elements as an indented tree. @mps containers shown as group headers.
35
+ # Returns the number of non-MPS elements printed, or just the rendered string
36
+ # if called for its side-effect-free return value.
37
+ def render_tree
38
+ sorted = @elements.sort_by { |k, _| k.split(".").map(&:to_i) }
39
+ return ["", 0] if sorted.empty?
40
+
41
+ root_segs = sorted.first.first.split(".").size
42
+ lines = []
43
+ shown = 0
44
+
45
+ sorted.each do |ref_key, el|
46
+ depth = ref_key.split(".").size - root_segs - 1
47
+ next if depth < 0
48
+
49
+ if el.is_a?(::MPS::Elements::MPS)
50
+ prefix = "#{ref_key}."
51
+ any_visible = @elements.any? { |k, v| k.start_with?(prefix) && !v.is_a?(::MPS::Elements::MPS) }
52
+ next unless any_visible
53
+ indent = " " * (depth + 1)
54
+ human = @resolver&.to_human(ref_key) || ref_key
55
+ ref_col = @with_refs ? "#{_colorize(human.ljust(12), :white)} " : ""
56
+ lines << "#{indent}#{ref_col}#{_colorize("[@mps]", :white)}"
57
+ else
58
+ indent = " " * (depth + 1)
59
+ human = @resolver&.to_human(ref_key) || ref_key
60
+ ref_col = @with_refs ? "#{_colorize(human.ljust(12), :white)} " : ""
61
+ lines << "#{indent}#{ref_col}#{_element_line(el)}"
62
+ shown += 1
63
+ end
64
+ end
65
+
66
+ [lines.join("\n"), shown]
67
+ end
68
+
69
+ # Renders a single element as a terminal line (no indentation).
70
+ def render_element(el, depth: 0)
71
+ indent = " " * (depth + 1)
72
+ "#{indent}#{_element_line(el)}"
73
+ end
74
+
75
+ # Returns a tag-frequency table as a string.
76
+ # elements_hash should already be filtered (MPS containers excluded).
77
+ def render_tag_table
78
+ counts = Hash.new(0)
79
+ @elements.each_value do |el|
80
+ next if el.is_a?(::MPS::Elements::MPS)
81
+ el.tags.each { |t| counts[t] += 1 }
82
+ end
83
+ return _colorize("(no tags found)", :yellow) if counts.empty?
84
+ counts.sort_by { |_, v| -v }
85
+ .map { |tag, n| " #{_colorize(tag, :white)} (#{n})" }
86
+ .join("\n")
87
+ end
88
+
89
+ private
90
+
91
+ def _element_line(el)
92
+ type_name = el.class::SIGNATURE_STAMP
93
+ badge = _colorize("[#{type_name}]", TYPE_COLORS.fetch(type_name, :white))
94
+ extra = _element_extra(el)
95
+ body_line = el.body_str.strip.lines.first&.strip
96
+ tags_str = el.tags.empty? ? "" : " #{_colorize("[#{el.tags.join(', ')}]", :white)}"
97
+ "#{badge} #{extra}#{body_line}#{tags_str}"
98
+ end
99
+
100
+ def _element_extra(el)
101
+ case el
102
+ when ::MPS::Elements::Task
103
+ status = el.parsed_args[:status] || "open"
104
+ color = status == "done" ? :green : :yellow
105
+ "(#{_colorize(status, color)}) "
106
+ when ::MPS::Elements::Log
107
+ dur = el.duration_str
108
+ dur ? "(#{_colorize(dur, :yellow)}) " : ""
109
+ when ::MPS::Elements::Reminder
110
+ at = el.parsed_args[:at]
111
+ at ? "(#{_colorize(at, :magenta)}) " : ""
112
+ else
113
+ ""
114
+ end
115
+ end
116
+
117
+ def _colorize(text, color)
118
+ @color_fn.call(text, color)
119
+ end
120
+
121
+ # Fallback colorizer using ANSI codes when no Thor color_fn is provided.
122
+ def _ansi_color(text, color)
123
+ code = ANSI_CODES[color]
124
+ return text unless code
125
+ "#{code}#{text}#{ANSI_CODES[:reset]}"
126
+ end
127
+ end
128
+ end
data/lib/mps/query.rb ADDED
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MPS
4
+ # Encapsulates filter predicates derived from CLI options.
5
+ # Has no dependency on Thor; fully unit-testable.
6
+ #
7
+ # Usage:
8
+ # q = Query.new(type: "task", status: "open", tag: "work")
9
+ # filtered_hash = q.apply(elements_hash)
10
+ class Query
11
+ def initialize(opts = {})
12
+ @type_filter = opts[:type]&.downcase
13
+ @tag_filter = opts[:tag]
14
+ # Collect schema-driven attribute filters from all element classes.
15
+ @attr_filters = _collect_attr_filters(opts)
16
+ end
17
+
18
+ # Returns a new hash containing only elements that pass all active filters.
19
+ # @mps container elements are always excluded.
20
+ def apply(elements_hash)
21
+ elements_hash.select { |_, el| !el.is_a?(::MPS::Elements::MPS) && _match?(el) }
22
+ end
23
+
24
+ # Applies filters while preserving the full tree structure including @mps
25
+ # containers. Used by the presenter's tree renderer so group headers are
26
+ # correctly suppressed when all their children are filtered out.
27
+ def apply_for_tree(elements_hash)
28
+ elements_hash.select do |ref_key, el|
29
+ if el.is_a?(::MPS::Elements::MPS)
30
+ prefix = "#{ref_key}."
31
+ elements_hash.any? { |k, v| k.start_with?(prefix) && !v.is_a?(::MPS::Elements::MPS) && _match?(v) }
32
+ else
33
+ _match?(el)
34
+ end
35
+ end
36
+ end
37
+
38
+ def match?(el)
39
+ _match?(el)
40
+ end
41
+
42
+ private
43
+
44
+ def _match?(el)
45
+ return false if el.is_a?(::MPS::Engines::Parser::Unknown)
46
+ return false if @type_filter && el.class::SIGNATURE_STAMP != @type_filter
47
+ return false if @tag_filter && !el.tags.include?(@tag_filter)
48
+
49
+ @attr_filters.each do |name, filter_val|
50
+ el_val = el.parsed_args[name]
51
+ return false if el_val.nil? || el_val.to_s != filter_val
52
+ end
53
+ true
54
+ end
55
+
56
+ def _collect_attr_filters(opts)
57
+ filters = {}
58
+ ::MPS::Elements.constants
59
+ .map { |k| ::MPS::Elements.const_get(k) }
60
+ .select { |x| x.class == Class }
61
+ .each do |klass|
62
+ klass.schema.each do |name, defn|
63
+ next unless defn[:flag]
64
+ flag_sym = defn[:flag].tr("-", "_").to_sym
65
+ filters[name] = opts[flag_sym].to_s if opts[flag_sym]
66
+ end
67
+ end
68
+ filters
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MPS
4
+ # Translates between epoch ref-paths (e.g. "20260428.2.1") and
5
+ # human-readable refs (e.g. "note-1.1").
6
+ #
7
+ # Human ref format:
8
+ # Top-level non-MPS: {type}-{n} where n counts per type across top-level
9
+ # First-level nested: {parent_human}.{m} where m is the child's sequential index
10
+ # Deeper nesting: same pattern extended (type-1.2.1, etc.)
11
+ # @mps containers: mps-{n}
12
+ class RefResolver
13
+ def initialize(elements_hash)
14
+ @epoch_to_human = {}
15
+ @human_to_epoch = {}
16
+ _build_maps(elements_hash)
17
+ end
18
+
19
+ # Returns the human ref for +epoch_ref+, or nil if not mapped.
20
+ def to_human(epoch_ref)
21
+ @epoch_to_human[epoch_ref]
22
+ end
23
+
24
+ # Returns the epoch ref for +human_ref+, or nil if not mapped.
25
+ def to_epoch(human_ref)
26
+ @human_to_epoch[human_ref]
27
+ end
28
+
29
+ # Resolves either form. Human refs are translated to epoch; epoch refs
30
+ # are returned as-is (validated to exist in the mapping).
31
+ def resolve(ref_str)
32
+ return @human_to_epoch[ref_str] if @human_to_epoch.key?(ref_str)
33
+ return ref_str if @epoch_to_human.key?(ref_str)
34
+ nil
35
+ end
36
+
37
+ # Returns all mapped epoch refs sorted by document order.
38
+ def all_epoch_refs
39
+ @epoch_to_human.keys
40
+ end
41
+
42
+ private
43
+
44
+ def _build_maps(elements_hash)
45
+ return if elements_hash.empty?
46
+
47
+ sorted = elements_hash.sort_by { |k, _| k.split(".").map(&:to_i) }
48
+ root_ref = sorted.first.first
49
+ root_depth = root_ref.split(".").size # always 1 (epoch only)
50
+
51
+ type_counters = Hash.new(0)
52
+
53
+ sorted.each do |epoch_ref, el|
54
+ depth = epoch_ref.split(".").size - root_depth # 0=root, 1=top-level, 2+=nested
55
+
56
+ next if depth <= 0 # skip synthetic root wrapper
57
+
58
+ if depth == 1
59
+ next if el.is_a?(Engines::Parser::Unknown)
60
+ type_name = el.class::SIGNATURE_STAMP
61
+ type_counters[type_name] += 1
62
+ human = "#{type_name}-#{type_counters[type_name]}"
63
+ else
64
+ parts = epoch_ref.split(".")
65
+ parent_epoch = parts[0..-2].join(".")
66
+ child_idx = parts.last
67
+ parent_human = @epoch_to_human[parent_epoch]
68
+ next unless parent_human
69
+ human = "#{parent_human}.#{child_idx}"
70
+ end
71
+
72
+ @epoch_to_human[epoch_ref] = human
73
+ @human_to_epoch[human] = epoch_ref
74
+ end
75
+ end
76
+ end
77
+ end
data/lib/mps/store.rb CHANGED
@@ -3,10 +3,13 @@
3
3
  module MPS
4
4
  class Store
5
5
  def initialize(storage_dir)
6
- @storage_dir = storage_dir
7
- @element_classes = Elements.constants
6
+ @storage_dir = storage_dir
7
+ @element_classes = Elements.constants
8
8
  .map { |k| Elements.const_get(k) }
9
9
  .select { |x| x.class == Class }
10
+ @interpolator_classes = Interpolators.constants
11
+ .map { |k| Interpolators.const_get(k) }
12
+ .select { |x| x.class == Class }
10
13
  end
11
14
 
12
15
  # First .mps file found for +date+, or nil.
@@ -31,7 +34,14 @@ module MPS
31
34
  def parse_date(date)
32
35
  path = find_file(date)
33
36
  return {} unless path
34
- Engines::Parser.parse_mps_file_to_elements_hash(path, @element_classes)
37
+ Engines::Parser.parse_mps_file_to_elements_hash(
38
+ path, @element_classes, interpolator_classes: @interpolator_classes
39
+ )
40
+ end
41
+
42
+ # Returns a RefResolver built from the parsed elements for +date+.
43
+ def resolver_for(date)
44
+ RefResolver.new(parse_date(date))
35
45
  end
36
46
 
37
47
  # Appends a new element to today's (or +date+'s) file. Returns the file path.
@@ -57,19 +67,98 @@ module MPS
57
67
  end
58
68
 
59
69
  # Full-text search across files. Returns [{element:, file:, date_str:}].
60
- # +since_date+ is a Date; +type_filter+ and +tag_filter+ are strings.
61
70
  def search(query, type_filter: nil, tag_filter: nil, since_date: nil)
62
71
  files = since_date ? files_since(since_date) : all_files
63
72
  files.flat_map do |file|
64
73
  date_str = File.basename(file).slice(0, 8)
65
- Engines::Parser.parse_mps_file_to_elements_hash(file, @element_classes)
74
+ Engines::Parser.parse_mps_file_to_elements_hash(
75
+ file, @element_classes, interpolator_classes: @interpolator_classes
76
+ )
66
77
  .values
67
- .reject { |e| e.is_a?(Elements::MPS) }
78
+ .reject { |e| e.is_a?(Elements::MPS) || e.is_a?(Engines::Parser::Unknown) }
68
79
  .select { |e| type_filter.nil? || e.class::SIGNATURE_STAMP == type_filter }
69
80
  .select { |e| tag_filter.nil? || e.tags.include?(tag_filter) }
70
81
  .select { |e| query.nil? || e.body_str.downcase.include?(query.downcase) }
71
82
  .map { |e| { element: e, file: file, date_str: date_str } }
72
83
  end
73
84
  end
85
+
86
+ # Rewrites an element's args bracket in-place and saves atomically.
87
+ #
88
+ # +ref_str+ may be an epoch ref ("20260428.1") or a human ref ("task-1").
89
+ # Human refs are resolved against +date+ (defaults to today).
90
+ # +new_attrs+ is a hash of attribute_name => new_value (symbol keys).
91
+ # Returns true on success, false if element not found or file unchanged.
92
+ def rewrite_element(ref_str, new_attrs, date: Date.today)
93
+ epoch_ref, path = _resolve_ref_to_path(ref_str, date)
94
+ return false unless epoch_ref && path
95
+
96
+ elements = Engines::Parser.parse_mps_file_to_elements_hash(path, @element_classes)
97
+ el = elements[epoch_ref]
98
+ return false unless el
99
+ return false if el.is_a?(Engines::Parser::Unknown)
100
+
101
+ _rewrite_element_in_file(path, el, new_attrs)
102
+ end
103
+
104
+ private
105
+
106
+ # Returns [epoch_ref, file_path] for the given ref_str, or [nil, nil].
107
+ def _resolve_ref_to_path(ref_str, date)
108
+ if ref_str =~ /\A\d{8}\.\d/
109
+ # Epoch ref — date is encoded in the prefix (YYYYMMDD)
110
+ date_str = ref_str[0, 8]
111
+ begin
112
+ d = Date.strptime(date_str, "%Y%m%d")
113
+ rescue ArgumentError
114
+ return [nil, nil]
115
+ end
116
+ path = find_file(d)
117
+ return [nil, nil] unless path
118
+ [ref_str, path]
119
+ else
120
+ # Human ref — resolve using the given date
121
+ path = find_file(date)
122
+ return [nil, nil] unless path
123
+ elements = Engines::Parser.parse_mps_file_to_elements_hash(path, @element_classes)
124
+ resolver = RefResolver.new(elements)
125
+ epoch_ref = resolver.to_epoch(ref_str)
126
+ return [nil, nil] unless epoch_ref
127
+ [epoch_ref, path]
128
+ end
129
+ end
130
+
131
+ def _rewrite_element_in_file(path, el, new_attrs)
132
+ content = File.read(path)
133
+ type = el.class::SIGNATURE_STAMP
134
+ raw = el.raw_args.to_s
135
+
136
+ # Merge new_attrs over existing parsed_args (preserve tags)
137
+ existing_tags = el.tags.dup
138
+ merged = el.parsed_args.reject { |k, _| k == :tags }.merge(new_attrs)
139
+ new_attr_parts = merged.reject { |_, v| v.nil? }.map { |k, v| "#{k}: #{v}" }
140
+ new_args = (new_attr_parts + existing_tags).join(", ")
141
+
142
+ if raw.empty?
143
+ old_pat = /@#{Regexp.escape(type)}(?:\[\])?\s*\{/
144
+ new_open = "@#{type}[#{new_args}]{"
145
+ else
146
+ old_pat = /@#{Regexp.escape(type)}\[#{Regexp.escape(raw)}\]\s*\{/
147
+ new_open = "@#{type}[#{new_args}]{"
148
+ end
149
+
150
+ new_content = content.sub(old_pat, new_open)
151
+ return false if new_content == content
152
+
153
+ tmp = "#{path}.tmp.#{Process.pid}"
154
+ begin
155
+ File.write(tmp, new_content)
156
+ File.rename(tmp, path)
157
+ true
158
+ rescue StandardError
159
+ File.delete(tmp) if File.exist?(tmp)
160
+ false
161
+ end
162
+ end
74
163
  end
75
164
  end
data/lib/mps/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MPS
4
- VERSION = "0.5.0"
4
+ VERSION = "1.0.1"
5
5
  end
data/lib/mps.rb CHANGED
@@ -4,18 +4,20 @@ require "yaml"
4
4
  require "logger"
5
5
  require "chronic"
6
6
  require "tty-editor"
7
- require "strscan"
8
7
  require_relative "mps/version"
9
8
  require_relative "mps/mps"
9
+ require_relative "mps/constants"
10
+ require_relative "mps/config"
11
+ require_relative "mps/interpolators/interpolators"
12
+ require_relative "mps/elements/elements"
13
+ require_relative "mps/engines/engines"
14
+ require_relative "mps/ref_resolver"
15
+ require_relative "mps/query"
16
+ require_relative "mps/presenter"
17
+ require_relative "mps/store"
18
+ require_relative "cli/mps"
19
+ require_relative "mps/cli/commands"
10
20
 
11
- ir "mps/constants"
12
- ir "mps/config"
13
- ir "mps/interpolators/interpolators"
14
- ir "mps/elements/elements"
15
- ir "mps/engines/engines"
16
- ir "mps/store"
17
- ir "cli/mps"
18
21
  module MPS
19
22
  class Error < StandardError; end
20
- # Your code goes here...
21
23
  end
data/mps.gemspec CHANGED
@@ -8,19 +8,20 @@ Gem::Specification.new do |spec|
8
8
  spec.authors = ["mash-97"]
9
9
  spec.email = ["itzmashz@gmail.com"]
10
10
 
11
- spec.summary = "MPS (MonoPsyches)"
12
- spec.description = "Manage MonoPsyches."
11
+ spec.summary = "Structured plain-text productivity CLI"
12
+ spec.description = "MPS (MonoPsyches) is a terminal-based productivity system that stores " \
13
+ "tasks, notes, reminders, logs, and nested workflow structures in plain-text " \
14
+ ".mps files. It provides composable typed elements with optional arguments, " \
15
+ "hierarchical organization, natural-language date handling, full-text search, " \
16
+ "statistics, export tools, and git integration while keeping all data " \
17
+ "human-readable and portable."
13
18
  spec.homepage = "https://github.com/mash-97/mps"
14
- spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
19
+ spec.license = "MIT"
20
+ spec.required_ruby_version = ">= 3.0.0"
15
21
 
16
- # spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
17
-
18
- spec.metadata["homepage_uri"] = spec.homepage
22
+ spec.metadata["homepage_uri"] = spec.homepage
19
23
  spec.metadata["source_code_uri"] = spec.homepage
20
- # spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
21
24
 
22
- # Specify which files should be added to the gem when it is released.
23
- # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
24
25
  spec.files = Dir.chdir(File.expand_path(__dir__)) do
25
26
  `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
26
27
  end
@@ -28,22 +29,12 @@ Gem::Specification.new do |spec|
28
29
  spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
29
30
  spec.require_paths = ["lib"]
30
31
 
31
- # Uncomment to register a new dependency of your gem
32
- # spec.add_dependency "example-gem", "~> 1.0"
33
- spec.add_runtime_dependency "strscan", ">= 3.0"
34
- spec.add_runtime_dependency "thor", "~> 1.3"
32
+ spec.add_runtime_dependency "thor", "~> 1.3"
35
33
  spec.add_runtime_dependency "tty-editor", "~> 0.7.0"
36
- spec.add_runtime_dependency "chronic", "~> 0.10.2"
37
- spec.add_runtime_dependency "cli-ui", "~> 2.2"
34
+ spec.add_runtime_dependency "chronic", "~> 0.10.2"
35
+ spec.add_runtime_dependency "cli-ui", "~> 2.2"
38
36
 
39
- spec.add_development_dependency "rake", "~> 13.2"
37
+ spec.add_development_dependency "rake", "~> 13.2"
40
38
  spec.add_development_dependency "minitest", "~> 5.0"
41
- spec.add_development_dependency "fakefs", "~> 2.5"
42
- spec.add_development_dependency "tmpdir", ">= 0.1.3"
43
- spec.add_development_dependency "yard", "~> 0.9.37"
44
- spec.add_development_dependency "rack", "~> 3.1"
45
- spec.add_development_dependency "webrick", "~> 1.8"
46
- spec.add_development_dependency "rackup", "~> 2.1"
47
- # For more information and examples about making a new gem, checkout our
48
- # guide at: https://bundler.io/guides/creating_gem.html
39
+ spec.add_development_dependency "fakefs", "~> 2.5"
49
40
  end
data/prompt.txt ADDED
@@ -0,0 +1,64 @@
1
+ Re-analyze this repository from first principles.
2
+
3
+ Do NOT rely only on previous memory/context. Re-read the codebase itself deeply, including:
4
+
5
+ - README.md
6
+ - GETTING_STARTED.md
7
+ - CLAUDE.md
8
+ - source code
9
+ - configs
10
+ - abstractions
11
+ - interfaces/contracts
12
+ - module boundaries
13
+ - orchestration/composition systems
14
+ - extension points
15
+ - naming conventions
16
+ - internal APIs
17
+ - metaprogramming/framework-like constructs
18
+
19
+ Your goal is NOT merely to explain what the code does.
20
+
21
+ Your task is to reconstruct and infer:
22
+
23
+ - the philosophy behind the system
24
+ - the intended architectural direction
25
+ - the design patterns and abstractions
26
+ - the implementation methodology
27
+ - the compositional/extensibility model
28
+ - the long-term ecosystem vision emerging from the code
29
+ - the difference between documented vision vs actual implementation
30
+ - the difference between intentional architecture vs emergent architecture
31
+
32
+ Treat this repository not just as an application, but potentially as:
33
+ - a framework
34
+ - DSL
35
+ - compositional architecture system
36
+ - ecosystem platform
37
+ - runtime abstraction layer
38
+ - modular engineering paradigm
39
+ - or another evolving architectural model
40
+
41
+ Analyze deeply:
42
+
43
+ 1. Core vision and goals
44
+ 2. Macro + micro architecture
45
+ 3. Design patterns (explicit and emergent)
46
+ 4. Dependency/layering philosophy
47
+ 5. Composition/extensibility strategy
48
+ 6. Naming and semantic modeling philosophy
49
+ 7. Current architectural maturity
50
+ 8. Strengths and architectural advantages
51
+ 9. Weaknesses, inconsistencies, and technical debt
52
+ 10. Scalability/evolution potential
53
+ 11. Architectural risks and future bottlenecks
54
+ 12. What this project is naturally evolving into
55
+
56
+ Important:
57
+ - Infer intent from implementation details.
58
+ - Explain WHY architectural decisions appear to exist.
59
+ - Identify unfinished abstractions and partially realized ideas.
60
+ - Distinguish accidental complexity from intentional abstraction.
61
+ - Be analytical and critical, not flattering.
62
+ - Do not oversimplify the project into a generic CRUD/application architecture unless the code genuinely reflects that.
63
+
64
+ Produce a comprehensive architecture understanding document that future AI agents and developers can use as the canonical mental model for evolving the system.