okf-mcp 1.0.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.
@@ -10,7 +10,7 @@ module OKF
10
10
  # deliberately no disk cache here — the process is the cache; disk-side
11
11
  # state is okf-sqlite3's territory.
12
12
  class MemoryBackend
13
- FILTER_KEYS = %i[type tag dir status].freeze
13
+ FILTER_KEYS = %i[type tag dir status trust].freeze
14
14
 
15
15
  # How many prepared corpora to hold at once, most-recently-used first.
16
16
  # It used to be unbounded, keyed on the queried subset — a long-lived
@@ -168,17 +168,21 @@ module OKF
168
168
  end
169
169
  end
170
170
 
171
+ # The kernel's one row predicate (Bundle::RowFilter), not a local copy:
172
+ # this seam diverged three recorded times, and the fix each time was to
173
+ # share the rule. This layer keeps only its own argument spelling —
174
+ # blanks mean "no filter" (an MCP client filling every optional property
175
+ # with "" is routine), and ""/"/" fold onto the root before the shared
176
+ # dir rule sees them.
171
177
  def matches?(row, filters)
172
- FILTER_KEYS.all? do |key|
178
+ wants = {}
179
+ FILTER_KEYS.each do |key|
173
180
  wanted = filters[key]
174
- next true if OKF.blank?(wanted)
181
+ next if OKF.blank?(wanted)
175
182
 
176
- case key
177
- when :tag then Array(row[:tags]).map { |tag| Filters.fold(tag) }.include?(Filters.fold(wanted))
178
- when :dir then Filters.under_dir?(row[:dir], wanted)
179
- else Filters.fold(row[key]) == Filters.fold(wanted)
180
- end
183
+ wants[key] = key == :dir ? Filters.normalize_dir(wanted) : wanted
181
184
  end
185
+ Bundle::RowFilter.matches?(row, **wants)
182
186
  end
183
187
 
184
188
  # What the residency cache watches: every markdown file with its mtime
@@ -62,7 +62,12 @@ module OKF
62
62
  bundles: ROWS,
63
63
  total: COUNT,
64
64
  results: ROWS,
65
- unparseable: UNPARSEABLE
65
+ unparseable: UNPARSEABLE,
66
+ # Present only when "*" or a group forgave a vanished bundle —
67
+ # conditional, so never required; slugs, not rows. Omitting it
68
+ # entirely is how a real field failed result validation the first
69
+ # time it appeared.
70
+ skipped: { type: "array", items: SLUG }
66
71
  },
67
72
  required: %w[query engine bundles total results]
68
73
  },
@@ -70,6 +75,13 @@ module OKF
70
75
  properties: { bundle: SLUG, total: COUNT, concepts: ROWS, unparseable: UNPARSEABLE },
71
76
  required: %w[bundle total concepts]
72
77
  },
78
+ # `dangling` is always present (empty when nothing misses): an absent
79
+ # list would read as "not checked", which is the one thing an
80
+ # inventory must never say by accident.
81
+ "references" => {
82
+ properties: { bundle: SLUG, total: COUNT, references: ROWS, dangling: ROWS, unparseable: UNPARSEABLE },
83
+ required: %w[bundle total references dangling]
84
+ },
73
85
  # `total` is entries across every log file and `files` how many files
74
86
  # they came from — two different counts, both named, because one
75
87
  # standing for the other is what let an unbounded 119 KB answer read
@@ -78,6 +90,30 @@ module OKF
78
90
  properties: { bundle: SLUG, total: COUNT, files: COUNT, logs: ROWS },
79
91
  required: %w[bundle total files logs]
80
92
  },
93
+ # Two shapes, like lint's: the plain inverted index (`tags`) and the
94
+ # `by` regrouping (`groups`). Only what both carry is required.
95
+ "tags" => {
96
+ properties: {
97
+ bundle: SLUG, total: COUNT, tags: ROWS,
98
+ by: { type: "string" }, groups: ROWS,
99
+ unparseable: UNPARSEABLE
100
+ },
101
+ required: %w[bundle total]
102
+ },
103
+ "types" => {
104
+ properties: { bundle: SLUG, total: COUNT, types: ROWS, unparseable: UNPARSEABLE },
105
+ required: %w[bundle total types]
106
+ },
107
+ "stats" => {
108
+ properties: {
109
+ bundle: SLUG,
110
+ concepts: COUNT, dirs: COUNT, top_dirs: COUNT, concept_types: COUNT,
111
+ cross_links: COUNT, distinct_tags: COUNT,
112
+ by_type: { type: "object" }, by_dir: { type: "object" }, by_top_dir: { type: "object" },
113
+ unparseable: UNPARSEABLE
114
+ },
115
+ required: %w[bundle concepts dirs top_dirs concept_types cross_links distinct_tags by_type by_dir by_top_dir]
116
+ },
81
117
  "validate" => {
82
118
  properties: {
83
119
  bundle: SLUG,
@@ -1,7 +1,7 @@
1
1
  # Prompt: search — retrieve knowledge without paying for the whole bundle
2
2
 
3
3
  Retrieval matters as much as curation: a bundle nobody can query cheaply is
4
- dead weight. The discipline is progressive disclosure (spec §6): every step
4
+ dead weight. The discipline is progressive disclosure (spec §8): every step
5
5
  pays a few hundred bytes to decide what the next step reads, and full bodies
6
6
  are read last, and only the winners.
7
7
 
@@ -49,7 +49,7 @@ are read last, and only the winners.
49
49
  an id home.
50
50
  4. **Read only the winners.** `read_concept` takes a result row's exact `id`
51
51
  and returns the file verbatim — that concept, not its folder, never the
52
- whole tree. Follow its links (§5) one hop at a time; check `log` when
52
+ whole tree. Follow its links (§6) one hop at a time; check `log` when
53
53
  freshness matters.
54
54
  5. **Answer, then surface the friction.** Cite the concept ids you used. This
55
55
  server never writes — if the answer was missing, stale, or needlessly hard