Module: OKF::Pro::Pairing
- Defined in:
- lib/okf/pro/pairing.rb
Overview
The board↔work invariants, in both directions: everything open is on the board, everything the board links is still real, and everything owed is still owed. These are what an editor-made commit bypasses entirely, which is why they are also the CI door's payload.
Pairing is by link target, never by substring. The first cut asked
board.include?(slug), which a project name mentioned in any unrelated
capture line satisfied — a false pass, in the fail-open direction this
checker exists to close.
Constant Summary collapse
- MARKER =
Closure is one marker, matched as one pattern: the word "closed" — not hyphen-joined into a name — then its date, with only marker punctuation between, read across the top of the index (three lines, the old tolerance), never the body. Every weaker reading failed in practice: bare /closed/i made titles and verbs into closures; word-plus-date-anywhere closed dated titles; a forty-character gap admitted prose; whitelisting "on" re-admitted "The office closed on 2026-01-05"; and an unguarded comma admitted "we closed, 2026-01-05, the books". The accepted spellings are enumerated ONCE, in the skill's closing ritual; MARKER is that enumeration, and the grammar tests pin both lists to each other.
/\bclosed\b(?!-)[[:blank:]*(\u2014\u2013:-]*\d{4}-\d{2}-\d{2}/i.freeze
- NEGATED =
A negation must not close the thing it denies. This is a blacklist of negators, and honestly so — a two-literal lookbehind briefly posed as "not negated" while "not closed", a double space, or "wasn't" walked straight past it. Markup and punctuation may sit between the negator and the verb, exactly as they may sit between the verb and its date.
/\b(?:not|never|no longer|isn['\u2019]?t|wasn['\u2019]?t|hasn['\u2019]?t)[\W_]*closed\b/i.freeze
Class Method Summary collapse
-
.awaiting_read?(concept) ⇒ Boolean
── the read-owed rule, in the kernel's vocabulary ─────────────────────.
-
.board_lines(board) ⇒ Object
Visible lines only — a commented-out sample line must not be a pairing target or a broken-link finding, for the same reason the counters do not count it.
- .board_targets(board) ⇒ Object
- .briefing?(concept) ⇒ Boolean
-
.broken_targets(root, board) ⇒ Object
Reverse: every bundle link the board carries lands on something real.
-
.bundle_entries(root) ⇒ Object
Downcased so pairing behaves the same on a case-sensitive filesystem as on the case-insensitive one the bundle was probably written on.
-
.closed?(root, index_path) ⇒ Boolean
Read through the kernel's containment primitive rather than File.read: this holds a root, and
SafeRead.read!resolves the path and refuses one whose symlinks escape it. -
.closed_project_links(root, board) ⇒ Object
Reverse: closure removes board lines wholesale, so a line still linking a closed project is the ritual left half-done.
- .closed_projects(root) ⇒ Object
-
.dirty_markdown?(root) ⇒ Boolean
The one shell-out in the whole checker.
-
.failures(root, board: nil, concepts: nil) ⇒ Object
board:andconcepts:let a caller that already paid for the read hand it over — the audit reads the board and parses the bundle for its other checks, and a second parse of every concept on the pre-commit hot path buys nothing. - .marker?(line) ⇒ Boolean
- .open_projects(root) ⇒ Object
- .owner_read?(concept) ⇒ Boolean
- .project_dirs(root) ⇒ Object
-
.stale_read_lines(concepts, board) ⇒ Object
Reverse: a read-owed line whose briefing is already verified.
-
.unpaired_briefings(concepts, targets) ⇒ Object
Forward: every generated-without-verified briefing is linked from some board line — the To-read line it was born paired with, or the conflict line that is also a claim on attention.
-
.unpaired_projects(root, targets) ⇒ Object
Forward: every open project is linked from some board line.
- .unverified_ids(concepts) ⇒ Object
- .verified_reference_targets(concepts) ⇒ Object
Class Method Details
.awaiting_read?(concept) ⇒ Boolean
── the read-owed rule, in the kernel's vocabulary ─────────────────────
These two are exact opposites and MUST stay so, or a briefing falls
through both: unverified_ids says "the board still owes this a To-read
line", verified_reference_targets says "the line can go". They are
read by unpaired_briefings and stale_read_lines respectively, plus
Attestation.report and the snapshot counter — four call sites, one
rule, and the rule is:
a briefing is owed a read until a HUMAN has verified it.
okf 2.0 answers that directly. declared_generated? is raw-key
detection, so it distinguishes hand-written (no provenance at all) from
v0.1-with-a-timestamp, which the old frontmatter["generated"] test
conflated with the fallback. And trust_tier reads §5.3: no verified
is unverified, a process: or agent actor is machine-confirmed, a
human:<id> actor is human-reviewed.
The tier is what makes these opposites rather than merely different.
Testing frontmatter["verified"] for truthiness made a nightly
process:-verified briefing simultaneously awaiting the owner's read
to one caller and verified, drop the line to the other — the board
line vanished and the read was never owed to anyone again. Moving one
site and not the other manufactures exactly that state, which is why
this comment sits over both of them.
217 218 219 |
# File 'lib/okf/pro/pairing.rb', line 217 def awaiting_read?(concept) concept.declared_generated? && concept.trust_tier != :human_reviewed end |
.board_lines(board) ⇒ Object
Visible lines only — a commented-out sample line must not be a pairing target or a broken-link finding, for the same reason the counters do not count it.
106 107 108 |
# File 'lib/okf/pro/pairing.rb', line 106 def board_lines(board) Board.visible(board).each_line.select { |l| l.start_with?("- ") } end |
.board_targets(board) ⇒ Object
110 111 112 |
# File 'lib/okf/pro/pairing.rb', line 110 def board_targets(board) board_lines(board).flat_map { |l| Board.targets(l) }.map(&:downcase) end |
.briefing?(concept) ⇒ Boolean
225 226 227 |
# File 'lib/okf/pro/pairing.rb', line 225 def briefing?(concept) concept.id.start_with?("reference/") end |
.broken_targets(root, board) ⇒ Object
Reverse: every bundle link the board carries lands on something real.
okf lint checks links inside concepts, but the board's capture lines
use the bare [/path] form lint never parses — rot here is invisible
to everything but this.
59 60 61 62 63 64 65 66 67 68 |
# File 'lib/okf/pro/pairing.rb', line 59 def broken_targets(root, board) entries = bundle_entries(root) board_lines(board).flat_map do |line| Board.targets(line).map do |target| next if entries.include?(target.downcase.chomp("/")) "— pairing: board line links #{target}, which does not exist in the bundle." end.compact end end |
.bundle_entries(root) ⇒ Object
Downcased so pairing behaves the same on a case-sensitive filesystem as on the case-insensitive one the bundle was probably written on.
116 117 118 119 |
# File 'lib/okf/pro/pairing.rb', line 116 def bundle_entries(root) Dir.glob(File.join(root, "**", "*")) .map { |p| "/#{p[(root.size + 1)..-1]}".downcase.chomp("/") } end |
.closed?(root, index_path) ⇒ Boolean
Read through the kernel's containment primitive rather than File.read:
this holds a root, and SafeRead.read! resolves the path and refuses
one whose symlinks escape it. Byte-identical output for every real
file — the .scrub is still this gem's, because read! tags the
encoding without validating it and one invalid byte anywhere raises out
of the first regex, which the hook protocol reads as non-blocking.
NOT applied at BundleRoot.root_index?, which has no root to contain
against — it is deciding what the root is — and where a raise would be a
permanent lockout and a rescue would answer "not a root", mis-rooting
the bundle and disarming the journal guard.
Contained against the BUNDLE root, which every caller already holds.
Deriving it as dirname(dirname(index_path)) gave <bundle>/projects,
so SafeRead refused any project index whose realpath left projects/
— including one that never leaves the bundle. A project legitimately
archived behind a symlink (projects/beta -> ../archive/beta) read as
open forever, and the audit demanded a board line for work closed
months ago. The containment rule is about the bundle, and a root
invented from the path being checked is not the bundle.
181 182 183 184 185 186 187 188 189 190 |
# File 'lib/okf/pro/pairing.rb', line 181 def closed?(root, index_path) return false unless File.exist?(index_path) Pro.read_contained(root, index_path).lines.first(3).any? { |line| marker?(line) } rescue ::OKF::Path::Error, ::Errno::ENOENT # A project index reached only through a symlink out of the bundle is # not a closure marker anyone may rely on. Open is the safe answer: an # open project keeps its board line, a closed one loses it. false end |
.closed_project_links(root, board) ⇒ Object
Reverse: closure removes board lines wholesale, so a line still linking a closed project is the ritual left half-done.
72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
# File 'lib/okf/pro/pairing.rb', line 72 def closed_project_links(root, board) closed = closed_projects(root) return [] if closed.empty? board_lines(board).flat_map do |line| Board.targets(line).map do |target| slug = target[%r{\A/projects/([^/\s]+)}i, 1] next unless slug && closed.include?(slug.downcase) "— pairing: board line still links projects/#{slug}, which is closed — " \ "closure removes its board lines." end.compact end end |
.closed_projects(root) ⇒ Object
127 128 129 130 131 |
# File 'lib/okf/pro/pairing.rb', line 127 def closed_projects(root) project_dirs(root).map do |dir| File.basename(dir).downcase if closed?(root, File.join(dir, "index.md")) end.compact end |
.dirty_markdown?(root) ⇒ Boolean
The one shell-out in the whole checker. -- plus the pathspec keeps it
to markdown, so a change confined to hooks or CI does not summon Rule 2's
end-of-day questions.
250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 |
# File 'lib/okf/pro/pairing.rb', line 250 def dirty_markdown?(root) out = IO.popen( [ "git", "-C", root, "status", "--porcelain", "--", "*.md" ], err: File::NULL, &:read ) # Only a git that ran and answered may disengage the gate. A git that # failed — dubious ownership, a corrupted .git, a root outside any # repository — is not "no work happened"; it is "nobody can tell", # and a gate that cannot tell must not wave the session through. # Returning false here was the silent-disarm hole: stderr was nulled, # the empty read looked clean, and the stop gate asked no questions. return true unless $?.success? !out.to_s.strip.empty? rescue Errno::ENOENT # No git at all is the same state as a failing one — nobody can tell # whether work happened — and gets the same answer. true end |
.failures(root, board: nil, concepts: nil) ⇒ Object
board: and concepts: let a caller that already paid for the read
hand it over — the audit reads the board and parses the bundle for
its other checks, and a second parse of every concept on the
pre-commit hot path buys nothing.
21 22 23 24 25 26 27 28 29 30 31 |
# File 'lib/okf/pro/pairing.rb', line 21 def failures(root, board: nil, concepts: nil) board ||= Pro.read_text(File.join(root, "board.md")) targets = board_targets(board) concepts ||= ::OKF::Bundle::Reader.read(root).concepts unpaired_projects(root, targets) + unpaired_briefings(concepts, targets) + broken_targets(root, board) + closed_project_links(root, board) + stale_read_lines(concepts, board) end |
.marker?(line) ⇒ Boolean
158 159 160 |
# File 'lib/okf/pro/pairing.rb', line 158 def marker?(line) line.match?(MARKER) && !line.match?(NEGATED) end |
.open_projects(root) ⇒ Object
121 122 123 124 125 |
# File 'lib/okf/pro/pairing.rb', line 121 def open_projects(root) project_dirs(root).map do |dir| File.basename(dir) unless closed?(root, File.join(dir, "index.md")) end.compact end |
.owner_read?(concept) ⇒ Boolean
221 222 223 |
# File 'lib/okf/pro/pairing.rb', line 221 def owner_read?(concept) concept.trust_tier == :human_reviewed end |
.project_dirs(root) ⇒ Object
133 134 135 |
# File 'lib/okf/pro/pairing.rb', line 133 def project_dirs(root) Dir.glob(File.join(root, "projects", "*", "")).sort end |
.stale_read_lines(concepts, board) ⇒ Object
Reverse: a read-owed line whose briefing is already verified. Reading is the one event that removes the line, and it happened. A To-read line with no bundle link at all is fine — documents live outside bundles too, and the check speaks only about what it can see.
91 92 93 94 95 96 97 98 99 100 101 |
# File 'lib/okf/pro/pairing.rb', line 91 def stale_read_lines(concepts, board) verified = verified_reference_targets(concepts) Board.section_lines(board, "To read").flat_map do |line| Board.targets(line).map do |target| next unless verified.include?(target.downcase) "— pairing: read-owed line links #{target}, which is already verified — " \ "reading removes the line, and the reading happened." end.compact end end |
.unpaired_briefings(concepts, targets) ⇒ Object
Forward: every generated-without-verified briefing is linked from some board line — the To-read line it was born paired with, or the conflict line that is also a claim on attention.
46 47 48 49 50 51 52 53 |
# File 'lib/okf/pro/pairing.rb', line 46 def unpaired_briefings(concepts, targets) unverified_ids(concepts).map do |id| next if targets.include?("/#{id}.md".downcase) "— pairing: unverified briefing #{id}.md has no read-owed board line — " \ "it silently left the attention system." end.compact end |
.unpaired_projects(root, targets) ⇒ Object
Forward: every open project is linked from some board line.
34 35 36 37 38 39 40 41 |
# File 'lib/okf/pro/pairing.rb', line 34 def unpaired_projects(root, targets) open_projects(root).map do |name| prefix = "/projects/#{name.downcase}" next if targets.any? { |t| t == prefix || t.start_with?("#{prefix}/") } "— pairing: projects/#{name} has no board line and no closed marker." end.compact end |
.unverified_ids(concepts) ⇒ Object
229 230 231 232 233 234 235 236 |
# File 'lib/okf/pro/pairing.rb', line 229 def unverified_ids(concepts) concepts.map do |concept| next unless briefing?(concept) next unless awaiting_read?(concept) concept.id end.compact end |
.verified_reference_targets(concepts) ⇒ Object
238 239 240 241 242 243 244 245 |
# File 'lib/okf/pro/pairing.rb', line 238 def verified_reference_targets(concepts) concepts.map do |concept| next unless briefing?(concept) next unless owner_read?(concept) "/#{concept.id}.md".downcase end.compact end |