Module: Ace::Assign::Atoms::StepNumbering

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

Overview

Pure functions for hierarchical step numbering operations.

Supports nested step structure where steps can have sub-steps:

  • Main steps: 010, 020, 030

  • Nested steps: 010.01, 010.02, 010.03

  • Deeply nested: 010.01.01 (if needed)

This enables verification-as-step patterns where parent steps wait for all children to complete before advancing.

Examples:

StepNumbering.parse("010.02")
# => { parent: "010", index: 2, depth: 1, full: "010.02" }

StepNumbering.next_sibling("010.02")
# => "010.03"

StepNumbering.first_child("010")
# => "010.01"

StepNumbering.child_of?("010.02", "010")
# => true

Constant Summary collapse

MAX_DEPTH =

Maximum allowed nesting depth for step numbers. Prevents unbounded hierarchy (e.g., 010.01.01.01.01…). Depth 0 = top-level (010), 1 = first nest (010.01), 2 = second nest (010.01.01). Maximum is 010.01.01 (3 levels total).

2
MAX_SIBLINGS_TOP_LEVEL =

Maximum siblings per level. Child indexes use %02d format (01-99). Top-level uses %03d (001-999). Exceeding these limits will cause lexicographical sorting issues.

999
MAX_SIBLINGS_NESTED =
99

Class Method Summary collapse

Class Method Details

.child_of?(child, parent) ⇒ Boolean

Check if a step number is a child (direct or nested) of another.

Parameters:

  • child (String)

    Potential child number

  • parent (String)

    Potential parent number

Returns:

  • (Boolean)

    True if child is descended from parent



127
128
129
# File 'lib/ace/assign/atoms/step_numbering.rb', line 127

def self.child_of?(child, parent)
  child.to_s.start_with?("#{parent}.")
end

.direct_child_of?(child, parent) ⇒ Boolean

Check if a step number is a direct (immediate) child of another.

Parameters:

  • child (String)

    Potential child number

  • parent (String)

    Potential parent number

Returns:

  • (Boolean)

    True if child is immediate child of parent



136
137
138
139
140
141
142
143
144
# File 'lib/ace/assign/atoms/step_numbering.rb', line 136

def self.direct_child_of?(child, parent)
  return false unless child_of?(child, parent)

  # Direct child has exactly one more level
  child_parsed = parse(child)
  parent_parsed = parse(parent)

  child_parsed[:depth] == parent_parsed[:depth] + 1
end

.direct_children(parent, all_numbers) ⇒ Array<String>

Get all direct children of a parent from a list of numbers.

Parameters:

  • parent (String)

    Parent step number

  • all_numbers (Array<String>)

    All step numbers to filter

Returns:

  • (Array<String>)

    Direct children of parent



151
152
153
# File 'lib/ace/assign/atoms/step_numbering.rb', line 151

def self.direct_children(parent, all_numbers)
  all_numbers.select { |n| direct_child_of?(n, parent) }
end

.first_child(number) ⇒ String

Generate the first child step number.

Parameters:

  • number (String)

    Parent step number

Returns:

  • (String)

    First child number (e.g., “010” -> “010.01”)

Raises:

  • (ArgumentError)

    If adding a child would exceed MAX_DEPTH



87
88
89
90
91
92
93
94
95
# File 'lib/ace/assign/atoms/step_numbering.rb', line 87

def self.first_child(number)
  parent_depth = parse(number)[:depth]
  child_depth = parent_depth + 1
  if child_depth > MAX_DEPTH
    raise ArgumentError, "Cannot create child: would exceed maximum nesting depth of #{MAX_DEPTH} " \
                         "(parent '#{number}' is at depth #{parent_depth})"
  end
  "#{number}.01"
end

.insert_after(after) ⇒ String

Generate a step number to insert after another step. This creates a sibling at the same nesting level.

Note: This method does not check for collisions. Use steps_to_renumber to determine which existing steps need to be shifted when inserting.

Parameters:

  • after (String)

    Step number to insert after

Returns:

  • (String)

    New step number (next sibling)



179
180
181
# File 'lib/ace/assign/atoms/step_numbering.rb', line 179

def self.insert_after(after)
  next_sibling(after)
end

.next_child(parent, existing_children = []) ⇒ String

Generate the next child step number based on existing children.

Parameters:

  • parent (String)

    Parent step number

  • existing_children (Array<String>) (defaults to: [])

    Existing child numbers

Returns:

  • (String)

    Next child number

Raises:

  • (ArgumentError)

    If adding a child would exceed MAX_DEPTH



103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/ace/assign/atoms/step_numbering.rb', line 103

