okf-pro 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a908d8394cc7ea78e25a744f080813d34eb656b77274e3bb8db4397d222c3950
4
- data.tar.gz: 687919c65db7bf0d42bc48f475cd88f636c01aaf87f49443d92c6de0fd434fb9
3
+ metadata.gz: 1f4b14821d80f4db8b76a6634a53b03fa87d8f04a279c197ed70bcfecb4e5ce1
4
+ data.tar.gz: 0ccde1c91b1e1156265e3a380c3c3ed6484cd66a70426bb3ff02bc761ad0ee1c
5
5
  SHA512:
6
- metadata.gz: '0589ad1bd8edb04b767044692d20b271614fe37d60f598fe1b51ba436384142049da27d94d8281df54b0a485b658751e7ef2fe3d4c84a292b4593a61502d6c87'
7
- data.tar.gz: 7afe1c08143bf0524f9deb5ce47e827276b59aa805f62cb65b2be5e5414bb542b1208a4b2a59826443ab1fde4e5680066fd047e6236007afc8b3e1b0e04e7ae3
6
+ metadata.gz: 90264f22dd742779c0600a9215e2e47c2269dd131e4de7f71d066974044e0dd0d23ba2eb966318b23f4703076dbda8e1a3e91d18d8a1c5c7d5bae08a2fbde4a5
7
+ data.tar.gz: daf911ed01988929bba82c7f056e94ae73c55f5c4979dbcf91c0c09c34012677b95a6547a1400c775611d02c4eb13ed42696a70cf0abede7377b71ebca7b28d9
@@ -21,7 +21,7 @@ never blocks, and reports "clean." That failure is [gates-only-at-the-hook-door]
21
21
  | `journal-guard` | `PreToolUse` on `Edit\|Write\|MultiEdit` | is this rewriting a past journal day? |
22
22
  | `shell-guard` | `PreToolUse` on `Bash` | is this command about to write into the bundle behind the other two? |
23
23
  | `post-edit` | `PostToolUse` on `Edit\|Write\|MultiEdit` | the three below, in one process, sharing one bundle read |
24
- | `check-okf` | (composed into `post-edit`) | does the bundle still validate and lint? |
24
+ | `check-okf` | (composed into `post-edit`) | does the bundle still validate and lint — minus the four findings one edit cannot decide? |
25
25
  | `cap-check` | (composed into `post-edit`) | is In flight over the cap? |
26
26
  | `reconcile-search` | (composed into `post-edit`) | what already says this? (**Law 1**) |
27
27
  | `stop-gate` | `Stop` | is the day's snapshot line written, and does it agree with the board? (**Law 2**) |
@@ -32,6 +32,12 @@ never blocks, and reports "clean." That failure is [gates-only-at-the-hook-door]
32
32
  `post-edit` instead, because the bundle read is the expensive part and three
33
33
  separate invocations paid for it three times over.
34
34
 
35
+ Both of the doors that reach `check-okf` ask it for `scope: :edit`, so the four
36
+ set-scoped findings are withheld there and confessed rather than dropped —
37
+ [the-gates](/structure/the-gates.md) has the argument. `stop-gate` and `audit`
38
+ ask the same questions with the write set complete, which is the only place
39
+ they can be answered.
40
+
35
41
  # What they do when they cannot answer
36
42
 
37
43
  This is the half that matters, and it is the contract rather than a courtesy.
@@ -37,6 +37,16 @@ the two together.
37
37
  It does not go through `dir_argument`: `setup` into an empty directory is the
38
38
  whole point, and refusing one that holds no bundle would refuse every first run.
39
39
 
40
+ It does go through `parse_flags`, and for a while it was the only family that
41
+ did not. `--help` therefore reached the verb as its destination, and the
42
+ generator answered the question by running. `setup --help` wrote twenty-five
43
+ seed files into a directory named `--help`. `upgrade --help` rewrote the four
44
+ gem-owned files of whatever repository you were standing in, because `upgrade`
45
+ defaults its destination to the working directory. Both exited 0.
46
+
47
+ None of the three declares a flag, and none needs a `FLAGS` entry to refuse
48
+ one: absence from that table already means "accepts none".
49
+
40
50
  **Readers** (`audit`, `records`, `snapshot`, `unverified`, `state`, `board`,
41
51
  `friction`) answer questions. Every one of them routes through `parse_flags`,
