ace-support-items 0.15.15 → 0.15.17

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: ee3fcee97215b4d7243d870366200e78369a85fc74fbe6c7ea8107efa23f8053
4
- data.tar.gz: 8c9914130df26e74948b15d84b3311beb326855ff657ab6ce41d8b13bb039ccd
3
+ metadata.gz: 377e58a49a35c4a328970fd2f2e88dc410eb7df235a56c5e513c3444f097f4d2
4
+ data.tar.gz: 4551619979ce7de032986074aba3a3b2da7f0bddd1825636a146ccba44266b63
5
5
  SHA512:
6
- metadata.gz: 90551754056d747f9f8a341f8f50601095b4c29e43725550652929b95479edac2e2b92e7e922c2ab552efb09decbf0340cd24765f255c47c1babb7ef0d4c7d9e
7
- data.tar.gz: d315633aeb2cce808bdeb0a4eb1bf626a50079fba04d74f35849ca06dd649b639d65d71cc69930ab5611aaf7ee9b5248b0457319dcaa3dcfc1d173b1e6fc3368
6
+ metadata.gz: 00fee712d7aa01022e168a41d4b096fc8d74802d2bcef7f57e50fea5afd129a0cef59dc970fa616947d7d0db3429fe1e5934cf9b247d50cd96eb8200913cb476
7
+ data.tar.gz: 42ba9cf8bdc9c901b95afaf0ac994938b1c167c3349c27ba5f1b6e71db3cbc298c03b732fabdd855eeacfca40917185df9dc938375471cbba2bdc9414940d789
data/CHANGELOG.md CHANGED
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.15.17] - 2026-11-10
11
+
12
+ ### Fixed
13
+
14
+ - Allow accessible-only discovery to skip unreadable descendants while preserving strict complete inventory and root checks.
15
+
16
+ ## [0.15.16] - 2026-10-29
17
+
18
+ ### Fixed
19
+
20
+ - 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).
21
+ - `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.
22
+
10
23
  ## [0.15.15] - 2026-10-18
11
24
 
12
25
  ### Added
@@ -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
@@ -40,17 +40,17 @@ module Ace
40
40
 
41
41
  # Scan root directory recursively for items
42
42
  # @return [Array<ScanResult>] List of scan results, sorted by ID (chronological)
43
- def scan
43
+ def scan(accessible_only: false)
44
44
  return [] unless Dir.exist?(@root_dir)
45
45
 
46
46
  results = []
47
- scan_directory(@root_dir, results)
47
+ scan_directory(@root_dir, results, accessible_only: accessible_only)
48
48
  results.sort_by(&:id)
49
49
  end
50
50
 
51
51
  private
52
52
 
53
- def scan_directory(dir, results)
53
+ def scan_directory(dir, results, accessible_only:)
54
54
  Dir.entries(dir).sort.each do |entry|
55
55
  next if entry.start_with?(".")
56
56
 
@@ -65,13 +65,17 @@ module Ace
65
65
  results << result
66
66
  else
67
67
  # Folder has spec files but isn't an item (e.g., _maybe with orphan files) — recurse
68
- scan_directory(full_path, results)
68
+ scan_directory(full_path, results, accessible_only: accessible_only)
69
69
  end
70
70
  else
71
71
  # Recurse into subdirectory (for special folders like _maybe/)
72
- scan_directory(full_path, results)
72
+ scan_directory(full_path, results, accessible_only: accessible_only)
73
73
  end
74
74
  end
75
+ rescue Errno::EACCES, Errno::EPERM
76
+ # Only discovery may omit inaccessible descendants. A requested
77
+ # root and the default complete inventory always fail closed.
78
+ raise unless accessible_only && dir != @root_dir
75
79
  end
76
80
 
77
81
  def build_result(dir_path, file_path)
@@ -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::Exception => 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
@@ -3,7 +3,7 @@
3
3
  module Ace
4
4
  module Support
5
5
  module Items
6
- VERSION = "0.15.15"
6
+ VERSION = "0.15.17"
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.15
4
+ version: 0.15.17
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michal Czyz
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-10-20 00:00:00.000000000 Z
10
+ date: 2026-11-10 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: ace-support-core