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 .gitignore already ignores, at the REPOSITORY root rather than inside the bundle: this is telemetry about the bundle, not knowledge in it, and okf validate walks every .md under 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.md was 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_HOME is a variable this ecosystem actually uses, and OKF_HOME=/tmp okf pro snapshot >> .okf/log.md is 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.md is 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_by can give, as constants — so a reader may be COMPARED against one instead of pattern-matched. verb_covered? below asks a narrower question than covered_by answers, 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

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.

Returns:

  • (Boolean)


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.

Returns:

  • (Boolean)


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

Returns:

  • (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.expand_path(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.

Returns:

  • (Boolean)


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