Module: OKF::Pro::Writes

Defined in:
lib/okf/pro/writes.rb

Overview

The mechanical writers — the shapes with exactly one correct form.

WHAT THESE ARE FOR. A dated Inbox line, a promotion, the day's journal file, the three mechanical moves of closure: each has one right answer, and each was being reconstructed from prose on every use — a third of a measured session's tool output was the skill's guides, read to learn the grammar of a line. Grammar a Ruby function knows is grammar nobody has to re-read.

THE WRITE CONTRACT, and it is the whole design:

Additive and targeted, never regenerative. A write verb may append a
line or edit the line it was given. No verb rewrites a file it did not
fully derive from that file's own prior contents. A writer never
satisfies its own gate.

Every method below is the same four steps: read, transform PURELY (in Board::Edit or Log::Edit, which cannot touch the disk), state the delta to Conserve and refuse if the actual delta differs, then write. The refusal is exit 2 and nothing lands — fail-closed applied to writes.

TWO SAFETY PROPERTIES, held by construction rather than by guarding — necessary, because a verb invoked through Bash is seen by neither guard-verified (Edit/Write only) nor shell-guard (no mutator pattern in okf pro capture):

1. No verb interpolates agent-supplied text into frontmatter, ever.
 Agent text reaches a board line body or a journal entry body and
 nowhere else — and a newline in it is refused rather than escaped,
 because a text that spans lines is a text that can carry a `---`.
2. Selectors are keyed, never positional, and refuse on ambiguity.

And one thing they deliberately do NOT do: snapshot gains no --write. A writer and a checker sharing a code path agree trivially and prove nothing, so the stop gate keeps its independent read.

Defined Under Namespace

Classes: Plan, Result

Constant Summary collapse

PROMOTE_FROM =

── promote / demote ─────────────────────────────────────────────────

%w[Inbox Backlog].freeze
EMPTY_JOURNAL_NOTE =

The seeded index's own sentence. Removed only when it is still exactly that — an adopter who reworded it owns their words, and a verb that pattern-matched near-misses would be editing prose it did not write.

"Nothing recorded yet. The first entry is the first day this bundle is used,\n" \
"dated by you, not by the template.\n"
HEADING =

A markdown heading, and nothing else. The marker goes on the TITLE — that is the spelling the skill teaches and the only place closed? looks, since it reads three lines from the top.

The check is the shape of the line, not merely whether the result satisfies Pairing::MARKER, and that distinction is the finding: an index carrying YAML frontmatter starts with ---, and Pairing.marker?("--- — closed 2026-08-17") is TRUE — the regex needs only the word and a date. So the satisfied-marker check passed, the fence was destroyed, and the concept silently lost its type, title and description while okf validate still exited 0. Conserve cannot see it either: the mangling was the declared edit.

/\A\#{1,6}[[:blank:]]+\S/.freeze

Class Method Summary collapse

Class Method Details

.board?(root) ⇒ Boolean

Returns:

  • (Boolean)


68
69
70
# File 'lib/okf/pro/writes.rb', line 68

def board?(root)
  File.file?(File.join(root, "board.md"))
end

.capture(root, text, today: Date.today) ⇒ Object



72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/okf/pro/writes.rb', line 72

def capture(root, text, today: Date.today)
  return no_board(root, "capture") unless board?(root)

  escape = escaping(root, "capture", "board.md")
  return escape if escape

  body = text.to_s
  return refuse("okf pro capture — takes the words to capture: `okf pro capture \"what you heard\"`.") if body.strip.empty?

  if body.include?("\n")
    return refuse("okf pro capture — the text spans lines, and a capture is one line: the Inbox " \
                  "counter reads a line at a time, and a multi-line write is the one shape that " \
                  "could carry a `---` into a file this verb is not allowed to restructure.")
  end

  board_path = File.join(root, "board.md")
  before = Pro.read_text(board_path)
  line = ::OKF::Pro::Board::Edit.capture_line(body, today)
  after, error = ::OKF::Pro::Board::Edit.append_to_section(before, "Inbox", line)
  return refuse("okf pro capture — #{error}") if error

  commit(board_path, before, after, "capture", added: [ line ]) do
    [ "okf pro capture — one line added to Inbox:", "  #{line}" ]
  end
end

.close(root, project, today: Date.today) ⇒ Object