def self.next_child(parent, existing_children = [])
  parent_depth = parse(parent)[:depth]
  child_depth = parent_depth + 1
  if child_depth > MAX_DEPTH
    raise ArgumentError, "Cannot create child: would exceed maximum nesting depth of #{MAX_DEPTH} " \
                         "(parent '#{parent}' is at depth #{parent_depth})"
  end

  return first_child(parent) if existing_children.empty?

  # Find highest existing child index
  max_index = existing_children
    .select { |n| child_of?(n, parent) && direct_child_of?(n, parent) }
    .map { |n| parse(n)[:index] }
    .max || 0

  "#{parent}.#{format("%02d", max_index + 1)}"
end

.next_sibling(number) ⇒ String

Generate the next sibling step number.

Parameters:

  • number (String)

    Current step number

Returns:

  • (String)

    Next sibling number with same parent



64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/ace/assign/atoms/step_numbering.rb', line 64

def self.next_sibling(number)
  parsed = parse(number)
  new_index = parsed[:index] + 1
  limit = parsed[:parent] ? MAX_SIBLINGS_NESTED : MAX_SIBLINGS_TOP_LEVEL

  if new_index > limit
    raise ArgumentError, "Cannot create sibling: would exceed maximum siblings " \
                         "(#{limit}) at this level (current index: #{parsed[:index]})"
  end

  if parsed[:parent]
    "#{parsed[:parent]}.#{format("%02d", new_index)}"
  else
    # Top-level numbers use 3-digit padding
    format("%03d", new_index)
  end
end

.parent_of(number) ⇒ String?

Get the parent number of a step, if it has one.

Parameters:

  • number (String)

    Step number

Returns:

  • (String, nil)

    Parent number or nil for top-level steps



159
160
161
# File 'lib/ace/assign/atoms/step_numbering.rb', line 159

def self.parent_of(number)
  parse(number)[:parent]
end

.parse(number) ⇒ Hash

Parse a step number into its components.

Parameters:

  • number (String)

    Step number (e.g., “010”, “010.02”, “010.02.03”)

Returns:

  • (Hash)

    Parsed components with keys:

    • :parent [String, nil] Parent step number (nil for top-level steps)

    • :index [Integer] The final sequence number

    • :depth [Integer] Nesting depth (0 for top-level, 1 for first nest, etc.)

    • :full [String] Original full number



49
50
51
52
53
54
55
56
57
58
# File 'lib/ace/assign/atoms/step_numbering.rb', line 49

def self.parse(number)
  parts = number.to_s.split(".")

  {
    parent: (parts.length > 1) ? parts[0..-2].join(".") : nil,
    index: parts.last.to_i,
    depth: parts.length - 1,
    full: number.to_s
  }
end

.shift_number(number, shift = 1) ⇒ String

Generate the shifted number for a step being renumbered.

Parameters:

  • number (String)

    Original step number

  • shift (Integer) (defaults to: 1)

    Amount to shift by (default: 1)

Returns:

  • (String)

    New shifted number

Raises:

  • (ArgumentError)

    If shifting would exceed sibling limits



208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
# File 'lib/ace/assign/atoms/step_numbering.rb', line 208

def self.shift_number(number, shift = 1)
  parsed = parse(number)
  new_index = parsed[:index] + shift
  limit = parsed[:parent] ? MAX_SIBLINGS_NESTED : MAX_SIBLINGS_TOP_LEVEL

  if new_index > limit
    raise ArgumentError, "Cannot shift step number: would exceed maximum siblings " \
                         "(#{limit}) at this level (new index would be: #{new_index})"
  end

  if parsed[:parent]
    "#{parsed[:parent]}.#{format("%02d", new_index)}"
  else
    format("%03d", new_index)
  end
end

.steps_to_renumber(at_number, existing) ⇒ Array<String>

Find step numbers that need to be renumbered when inserting at a position. Returns steps that have numbers >= the insertion point.

Parameters:

  • at_number (String)

    Number where new step will be inserted

  • existing (Array<String>)

    All existing step numbers

Returns:

  • (Array<String>)

    Numbers that need to be shifted (in ascending order)



189
190
191
192
193
194
195
196
197
198
199
200
# File 'lib/ace/assign/atoms/step_numbering.rb', line 189

def self.steps_to_renumber(at_number, existing)
  parsed_at = parse(at_number)
  parent = parsed_at[:parent]

  existing
    .select { |n|
      parsed = parse(n)
      # Same parent (or both top-level) and index >= insertion point
      parsed[:parent] == parent && parsed[:index] >= parsed_at[:index]
    }
    .sort_by { |n| parse(n)[:index] }
end

.top_level?(number) ⇒ Boolean

Check if a number is a top-level (root) step.

Parameters:

  • number (String)

    Step number

Returns:

  • (Boolean)

    True if top-level



167
168
169
# File 'lib/ace/assign/atoms/step_numbering.rb', line 167

def self.top_level?(number)
  parse(number)[:depth] == 0
end