Module: OKF::Pro::Friction
- Defined in:
- lib/okf/pro/friction.rb
Overview
What the verbs did not cover — recorded, never enforced.
Every verb in this gem is a gate or a report. This one is neither: it
writes down the moments an agent did by hand something a command could
have done, so the question "which verb is missing?" is answered by
evidence instead of by the maintainer's imagination. Two points feed it,
and both are code paths that already run — no new hook event, because
settings.json is SEEDED and a new registration would never reach an
adopter through upgrade.
It records INTENT, not outcome. shell-guard fires at PreToolUse, before
the owner may deny, so a recorded line means "this was attempted by
hand", which is exactly the question being asked.
THE THIRD CLAUSE, APPLIED TO TELEMETRY. This is not a check: it neither
refuses nor blocks, and a recorder that crashed a gate would be a
measurement worth less than the thing it measures. But it does not lie
about having counted. A write that fails leaves a marker beside the log,
and an unwritable scratch directory is the same state — both make
report answer available == false, and the banner says the data is
unavailable rather than printing a zero nobody can distinguish from a
session that used the verbs.
Defined Under Namespace
Classes: Report
Constant Summary collapse
- SCRATCH =
The scratch directory the seeded
.gitignorealready ignores, at the REPOSITORY root rather than inside the bundle: this is telemetry about the bundle, not knowledge in it, andokf validatewalks every.mdunder the root. ".tmp"- LOG =
"okf-pro-friction.log"- MARKER =
"okf-pro-friction.unavailable"- WRAPPERS =
The good path must not count as friction, or the recorder reports the verbs being used as evidence that they are not.
Anchored to the START of the segment, because the question is "is this an invocation of the gem?" and not "is the gem mentioned?". Matched anywhere, a board line's own TEXT excused the hand-write that appended it —
echo "- see okf pro docs" >> .okf/board.mdwas never recorded, and board lines routinely name these verbs.ShellGuard.own_write?does the splitting; this is asked of one segment.An assignment prefix is part of the invocation and not a command of its own, so it is skipped rather than breaking the anchor:
$OKF_HOMEis a variable this ecosystem actually uses, andOKF_HOME=/tmp okf pro snapshot >> .okf/log.mdis the prescribed move with a prefix on it.So is a wrapper, and the list is short and named rather than open:
bundle exec okf pro snapshot >> .okf/log.mdis this repo's own invocation of the prescribed move, and an anchor that missed it counted a contributor's first command as friction. An open rule would be a way back to matching a mention. /(?:bundle\s+exec|env|time|nice|nohup|sudo|command)\s+/.freeze
- OWN_COMMAND =
/\A\s*(?:[A-Za-z_]\w*=\S*\s+)*(?:#{WRAPPERS})*okf\s+pro\b/.freeze
- SHELL_ANSWER =
The two answers
covered_bycan give, as constants — so a reader may be COMPARED against one instead of pattern-matched.verb_covered?below asks a narrower question thancovered_byanswers, and a prefix test on the prose would have coupled it to wording nobody would think to preserve. "Edit or Write — the trust guards read a tool event, and a shell redirect is none"- BOARD_ANSWER =
"okf pro capture / promote / demote"
Class Method Summary collapse
-
.available?(root) ⇒ Boolean
Writable, and never marked.
-
.classify_command(command) ⇒ Object
The covered classes, read out of a shell COMMAND instead of a path.
-
.clear(root) ⇒ Object
Everything the log holds, cleared — the log itself and the marker.
-
.covered_by(via, what) ⇒ Object
What a recorded row maps to, where something already covers it — so the report says "this is a verb now" rather than making the reader guess, and phase 3's question keeps being answered after phase 3.
-
.covered_path?(rel) ⇒ Boolean
The board, and ONLY the board.
- .log_path(root) ⇒ Object
- .mark_unavailable(root) ⇒ Object
- .marker_path(root) ⇒ Object
- .own_command?(command) ⇒ Boolean
- .parse(line) ⇒ Object
-
.record(root, via, what, detail = nil, today: Date.today) ⇒ Object
One line appended, or a marker set.
-
.report(root) ⇒ Object
Entries in file order, plus the two things a reader must not be told by omission: how many lines would not parse, and whether the recorder was able to run at all.
-
.scratch_root(root) ⇒ Object
The repository the bundle sits in.
-
.verb_covered?(via, what) ⇒ Boolean
Whether an
okf proVERB covers it — which is not the same question as whether anything does.
Class Method Details
.available?(root) ⇒ Boolean
Writable, and never marked. An unwritable scratch directory has recorded nothing and cannot say so in a marker either, so it is asked directly rather than inferred from an empty log.
120 121 122 123 124 125 126 127 128 129 130 131 132 133 |
# File 'lib/okf/pro/friction.rb', line 120 def available?(root) return false if File.exist?(marker_path(root)) dir = File.join(scratch_root(root), SCRATCH) return File.writable?(dir) if File.directory?(dir) # Something is there and it is not a directory, so `mkdir_p` will raise # every time and the marker cannot be written either — which is the # case that proves the marker alone is not enough to answer this. return false if File.exist?(dir) File.writable?(scratch_root(root)) rescue StandardError false end |
.classify_command(command) ⇒ Object
The covered classes, read out of a shell COMMAND instead of a path. A
command has no file_path — that is the whole reason shell-guard
exists — so this reads the names it mentions, and answers "unclassified"
when it cannot tell. That is not a failure: the report's question is which
verb is missing, and a write it cannot attribute is exactly the row that
says "we do not know yet".
Wider than covered_path?, deliberately, and the two doors are asking
different questions: the edit door asks "is this a shape a verb covers?",
and a redirect is a bypass whatever it touched. journal collapses a year
of days into one row rather than three hundred.
252 253 254 255 256 257 258 259 |
# File 'lib/okf/pro/friction.rb', line 252 def classify_command(command) text = command.to_s return "board.md" if text.include?("board.md") return "log.md" if text.include?("log.md") return "journal" if text.match?(%r{journal/\d{4}-\d{2}-\d{2}\.md}) "unclassified" end |
.clear(root) ⇒ Object
Everything the log holds, cleared — the log itself and the marker.
The marker is deliberately sticky: one failed write means the count is short by an unknown amount forever after, and a recorder that quietly forgave itself would be back to reporting a zero it did not count. But sticky with no way out is a report that nags permanently about a full disk from three weeks ago, so the way out is explicit, named in the message, and belongs to the reader.
230 231 232 233 234 235 236 237 238 239 |
# File 'lib/okf/pro/friction.rb', line 230 def clear(root) [ log_path(root), marker_path(root) ].count do |path| next false unless File.exist?(path) File.unlink(path) true end rescue StandardError nil end |
.covered_by(via, what) ⇒ Object
What a recorded row maps to, where something already covers it — so the report says "this is a verb now" rather than making the reader guess, and phase 3's question keeps being answered after phase 3.
Keyed on via as well as what, because the two doors mean different
things about the same file. An Edit to the board is a verb's job. A
SHELL write is a bypass whatever it touched — the trust guards read a
tool event and a redirect produces none — so what covers it is Edit or
Write, not an okf pro verb.
206 207 208 209 210 211 |
# File 'lib/okf/pro/friction.rb', line 206 def covered_by(via, what) return SHELL_ANSWER if via == "shell" return BOARD_ANSWER if what == "board.md" nil end |
.covered_path?(rel) ⇒ Boolean
The board, and ONLY the board.
The obvious wider list — board, log, journal day — was wrong in the
direction that destroys the measurement: it counted the PRESCRIBED path
as friction. snapshot deliberately has no --write, so appending the
Snapshot line to log.md by hand is exactly what this gem tells you to
do; journal open says in as many words that the day's content is
yours to write. Recording either inflates the banner's request and
points the maintainer at verbs that already exist and already declined
to do that job — which is the same mistake own_command? exists to
prevent, made one layer up.
An Edit to a concept body is judgment and always will be. What is left
is board.md: a shape with exactly one correct form, edited by hand
while three verbs cover it.
The path is bundle-relative, so this is an equality rather than a
basename test: projects/x/board.md is somebody's notes, not the one
page Rule 3 counts, and the verbs do not touch it.
One covered path also means the class a row records as IS the path, so
the call site passes it straight through. The classify that used to
fold three paths into three classes had two answers nothing could reach
from this door, and a unit test calling it directly kept them green.
185 186 187 |
# File 'lib/okf/pro/friction.rb', line 185 def covered_path?(rel) rel.to_s == "board.md" end |
.log_path(root) ⇒ Object
56 57 58 |
# File 'lib/okf/pro/friction.rb', line 56 def log_path(root) File.join(scratch_root(root), SCRATCH, LOG) end |
.mark_unavailable(root) ⇒ Object
79 80 81 82 83 84 85 86 87 88 |
# File 'lib/okf/pro/friction.rb', line 79 def mark_unavailable(root) path = marker_path(root) FileUtils.mkdir_p(File.dirname(path)) File.open(path, "a") { |file| file.puts Date.today.to_s } nil rescue StandardError # Nothing left to try. `report` reaches the same verdict from the # unwritable directory itself, which is why this may give up quietly. nil end |
.marker_path(root) ⇒ Object
60 61 62 |
# File 'lib/okf/pro/friction.rb', line 60 def marker_path(root) File.join(scratch_root(root), SCRATCH, MARKER) end |
.own_command?(command) ⇒ Boolean
158 159 160 |
# File 'lib/okf/pro/friction.rb', line 158 def own_command?(command) command.to_s.match?(OWN_COMMAND) end |
.parse(line) ⇒ Object
110 111 112 113 114 115 |
# File 'lib/okf/pro/friction.rb', line 110 def parse(line) value = JSON.parse(line) value.is_a?(Hash) ? value : nil rescue JSON::ParserError nil end |
.record(root, via, what, detail = nil, today: Date.today) ⇒ Object
One line appended, or a marker set. Never raises: the callers are a PreToolUse guard and a PostToolUse check, and an exception out of either is an edit sailing through while the gate lies on the floor.
67 68 69 70 71 72 73 74 75 76 77 |
# File 'lib/okf/pro/friction.rb', line 67 def record(root, via, what, detail = nil, today: Date.today) path = log_path(root) FileUtils.mkdir_p(File.dirname(path)) entry = { "at" => today.to_s, "via" => via, "what" => what } entry["detail"] = detail if detail File.open(path, "a") { |file| file.puts JSON.generate(entry) } true rescue StandardError mark_unavailable(root) false end |
.report(root) ⇒ Object
Entries in file order, plus the two things a reader must not be told by omission: how many lines would not parse, and whether the recorder was able to run at all.
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 |
# File 'lib/okf/pro/friction.rb', line 93 def report(root) events = [] unreadable = 0 path = log_path(root) if File.exist?(path) Pro.read_text(path).each_line do |line| next if line.strip.empty? parsed = parse(line) parsed ? events << parsed : unreadable += 1 end end Report.new(events, unreadable, available?(root)) rescue StandardError Report.new([], 0, false) end |
.scratch_root(root) ⇒ Object
The repository the bundle sits in. .okf/board.md is a bundle under a
repository; a flat root IS the repository. Nothing else distinguishes
them, and the scratch directory belongs to the outer one either way.
51 52 53 54 |
# File 'lib/okf/pro/friction.rb', line 51 def scratch_root(root) base = File.(root.to_s) File.basename(base) == BundleRoot::DIR ? File.dirname(base) : base end |
.verb_covered?(via, what) ⇒ Boolean
Whether an okf pro VERB covers it — which is not the same question as
whether anything does. A shell redirect at the board is covered, by Edit
or Write, and there is no verb to ask for; a banner counting it told the
adopter a verb could have done something no verb does.
217 218 219 220 |
# File 'lib/okf/pro/friction.rb', line 217 def verb_covered?(via, what) answer = covered_by(via, what) !answer.nil? && answer != SHELL_ANSWER end |