Module: SplttyCLI::NotesSync
- Defined in:
- lib/spltty_cli/notes_sync.rb
Overview
Two-way sync of ledger config between each ledger's *.notes.md frontmatter
(the spltty: sub-map) and the ledgers block of config.json.
The notes file is the source of truth. A per-ledger synced_at timestamp in
config is compared against the notes file's mtime to pick the direction:
* notes edited more recently (or config never synced) -> notes -> config
* config as-new-or-newer, or notes carries no managed field -> config -> notes
After a config -> notes write, synced_at is stamped to the file's NEW mtime so the write does not re-trigger a notes -> config on the next run.
Every managed field is optional: only keys present on the winning side are merged; an absent key is left untouched on the other side (never cleared).
Constant Summary collapse
- MANAGED =
%w[title default_responsible default_currency reference groups].freeze
Class Method Summary collapse
-
.config_to_notes(path, entry, fm_full, spltty, body) ⇒ Object
Write config's managed fields into the notes header.
-
.globalize(config) ⇒ Object
Cross-ledger promotion: any group definition (name + participants + percentages) that appears identically in >= 2 ledgers is recorded in the global config.groups.
- .parse_time(str) ⇒ Object
- .render(frontmatter, body) ⇒ Object
-
.run(config) ⇒ Object
Sync all ledgers in
config.
Class Method Details
.config_to_notes(path, entry, fm_full, spltty, body) ⇒ Object
Write config's managed fields into the notes header. Returns true when the file content actually changed (and stamps synced_at to the new mtime).
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 |
# File 'lib/spltty_cli/notes_sync.rb', line 96 def config_to_notes(path, entry, fm_full, spltty, body) cfg_fields = MANAGED.each_with_object({}) do |k, h| v = entry[k] h[k] = v unless v.nil? || v.to_s.empty? end return false if cfg_fields.empty? && spltty.empty? new_spltty = spltty.dup cfg_fields.each { |k, v| new_spltty[k] = v } new_fm = fm_full.dup if new_spltty.empty? new_fm.delete("spltty") else new_fm["spltty"] = new_spltty end old_raw = File.read(path) new_raw = render(new_fm, body) return false if new_raw == old_raw File.write(path, new_raw) entry["synced_at"] = File.mtime(path).utc.iso8601(9) true end |
.globalize(config) ⇒ Object
Cross-ledger promotion: any group definition (name + participants + percentages) that appears identically in >= 2 ledgers is recorded in the global config.groups. Additive only — per-ledger copies and headers are left intact, and globals are never written back into any header. Returns the list of names newly added to config.groups.
63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
# File 'lib/spltty_cli/notes_sync.rb', line 63 def globalize(config) seen = Hash.new { |h, k| h[k] = { count: 0, name: nil, hash: nil } } config.ledgers.each_value do |entry| groups = entry["groups"] next unless groups.is_a?(Hash) groups.each do |name, hash| next unless hash.is_a?(Hash) slot = seen[Groups.canonical(name, hash)] slot[:count] += 1 slot[:name] = name slot[:hash] = hash end end # Only touch config.groups when there is a shared def — avoids the lazy # accessor materializing an empty "groups": {} on every sync. candidates = seen.each_value.select { |slot| slot[:count] >= 2 } return [] if candidates.empty? promoted = [] candidates.each do |slot| next if config.groups[slot[:name]] == slot[:hash] config.groups[slot[:name]] = slot[:hash] promoted << slot[:name] end promoted end |
.parse_time(str) ⇒ Object
129 130 131 132 133 134 135 |
# File 'lib/spltty_cli/notes_sync.rb', line 129 def parse_time(str) return nil if str.nil? || str.to_s.empty? Time.iso8601(str.to_s) rescue ArgumentError nil end |
.render(frontmatter, body) ⇒ Object
121 122 123 124 125 126 127 |
# File 'lib/spltty_cli/notes_sync.rb', line 121 def render(frontmatter, body) if frontmatter.nil? || frontmatter.empty? body else "#{YAML.dump(frontmatter)}#{Notes::FENCE}\n#{body}" end end |
.run(config) ⇒ Object
Sync all ledgers in config. Mutates config in place (caller saves).
Returns { to_config: [names], to_notes: [names], globalized: [names] }.
25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
# File 'lib/spltty_cli/notes_sync.rb', line 25 def run(config) to_config = [] to_notes = [] config.ledgers.each do |name, entry| next if entry["missing"] path = Notes.path_for(config.accounts_dir, name, entry) next unless File.exist?(path) parsed = Notes.parse(path) fm_full = parsed[:frontmatter] spltty = fm_full["spltty"].is_a?(Hash) ? fm_full["spltty"] : {} header_fields = MANAGED.each_with_object({}) do |k, h| h[k] = spltty[k] if spltty.key?(k) && !spltty[k].nil? end s = parse_time(entry["synced_at"]) mtime = parsed[:mtime] notes_wins = !header_fields.empty? && (s.nil? || mtime > s) if notes_wins header_fields.each { |k, v| entry[k] = v } entry["synced_at"] = mtime.utc.iso8601(9) to_config << name elsif config_to_notes(path, entry, fm_full, spltty, parsed[:body]) to_notes << name end end { to_config: to_config, to_notes: to_notes, globalized: globalize(config) } end |