okf 1.8.0 → 1.10.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 +4 -4
- data/CHANGELOG.md +615 -38
- data/README.md +109 -15
- data/lib/okf/bundle/folder.rb +20 -0
- data/lib/okf/bundle/search/index.rb +65 -0
- data/lib/okf/bundle/search/scan.rb +89 -0
- data/lib/okf/bundle/search.rb +262 -66
- data/lib/okf/bundle.rb +27 -3
- data/lib/okf/cli/catalog.rb +66 -0
- data/lib/okf/cli/command.rb +495 -0
- data/lib/okf/cli/files.rb +68 -0
- data/lib/okf/cli/graph.rb +82 -0
- data/lib/okf/cli/index.rb +127 -0
- data/lib/okf/cli/lint.rb +139 -0
- data/lib/okf/cli/loose.rb +78 -0
- data/lib/okf/cli/registry.rb +229 -0
- data/lib/okf/cli/render.rb +66 -0
- data/lib/okf/cli/search.rb +285 -0
- data/lib/okf/cli/server.rb +179 -0
- data/lib/okf/cli/skill.rb +57 -0
- data/lib/okf/cli/stats.rb +88 -0
- data/lib/okf/cli/tags.rb +122 -0
- data/lib/okf/cli/types.rb +37 -0
- data/lib/okf/cli/validate.rb +66 -0
- data/lib/okf/cli.rb +418 -1633
- data/lib/okf/{server → render}/graph/template.html.erb +1553 -175
- data/lib/okf/{server → render}/graph.rb +85 -9
- data/lib/okf/server/app.rb +17 -48
- data/lib/okf/server/hub/not_found.rb +663 -0
- data/lib/okf/server/hub.rb +504 -38
- data/lib/okf/skill/SKILL.md +41 -26
- data/lib/okf/skill/playbooks/consume.md +5 -3
- data/lib/okf/skill/playbooks/curate.md +3 -1
- data/lib/okf/skill/playbooks/maintain.md +4 -3
- data/lib/okf/skill/playbooks/menu.md +5 -0
- data/lib/okf/skill/playbooks/refine.md +92 -0
- data/lib/okf/skill/playbooks/search.md +47 -7
- data/lib/okf/skill/reference/authoring.md +3 -2
- data/lib/okf/skill/reference/cli.md +98 -21
- data/lib/okf/version.rb +1 -1
- data/lib/okf.rb +8 -0
- metadata +37 -3
data/README.md
CHANGED
|
@@ -35,7 +35,7 @@ The package, end to end:
|
|
|
35
35
|
<p align="center">
|
|
36
36
|
<picture>
|
|
37
37
|
<source media="(prefers-color-scheme: dark)" srcset=".github/overview-dark.png">
|
|
38
|
-
<img src=".github/overview-light.png" width="760" alt="The package: the Agent Skill (your coding agent authors and curates, you stay the editor) writes and maintains the bundle, a folder of Markdown + YAML in your repo where one concept is one file and links between files are the knowledge graph. The bundle is read by the CLI/Lib (validate: legal OKF per section 9; lint: well-curated and fresh; search: ranked retrieval; require okf for Ruby objects) and by the Graph, in four modes: okf server (a live local server), okf render (the same page exported as one static, self-contained HTML file you can host anywhere), okf registry (every registered bundle behind one hub), and OKF::Server::App (the Rack app mounted in a Rails route). One gem, 100% local, Ruby 2.4 or newer, only rack and
|
|
38
|
+
<img src=".github/overview-light.png" width="760" alt="The package: the Agent Skill (your coding agent authors and curates, you stay the editor) writes and maintains the bundle, a folder of Markdown + YAML in your repo where one concept is one file and links between files are the knowledge graph. The bundle is read by the CLI/Lib (validate: legal OKF per section 9; lint: well-curated and fresh; search: ranked retrieval; require okf for Ruby objects) and by the Graph, in four modes: okf server (a live local server), okf render (the same page exported as one static, self-contained HTML file you can host anywhere), okf registry (every registered bundle behind one hub), and OKF::Server::App (the Rack app mounted in a Rails route). One gem, 100% local, Ruby 2.4 or newer, only rack, webrick and minifts as dependencies.">
|
|
39
39
|
</picture>
|
|
40
40
|
</p>
|
|
41
41
|
|
|
@@ -72,10 +72,13 @@ It is deliberately light so it runs on the Ruby your OS already ships:
|
|
|
72
72
|
|
|
73
73
|
- works on every Ruby since 2.4, the same floor as [rack](https://github.com/rack/rack),
|
|
74
74
|
its core dependency;
|
|
75
|
-
- only
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
75
|
+
- only three runtime dependencies: `rack` (the server is a mountable Rack app),
|
|
76
|
+
`webrick` (unbundled from Ruby in 3.0), and
|
|
77
|
+
[`minifts`](https://github.com/serradura/minifts) (the search engine — pure
|
|
78
|
+
Ruby, no dependencies of its own, same 2.4 floor);
|
|
79
|
+
- no ActiveSupport, no native extension, no build step, no JavaScript
|
|
80
|
+
toolchain — the [design constraints](.okf/design/) that hold this line are
|
|
81
|
+
enforced by tests.
|
|
79
82
|
|
|
80
83
|
That range is not aspirational: CI runs the full test suite and RuboCop on every
|
|
81
84
|
one of these on each push.
|
|
@@ -222,10 +225,15 @@ docker run --rm -v "$PWD:/data" ghcr.io/serradura/okf validate .
|
|
|
222
225
|
docker run --rm -v "$PWD:/data" -p 8808:8808 ghcr.io/serradura/okf server . --bind 0.0.0.0
|
|
223
226
|
```
|
|
224
227
|
|
|
228
|
+
A container has to bind `0.0.0.0` for the host to reach it at all, which is
|
|
229
|
+
exactly the bind that makes the registry read-only. Reading the graph is
|
|
230
|
+
unaffected, and there is no flag that opens the registry on that bind — it is a
|
|
231
|
+
per-user file, managed from the machine that owns it or from `okf registry`.
|
|
232
|
+
|
|
225
233
|
Then open <http://127.0.0.1:8808>. Images are published for `linux/amd64` and
|
|
226
234
|
`linux/arm64` on
|
|
227
235
|
[ghcr.io](https://github.com/serradura/okf-gem/pkgs/container/okf): `:latest`
|
|
228
|
-
tracks the newest release, or pin a version like `:1.
|
|
236
|
+
tracks the newest release, or pin a version like `:1.10.0`.
|
|
229
237
|
|
|
230
238
|
Tired of the long line? Install a Docker-backed [`okf` command](https://docker.okfgem.com),
|
|
231
239
|
so every verb drops the `docker run` prefix and reads exactly like the native CLI
|
|
@@ -243,13 +251,20 @@ instead: `irm https://docker.okfgem.com/install.ps1 | iex`.
|
|
|
243
251
|
|
|
244
252
|
## Command line
|
|
245
253
|
|
|
254
|
+
These verbs are written to be read by an **agent first and a person second** —
|
|
255
|
+
that is what the skill drives, with no wrapper in between. Every read verb takes
|
|
256
|
+
`--json`, `index`/`catalog`/`files` project down to the fields you ask for
|
|
257
|
+
(`--fields`/`--except`), so nothing pays for output it will not read, and the
|
|
258
|
+
exit codes are stable enough to branch on in CI. The same commands render as
|
|
259
|
+
scannable plain text when a human is the one looking.
|
|
260
|
+
|
|
246
261
|
```bash
|
|
247
262
|
okf validate <dir|@slug> [--json] # check OKF v0.1 conformance (§9)
|
|
248
263
|
okf lint <dir|@slug> [--json] [--fail-on warn] [...] # report curation-quality issues
|
|
249
264
|
okf loose <dir|@slug> [--json] # list files with no graph links, by folder
|
|
250
|
-
okf search <dir|@slug…|@all> <term…> [
|
|
265
|
+
okf search <dir|@slug…|@all> <term…> [--regexp|--fuzzy] # ranked retrieval; @slugs or @all span bundles
|
|
251
266
|
okf index <dir|@slug> [--json] [--area A] [--no-body] # progressive-disclosure map (§6): bodies, rollups, listings
|
|
252
|
-
okf server [DIR|@slug…] [-p PORT] [--bind ADDR] [...] # serve one bundle, or many behind a hub (⌘K
|
|
267
|
+
okf server [DIR|@slug…] [-p PORT] [--bind ADDR] [...] # serve one bundle, or many behind a hub (⌘K searches every one)
|
|
253
268
|
okf render <dir|@slug> [-o FILE] [--layout NAME] [...] # export the graph as one static, self-contained HTML file
|
|
254
269
|
okf registry list [--json] # list registered bundles (* marks the default)
|
|
255
270
|
okf registry set <dir|@slug> [--as SLUG] [--default] # add or update a bundle (a bare `server` serves it)
|
|
@@ -266,8 +281,11 @@ okf registry default <@slug> | rename <@slug> <new> # move a bundle to th
|
|
|
266
281
|
# set`, or bare @ for the default. Anywhere a <dir> goes, an @slug goes:
|
|
267
282
|
# okf lint @handbook works from anywhere. Set $OKF_HOME to point every verb
|
|
268
283
|
# at another registry.
|
|
269
|
-
okf graph <dir|@slug> [--json] [--minimal] [--
|
|
284
|
+
okf graph <dir|@slug> [--json] [--minimal] [--hubs] # print the knowledge graph (--hubs ranks by inbound links)
|
|
270
285
|
okf catalog | files | tags | types | stats <dir|@slug> [--json] # the browser views, on the CLI
|
|
286
|
+
# `tags --by type|area` regroups the tag index per concept dimension; each row
|
|
287
|
+
# shows count/total, so a tag confined to one area (a domain) reads differently
|
|
288
|
+
# from one spread across several (a cross-cutting concern).
|
|
271
289
|
okf skill <dest> [--here] [--force] # install the companion agent skill
|
|
272
290
|
okf --version
|
|
273
291
|
```
|
|
@@ -300,12 +318,20 @@ _The graph server on this repo's own [`.okf`](.okf) bundle, with the
|
|
|
300
318
|
|
|
301
319
|
The page is one template from a phone to a desktop: on small screens the
|
|
302
320
|
navigation rail becomes a drawer, the toolbar folds into a `⚙` sheet, and the
|
|
303
|
-
panels go full-bleed — rotate a tablet and the layout re-evaluates.
|
|
304
|
-
|
|
321
|
+
panels go full-bleed — rotate a tablet and the layout re-evaluates. On a touch
|
|
322
|
+
screen a tap opens a preview card at the bottom edge rather than a panel over
|
|
323
|
+
the whole viewport, so the graph stays on screen and live while you read: drag
|
|
324
|
+
the card up for the neighbourhood, tap a link in it and it walks there in place.
|
|
325
|
+
It is keyboard-first too: `⌘/Ctrl-K` opens a command palette in every mode (views
|
|
305
326
|
always; bundles too when a [hub](#one-registry-many-bundles) is serving), `/`
|
|
306
327
|
jumps to the current view's search, and `?` answers with a sheet of every
|
|
307
328
|
shortcut.
|
|
308
329
|
|
|
330
|
+
The search box says what it is doing while it does it: a live `7/8` count of
|
|
331
|
+
what the filter kept, and — when a word matches nothing here — a panel naming
|
|
332
|
+
the bundle and the query, offering to search every other bundle instead. The
|
|
333
|
+
query carries over, so nothing is typed twice.
|
|
334
|
+
|
|
309
335
|
To skip the server entirely, **`okf render <dir>`** writes that same page as one
|
|
310
336
|
self-contained HTML file, the whole bundle baked in, so you can publish the
|
|
311
337
|
graph on GitHub Pages or any static host.
|
|
@@ -326,10 +352,32 @@ okf server # no args: the whole registry behind one
|
|
|
326
352
|
|
|
327
353
|
The first entry still on disk is the **default** — the bundle a bare `okf
|
|
328
354
|
server` opens at `/` and a bare `@` names; `okf registry default @slug` moves an
|
|
329
|
-
entry to the front. Behind the hub each bundle mounts at `/b/<slug>/`,
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
355
|
+
entry to the front. Behind the hub each bundle mounts at `/b/<slug>/`, and the
|
|
356
|
+
`⌘/Ctrl-K` palette both switches bundles and **searches every one of them at
|
|
357
|
+
once** — type a few words and the matching concepts appear with their bundle and
|
|
358
|
+
a snippet, from wherever you are.
|
|
359
|
+
|
|
360
|
+
The ⚙ in the rail opens **Bundles** — the registry on the graph page itself, so
|
|
361
|
+
switching the default, renaming an entry or dropping one no longer means finding
|
|
362
|
+
`/b/` first. Each row carries its size, its health as a *word*, and which one `/`
|
|
363
|
+
opens. There is no Add: registering a bundle means naming a filesystem path,
|
|
364
|
+
which a browser cannot hand over and an agent can (`okf registry set <dir>`), and
|
|
365
|
+
the panel says so instead of leaving the absence to be noticed.
|
|
366
|
+
|
|
367
|
+
`/b/` is the **bundles list**: every bundle with its size, its health
|
|
368
|
+
(conformance plus curation, so a warning is a warning and not a failure), the
|
|
369
|
+
default marked, and any entry whose folder has gone missing shown rather than
|
|
370
|
+
quietly dropped.
|
|
371
|
+
|
|
372
|
+
Managing the registry without a terminal happens on the graph page instead, in
|
|
373
|
+
the ⚙ **Bundles** panel — make default, rename, remove, where you are already
|
|
374
|
+
reading. Those controls are the one thing that does not follow you onto a
|
|
375
|
+
network: bind anywhere but loopback and they are refused outright, with no flag
|
|
376
|
+
that says otherwise, since `--bind 0.0.0.0` is how a personal tool becomes a
|
|
377
|
+
public one. `--read-only` declines them on loopback too, and is the only switch
|
|
378
|
+
there is. Writes rebuild the hub's set as they go, so a rename takes effect on
|
|
379
|
+
the next click; a registry change made *elsewhere* while it runs still wants a
|
|
380
|
+
restart to be served. Set `$OKF_HOME` to point every verb at a different registry.
|
|
333
381
|
|
|
334
382
|
`graph` and `server` are best-effort (§9): a file with invalid frontmatter is
|
|
335
383
|
skipped (and noted on stderr), not fatal, so one bad file never breaks the rest.
|
|
@@ -359,6 +407,22 @@ OKF lint — docs
|
|
|
359
407
|
⚠ 3 warn, 31 info
|
|
360
408
|
```
|
|
361
409
|
|
|
410
|
+
That `hubs:` line is the bundle's centre of gravity — the concepts the most links
|
|
411
|
+
point *at*, with their inbound count. **`okf graph <dir> --hubs`** expands it into
|
|
412
|
+
the full ranking and, for each hub, the areas those links come *from*:
|
|
413
|
+
|
|
414
|
+
```bash
|
|
415
|
+
$ okf graph docs --hubs
|
|
416
|
+
Hubs — docs (38 of 40 concepts with inbound links)
|
|
417
|
+
|
|
418
|
+
storage/reconcile ×16 storage 6, shape 4, exploration 3, (root) 1, …
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
That second half is what decides whether a concept sits where it belongs: a hub
|
|
422
|
+
pulled in mostly from outside its own folder is usually homed by history rather
|
|
423
|
+
than by meaning. Together with `okf tags <dir> --by area`, it is the evidence the
|
|
424
|
+
skill's [`refine`](#agent-skill) verb reads before proposing anything.
|
|
425
|
+
|
|
362
426
|
`loose` lists the files that float in the graph: concepts with no cross-links
|
|
363
427
|
in or out (graph degree 0), grouped by folder. It is a curation lens over
|
|
364
428
|
`lint`'s `unlinked` check, distinct from `orphan`. An `index.md` listing makes a
|
|
@@ -388,11 +452,21 @@ The skill routes a small set of verbs. In Claude Code they run as `/okf:gem
|
|
|
388
452
|
| `produce` | Create or extend a bundle from code, docs, or knowledge in people's heads |
|
|
389
453
|
| `migrate` | Adopt existing Markdown docs in place: frontmatter and reserved files added, bodies kept verbatim |
|
|
390
454
|
| `maintain` | Sync the bundle's content with reality after the code or docs change |
|
|
455
|
+
| `refine` | Restructure it for retrieval: evidence-first, cohesion over balance — proposes, never applies |
|
|
391
456
|
| `consume` | Use the bundle as context for a task, writing back what you learn |
|
|
392
457
|
| `curate` | Structural upkeep as it stands: `validate` + `lint` + `loose` |
|
|
393
458
|
| `doctor` | Install and verify the CLI, then doctor the bundle |
|
|
394
459
|
| `<okf-cli-verb>` | Run any CLI verb (`validate`, `lint`, `search`, `index`, `server`, the read views) and interpret its output |
|
|
395
460
|
|
|
461
|
+
Three of those look alike and are not, which is the distinction worth learning
|
|
462
|
+
first: **`curate`** keeps the bundle *sound* (the structure as it stands),
|
|
463
|
+
**`maintain`** keeps it *true* (the code changed, so the content must catch up),
|
|
464
|
+
and **`refine`** changes *where knowledge lives* — the folder a concept sits in,
|
|
465
|
+
a fact re-explained in three overviews. Reach for `refine` when nothing is wrong
|
|
466
|
+
and everything is hard to find: a folder grown to twenty concepts, a hub homed by
|
|
467
|
+
history, tags that neither connect nor mark. It reads the evidence, then hands
|
|
468
|
+
you a proposal — it never rearranges your bundle on its own.
|
|
469
|
+
|
|
396
470
|
Point it at your agent's config directory (or its skills directory) and the tree
|
|
397
471
|
settles in its own `skills/okf/` folder, so a shared skills directory never gets
|
|
398
472
|
the files loose:
|
|
@@ -510,6 +584,26 @@ Two loop concerns from the format's own guidance, _contradictions_ and _semantic
|
|
|
510
584
|
staleness, need to understand meaning and are not computed here; `lint --json`
|
|
511
585
|
is the structured input an agent consumes to reason about those.
|
|
512
586
|
|
|
587
|
+
## Extending okf
|
|
588
|
+
|
|
589
|
+
This is the other kind of plugin, and it is worth separating from the
|
|
590
|
+
[Claude Code plugin](#claude-code-plugin) below: that one teaches an *agent* to
|
|
591
|
+
use okf, while this one adds behavior to the `okf` command everyone runs.
|
|
592
|
+
|
|
593
|
+
Publish a gem named `okf-*` that carries an `okf/plugin.rb`, and installing it is
|
|
594
|
+
the whole installation: your verb answers to `okf`, appears in `okf help` under
|
|
595
|
+
`installed extensions:`, and behaves like a built-in — nothing to register,
|
|
596
|
+
nothing to configure, no list of known addons in this gem. The same seam takes
|
|
597
|
+
retrieval backends, which is what `okf search --engine NAME` chooses between.
|
|
598
|
+
|
|
599
|
+
Two promises make that safe to install: nothing an addon registers can displace a
|
|
600
|
+
built-in, and a broken addon is skipped and reported rather than taking the CLI
|
|
601
|
+
down with it.
|
|
602
|
+
|
|
603
|
+
The [extension points](.okf/design/extension-points.md) concept has the contract
|
|
604
|
+
and the threat model; `test/integration/cli/cli_plugin_test.rb` is a working
|
|
605
|
+
example to copy.
|
|
606
|
+
|
|
513
607
|
## Server trust boundary
|
|
514
608
|
|
|
515
609
|
> [!NOTE]
|
data/lib/okf/bundle/folder.rb
CHANGED
|
@@ -46,10 +46,24 @@ module OKF
|
|
|
46
46
|
@bundle.catalog
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
+
def hubs
|
|
50
|
+
@bundle.hubs
|
|
51
|
+
end
|
|
52
|
+
|
|
49
53
|
def directory_index
|
|
50
54
|
@bundle.directory_index
|
|
51
55
|
end
|
|
52
56
|
|
|
57
|
+
# Every log.md with its content, root scope first — read live from disk so a
|
|
58
|
+
# just-appended entry shows without a reload; the reserved snapshot is the
|
|
59
|
+
# fallback if the file has since vanished. Shared by `okf render`'s bake
|
|
60
|
+
# (OKF::Render::Graph.payload) and OKF::Server::App's /log endpoint.
|
|
61
|
+
def log_entries
|
|
62
|
+
@bundle.log_files.sort_by { |path| [ path == "log.md" ? 0 : 1, path ] }.map do |path|
|
|
63
|
+
{ path: path, dir: File.dirname(path), content: log_content(path) }
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
53
67
|
# Human-readable "parent/dir" name — the default HTML title.
|
|
54
68
|
# The bundle's display label, "parent/dir" — path arithmetic, no disk. It
|
|
55
69
|
# is a class method so a caller that only wants the label (the registry
|
|
@@ -96,6 +110,12 @@ module OKF
|
|
|
96
110
|
.select { |entry| File.basename(entry.path) == basename }
|
|
97
111
|
.each_with_object({}) { |entry, hash| hash[entry.path] = entry.content }
|
|
98
112
|
end
|
|
113
|
+
|
|
114
|
+
def log_content(path)
|
|
115
|
+
File.read(File.join(@root, path), encoding: "UTF-8")
|
|
116
|
+
rescue SystemCallError
|
|
117
|
+
@bundle.reserved_content(path)
|
|
118
|
+
end
|
|
99
119
|
end
|
|
100
120
|
end
|
|
101
121
|
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "minifts"
|
|
4
|
+
require "okf/bundle/search"
|
|
5
|
+
|
|
6
|
+
module OKF
|
|
7
|
+
class Bundle
|
|
8
|
+
class Search
|
|
9
|
+
# The default engine: a MiniFTS full-text index — the same engine, and the
|
|
10
|
+
# same BM25+ arithmetic, the browser page already runs as MiniSearch, so a
|
|
11
|
+
# Ruby-built index and the page's rank identically.
|
|
12
|
+
#
|
|
13
|
+
# Matching is by *token*: a term matches a whole word or a word it prefixes
|
|
14
|
+
# ("dedup" reaches "deduplication"), and `fuzzy:` opts into typo tolerance.
|
|
15
|
+
# The index is built per call — see .okf/capabilities/search.md for why that
|
|
16
|
+
# ceiling stands and what lifts it.
|
|
17
|
+
module Index
|
|
18
|
+
CAPABILITIES = %i[fuzzy prefix].freeze
|
|
19
|
+
|
|
20
|
+
class << self
|
|
21
|
+
def id
|
|
22
|
+
:index
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def capabilities
|
|
26
|
+
CAPABILITIES
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# minifts is a hard runtime dependency with no native extension, so it
|
|
30
|
+
# is here whenever the gem is. An addon backed by a native build is the
|
|
31
|
+
# case this predicate exists for.
|
|
32
|
+
def available?
|
|
33
|
+
true
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# `fields:` narrows where a term may hit, so a field the caller excluded
|
|
37
|
+
# can neither match nor be credited. The hit's `terms` are MiniFTS's
|
|
38
|
+
# matched *document* terms — already lowercased, and present in the text
|
|
39
|
+
# verbatim even when the query only prefixed them.
|
|
40
|
+
def call(documents, terms, fields:, fuzzy: false, **_options)
|
|
41
|
+
index = MiniFTS.new(fields: FIELDS, id_field: "key")
|
|
42
|
+
index.add_all(documents)
|
|
43
|
+
|
|
44
|
+
options = { combine_with: "AND", prefix: true, boost: WEIGHTS, fields: fields }
|
|
45
|
+
options[:fuzzy] = FUZZY_DISTANCE if fuzzy
|
|
46
|
+
|
|
47
|
+
index.search(terms.join(" "), options).map do |hit|
|
|
48
|
+
{ key: hit[:id], matched: matched_in(hit), score: hit[:score], terms: hit[:terms] }
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
# The union of fields any term hit, in WEIGHTS order. MiniFTS reports it
|
|
55
|
+
# per query term as { term => [field, …] }.
|
|
56
|
+
def matched_in(hit)
|
|
57
|
+
FIELDS.select { |field| hit[:match].any? { |_term, found| found.include?(field) } }
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
Search.register(self)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "okf/bundle/search"
|
|
4
|
+
|
|
5
|
+
module OKF
|
|
6
|
+
class Bundle
|
|
7
|
+
class Search
|
|
8
|
+
# The linear engine: terms matched against raw field text, one document at a
|
|
9
|
+
# time. No index, so nothing is tokenized and nothing is normalized — a
|
|
10
|
+
# phrase stays a phrase, `7.2.0` stays one string, and an infix matches.
|
|
11
|
+
# That is the exactness a token index gives up, and the reason this engine
|
|
12
|
+
# survived the swap.
|
|
13
|
+
#
|
|
14
|
+
# Two readings of a term, and the engine is the *raw text* half of the split
|
|
15
|
+
# rather than the regexp half:
|
|
16
|
+
#
|
|
17
|
+
# `regexp: false` — literal substring, which is what this engine did
|
|
18
|
+
# before the index landed, and what `--engine scan`
|
|
19
|
+
# restores. Terms are escaped, so `7.2.0` does not
|
|
20
|
+
# match `7x2y0` and `[draft]` is not a character class.
|
|
21
|
+
# `regexp: true` — the term is a pattern, opted into with `-e`.
|
|
22
|
+
#
|
|
23
|
+
# Conflating the two would make choosing the engine silently change what the
|
|
24
|
+
# terms mean, and turn an ordinary term like `review (pending` into exit 2.
|
|
25
|
+
#
|
|
26
|
+
# Scoring is the summed weight of the fields that matched: absolute, and so
|
|
27
|
+
# comparable across bundles without a corpus to normalize against.
|
|
28
|
+
module Scan
|
|
29
|
+
CAPABILITIES = %i[regexp].freeze
|
|
30
|
+
|
|
31
|
+
class << self
|
|
32
|
+
def id
|
|
33
|
+
:scan
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def capabilities
|
|
37
|
+
CAPABILITIES
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# No backing store to fail: the engine is Regexp and Enumerable.
|
|
41
|
+
def available?
|
|
42
|
+
true
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Raises RegexpError on an invalid pattern under `regexp: true` — the
|
|
46
|
+
# caller owns turning that into a usage error. A literal term cannot
|
|
47
|
+
# raise, because it is escaped before it is compiled. The hit's `terms`
|
|
48
|
+
# are the compiled patterns, which is what the facade points its snippet
|
|
49
|
+
# window at.
|
|
50
|
+
def call(documents, terms, fields:, regexp: false, **_options)
|
|
51
|
+
patterns = terms.map do |term|
|
|
52
|
+
Regexp.new(regexp ? term : Regexp.escape(term), Regexp::IGNORECASE)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
hits = []
|
|
56
|
+
documents.each do |document|
|
|
57
|
+
matched = matched_fields(document, patterns, fields)
|
|
58
|
+
next if matched.nil?
|
|
59
|
+
|
|
60
|
+
hits << {
|
|
61
|
+
key: document["key"],
|
|
62
|
+
matched: matched,
|
|
63
|
+
score: matched.map { |field| WEIGHTS[field] }.reduce(0, :+),
|
|
64
|
+
terms: patterns
|
|
65
|
+
}
|
|
66
|
+
end
|
|
67
|
+
hits
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
# The union of fields any pattern hit, in WEIGHTS order — or nil when
|
|
73
|
+
# some pattern hit nothing (terms are ANDed).
|
|
74
|
+
def matched_fields(document, patterns, fields)
|
|
75
|
+
hits = patterns.map do |pattern|
|
|
76
|
+
found = fields.select { |field| pattern.match?(document[field]) }
|
|
77
|
+
return nil if found.empty?
|
|
78
|
+
|
|
79
|
+
found
|
|
80
|
+
end
|
|
81
|
+
FIELDS.select { |field| hits.any? { |found| found.include?(field) } }
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
Search.register(self)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|