299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
# File 'lib/okf/pro/writes.rb', line 299

def close(root, project, today: Date.today)
  return no_board(root, "close") unless board?(root)

  # The three spellings `Board::Edit.by_target` treats as one commitment
  # are one commitment here too — `alpha`, `/projects/alpha/`, and the
  # `/projects/alpha/index.md` a board line actually carries, which is
  # what `okf pro board` prints. A verb that refuses the string it just
  # showed you is a verb you stop trusting.
  #
  # Dropping the index filename is not the normalisation the check below
  # forbids: that one is about a path that LEAVES `projects/`, and this
  # runs before it, so `/projects/../../etc/index.md` still arrives at
  # `../../etc` and is still refused.
  slug = project.to_s.sub(%r{\A/?projects/}, "").chomp("/").sub(%r{/index\.md\z}, "")

  # One directory name, and nothing that can leave `projects/`. This verb
  # marks a file's first line and removes board lines, and `File.join`
  # resolves `..` happily — `okf pro close ../../somewhere` would put a
  # closure marker on a stranger's index. A project is one segment by the
  # structure's own rule (`projects/<slug>/`), so anything else is a
  # refusal rather than a normalisation: quietly rewriting a path the
  # caller gave is how a traversal becomes an edit nobody sees.
  unless slug.match?(%r{\A[^/\\]+\z}) && ![ ".", ".." ].include?(slug)
    return refuse("okf pro close — '#{project}' is not a project name. A project is one " \
                  "directory under projects/, so its name carries no slash and is not a " \
                  "relative path. `okf pro state` lists the open ones.")
  end

  dir = File.join(root, "projects", slug)
  return refuse("okf pro close — #{root}/projects/#{slug}/ does not exist. `okf pro state` lists the open projects.") unless File.directory?(dir)

  index_path = File.join(dir, "index.md")
  return refuse("okf pro close — projects/#{slug}/index.md does not exist, and closure is a marker on its first line.") unless File.file?(index_path)

  escape = escaping(root, "close", "projects/#{slug}/index.md", "board.md", "log.md")
  return escape if escape

  board_path = File.join(root, "board.md")
  log_path = File.join(root, "log.md")
  already = Pairing.closed?(root, index_path)
  lines = Board.rows(Pro.read_text(board_path)).select { |row| ::OKF::Pro::Board::Edit.by_target([ row ], slug).any? }

  return ok("okf pro close — projects/#{slug} is already closed and carries no board line. Nothing to do.") if already && lines.empty?

  plans = []
  unless already
    plan, refusal = plan_marker(index_path, slug, today)
    return refusal if refusal

    plans << plan
  end

  board_plan, refusal = plan_board(board_path, lines)
  return refusal if refusal

  plans << board_plan if board_plan

  log_plan, refusal = plan_log(log_path, slug, lines.size, today)
  return refusal if refusal

  plans << log_plan
  plans.each { |plan| Scaffold.write_atomically(plan.path, plan.text, false) }

  ok([ "okf pro close — projects/#{slug}:",
       already ? "  index.md already carried the closure marker" : "  index.md marked closed #{today}",
       "  #{lines.size} board line(s) removed",
       "  log.md entry added under #{today}",
       *snapshot_owed(log_path, today),
       "Owed, and yours: the durable part of this work belongs in learnings/ or glossary/ —",
       "extract it now, or it is archived with the project and relearned later at full price." ])
end

.closure_marker(first_line, today) ⇒ Object

The one place this gem WRITES the closure marker, and it is separate so the grammar can be pinned in all three directions at once: what the skill teaches, what Pairing::MARKER accepts, and what this emits. Two of those were already pinned to each other; a writer that emitted a fourth spelling would leave a project the checker still reads as open, with a sentence about closure in it.



492
493
494
# File 'lib/okf/pro/writes.rb', line 492

def closure_marker(first_line, today)
  "#{first_line.to_s.chomp.rstrip} — closed #{today}"
end

.commit(path, before, after, verb, added: [], removed: [], moved: []) ⇒ Object

Guard, then write, in that order and nowhere else. Every verb above funnels through here, so "refuses rather than writing" is a property of one method rather than a habit five of them share.



533
534
535
536
537
538
539
# File 'lib/okf/pro/writes.rb', line 533

