Class: Agentilda::Subject

Inherits:
Object
  • Object
show all
Defined in:
lib/agentilda/feature.rb

Overview

A plan folder as the state machine sees it: which files exist, what they say, and which pull requests they record.

It exists because the invariants ask the same questions repeatedly and Feature is a frozen Data with nowhere to memoize the answers.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(feature) ⇒ Subject

Returns a new instance of Subject.

Parameters:



135
136
137
138
# File 'lib/agentilda/feature.rb', line 135

def initialize(feature)
  @feature = feature
  @reads = {}
end

Instance Attribute Details

#featureAgentilda::Feature (readonly)

Returns:



141
142
143
# File 'lib/agentilda/feature.rb', line 141

def feature
  @feature
end

Instance Method Details

#allowedArray<Symbol>

Returns states reachable right now, guards applied.

Returns:

  • (Array<Symbol>)

    states reachable right now, guards applied



227
# File 'lib/agentilda/feature.rb', line 227

def allowed = machine.allowed

#best_fitAgentilda::Status?

Returns the state these contents justify.

Returns:



230
# File 'lib/agentilda/feature.rb', line 230

def best_fit = machine.best_fit

#block_answersArray<Integer>

The answers waiting in blocked.md, by number. ## A1 settles ## B1.

Waiting, not folded. An ## A<n> heading is not the same as a settled question: one may say in its own body that it is a draft pending a conversation. lando-broker makes that call; this only counts headings.

Returns:

  • (Array<Integer>)


208
# File 'lib/agentilda/feature.rb', line 208

def block_answers = Agentilda.block_numbers(read("blocked.md"), ANSWER_BLOCK)

#consistent?Boolean

Returns whether the name matches the contents.

Returns:

  • (Boolean)

    whether the name matches the contents



221
# File 'lib/agentilda/feature.rb', line 221

def consistent? = violation.nil?

#file?(name) ⇒ Boolean

Parameters:

  • name (String)

    a bare filename

Returns:

  • (Boolean)


148
# File 'lib/agentilda/feature.rb', line 148

def file?(name) = File.file?(File.join(feature.path, name))

#goalArray<String>

The specification's own Goal section, verbatim and at most two paragraphs. Anything that paraphrases the spec is a second copy that drifts; quoting it is not — which is why the pull request body and the index both read it from here rather than each writing their own.

Returns:

  • (Array<String>)

    paragraphs, empty when there is no Goal to read



167
168
169
170
171
172
# File 'lib/agentilda/feature.rb', line 167

def goal
  body = read("spec.md").to_s
  section = body[/^\#{"#"}{2,3}\s*Goals?\b[^\n]*\n+(.*?)(?=\n\#{"#"}{1,3}\s|\z)/mi, 1]

  paragraphs(section) || paragraphs(body.sub(/\A\s*\#{"#"}[^\n]*\n/, "")) || []
end

#machineAgentilda::StateMachine

Returns positioned at the current state.

Returns:



224
# File 'lib/agentilda/feature.rb', line 224

def machine = StateMachine.new(self)

#open_blocksArray<Integer>

The questions blocked.md still names, by number.

Empty means one of two very different things, and a caller that treats them alike is how a folder with thirty kilobytes of open questions gets reported as "nothing left open": either there is no blocked.md at all, or there is one whose questions are not written as ## B<n> and are therefore invisible to every part of this tool. Ask #file? which.

Returns:

  • (Array<Integer>)


199
# File 'lib/agentilda/feature.rb', line 199

def open_blocks = Agentilda.block_numbers(read("blocked.md"), OPEN_BLOCK)

#paragraphs(text) ⇒ Array<String>?

Specifications written before the template existed have no Goal section, and they are exactly the ones an index most needs to describe. So the opening prose stands in — skipping headings, quotes, lists and tables, which describe the document rather than the work.

Parameters:

  • text (String, nil)

Returns:

  • (Array<String>, nil)

    nil when there is no prose to be had



181
182
183
184
185
186
187
188
# File 'lib/agentilda/feature.rb', line 181

def paragraphs(text)
  found = text.to_s.strip.split(/\n{2,}/)
    .map(&:strip)
    .reject { |p| p.empty? || p.match?(/\A[\#>|\-*\d`_=]/) }
    .first(2)

  found.empty? ? nil : found
end

#pull_requestsArray<Agentilda::PullRequest>

Returns:



159
# File 'lib/agentilda/feature.rb', line 159

def pull_requests = @pull_requests ||= PullRequests.new(dir: feature.path).all

#read(name) ⇒ String?

Returns contents, or nil when absent.

Parameters:

  • name (String)

Returns:

  • (String, nil)

    contents, or nil when absent



152
153
154
155
156
# File 'lib/agentilda/feature.rb', line 152

def read(name)
  @reads.fetch(name) do
    @reads[name] = file?(name) ? File.read(File.join(feature.path, name), encoding: "UTF-8") : nil
  end
end

#rename_to(status) ⇒ Agentilda::Feature

Move the folder into status — the side effect a transition is.

The Feature is a frozen Data holding the old name, so it is replaced rather than mutated, and the memoized reads go with it.

Parameters:

Returns:

Raises:



240
241
242
243
244
245
246
247
248
249
250
251
# File 'lib/agentilda/feature.rb', line 240

def rename_to(status)
  return @feature if status.key == @feature.status.key

  target = File.join(File.dirname(@feature.path), @feature.dirname_as(status))
  unless Agentilda.move_directory(@feature.path, target)
    raise Error, "cannot rename #{@feature.dirname}#{File.basename(target)} already exists"
  end

  @reads = {}
  @pull_requests = nil
  @feature = Feature.parse(target) or raise Error, "#{File.basename(target)} is not a plan folder"
end

#statusAgentilda::Status

Returns the status the folder name claims.

Returns:



144
# File 'lib/agentilda/feature.rb', line 144

def status = feature.status

#unreadable_block?Boolean

A blocked.md this tool cannot read: the file is there, and not one question in it is written as ## B<n>. Nothing can drain it and nothing currently says so, which is the whole reason this exists.

Returns:

  • (Boolean)


215
# File 'lib/agentilda/feature.rb', line 215

def unreadable_block? = file?("blocked.md") && open_blocks.empty?

#violationString?

Returns why the folder's name is not justified.

Returns:

  • (String, nil)

    why the folder's name is not justified



218
# File 'lib/agentilda/feature.rb', line 218

def violation = status.violation(self)