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.
- checksums.yaml +4 -4
- data/.github/workflows/main.yml +23 -18
- data/.gitignore +4 -3
- data/ARCHITECTURE.md +298 -0
- data/CLAUDE.md +6 -10
- data/GETTING_STARTED.md +176 -8
- data/Gemfile.lock +46 -0
- data/Rakefile +6 -5
- data/lib/cli/mps.rb +88 -333
- data/lib/mps/cli/commands/append.rb +32 -0
- data/lib/mps/cli/commands/config_cmd.rb +28 -0
- data/lib/mps/cli/commands/export.rb +51 -0
- data/lib/mps/cli/commands/git.rb +44 -0
- data/lib/mps/cli/commands/list.rb +56 -0
- data/lib/mps/cli/commands/open.rb +25 -0
- data/lib/mps/cli/commands/search.rb +34 -0
- data/lib/mps/cli/commands/stats.rb +77 -0
- data/lib/mps/cli/commands/tags.rb +57 -0
- data/lib/mps/cli/commands/update.rb +57 -0
- data/lib/mps/cli/commands.rb +5 -0
- data/lib/mps/config.rb +8 -3
- data/lib/mps/constants.rb +7 -5
- data/lib/mps/elements/element.rb +41 -11
- data/lib/mps/elements/elements.rb +8 -6
- data/lib/mps/elements/log.rb +2 -4
- data/lib/mps/elements/mps.rb +1 -4
- data/lib/mps/elements/note.rb +1 -4
- data/lib/mps/elements/reminder.rb +1 -4
- data/lib/mps/elements/task.rb +1 -4
- data/lib/mps/engines/engines.rb +3 -1
- data/lib/mps/engines/mps.rb +20 -10
- data/lib/mps/interpolators/interpolators.rb +3 -1
- data/lib/mps/mps.rb +11 -19
- data/lib/mps/presenter.rb +128 -0
- data/lib/mps/query.rb +71 -0
- data/lib/mps/ref_resolver.rb +77 -0
- data/lib/mps/store.rb +95 -6
- data/lib/mps/version.rb +1 -1
- data/lib/mps.rb +11 -9
- data/mps.gemspec +15 -24
- data/prompt.txt +64 -0
- metadata +28 -90
|
@@ -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
|
|
7
|
-
@element_classes
|
|
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(
|
|
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(
|
|
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
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
|
|
12
|
-
spec.description
|
|
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.
|
|
19
|
+
spec.license = "MIT"
|
|
20
|
+
spec.required_ruby_version = ">= 3.0.0"
|
|
15
21
|
|
|
16
|
-
|
|
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
|
-
|
|
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",
|
|
37
|
-
spec.add_runtime_dependency "cli-ui",
|
|
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",
|
|
37
|
+
spec.add_development_dependency "rake", "~> 13.2"
|
|
40
38
|
spec.add_development_dependency "minitest", "~> 5.0"
|
|
41
|
-
spec.add_development_dependency "fakefs",
|
|
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.
|