def commit(path, before, after, verb, added: [], removed: [], moved: [])
  planned, refusal = plan(path, before, after, verb, added: added, removed: removed, moved: moved)
  return refusal if refusal

  Scaffold.write_atomically(planned.path, planned.text, false)
  ok(yield)
end

.conservation_refusal(verb, path, findings) ⇒ Object



551
552
553
554
555
556
557
# File 'lib/okf/pro/writes.rb', line 551

def conservation_refusal(verb, path, findings)
  [ "okf pro #{verb} — refused, and #{path} is untouched. The edit this verb computed does not " \
    "match the change it declared, which is the one failure a mechanical writer must not " \
    "commit: a view rewritten with a line quietly gone is failure mode 07, and this check is " \
    "why this verb cannot be it.",
    *findings.map { |f| "  #{f}" } ]
end

.contained?(root, path) ⇒ Boolean

THE DECISION THE CHECKER ALREADY MAKES, MADE AGAIN AT THE WRITE DOOR.

Pairing.closed? reads this same index through Pro.read_contained and answers "open" when the link leaves the bundle. Nothing carried that answer across to the writer, which read and rewrote the path with the uncontained pair — so okf pro close escapee marked a stranger's index.md, exited 0, and left okf pro audit still reporting the project as neither on the board nor closed. Run twice it appended a second marker, so it was unbounded as well as wrong.

.okf/contract/containment-directions.md is the rule and it decides this direction too: pick the one that leaves the demand visible. A refusal keeps the board line and keeps the project being asked about; a marker written outside the bundle is a stranger's file edited to satisfy a checker that will not read it back.

Asked of the INDEX, not of the directory holding it, because containing the directory does not contain the file: an index.md symlinked out of a perfectly contained projects/<slug>/ is read by Pro.read_text (which follows the link) and written back by File.rename (which does not), so the marker came from a stranger's title and landed as a real file inside the bundle. Containing the leaf contains both, because a directory that escapes takes every path under it with it.

Unrescued, deliberately. File.file? above has already resolved the whole path, so a SystemCallError here is the ground moving under the check — and write_verb runs every writer inside guarded, which answers exactly that with exit 2 and "nothing was read or written". A rescue returning false would say the same thing in a message about symlinks, and would be a branch no fixture can reach.

Returns:

  • (Boolean)


414
415
416
# File 'lib/okf/pro/writes.rb', line 414

def contained?(root, path)
  ::OKF::Path.under?(File.realpath(root), File.realpath(path))
end

.demote(root, selector) ⇒ Object



126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/okf/pro/writes.rb', line 126

def demote(root, selector)
  return no_board(root, "demote") unless board?(root)

  escape = escaping(root, "demote", "board.md")
  return escape if escape

  board_path = File.join(root, "board.md")
  before = Pro.read_text(board_path)
  row, error = ::OKF::Pro::Board::Edit.select(Board.rows(before), selector, sections: [ "In flight" ])
  return refuse("okf pro demote — #{error}") if error

  return refuse(no_header("demote")) if Board.budget(before).nil?

  move(board_path, before, row, "Backlog", Board.count(before, "In flight") - 1, "demote")
end

.dormancy_note(to_section) ⇒ Object

The judgment half, said out loud rather than done. A promoted demand owes a next action and a journal line; neither is mechanical, and a verb that wrote either would be writing prose.



173
174
175
176
177
178
# File 'lib/okf/pro/writes.rb', line 173

def dormancy_note(to_section)
  return [] unless to_section == "In flight"

  [ "  Owed, and yours: a next-action line and a journal entry linking it — a promotion nobody",
    "  journaled is a slot the dormancy question cannot see moving." ]
end

.escape_refusal(verb, root, rel) ⇒ Object



438
439
440
441
442
443
444
445
# File 'lib/okf/pro/writes.rb', line 438

def escape_refusal(verb, root, rel)
  refuse("okf pro #{verb}#{rel} resolves outside #{root}, so this verb will not write " \
         "there. `Pro.read_text` follows a symlink and the atomic rename does not, so the " \
         "read would come from a file this bundle does not own and the write would replace " \
         "the link with it. Every gate reads this path contained (`Pairing.closed?` answers " \
         "\"open\" rather than read one), and a writer that did not would be the loosest " \
         "door on the strictest file. Edit it by hand if the link is deliberate.")
end

.escaping(root, verb, *rels) ⇒ Object

