Module: OKF::Pro::Budget
- Defined in:
- lib/okf/pro/budget.rb
Overview
Rule 3 — in flight is a budget.
Two refusals, and the second matters as much as the first. Over the cap is overload. A header that disagrees with the section under it is worse than no header: the board's whole job is to be the one page you can trust at a glance, and a budget face that lies costs more than an absent one.
Constant Summary collapse
- DORMANCY_DAYS =
Five working days without a journal link is the dormancy window — the budget's own question, not a verdict.
It is NOT tunable, and the comment here used to say it was ("tuned in the skill's Rule 3"), which was false in the direction that costs the most: the skill states the number in prose, an adopter who changed it there would see nothing happen, and the gate would keep asking on the old window while the written rule said otherwise. Nobody has asked for a knob; what the situation needed was for the two statements to be one fact.
test/unit/dormancy_window_test.rbpins the skill's number to this constant, so changing it here fails until the skill follows. 5
Class Method Summary collapse
- .cap_check(target) ⇒ Object
- .check_text(board) ⇒ Object
-
.dormancy_questions(root, today: Date.today, board: nil) ⇒ Object
The dormancy questions: in-flight demands no journal entry has linked in DORMANCY_DAYS working days.
- .journal_entries(root) ⇒ Object
- .safely(root, path) ⇒ Object
-
.window_start(today, days = DORMANCY_DAYS) ⇒ Object
Walk back from today until the window holds DORMANCY_DAYS working days.
Class Method Details
.cap_check(target) ⇒ Object
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
# File 'lib/okf/pro/budget.rb', line 27 def cap_check(target) return [] if target.nil? return [] unless target.rel == "board.md" return [] unless target.exist?("board.md") board = target.read("board.md") # Grammar rides along at the write-time door on purpose: the stray # bullet and the unreadable date are exactly the lines the counters # below cannot see, and a cap check that stays quiet while its own # input is partly invisible has already lost the argument. Audit # and stop gate run the same pass; this door ran only the counters, # so the one moment the writer was still holding the pen was the # one moment nothing spoke. check_text(board) + Board.grammar(board) end |
.check_text(board) ⇒ Object
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
# File 'lib/okf/pro/budget.rb', line 43 def check_text(board) board = Board.visible(board) budget = Board.budget(board) unless budget return [ "board.md lost its 'In flight: k/CAP' header — Rule 3's visible budget. Restore it first." ] end count = Board.count(board, "In flight") if count > budget.cap return [ "RULE 3 — #{count} in flight against a cap of #{budget.cap}. Promotion requires demotion, " \ "or a visible renegotiation (new cap in the header; renegotiations are journal-worthy)." ] end if budget.declared != count return [ "Header claims #{budget.declared} in flight; the section holds #{count}. " \ "Fix the header — a wrong budget face is worse than none." ] end [] end |
.dormancy_questions(root, today: Date.today, board: nil) ⇒ Object
The dormancy questions: in-flight demands no journal entry has linked in DORMANCY_DAYS working days. Questions, never verdicts — "still in flight, or backlog pretending? Either answer is fine; holding a slot without moving is not."
Two silences are deliberate. A journal younger than the window stays quiet — a bundle in its first week cannot be dormant, only new. And a demand whose promotion was journaled (promotions are journal-worthy) is linked from day one, so the fresh-promotion false positive is already covered by the discipline this question serves.
75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 |
# File 'lib/okf/pro/budget.rb', line 75 def dormancy_questions(root, today: Date.today, board: nil) board_path = File.join(root, "board.md") return [] if board.nil? && !File.exist?(board_path) entries = journal_entries(root) return [] if entries.empty? start = window_start(today) return [] if entries.map(&:first).min > start # Contained: the root is in hand, and a journal entry symlinked in from # outside the bundle is not this bundle's record of a day. One that # escapes is dropped rather than raising — dormancy is a question, not a # gate, and an unreadable entry means "no evidence of work", which is # the answer that keeps the question being asked. recent = entries.select { |day, _| day >= start } .map { |_, path| safely(root, path) } .join("\n").downcase lines = Board.section_lines(board || safely(root, board_path), "In flight") questions = lines.map do |line| slugs = Snapshot.project_slugs(line) next if slugs.empty? next if slugs.any? { |slug| recent.include?("/projects/#{slug}") } "Rule 3, dormancy — no journal entry has linked /projects/#{slugs.first}/ in " \ "#{DORMANCY_DAYS} working days: still in flight, or backlog pretending?" end.compact # Law 2: the check confesses its own blind spot rather than skipping it # in silence — a demand with no project link is one dormancy cannot see. unlinked = lines.count { |line| Snapshot.project_slugs(line).empty? } questions << "Note: #{unlinked} in-flight line(s) carry no /projects/ link — dormancy cannot see them." if unlinked.positive? questions end |
.journal_entries(root) ⇒ Object
118 119 120 121 122 123 124 125 126 |
# File 'lib/okf/pro/budget.rb', line 118 def journal_entries(root) Dir.glob(File.join(root, "journal", "*.md")).map do |path| match = File.basename(path).match(/\A(\d{4}-\d{2}-\d{2})\.md\z/) next unless match day = Board.parse_date(match[1]) [ day, path ] if day end.compact end |
.safely(root, path) ⇒ Object
112 113 114 115 116 |
# File 'lib/okf/pro/budget.rb', line 112 def safely(root, path) Pro.read_contained(root, path) rescue ::OKF::Path::Error, ::Errno::ENOENT "" end |
.window_start(today, days = DORMANCY_DAYS) ⇒ Object
Walk back from today until the window holds DORMANCY_DAYS working days. Weekends extend the window rather than counting against it — legitimate stillness is what keeps the dormancy alarm honest.
131 132 133 134 135 136 137 138 139 140 141 |
# File 'lib/okf/pro/budget.rb', line 131 def window_start(today, days = DORMANCY_DAYS) date = today counted = 0 loop do counted += 1 if (1..5).cover?(date.wday) break if counted >= days date -= 1 end date end |