42
52
  listed in `FLAGS` or not — because absence from that table means "accepts none",
data/.okf/log.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # Update Log
2
2
 
3
+ ## 2026-08-30
4
+
5
+ * **A per-edit check now only asks what one edit can answer.** Four of
6
+ `Conformance`'s findings — `broken_link`, `broken_index_entry`, `orphan`,
7
+ `not_in_index` — are questions about a set of files, and no write order
8
+ avoids them at the per-edit door: write the index first and its entry is
9
+ broken, write the concept first and it is the orphan. They are withheld
10
+ under `scope: :edit`, confessed by count rather than dropped, and asked at
11
+ the Stop gate and in `audit` where the write set is complete
12
+ ([structure/the-gates](/structure/the-gates.md)). The Stop gate ran no
13
+ conformance at all before this, so half the change is an addition rather
14
+ than a move.
15
+
16
+ * **The scaffold verbs go through `parse_flags` too, and the catalogue says so.**
17
+ [capabilities/verbs](/capabilities/verbs.md) described the family by what it
18
+ does *not* do: no `dir_argument`, because `setup` into an empty directory is
19
+ the point. That omission read as an exemption from the parser as well, and it
20
+ was one. `--help` reached the verb as its destination, so the generator
21
+ answered the question by running. The paragraph now names both halves, and
22
+ [testing/adding-a-verb](/testing/adding-a-verb.md) step 5 says it too. **A
23
+ family defined only by its exception gets extended in the direction of the
24
+ exception**, and the next reader pays for that.
25
+
3
26
  ## 2026-08-19
4
27
 
5
28
  * **Every concept declares `generated:`.** The dates are read from git with
@@ -42,6 +42,24 @@ clause: `Linter.call` with no options skips `expired` and `stale` and still
42
42
  reports `healthy?`, so `confession` surfaces what was not run rather than
43
43
  reporting clean over a silent skip.
44
44
 
45
+ **The policy on top is a scope.** Four findings — `broken_link`,
46
+ `broken_index_entry`, `orphan`, `not_in_index`, held in `SET_SCOPED` — are
47
+ questions about a *set* of files asked after one of them. An index entry is
48
+ decided by the concept it names; an orphan by whether anything else links it.
49
+ At the per-edit door the write set is incomplete by definition, and no write
50
+ order avoids that: write the index first and the entry is broken, write the
51
+ concept first and it is the orphan. So `check(target, scope: :edit)` withholds
52
+ those four, and the per-edit door is the only caller that asks for it.
53
+
54
+ Withholding is not dropping, which is the same clause again. `deferral` says
55
+ how many were withheld and names the two doors that ask them with the write set
56
+ complete — `Closing.stop_gate`, through `Conformance.findings` on the bundle it
57
+ has already parsed, and `Audit`, which never scoped anything and needed no
58
+ change. `findings` takes a bundle and an optional `rel:` rather than a Target
59
+ because neither of those doors has an edited file to be the subject: without a
60
+ `rel` there is no "your edit" and no "elsewhere", and inventing the distinction
61
+ would file every finding under a heading saying somebody else did it.
62
+
45
63
  # Two doors, one set of invariants
46
64
 
47
65
  `Audit.call` is the CI door. It asks `structure`, `conformance`, `curation`,
@@ -39,7 +39,9 @@ its caller's typo as a broken bundle; a check that returns 1 is read as
39
39
  would make every `okf help` load the whole library — and they are held
40
40
  together by a test rather than by discipline.
41
41
  5. **Declare its flags** in `FLAGS` if it takes any. Absence means "accepts
42
- none", not "is exempt": every reader routes through `parse_flags` either way.
42
+ none", not "is exempt". Every reader routes through `parse_flags` either
43
+ way, and so does every scaffold verb — that second half cost a release to
44
+ become true.
43
45
  6. **A writer owes a pure transform.** Compute the new text in `board/edit.rb`