EVERY file a verb reads and rewrites, not just the one that was found first. Containing projects/<slug>/index.md and leaving board.md, log.md and journal/index.md open left the identical hazard three files over: okf pro capture read a stranger's board through the link, appended, and renamed a temp over it — so the link became a real file inside the bundle carrying content the bundle never owned.

Checked before anything is read, and only for a file that exists: a missing one is each verb's own refusal to make, with its own message.



427
428
429
430
431
432
433
434
435
436
# File 'lib/okf/pro/writes.rb', line 427

def escaping(root, verb, *rels)
  rels.each do |rel|
    path = File.join(root, rel)
    next unless File.exist?(path)
    next if contained?(root, path)

    return escape_refusal(verb, root, rel)
  end
  nil
end

.journal_entry(day) ⇒ Object



267
268
269
270
271
272
273
274
275
276
277
278
279
# File 'lib/okf/pro/writes.rb', line 267

def journal_entry(day)
  <<~ENTRY
    ---
    type: Journal Entry
    title: "#{day}"
    description: What happened on #{day}, what it meant, and what was decided.
    ---

    # #{day}

    <!-- What happened, what it meant, what was decided. Delete this line. -->
  ENTRY
end

.journal_index(before, day) ⇒ Object

The delta is declared NET, because that is the only delta Conserve can see: it compares line multisets, so a blank line removed and then added back is invisible to it and undeclarable. Hence no tidying pass — the note comes out, the entry goes on the end, and a separator is added only where there is not one already.



247
248
249
250
251
252
253
254
255
256
257
258
259
# File 'lib/okf/pro/writes.rb', line 247

def journal_index(before, day)
  lines = Pro.newline_terminated(before).lines
  note = EMPTY_JOURNAL_NOTE.lines
  at = sublist_index(lines, note)
  removed = at ? lines.slice!(at, note.size) : []

  entry = "* [#{day}](#{day}.md) - the day's record.\n"
  added = []
  added << "\n" unless lines.empty? || lines.last.strip.empty? || lines.last.start_with?("*")
  added << entry
  lines.concat(added)
  [ lines.join, added, removed ]
end

.journal_open(root, today: Date.today) ⇒ Object



201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
# File 'lib/okf/pro/writes.rb', line 201

def journal_open(root, today: Date.today)
  day = today.to_s
  entry_path = File.join(root, "journal", "#{day}.md")
  index_path = File.join(root, "journal", "index.md")
  return refuse("okf pro journal open — #{root}/journal/ does not exist.") unless File.directory?(File.dirname(entry_path))
  return no_index(root) unless File.file?(index_path)

  escape = escaping(root, "journal open", "journal/index.md", "journal/#{day}.md")
  return escape if escape

  before = Pro.read_text(index_path)
  indexed = before.include?("(#{day}.md)")
  exists = File.exist?(entry_path)

  if exists && indexed
    return ok("okf pro journal open — journal/#{day}.md is already open. Write the day into it " \
              "with Edit; this verb only creates the file and its index line.")
  end

  unless indexed
    after, added, removed = journal_index(before, day)
    findings = Conserve.check(before, after, added: added, removed: removed)
    return refuse(conservation_refusal("journal open", index_path, findings)) unless findings.empty?
  end

  # A file that did not exist has nothing to conserve — the guard
  # compares a delta against a prior text, and there is none. The
  # conservation that matters here is the INDEX's, which did exist.
  #
  # Two writes, and atomicity stated honestly, exactly as `Scaffold` does:
  # each file is written atomically and the PAIR is not. The residue of a
  # half-done pair is a day with no index line, which is why the two
  # conditions above are asked SEPARATELY rather than off `File.exist?`
  # alone: re-running the verb finishes the half that did not land, and
  # a fixed-up state repairs itself instead of needing a maintainer.
  Scaffold.write_atomically(entry_path, journal_entry(day), false) unless exists
  Scaffold.write_atomically(index_path, after, false) unless indexed
  ok("okf pro journal open — journal/#{day}.md #{exists ? "was already there and is now" : "created and"} indexed.",
    "  The day itself is yours to write: what happened, what it meant, what was decided.")
end

.move(board_path, before, row, to_section, declared, verb) ⇒ Object

One line between two sections, and the budget face kept truthful in the same write. The face is not decoration: cap-check refuses a header that disagrees with the section under it, so a move that left it stale would hand the next edit somebody else's refusal.



