Module: OKF::Pro::Board
- Defined in:
- lib/okf/pro/board.rb,
lib/okf/pro/board/edit.rb
Overview
board.md as data. Pure text in, numbers out — no disk, no bundle, so the counting rules that Rule 3 enforces can be tested without a fixture.
Defined Under Namespace
Modules: Edit Classes: Budget, Row
Constant Summary collapse
- INLINE_COMMENT =
Comments, as the BOARD defines them — one line, balanced:
<!-- ... -->is stripped wherever it sits, and a longer note is a stack of single-line comments. Every richer model failed open in a review round: character-granular regions let a<!--in one capture's prose swallow real lines up to a-->in another's; and line-start regions were position-dependent, so splicing the text after a closer changed what a second strip saw and the counters read a different board than the grammar. Balanced-on-one-line is the shape that survives ALMOST any splice — nested fragments can still juxtapose into a span no single reading of the source contains, which is why strip_comments runs to a fixpoint and confesses when it needed more than one pass. The rule is narrow enough to be predictable, not narrow enough to be inviolable, and the difference is a finding rather than a comment's promise. /<!--.*?-->/.freeze
- STRAY_BULLET =
A board line the counters can see starts
-at column zero — section_lines reads exactly that, everywhere. Any other bullet is a line every counter is blind to, including the cap:* 2026-08-18 — file the claimunder Deadlines counted as zero lines, and so did an in-flight demand behind an indented dash. The counters' own bullet rule, flagged instead of silently skipped. /\A(?:[*+][[:blank:]]|[[:blank:]]+[-*+][[:blank:]])/.freeze
- SECTIONS =
The six sections the counters read, exactly as the skill names them.
[ "In flight", "Backlog", "Waiting", "Inbox", "To read", "Deadlines" ].freeze
Class Method Summary collapse
-
.budget(text) ⇒ Object
The
In flight: k/CAPheader, or nil when the board has lost it. -
.chase_date(line) ⇒ Object
The
chase YYYY-MM-DDdate on a Waiting line, or nil. - .count(text, name) ⇒ Object
-
.date_findings(visible_text) ⇒ Object
The three date shapes, checked over text the caller already made visible — grammar() strips once and every pass here reads that one strip, through visible_section_lines.
-
.grammar(text) ⇒ Object
The counters read three shapes, exactly — a leading
- YYYY-MM-DDon Inbox and Deadlines lines, a literalchase YYYY-MM-DDon Waiting lines — and a dated line in any other spelling parses to nil and is silently compacted out of every count. -
.line_date(line) ⇒ Object
The leading
- YYYY-MM-DDdate a capture line carries, or nil. -
.missing_sections(visible_text) ⇒ Object
The section invariant, checked directly: each of the six canonical sections is present as an EXACT heading.
- .parse_date(text) ⇒ Object
- .rows(text) ⇒ Object
-
.section_lines(text, name) ⇒ Object
Lines under a
## <name>heading, up to the next##. -
.stray_bullets(text) ⇒ Object
past_first_headingis exactly what it says: the pass covers the whole board body from the first section on — the sections' order is the board's own, and a stray bullet between two sections is as invisible to the counters as one inside them. -
.strip_comments(text) ⇒ Object
Board text with comments removed — THE definition of "visible", and every reader consumes it: the counters, the budget face, the date checks, the stray-bullet pass, pairing's link scan.
-
.targets(line) ⇒ Object
Every bundle-absolute link target on a line.
- .visible(text) ⇒ Object
-
.visible_section_lines(visible_text, name) ⇒ Object
The section walk over text a caller already made visible — grammar() strips once and shares; the public section_lines strips for everyone else, and the fixpoint strip makes the double path harmless.
Class Method Details
.budget(text) ⇒ Object
The In flight: k/CAP header, or nil when the board has lost it. Matched
by its shape rather than by the first line containing the words, so a
sentence mentioning "in flight" elsewhere cannot be mistaken for the
budget face. The whole shape must sit on one line that IS the header:
anchored at line start (a capture line's "- deck in flight: 3/4" is
not the budget), digits and slash on that same line (\D* crawled
across newlines and fabricated a cap from the next N/M below; \s*
around the slash did the same one token later, reading a date on the
following line as a cap of 2026).
131 132 133 134 135 136 |
# File 'lib/okf/pro/board.rb', line 131 def budget(text) match = visible(text).match(%r{^\*{0,2}[Ii]n [Ff]light:\*{0,2}[^\S\n]*(\d+)[^\S\n]*/[^\S\n]*(\d+)}) return nil unless match Budget.new(match[1].to_i, match[2].to_i) end |
.chase_date(line) ⇒ Object
The chase YYYY-MM-DD date on a Waiting line, or nil. The chase date
is the only thing standing between a dependency and a month of drift,
which is why the snapshot counts the ones already past. The word is
boundary-anchored: without it, "purchase 2026-09-01" read as a chase
date, satisfied the grammar check built to demand one, and reported
an overdue chase nobody ever set.
169 170 171 172 173 174 |
# File 'lib/okf/pro/board.rb', line 169 def chase_date(line) match = line.to_s.match(/\bchase\s+(\d{4}-\d{2}-\d{2})/) return nil unless match parse_date(match[1]) end |
.count(text, name) ⇒ Object
93 94 95 |
# File 'lib/okf/pro/board.rb', line 93 def count(text, name) section_lines(text, name).size end |
.date_findings(visible_text) ⇒ Object
The three date shapes, checked over text the caller already made visible — grammar() strips once and every pass here reads that one strip, through visible_section_lines.
248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 |
# File 'lib/okf/pro/board.rb', line 248 def date_findings(visible_text) findings = [] visible_section_lines(visible_text, "Inbox").each do |line| next if line_date(line) findings << "Inbox line carries no leading '- YYYY-MM-DD' date, so the oldest-capture " \ "counter cannot see it: #{line.strip}" end visible_section_lines(visible_text, "Deadlines").each do |line| next if line_date(line) findings << "Deadlines line carries no leading '- YYYY-MM-DD' date, so the 7-day " \ "warning cannot see it: #{line.strip}" end visible_section_lines(visible_text, "Waiting").each do |line| next if chase_date(line) findings << "Waiting line carries no 'chase YYYY-MM-DD' date, so the past-chase " \ "counter cannot see it: #{line.strip}" end findings end |
.grammar(text) ⇒ Object
The counters read three shapes, exactly — a leading - YYYY-MM-DD on
Inbox and Deadlines lines, a literal chase YYYY-MM-DD on Waiting
lines — and a dated line in any other spelling parses to nil and is
silently compacted out of every count. Silent is the problem:
"- Due Aug 18: filing" made "deadlines within 7d" say 0, the stop
gate agreed with itself, and the deadline landed unclaimed. So the
unreadable line is a finding, raised at the audit and the stop gate,
instead of a zero nobody can distinguish from a quiet board. The
same goes for the bullet itself: a check that only read the lines
the counters read would have inherited their blindness, so the
stray-bullet pass walks the raw section text instead.
201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 |
# File 'lib/okf/pro/board.rb', line 201 def grammar(text) visible_text, unclosed, respliced = strip_comments(text) findings = [] if unclosed findings << "a '<!--' has no '-->' on its own line. Nothing is hidden — only a " \ "balanced single-line comment is a comment — but finish it or remove it: " \ "comment-intended text is being read as board content. For longer notes, " \ "stack single-line comments." end if respliced findings << "comment stripping only settled after a second pass — fragments spliced " \ "into a comment no single reading of the board contains, and text may " \ "have been swallowed with them. Rewrite the line without nested '<!--' " \ "fragments." end findings.concat(missing_sections(visible_text)) findings.concat(stray_bullets(visible_text)) findings.concat(date_findings(visible_text)) findings end |
.line_date(line) ⇒ Object
The leading - YYYY-MM-DD date a capture line carries, or nil. A date
the calendar rejects (month 13) is treated as absent rather than
guessed at — these feed counters, and a counter must not invent.
156 157 158 159 160 161 |
# File 'lib/okf/pro/board.rb', line 156 def line_date(line) match = line.to_s.match(/\A-\s+(\d{4}-\d{2}-\d{2})/) return nil unless match parse_date(match[1]) end |
.missing_sections(visible_text) ⇒ Object
The section invariant, checked directly: each of the six canonical sections is present as an EXACT heading. A near-miss heuristic at this spot was at the wrong depth — it missed the commonest quiet-zero typos ('## inbox', '## In Flight', '## Inbox' all disengaged every counter in silence) while permanently nagging the legitimate sibling pattern ('## In flight — parked' beside the real section). The claim was never "no heading resembles a section"; it is "every section exists", and checking that subsumes every typo, decoration, and deletion class at once.
234 235 236 237 238 239 240 241 242 243 |
# File 'lib/okf/pro/board.rb', line 234 def missing_sections(visible_text) present = visible_text.each_line.map do |line| line.chomp.rstrip.sub(/\A## /, "") if line.start_with?("## ") end.compact (SECTIONS - present).map do |name| "board has no '## #{name}' section — the counters match that heading exactly, so " \ "its class of lines has nowhere to land. Restore it; a re-spelled or decorated " \ "variant does not count." end end |
.parse_date(text) ⇒ Object
176 177 178 179 180 |
# File 'lib/okf/pro/board.rb', line 176 def parse_date(text) Date.parse(text) rescue ArgumentError nil end |
.rows(text) ⇒ Object
108 109 110 111 112 113 114 115 116 117 118 119 120 |
# File 'lib/okf/pro/board.rb', line 108 def rows(text) section = nil visible(text).each_line.with_object([]) do |line, acc| if line.start_with?("## ") section = line.chomp.rstrip.sub(/\A## /, "") next end next unless section && line.start_with?("- ") stripped = line.chomp acc << Row.new(section, stripped, line_date(stripped), chase_date(stripped), targets(stripped)) end end |
.section_lines(text, name) ⇒ Object
Lines under a ## <name> heading, up to the next ## . The board's
sections are the unit Rule 3 counts, so one reader serves the cap check
and the session banner alike.
70 71 72 |
# File 'lib/okf/pro/board.rb', line 70 def section_lines(text, name) visible_section_lines(visible(text), name) end |
.stray_bullets(text) ⇒ Object
past_first_heading is exactly what it says: the pass covers the
whole board body from the first section on — the sections' order is
the board's own, and a stray bullet between two sections is as
invisible to the counters as one inside them. Callers hand this the
board's VISIBLE text; grammar() strips comments once for the whole
pass, because a bulleted example inside documentation is not a
board line, and flagging one turned a comment into a refusal at
every Stop until the documentation was deleted.
279 280 281 282 283 284 285 286 287 288 |
# File 'lib/okf/pro/board.rb', line 279 def stray_bullets(text) past_first_heading = false text.to_s.each_line.with_object([]) do |line, acc| past_first_heading = true if line.start_with?("## ") next unless past_first_heading && line.match?(STRAY_BULLET) acc << "board line does not start with '- ' at column zero, so no counter " \ "can see it: #{line.strip}" end end |
.strip_comments(text) ⇒ Object
Board text with comments removed — THE definition of "visible", and every reader consumes it: the counters, the budget face, the date checks, the stray-bullet pass, pairing's link scan. Comment-awareness bolted onto one pass split the module against itself: a commented sample line was invisible to the stray check but counted by the cap and flagged by the date check that read it anyway.
The strip runs to a FIXPOINT, so re-stripping visible text is a
no-op by construction rather than by hope — pathological splices
can juxtapose fragments into a new balanced span, and a strip that
stopped after one pass let the second reader see a different board.
Returns [visible, unclosed?, respliced?]: any <!-- still standing has no
closer on its line — an intended comment gone wrong, and it is NOT
hidden. The caller confesses it, because comment-intended text read
as board content (a stray chase date, a sample capture) must not
drive counters in silence.
44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
# File 'lib/okf/pro/board.rb', line 44 def strip_comments(text) visible = text.to_s passes = 0 loop do stripped = visible.gsub(INLINE_COMMENT, "") break if stripped == visible visible = stripped passes += 1 end # A second effective pass means the first strip spliced fragments # into a comment no single reading of the source contains — text # was swallowed that the writer never wrapped. Rare and contrived, # but silent was exactly how the docstring's old "splicing cannot # break this" claim failed, so convergence beyond one pass is a # confession, not a curiosity. [ visible, visible.include?("<!--"), passes > 1 ] end |
.targets(line) ⇒ Object
Every bundle-absolute link target on a line. Two spellings, both real:
the markdown form [text](/path) project lines use, and the bare
[/path] form capture and conflict lines use — five-second capture
does not stop to type a link twice. Anything not starting with / is
not a bundle link and is not this module's business.
143 144 145 146 147 148 149 150 151 |
# File 'lib/okf/pro/board.rb', line 143 def targets(line) text = line.to_s markdown = text.scan(%r{\]\((/[^)\s]+)\)}).flatten = text.scan(%r{\[(/[^\]\s]+)\](?!\()}).flatten # Fragments are for readers; the file they resolve against has none. # Kept, `/reference/x.md#tiers` failed the existence check against an # entry list that only ever holds `/reference/x.md`. (markdown + ).map { |t| t.sub(/#.*/, "") } end |
.visible(text) ⇒ Object
63 64 65 |
# File 'lib/okf/pro/board.rb', line 63 def visible(text) strip_comments(text)[0] end |
.visible_section_lines(visible_text, name) ⇒ Object
The section walk over text a caller already made visible — grammar() strips once and shares; the public section_lines strips for everyone else, and the fixpoint strip makes the double path harmless.
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
# File 'lib/okf/pro/board.rb', line 77 def visible_section_lines(visible_text, name) heading = "## #{name}" inside = false visible_text.each_line.with_object([]) do |line, acc| if line.start_with?("## ") # Exact, not a prefix — the same defect log.rb already paid # for: start_with? folded "## In flight — parked" into # "In flight", inflating the count and turning a correct # budget header into a refusal. inside = (line.chomp.rstrip == heading) next end acc << line if inside && line.start_with?("- ") end end |