Module: OKF::Pro::Log
- Defined in:
- lib/okf/pro/log.rb,
lib/okf/pro/log/edit.rb
Overview
log.md as data. Pure, for the same reason Board is.
Defined Under Namespace
Modules: Edit
Constant Summary collapse
- SNAPSHOT_LINE =
The shape of the line Rule 2 appends: a list bullet, then the word, then its colon — bolding optional, spacing free. Substring matching ("any line containing Snapshot") was a hole in both directions: a prose bullet like "- Rewrote the Snapshot checker" under today's heading posed as the day's snapshot, so the stop gate skipped its accurate missing-line refusal and reported twelve bogus disagreements instead — and the audit passed CI on a mention with no real snapshot anywhere.
/\A[[:blank:]]*[*+-][[:blank:]]+\*{0,2}Snapshot\*{0,2}[[:blank:]]*:/.freeze
- DAY_HEADING =
The full shape, anchored at both ends: unanchored,
## 2026-08-123captured a valid-looking date out of a typo and adopted the block, while## 2026-8-12(missing zero-pad) matched nothing and fell out of BOTH buckets — neither checked nor reported. /^## (\d{4}-\d{2}-\d{2})[[:blank:]\r]*$/.freeze
- DAYISH_HEADING =
What counts as an ATTEMPT at a day heading:
##then a digit. "## Notes" is a legal non-day heading; "## 2026-8-12" is a day heading gone wrong, and the difference is what keeps malformed_days from flagging prose while still catching every typo class. /^## [[:blank:]]*\d/.freeze
Class Method Summary collapse
-
.accepted_day(line) ⇒ Object
The heading acceptance rule, ONCE: the captured date when the line is a calendar-valid day heading, else nil.
-
.day_partition(text) ⇒ Object
One scan, one partition: every heading that attempts a date lands in exactly one bucket, so a rule change cannot open a gap where a heading is neither checked nor reported.
- .days(text) ⇒ Object
-
.each_day_block(text) {|[ day, line ]| ... } ⇒ Object
The one walk over the log's day blocks — every public reader below consumes it, so the heading rule, the calendar rule, and the last-line-wins rule cannot drift apart again (they did, twice: a hand-rolled heading compare beside DAY_HEADING, then a calendar check present in one loop and absent from the other).
-
.latest_snapshot_entry(text) ⇒ Object
The most recent snapshot in the file, by date, WITH the day it was found under.
-
.malformed_days(text) ⇒ Object
The headings days() refused, for the audit to say out loud — a heading no check will ever look under must not rot in silence.
-
.newest_day(text) ⇒ Object
The maximum date, not the first heading.
-
.snapshot_line(text, day) ⇒ Object
The day's Snapshot line.
-
.snapshot_under?(text, day) ⇒ Boolean
Under that day's heading and no further.
Class Method Details
.accepted_day(line) ⇒ Object
The heading acceptance rule, ONCE: the captured date when the line is a calendar-valid day heading, else nil. Both walks — the block iterator above and the audit's partition below — call this, so a tolerance change cannot land in one and leave the other silently disagreeing about which days exist.
56 57 58 59 |
# File 'lib/okf/pro/log.rb', line 56 def accepted_day(line) match = line.match(DAY_HEADING) match && Board.parse_date(match[1]) ? match[1] : nil end |
.day_partition(text) ⇒ Object
One scan, one partition: every heading that attempts a date lands in
exactly one bucket, so a rule change cannot open a gap where a
heading is neither checked nor reported. days() is the calendar's
accepted list — a typo'd ## 2026-08-32 string-sorts above every
real date, and unvalidated it became the "newest day" the audit
interrogated while the genuine newest day carried none.
91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 |
# File 'lib/okf/pro/log.rb', line 91 def day_partition(text) good = [] bad = [] text.to_s.each_line do |line| next unless line.match?(DAYISH_HEADING) day = accepted_day(line) if day good << day else bad << line.chomp.rstrip.sub(/\A## /, "") end end [ good, bad ] end |
.days(text) ⇒ Object
107 108 109 |
# File 'lib/okf/pro/log.rb', line 107 def days(text) day_partition(text)[0] end |
.each_day_block(text) {|[ day, line ]| ... } ⇒ Object
The one walk over the log's day blocks — every public reader below consumes it, so the heading rule, the calendar rule, and the last-line-wins rule cannot drift apart again (they did, twice: a hand-rolled heading compare beside DAY_HEADING, then a calendar check present in one loop and absent from the other). Yields [day, line] per block in file order: day is the heading's captured, calendar-valid date; line is the LAST snapshot line within that block — a corrected line is appended rather than history rewritten, and the last word is the current one — or nil when the block has none.
36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
# File 'lib/okf/pro/log.rb', line 36 def each_day_block(text) day = nil line = nil text.to_s.each_line do |l| if l.start_with?("## ") yield [ day, line ] if day day = accepted_day(l) line = nil next end line = l if day && l.match?(SNAPSHOT_LINE) end yield [ day, line ] if day end |
.latest_snapshot_entry(text) ⇒ Object
The most recent snapshot in the file, by date, WITH the day it was found
under. The session banner and okf pro state both label the parsed
counters "as of" that day rather than as live, and a label re-derived
by a second scan would be a second chance to disagree with the line it
labels. (A line-only sibling stood here until state became the one
source both surfaces read; it was a two-line delegation to this, and
only a unit test still called it.)
By DATE, not by position. Grepping the whole file and taking the last hit read the file's bottom, which in a newest-first log is the oldest entry: from day two onward the banner reported the first-ever counters, inverting the delta it exists to show.
One pass, O(filesize), because this runs at every session start on a file that only grows: the composed version cost a full rescan per candidate day, which was seconds on a years-old log exactly when discipline had lapsed and the banner mattered most. Two scalars, not per-day hashes — the strict greater-than keeps the first block of a repeated day, the newest-first winner, for free, and there is no second copy of the walk left to drift.
148 149 150 151 152 153 154 155 156 157 158 159 |
# File 'lib/okf/pro/log.rb', line 148 def latest_snapshot_entry(text) best_day = nil best_line = nil each_day_block(text) do |day, line| next unless line next unless best_day.nil? || day > best_day best_day = day best_line = line end [ best_day, best_line ] end |
.malformed_days(text) ⇒ Object
The headings days() refused, for the audit to say out loud — a heading no check will ever look under must not rot in silence.
113 114 115 |
# File 'lib/okf/pro/log.rb', line 113 def malformed_days(text) day_partition(text)[1] end |
.newest_day(text) ⇒ Object
The maximum date, not the first heading. The file's own convention is newest-first, but prose is the only thing that ever said so — an entry appended at the bottom made the audit interrogate the oldest day, which had its snapshot, and CI passed while the real newest day had none. What CI asks about is the newest day rather than today, because a push can land on a day nobody worked and a gate that fails on the calendar is a gate people learn to ignore.
124 125 126 |
# File 'lib/okf/pro/log.rb', line 124 def newest_day(text) days(text).max end |
.snapshot_line(text, day) ⇒ Object
The day's Snapshot line. The same date can head two blocks — two sessions in one day, or a merge — and every block counts, but the file runs newest-first, so the FIRST block holding a snapshot wins: taking the last block in file order handed the stale bottom copy to the very gate that verifies counters against the board.
66 67 68 69 70 71 |
# File 'lib/okf/pro/log.rb', line 66 def snapshot_line(text, day) each_day_block(text) do |d, line| return line if line && d == day.to_s end nil end |
.snapshot_under?(text, day) ⇒ Boolean
Under that day's heading and no further. The shell version set its flag at the day heading and never cleared it, so yesterday's snapshot satisfied today — which is exactly the check being asked for.
22 23 24 |
# File 'lib/okf/pro/log.rb', line 22 def snapshot_under?(text, day) !snapshot_line(text, day).nil? end |