146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# File 'lib/okf/pro/writes.rb', line 146

def move(board_path, before, row, to_section, declared, verb)
  moved, error = ::OKF::Pro::Board::Edit.move_line(before, row.text, to_section)
  return refuse("okf pro #{verb}#{error}") if error

  after, old_header, new_header = ::OKF::Pro::Board::Edit.set_declared(moved, declared)
  return refuse(no_header(verb)) if after.nil?

  commit(board_path, before, after, verb,
    moved: [ row.text ],
    added: [ new_header ].compact,
    removed: [ old_header ].compact) do
    [ "okf pro #{verb} — moved from #{row.section} to #{to_section}:",
      "  #{row.text}",
      new_header ? "  header now reads #{new_header.strip}" : nil,
      *dormancy_note(to_section) ].compact
  end
end

.no_board(root, verb) ⇒ Object

BundleRoot.resolve accepts an index beside EITHER core file, so a bundle can legitimately resolve with no board at all. Every verb below edits one, and "could not run (Errno::ENOENT)" is a true answer to the wrong question.



62
63
64
65
66
# File 'lib/okf/pro/writes.rb', line 62

def no_board(root, verb)
  refuse("okf pro #{verb}#{root} has no board.md to edit. `okf pro audit` reports the " \
         "missing core file; this verb will not create one, because a board written by a " \
         "verb is a board nobody decided the shape of.")
end

.no_header(verb) ⇒ Object



164
165
166
167
168
# File 'lib/okf/pro/writes.rb', line 164

def no_header(verb)
  "okf pro #{verb} — board.md has lost its 'In flight: k/CAP' header, which is Rule 3's " \
    "visible budget and this verb's only source for the cap. Restore it first; guessing a " \
    "cap is how a budget stops meaning anything."
end

.no_index(root) ⇒ Object

no_board's policy, for the other seeded file a verb edits. An index rebuilt from the one line this verb knows how to write comes back holding only that line — the # Journal heading and the seeded prose gone — which is regeneration wearing an append's clothes, and the shape constraint 8 forbids outright.



193
194
195
196
197
198
199
# File 'lib/okf/pro/writes.rb', line 193

def no_index(root)
  refuse("okf pro journal open — #{root}/journal/index.md does not exist, and this verb " \
         "adds a line to that index rather than writing one: an index rebuilt from the " \
         "single line it knows would lose the heading and the prose around it. Restore it " \
         "first — a `# Journal` heading is all this verb needs — because every day already " \
         "written is an orphan until it is back, which is what `okf pro audit` reports.")
end

.ok(*messages) ⇒ Object



48
49
50
# File 'lib/okf/pro/writes.rb', line 48

def ok(*messages)
  Result.new(true, messages.flatten)
end

.plan(path, before, after, verb, added: [], removed: [], moved: []) ⇒ Object

The guard, without the write — for a verb that changes more than one file and must decide about all of them before it changes any. Returns [Plan, nil] or [nil, Result].



544
545
546
547
548
549
# File 'lib/okf/pro/writes.rb', line 544

def plan(path, before, after, verb, added: [], removed: [], moved: [])
  findings = Conserve.check(before, after, added: added, removed: removed, moved: moved)
  return [ nil, refuse(conservation_refusal(verb, path, findings)) ] unless findings.empty?

  [ Plan.new(path, after), nil ]
end

.plan_board(board_path, rows) ⇒ Object



496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
# File 'lib/okf/pro/writes.rb', line 496

def plan_board(board_path, rows)
  return [ nil, nil ] if rows.empty?

  before = Pro.read_text(board_path)
  after = before
  rows.each { |row| after = ::OKF::Pro::Board::Edit.remove_line(after, row.text)[0] }

  # The face follows the section, and ONLY when the section moved. A
  # header that was already lying about a count this verb did not change
  # is Rule 3's finding to raise, not this verb's to quietly correct —
  # a writer that tidies what it was not asked about is regenerating.
  added = []
  removed = rows.map(&:text)
  if rows.any? { |row| row.section == "In flight" }
    after, old_header, new_header = ::OKF::Pro::Board::Edit.set_declared(after, Board.count(after, "In flight"))
    return [ nil, refuse(no_header("close")) ] if after.nil?

    if new_header
      added << new_header
      removed << old_header
    end
  end
  plan(board_path, before, after, "close", removed: removed, added: added)