44
46
  or `log/edit.rb` — they cannot touch the disk — declare the added, removed
45
47
  and moved lines, and let `Conserve` refuse the mismatch. See
data/CHANGELOG.md CHANGED
@@ -4,6 +4,63 @@ All notable changes to okf-pro are documented here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this gem uses
5
5
  [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [1.2.0] - 2026-08-30
8
+
9
+ ### Changed
10
+
11
+ - **The per-edit checks only ask what one edit can decide.** Four of the
12
+ findings `check-okf` reported are questions about a *set* of files, asked
13
+ after one of them: `broken_index_entry` and its validator twin `broken_link`
14
+ are decided by the concept an index names, and `orphan` and `not_in_index` by
15
+ whether anything else in the bundle links or lists the file. No write order
16
+ avoids them at that door — write the index first and its entry is broken,
17
+ write the concept first and it is the orphan — so every one of them there was
18
+ noise by construction, on the hook that fires on every edit.
19
+
20
+ `Conformance.check` takes a `scope:` now. The per-edit door asks for `:edit`
21
+ and withholds those four; everything else asks for `:all` and is unchanged.
22
+
23
+ - **The Stop gate asks the four instead**, which is the half of this that is an
24
+ addition rather than a move: `Closing.stop_gate` ran the snapshot, the board
25
+ grammar and the pairing invariants and **no conformance at all**. It now runs
26
+ it over the bundle it has already parsed, so the one-parse-per-Stop pin still
27
+ holds at one. `okf pro audit` needed no change — it never scoped anything.
28
+
29
+ ### Fixed
30
+
31
+ - **A withheld finding is confessed, not dropped.** The edit door reports how
32
+ many findings needed the whole write set and names the doors that ask them,
33
+ because a gate that quietly stops reporting converts "unchecked" into
34
+ "checked and fine" — the failure the contract's third clause exists to name.
35
+ Nothing is said when nothing was withheld: a warning that is always present
36
+ carries no information.
37
+
38
+ ## [1.1.1] - 2026-08-30
39
+
40
+ ### Fixed
41
+
42
+ - **`okf pro setup --help` prints the usage instead of running the generator.**
43
+ The three scaffold verbs read their destination from `argv.shift` and never
44
+ reached `parse_flags`. The flag was therefore the destination. `setup --help`
45
+ wrote the twenty-five seed files into a directory named `--help` and exited 0.
46
+ `upgrade --help` was worse: `upgrade` defaults its destination to the working
47
+ directory, so it rewrote the four gem-owned files of whatever repository you
48
+ were standing in.
49
+
50
+ A help flag that answers the question by running the verb is the worst shape
51
+ it can have. This is the one family where the answer is files on disk.
52
+
53
+ The fix routes the three verbs through the parser the readers already use.
54
+ None of them declares a flag, and none gains a `FLAGS` entry. Absence from
55
+ that table already means "accepts none" rather than "is exempt", so `setup
56
+ --json` is now a typo the parser names. Before, it was a seeded repository in
57
+ a directory with a leading dash. `--` still separates a destination from the
58
+ flags, for the rare path that starts with one.
59
+
60
+ The write verbs carried this defect and fixed it: `okf pro capture --help`
61
+ once appended `- <date> — --help` to the Inbox. The scaffold verbs are the
62
+ last family nobody had checked for it.
63
+
7
64
  ## [1.1.0] - 2026-08-22
8
65
 
9
66
  ### Changed
@@ -250,5 +307,8 @@ every use. This surface answers both.
250
307
  - **The `.bin/okf_pro` binary is gone.** `okf pro` is the only door, which is
251
308
  what lets the wrapper refuse anything that is not it.
252
309
 
310
+ [1.2.0]: https://github.com/serradura/okf/compare/okf-pro/v1.1.1...okf-pro/v1.2.0
311
+ [1.1.1]: https://github.com/serradura/okf/compare/okf-pro/v1.1.0...okf-pro/v1.1.1
312
+ [1.1.0]: https://github.com/serradura/okf/compare/okf-pro/v1.0.1...okf-pro/v1.1.0
253
313
  [1.0.1]: https://github.com/serradura/okf/compare/okf-pro/v1.0.0...okf-pro/v1.0.1
254
314
  [1.0.0]: https://github.com/serradura/okf/releases/tag/okf-pro%2Fv1.0.0
data/lib/okf/plugin.rb CHANGED
@@ -75,8 +75,6 @@ module OKF
75
75
  # rather than swallowed — `okf/pro.rb` refuses an under-floor Ruby with
76
76
  # `exit 2`, and a rescue that turned that refusal into an error report
77
77
  # would be the fail-open this method exists to close.
78
- #
79
- # rubocop:disable Lint/RescueException
80
78
  def call(argv)
81
79
  verb = argv.first.to_s
82
80
 
@@ -93,10 +91,9 @@ module OKF
93
91
  status.is_a?(Integer) ? status : blocked("returned #{status.inspect} instead of an exit status")
94
92
  rescue ::SystemExit
95
93
  raise
96
- rescue ::Exception => e
94
+ rescue ::Exception => e # rubocop:disable Lint/RescueException
97
95
  blocked("#{e.class}: #{e.message}")
98
96
  end
99
- # rubocop:enable Lint/RescueException
100
97
 
101
98
  private
102
99
 
data/lib/okf/pro/cli.rb CHANGED
@@ -21,7 +21,7 @@ module OKF
21
21
  "guard-verified" => ->(event) { Guards.guard_verified(event) },
22
22
  "journal-guard" => ->(event) { Guards.journal_guard(event) },
23
23
  "shell-guard" => ->(event) { ShellGuard.check(event) },
24
- "check-okf" => ->(event) { Conformance.check(Target.for(event)) },
24
+ "check-okf" => ->(event) { Conformance.check(Target.for(event), scope: :edit) },
25
25
  "cap-check" => ->(event) { Budget.cap_check(Target.for(event)) },
26
26
  "reconcile-search" => ->(event) { Reconcile.search(Target.for(event), event) },
27
27
  "post-edit" => ->(event) { CLI.post_edit(event) },
@@ -154,7 +154,7 @@ module OKF
154
154
  # covers count — an Edit to a concept body is judgment and always will
155
155
  # be, and counting it would report the system working as friction.
156
156
  Friction.record(target.root, "edit", target.rel) if target && Friction.covered_path?(target.rel)
157
- Conformance.check(target) + Budget.cap_check(target) + Reconcile.search(target, event)
157
+ Conformance.check(target, scope: :edit) + Budget.cap_check(target) + Reconcile.search(target, event)
158
158
  end
159
159
 
160
160
  def run(argv, stdin: $stdin, stdout: $stdout, stderr: $stderr)
@@ -795,6 +795,18 @@ module OKF
795
795
  end
796
796
 
797
797
  def scaffold(verb, argv, stdout: $stdout, stderr: $stderr)
798
+ # The one verb group that used to skip the parser, and the one where
799
+ # skipping it was worst: `--help` became the destination, so asking
800
+ # what the generator does ran it, into a directory called `--help`.
801
+ #
802
+ # No `FLAGS` entry, deliberately — absence already means "accepts
803
+ # none", and `parse_flags` refuses an undeclared flag either way. An
804
+ # empty entry would say these three verbs were considered and given
805
+ # nothing, which is a distinction this table does not draw for `audit`
806
+ # or `records` either.
807
+ options = parse_flags(argv, verb, stdout, stderr)
808
+ return options == :handled ? PASS : BLOCK unless options.is_a?(Hash)
809
+
798
810
  dest = argv.shift
799
811
 
800
812
  unless argv.empty?
@@ -39,7 +39,8 @@ module OKF
39
39
  # check below — this gate runs on every Stop, and it used to pay for
40
40
  # both twice.
41
41
  board = Pro.read_text(File.join(root, "board.md"))
42
- concepts = ::OKF::Bundle::Reader.read(root).concepts
42
+ bundle = ::OKF::Bundle::Reader.read(root)
43
+ concepts = bundle.concepts
43
44
 
44
45
  msgs = []
45
46
  snap = Log.snapshot_line(Pro.read_text(File.join(root, "log.md")), today.to_s)
@@ -57,6 +58,11 @@ module OKF
57
58
  # just verified would agree with itself about a deadline it cannot
58
59
  # see. The unreadable line is a refusal, not a rounding error.
59
60
  msgs.concat(Board.grammar(board).map { |m| "— board: #{m}" })
61
+ # The four findings the per-edit door withheld, asked where the write
62
+ # set is finally complete. `Conformance` takes the bundle this gate has
63
+ # already parsed rather than a Target: there is no edited file here to
64
+ # be the subject, and a second read would break the one-parse pin.
65
+ msgs.concat(Conformance.findings(bundle).map { |m| "— #{m}" })
60
66
  msgs.concat(Pairing.failures(root, board: board, concepts: concepts))
61
67
  return [] if msgs.empty?
62
68
 
@@ -6,12 +6,36 @@ module OKF
6
6
  # answer the same questions at the cost of two interpreter boots per edit;
7
7
  # the analyzers are pure and take the bundle directly.
8
8
  module Conformance
9
+ # The four findings that are questions about a SET of files, not about
10
+ # the one just written. An index entry is decided by the concept it
11
+ # names; an orphan by whether anything else in the bundle links it. At
12
+ # the per-edit door they are therefore answered against a write set that
13
+ # is incomplete by definition — the concept an index will list does not
14
+ # exist yet while the index is being written, and no write order avoids
15
+ # that, because the reverse order makes the concept the orphan.
16
+ #
17
+ # Three are the linter's and one is the validator's. They are one list
18
+ # because they share a property, not a producer.
19
+ SET_SCOPED = %i[broken_link broken_index_entry orphan not_in_index].freeze
20
+
9
21
  module_function
10
22
 
11
- def check(target)
23
+ # `scope:` is `:all` — every question, for the doors that see the whole
24
+ # write set — or `:edit`, which withholds the four above and says how
25
+ # many it withheld.
26
+ def check(target, scope: :all)
12
27
  return [] if target.nil?
13
28
 
14
- result = ::OKF::Bundle::Validator.call(target.bundle)
29
+ findings(target.bundle, rel: target.rel, scope: scope)
30
+ end
31
+
32
+ # The same questions asked of a bundle that is already parsed, and of no
33
+ # file in particular. The Stop gate holds one parse and must not pay for
34
+ # a second — `closing_test.rb` pins the count at one — and it is asking
35
+ # about the write set rather than about an edit, which is why `rel:` is
36
+ # optional here and required nowhere.
37
+ def findings(bundle, rel: nil, scope: :all)
38
+ result = ::OKF::Bundle::Validator.call(bundle)
15
39
  unless result.valid?
16
40
  lines = result.errors.map { |e| " #{e[:path]}: #{e[:message]}" }
17
41
  return [ "okf validate failed after your edit:\n#{lines.join("\n")}" ]
@@ -34,7 +58,8 @@ module OKF
34
58
  # They warn rather than refuse: §9's separation is the kernel's, and a
35
59
  # gate that turned the validator's warnings into errors would be
36
60
  # overruling it from outside.
37
- soft = warn_lines(result.warnings)
61
+ soft_warnings, deferred = sift(result.warnings, scope)
62
+ soft = warn_lines(soft_warnings)
38
63
 
39
64
  # WHAT THIS GATE RUNS, AND WHY IT SAYS SO.
40
65
  #
@@ -63,9 +88,12 @@ module OKF
63
88
  # `skipped_checks` is then empty, and `confession` below is a live guard
64
89
  # rather than a formality: the day okf adds a third clock-gated check,
65
90
  # this gate reports it instead of quietly not running it.
66
- report = ::OKF::Bundle::Linter.call(target.bundle, today: Date.today, except: [ :stale ])
67
- notes = confession(report) + soft
68
- return notes if report.healthy? && notes.empty?
91
+ report = ::OKF::Bundle::Linter.call(bundle, today: Date.today, except: [ :stale ])
92
+ lint, held = sift(report.warnings, scope)
93
+ deferred += held
94
+
95
+ notes = confession(report) + deferral(deferred) + soft
96
+ return notes if lint.empty? && notes.empty?
69
97
 
70
98
  # Warnings only. Lint's `:info` findings are observations, and a gate
71
99
  # that refuses on an observation stops being read as a refusal.
@@ -79,16 +107,46 @@ module OKF
79
107
  # pre-existing warning elsewhere used to arrive under the heading
80
108
  # "your edit" on every edit in the bundle, and a gate that blames
81
109
  # you for what you did not do is a gate people switch off.
82
- mine, theirs = report.warnings.partition { |w| same_file?(w[:path], target.rel) }
83
110
  parts = notes
84
- parts << "okf lint flagged your edit:\n#{format_warnings(mine)}" unless mine.empty?
85
- unless theirs.empty?
86
- parts << "okf lint findings elsewhere in the bundle pre-existing, or a neighbour " \
87
- "your edit affected:\n#{format_warnings(theirs)}"
111
+ if rel.nil?
112
+ # No edit to be blamed for or exonerated from. The Stop gate asks
113
+ # about the write set, so "yours" and "elsewhere" are not a
114
+ # distinction it can draw, and pretending otherwise would file every
115
+ # finding under a heading that says someone else did it.
116
+ parts << "okf lint findings across the bundle:\n#{format_warnings(lint)}"
117
+ else
118
+ mine, theirs = lint.partition { |w| same_file?(w[:path], rel) }
119
+ parts << "okf lint flagged your edit:\n#{format_warnings(mine)}" unless mine.empty?
120
+ unless theirs.empty?
121
+ parts << "okf lint findings elsewhere in the bundle — pre-existing, or a neighbour " \
122
+ "your edit affected:\n#{format_warnings(theirs)}"
123
+ end
88
124
  end
89
125
  [ parts.join("\n") ]
90
126
  end
91
127
 
128
+ # Splits a warning list into what this scope answers and a count of what
129
+ # it withheld. The count is what `deferral` confesses; the findings
130
+ # themselves are not carried, because the door that can answer them
131
+ # recomputes them rather than trusting a hand-off.
132
+ def sift(warnings, scope)
133
+ return [ warnings, 0 ] unless scope == :edit
134
+
135
+ kept, held = warnings.partition { |w| !SET_SCOPED.include?(w[:check]) }
136
+ [ kept, held.size ]
137
+ end
138
+
139
+ # Withholding is not dropping, and the difference has to be audible. A
140
+ # gate that silently stopped asking four questions would have converted
141
+ # "unchecked" into "checked and fine" — the one failure this file's
142
+ # whole design is against.
143
+ def deferral(count)
144
+ return [] if count.zero?
145
+
146
+ [ "#{count} finding(s) need the whole write set and were deferred to the Stop gate " \
147
+ "and `okf pro audit`, which see every file this edit will be part of." ]
148
+ end
149
+
92
150
  # What the linter did not run, said out loud. Empty in normal operation
93
151
  # (see the call above); non-empty means okf grew a check this gate does
94
152
  # not know how to supply, and the right answer then IS to stop — an
@@ -2,6 +2,6 @@
2
2
 
3
3
  module OKF
4
4
  module Pro
5
- VERSION = "1.1.0"
5
+ VERSION = "1.2.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: okf-pro
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rodrigo Serradura
@@ -174,7 +174,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
174
174
  - !ruby/object:Gem::Version
175
175
  version: '0'
176
176
  requirements: []
177
- rubygems_version: 4.0.16
177
+ rubygems_version: 4.0.10
178
178
  specification_version: 4
179
179
  summary: 'A profile of OKF: one opinionated shape of knowledge bundle, and the gates
180
180
  that hold it there. Structure for making things happen.'