ace-support-items 0.15.14 → 0.15.16

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 36c95e46f3570063b5651e2b3d91bf46edafd37278c5c3250d1a70c73071545a
4
- data.tar.gz: b1b2894f143a00d647c6d984ef2af96c419bbb1be25f82f1ce85cb40a7c14048
3
+ metadata.gz: a38caecfe7baec161ef92edffb6b0682c8347a2aa5595bf16659806af50556c0
4
+ data.tar.gz: f1a1c662c2cf9f570e7cded9997632ec0462953acb5aa772a6fe2c951d0ed394
5
5
  SHA512:
6
- metadata.gz: 975ade85ed637013761e3a953dc0f6eb9c9c3a82932b70e269ab17b7eead7cd52f4426e6b0c68a901697ebc3acbfdb3bd5c229f84de89f1d31e933c5b5788a62
7
- data.tar.gz: e51e3e3eff0172dad565df58d7df98e8f663ccd926ad47c13d1a9df904d1819bab0dc1489359713607fcdc22ac025302859f0635d021f06cb09c9f202b21bff8
6
+ metadata.gz: 784f3f6d39072de1b71fdab2fa6a22bc5c8ac06289cb642fb724bac3c6b062260b1f53a27e882259ed8d248cb34805d89bb2479c220cacfbe098811cb27b27cc
7
+ data.tar.gz: f8b3d574bfc6c5033bc9138f813010ecf48b46d7dd979f27a9d45a908a35a59788b3b6c4b2b4374ab5dd7790034bbc9cb8185c9478a5eef4c23cf94685c3d2bb
data/CHANGELOG.md CHANGED
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.15.16] - 2026-10-29
11
+
12
+ ### Fixed
13
+
14
+ - Frontmatter flow sequences serialize every string element with the same quoting rules as flat values, so a question or tag containing `: ` parses back as the same string instead of a single-pair hash; compound elements raise `ArgumentError` instead of emitting Ruby rocket syntax that is not YAML (the 8xsdfo ask-flow corruption: recipient/reply_to frontmatter was silently dropped, so native sync refused the captain's reply and the answer wake never fired).
15
+ - `FrontmatterParser.parse!` gives mutating paths a strict parse, and `FieldUpdater` refuses to rewrite a file whose frontmatter YAML is invalid — an unparsable block now aborts the update loudly instead of rebuilding the file from an empty frontmatter.
16
+
17
+ ## [0.15.15] - 2026-10-18
18
+
19
+ ### Added
20
+
21
+ - `FolderMover#destination_path` computes the destination a move would create
22
+ without touching the filesystem, so callers can run pre-flight checks (for
23
+ example a Git-ignored destination guard).
24
+
10
25
  ## [0.15.14] - 2026-10-06
11
26
 
12
27
  ### Fixed
@@ -13,6 +13,21 @@ module Ace
13
13
  # @param content [String] The markdown content with optional frontmatter
14
14
  # @return [Hash] Parsed frontmatter data (empty hash if no frontmatter)
15
15
  def self.parse_frontmatter(content)
16
+ parse_frontmatter!(content)
17
+ rescue Psych::SyntaxError
18
+ # Read paths tolerate arbitrary user files: an unparsable block
19
+ # yields an empty hash instead of crashing scans.
20
+ {}
21
+ end
22
+
23
+ # Strict variant for mutating paths (FieldUpdater and friends): a
24
+ # syntax error must stop the change instead of silently rebuilding
25
+ # the file from an empty frontmatter (the 8xsdfo ask-flow corruption
26
+ # amplified one bad line into total frontmatter loss).
27
+ # @param content [String] The markdown content with optional frontmatter
28
+ # @return [Hash] Parsed frontmatter data (empty hash if no frontmatter)
29
+ # @raise [Psych::SyntaxError] when a frontmatter block is unparsable
30
+ def self.parse_frontmatter!(content)
16
31
  return {} if content.nil? || content.empty?
17
32
  return {} unless content.start_with?("---\n")
18
33
 
@@ -21,11 +36,7 @@ module Ace
21
36
 
22
37
  yaml_content = content[4...end_index]
23
38
 
24
- begin
25
- YAML.safe_load(yaml_content, permitted_classes: [Date, Time, Symbol]) || {}
26
- rescue Psych::SyntaxError
27
- {}
28
- end
39
+ YAML.safe_load(yaml_content, permitted_classes: [Date, Time, Symbol]) || {}
29
40
  end
30
41
 
31
42
  # Extract content after frontmatter
@@ -50,6 +61,14 @@ module Ace
50
61
  def self.parse(content)
51
62
  [parse_frontmatter(content), extract_body(content)]
52
63
  end
64
+
65
+ # Strict parse for mutating paths: raises on an unparsable block.
66
+ # @param content [String] The markdown content
67
+ # @return [Array(Hash, String)] Tuple of [frontmatter, body]
68
+ # @raise [Psych::SyntaxError] when a frontmatter block is unparsable
69
+ def self.parse!(content)
70
+ [parse_frontmatter!(content), extract_body(content)]
71
+ end
53
72
  end
54
73
  end
55
74
  end
@@ -31,7 +31,7 @@ module Ace
31
31
  end
32
32
 
33
33
  # Serialize a single key-value entry with indentation support.
34
- def self.serialize_entry(lines, key, value, indent:)
34
+ private_class_method def self.serialize_entry(lines, key, value, indent:)
35
35
  prefix = " " * indent
36
36
  case value
37
37
  when Hash
@@ -43,7 +43,7 @@ module Ace
43
43
  lines << if value.empty?
44
44
  "#{prefix}#{key}: []"
45
45
  else
46
- "#{prefix}#{key}: [#{value.join(", ")}]"
46
+ "#{prefix}#{key}: [#{value.map { |element| flow_element(element, key) }.join(", ")}]"
47
47
  end
48
48
  when String
49
49
  lines << if needs_quoting?(value)
@@ -56,7 +56,27 @@ module Ace
56
56
  end
57
57
  end
58
58
 
59
- private_class_method :serialize_entry
59
+ # One element inside a flow sequence: strings follow the same
60
+ # quoting rules as flat values (a plain scalar containing `: ` would
61
+ # parse back as a hash, silently corrupting the value across
62
+ # read-modify-write cycles). Numbers, booleans and nil serialize to
63
+ # their YAML literals; compound values have no safe flow form and
64
+ # fail loudly instead of emitting YAML that parses back as a
65
+ # different structure (the 8xsdfo ask-flow corruption).
66
+ def self.flow_element(element, key)
67
+ case element
68
+ when String
69
+ needs_quoting?(element) ? "\"#{escape_yaml_string(element)}\"" : element
70
+ when Integer, Float, true, false
71
+ element.to_s
72
+ when nil
73
+ "null"
74
+ else
75
+ raise ArgumentError,
76
+ "frontmatter array '#{key}' cannot safely carry a #{element.class} element; use scalar strings"
77
+ end
78
+ end
79
+ private_class_method :flow_element
60
80
 
61
81
  # Check if a string value needs YAML quoting
62
82
  # @param value [String] Value to check
@@ -25,7 +25,7 @@ module Ace
25
25
 
26
26
  file.rewind
27
27
  content = file.read
28
- frontmatter, body = Atoms::FrontmatterParser.parse(content)
28
+ frontmatter, body = parse_strict(file_path, content)
29
29
  # Strip leading newline from body so rebuild doesn't double-space
30
30
  body = body.sub(/\A\n/, "")
31
31
 
@@ -118,6 +118,16 @@ module Ace
118
118
  file.fsync
119
119
  end
120
120
 
121
+ # Strict parse with an actionable message: an unparsable block must
122
+ # abort the update (the rewrite would silently drop the whole
123
+ # frontmatter), never proceed with an empty hash.
124
+ def self.parse_strict(file_path, content)
125
+ Atoms::FrontmatterParser.parse!(content)
126
+ rescue Psych::SyntaxError => e
127
+ raise ArgumentError,
128
+ "#{file_path}: frontmatter YAML is invalid (#{e.message}); refusing to rewrite the file"
129
+ end
130
+
121
131
  private_class_method :apply_nested_set, :rewrite_locked_file
122
132
  end
123
133
  end
@@ -17,35 +17,38 @@ module Ace
17
17
  @root_dir = root_dir
18
18
  end
19
19
 
20
- # Move an item folder to a target location.
20
+ # Compute the destination path for an item without moving it.
21
+ # Used by pre-flight checks (e.g. Git-ignored task guards).
21
22
  #
22
23
  # @param item [#path] Item with a path attribute (directory to move)
23
24
  # @param to [String] Target folder name (short or full, e.g., "maybe", "_archive")
24
25
  # @param date [Time, nil] Date used to compute archive partition (default: Time.now)
25
- # @return [String] New path of the item directory
26
- def move(item, to:, date: nil)
26
+ # @return [String] Destination directory path
27
+ def destination_path(item, to:, date: nil)
27
28
  if Atoms::SpecialFolderDetector.virtual_filter?(to)
28
29
  raise ArgumentError, "Cannot move to virtual filter '#{to}' — it is not a physical folder"
29
30
  end
30
31
 
31
32
  normalized = Atoms::SpecialFolderDetector.normalize(to)
32
-
33
- target_parent = if normalized == "_archive"
34
- partition = Atoms::DatePartitionPath.compute(date || Time.now)
35
- File.expand_path(File.join(@root_dir, normalized, partition))
36
- else
37
- File.expand_path(File.join(@root_dir, normalized))
38
- end
39
-
33
+ target_parent = destination_parent(normalized, date)
40
34
  validate_path_traversal!(target_parent)
41
- FileUtils.mkdir_p(target_parent)
42
35
 
43
- folder_name = File.basename(item.path)
44
- new_path = File.join(target_parent, folder_name)
36
+ File.join(target_parent, File.basename(item.path))
37
+ end
38
+
39
+ # Move an item folder to a target location.
40
+ #
41
+ # @param item [#path] Item with a path attribute (directory to move)
42
+ # @param to [String] Target folder name (short or full, e.g., "maybe", "_archive")
43
+ # @param date [Time, nil] Date used to compute archive partition (default: Time.now)
44
+ # @return [String] New path of the item directory
45
+ def move(item, to:, date: nil)
46
+ new_path = destination_path(item, to: to, date: date)
45
47
 
46
48
  # Same-location no-op check
47
49
  return item.path if File.expand_path(item.path) == File.expand_path(new_path)
48
50
 
51
+ FileUtils.mkdir_p(File.dirname(new_path))
49
52
  atomic_move(item.path, new_path)
50
53
  end
51
54
 
@@ -65,6 +68,15 @@ module Ace
65
68
 
66
69
  private
67
70
 
71
+ def destination_parent(normalized, date)
72
+ if normalized == "_archive"
73
+ partition = Atoms::DatePartitionPath.compute(date || Time.now)
74
+ File.expand_path(File.join(@root_dir, normalized, partition))
75
+ else
76
+ File.expand_path(File.join(@root_dir, normalized))
77
+ end
78
+ end
79
+
68
80
  def validate_path_traversal!(target_parent)
69
81
  root_real = File.expand_path(@root_dir)
70
82
  unless target_parent.start_with?(root_real + File::SEPARATOR)
@@ -3,7 +3,7 @@
3
3
  module Ace
4
4
  module Support
5
5
  module Items
6
- VERSION = "0.15.14"
6
+ VERSION = "0.15.16"
7
7
  end
8
8
  end
9
9
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ace-support-items
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.15.14
4
+ version: 0.15.16
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michal Czyz
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-10-08 00:00:00.000000000 Z
10
+ date: 2026-10-29 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: ace-support-core