Module: Ace::Assign::Atoms::StepFileParser

Defined in:
lib/ace/assign/atoms/step_file_parser.rb

Overview

Pure functions for parsing step markdown files.

Step files have frontmatter + body structure:


name: step-name status: pending


# Instructions …

Constant Summary collapse

FRONTMATTER_REGEX =
/\A---\s*\n(.*?)\n---\s*\n/m

Class Method Summary collapse

Class Method Details

.extract_fields(parsed) ⇒ Hash

Extract specific fields from parsed content

Parameters:

  • parsed (Hash)

    Result from parse()

Returns:

  • (Hash)

    Extracted fields



42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/ace/assign/atoms/step_file_parser.rb', line 42

def self.extract_fields(parsed)
  fm = parsed[:frontmatter]
  body = parsed[:body]

  context = fm["context"]
  validate_context!(context)

  {
    name: fm["name"],
    status: (fm["status"] || "pending").to_sym,
    source: normalize_source(fm["source"], fm["workflow"], fm["skill"]),
    skill: fm["skill"],
    workflow: fm["workflow"],
    context: context, # "fork" triggers Task tool execution
    batch_parent: parse_boolean(fm["batch_parent"]),
    parallel: parse_boolean(fm["parallel"]),
    max_parallel: parse_positive_integer(fm["max_parallel"]),
    fork_retry_limit: parse_non_negative_integer(fm["fork_retry_limit"]),
    fork_options: parse_hash(fm["fork"]),
    started_at: parse_time(fm["started_at"]),
    completed_at: parse_time(fm["completed_at"]),
    fork_launch_pid: parse_integer(fm["fork_launch_pid"]),
    fork_tracked_pids: parse_integer_array(fm["fork_tracked_pids"]),
    fork_pid_updated_at: parse_time(fm["fork_pid_updated_at"]),
    fork_pid_file: fm["fork_pid_file"],
    error: fm["error"],
    stall_reason: fm["stall_reason"],
    added_by: fm["added_by"],
    parent: fm["parent"],
    instructions: body.strip,
    report: nil # Reports are loaded separately from reports/ dir
  }
end

.extract_instructions(body) ⇒ String

Extract instructions section from body Body is now just instructions (report is in separate file)

Parameters:

  • body (String)

    File body after frontmatter

Returns:

  • (String)

    Instructions content



81
82
83
# File 'lib/ace/assign/atoms/step_file_parser.rb', line 81

def self.extract_instructions(body)
  body.strip
end

.extract_parent_from_number(number) ⇒ String?

Extract parent number from a hierarchical step number.

Parameters:

  • number (String)

    Step number (e.g., “010.01”)

Returns:

  • (String, nil)

    Parent number or nil for top-level



110
111
112
113
114
115
116
117
# File 'lib/ace/assign/atoms/step_file_parser.rb', line 110

def self.extract_parent_from_number(number)
  return nil if number.nil?

  parts = number.split(".")
  return nil if parts.length <= 1

  parts[0..-2].join(".")
end

.generate_filename(number, name) ⇒ String

Generate step filename from number and name

Parameters:

  • number (String)

    Step number

  • name (String)

    Step name

Returns:

  • (String)

    Step filename with .st.md extension



124
125
126
127
128
# File 'lib/ace/assign/atoms/step_file_parser.rb', line 124

def self.generate_filename(number, name)
  # Sanitize name for filename
  safe_name = name.to_s.downcase.gsub(/[^a-z0-9]+/, "-").gsub(/^-|-$/, "")
  "#{number}-#{safe_name}.st.md"
end

.generate_report_filename(number, name) ⇒ String

Generate report filename from number and name

Parameters:

  • number (String)

    Step number

  • name (String)

    Step name

Returns:

  • (String)

    Report filename with .r.md extension



135
136
137
138
139
# File 'lib/ace/assign/atoms/step_file_parser.rb', line 135

def self.generate_report_filename(number, name)
  # Sanitize name for filename
  safe_name = name.to_s.downcase.gsub(/[^a-z0-9]+/, "-").gsub(/^-|-$/, "")
  "#{number}-#{safe_name}.r.md"
end

.parse(content) ⇒ Hash

Parse step file content into structured data

Parameters:

  • content (String)

    File content with frontmatter + body

Returns:

  • (Hash)

    Parsed data with :frontmatter and :body keys



24
25
26
27
28
29
30
31
32
33
34
35
36
# File 'lib/ace/assign/atoms/step_file_parser.rb', line 24

def self.parse(content)
  match = content.match(FRONTMATTER_REGEX)

  if match
    frontmatter_yaml = match[1]
    body = content[match.end(0)..]

    frontmatter = YAML.safe_load(frontmatter_yaml, permitted_classes: [Time, Date]) || {}
    {frontmatter: frontmatter, body: body.strip}
  else
    {frontmatter: {}, body: content.strip}
  end
end

.parse_filename(filename) ⇒ Hash

Parse filename to extract number, name, and parent.

Parameters:

  • filename (String)

    Filename like “010-init-project.st.md” or “010-init-project.r.md”

Returns:

  • (Hash)

    Extracted number, name, and parent (if nested)



89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/ace/assign/atoms/step_file_parser.rb', line 89

def self.parse_filename(filename)
  # Remove .st.md or .r.md extension
  base = filename.sub(/\.(st|r)\.md$/, "")

  # Match number pattern (with optional dot-separated parts) and name
  match = base.match(/^([\d.]+)-(.+)$/)

  if match
    number = match[1]
    name = match[2]
    parent = extract_parent_from_number(number)
    {number: number, name: name, parent: parent}
  else
    {number: nil, name: base, parent: nil}
  end
end