end

.plan_log(log_path, slug, removed, today) ⇒ Object



521
522
523
524
525
526
# File 'lib/okf/pro/writes.rb', line 521

def plan_log(log_path, slug, removed, today)
  before = File.exist?(log_path) ? Pro.read_text(log_path) : ""
  line = "* Closed [/projects/#{slug}/](/projects/#{slug}/index.md) — #{removed} board line(s) removed."
  after, added = ::OKF::Pro::Log::Edit.add_entry(before, today, line)
  plan(log_path, before, after, "close", added: added)
end

.plan_marker(index_path, slug, today) ⇒ Object



461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
# File 'lib/okf/pro/writes.rb', line 461

def plan_marker(index_path, slug, today)
  before = Pro.read_text(index_path)
  first = before.lines.first
  return [ nil, refuse("okf pro close — projects/#{slug}/index.md is empty; there is no first line to mark.") ] if first.nil?

  unless first.match?(HEADING)
    return [ nil, refuse("okf pro close — projects/#{slug}/index.md does not open with a heading " \
                         "(#{first.strip.inspect}). Closure marks the title, and this verb will not " \
                         "restructure a file to find one: an index carrying YAML frontmatter opens " \
                         "with `---`, and a marker appended there would destroy the fence while " \
                         "still reading as closed. Give it a `# Title` first line, or mark it by hand.") ]
  end

  marked = closure_marker(first, today)
  unless Pairing.marker?(marked)
    return [ nil, refuse("okf pro close — marking projects/#{slug}/index.md would not produce a closure " \
                         "marker the checker accepts (`#{marked}`). Fix its first line first.") ]
  end

  # Block form: a title carrying `\1` or `\0` would otherwise be read as a
  # backreference by the replacement string and splice the match back in.
  after = before.sub(first) { "#{marked}\n" }
  plan(index_path, before, after, "close", added: [ marked ], removed: [ first.chomp ])
end

.promote(root, selector) ⇒ Object



102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/okf/pro/writes.rb', line 102

def promote(root, selector)
  return no_board(root, "promote") unless board?(root)

  escape = escaping(root, "promote", "board.md")
  return escape if escape

  board_path = File.join(root, "board.md")
  before = Pro.read_text(board_path)
  row, error = ::OKF::Pro::Board::Edit.select(Board.rows(before), selector, sections: PROMOTE_FROM)
  return refuse("okf pro promote — #{error}") if error

  budget = Board.budget(before)
  return refuse(no_header("promote")) if budget.nil?

  count = Board.count(before, "In flight")
  if count + 1 > budget.cap
    return refuse("okf pro promote — RULE 3: #{count} in flight against a cap of #{budget.cap}, " \
                  "so promoting makes #{count + 1}. Promotion requires demotion (`okf pro demote`), " \
                  "or a visible renegotiation of the cap — which is journal-worthy, and yours to make.")
  end

  move(board_path, before, row, "In flight", count + 1, "promote")
end

.refuse(*messages) ⇒ Object



52
53
54
# File 'lib/okf/pro/writes.rb', line 52

def refuse(*messages)
  Result.new(false, messages.flatten)
end

.snapshot_owed(log_path, today) ⇒ Object

Said here because this verb is what creates the obligation. Writing a log entry gives the day a ## <date> heading, and from that moment the audit — which the pre-commit door runs — asks that day for its Snapshot line. A verb that quietly turned the next commit into a refusal without naming the fix would be teaching its user that the gates are arbitrary.



376
377
378
379
380
381
382
# File 'lib/okf/pro/writes.rb', line 376

def snapshot_owed(log_path, today)
  return [] unless File.exist?(log_path)
  return [] if Log.snapshot_line(Pro.read_text(log_path), today.to_s)

  [ "  #{today} now has a log heading and owes its Snapshot line — the audit the pre-commit",
    "  door runs asks the newest day for one. `okf pro snapshot` computes it." ]
end

.sublist_index(lines, sub) ⇒ Object



261
262
263
264
265
# File 'lib/okf/pro/writes.rb', line 261

def sublist_index(lines, sub)
  return nil if sub.empty?

  (0..(lines.size - sub.size)).find { |i| lines[i, sub.size] == sub }
end