Class: Agentilda::Subject
- Inherits:
-
Object
- Object
- Agentilda::Subject
- 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
- #feature ⇒ Agentilda::Feature readonly
Instance Method Summary collapse
-
#allowed ⇒ Array<Symbol>
States reachable right now, guards applied.
-
#best_fit ⇒ Agentilda::Status?
The state these contents justify.
-
#block_answers ⇒ Array<Integer>
The answers waiting in
blocked.md, by number. -
#consistent? ⇒ Boolean
Whether the name matches the contents.
- #file?(name) ⇒ Boolean
-
#goal ⇒ Array<String>
The specification's own Goal section, verbatim and at most two paragraphs.
-
#initialize(feature) ⇒ Subject
constructor
A new instance of Subject.
-
#machine ⇒ Agentilda::StateMachine
Positioned at the current state.
-
#open_blocks ⇒ Array<Integer>
The questions
blocked.mdstill names, by number. -
#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.
- #pull_requests ⇒ Array<Agentilda::PullRequest>
-
#read(name) ⇒ String?
Contents, or nil when absent.
-
#rename_to(status) ⇒ Agentilda::Feature
Move the folder into
status— the side effect a transition is. -
#status ⇒ Agentilda::Status
The status the folder name claims.
-
#unreadable_block? ⇒ Boolean
A
blocked.mdthis tool cannot read: the file is there, and not one question in it is written as## B<n>. -
#violation ⇒ String?
Why the folder's name is not justified.
Constructor Details
#initialize(feature) ⇒ Subject
Returns a new instance of Subject.
135 136 137 138 |
# File 'lib/agentilda/feature.rb', line 135 def initialize(feature) @feature = feature @reads = {} end |
Instance Attribute Details
#feature ⇒ Agentilda::Feature (readonly)
141 142 143 |
# File 'lib/agentilda/feature.rb', line 141 def feature @feature end |
Instance Method Details
#allowed ⇒ Array<Symbol>
Returns states reachable right now, guards applied.
227 |
# File 'lib/agentilda/feature.rb', line 227 def allowed = machine.allowed |
#best_fit ⇒ Agentilda::Status?
Returns the state these contents justify.
230 |
# File 'lib/agentilda/feature.rb', line 230 def best_fit = machine.best_fit |
#block_answers ⇒ Array<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.
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.
221 |
# File 'lib/agentilda/feature.rb', line 221 def consistent? = violation.nil? |
#file?(name) ⇒ Boolean
148 |
# File 'lib/agentilda/feature.rb', line 148 def file?(name) = File.file?(File.join(feature.path, name)) |
#goal ⇒ Array<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.
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 |
#machine ⇒ Agentilda::StateMachine
Returns positioned at the current state.
224 |
# File 'lib/agentilda/feature.rb', line 224 def machine = StateMachine.new(self) |
#open_blocks ⇒ Array<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.
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.
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_requests ⇒ Array<Agentilda::PullRequest>
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.
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.
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 |
#status ⇒ Agentilda::Status
Returns the status the folder name claims.
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.
215 |
# File 'lib/agentilda/feature.rb', line 215 def unreadable_block? = file?("blocked.md") && open_blocks.empty? |
#violation ⇒ String?
Returns why the folder's name is not justified.
218 |
# File 'lib/agentilda/feature.rb', line 218 def violation = status.violation(self) |