okf 1.13.0 → 2.1.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.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +222 -0
  3. data/README.md +34 -11
  4. data/lib/okf/bundle/folder.rb +27 -1
  5. data/lib/okf/bundle/graph.rb +12 -3
  6. data/lib/okf/bundle/linter.rb +470 -47
  7. data/lib/okf/bundle/reader.rb +10 -4
  8. data/lib/okf/bundle/references.rb +111 -0
  9. data/lib/okf/bundle/row_filter.rb +53 -0
  10. data/lib/okf/bundle/search.rb +20 -2
  11. data/lib/okf/bundle/validator/result.rb +6 -3
  12. data/lib/okf/bundle/validator.rb +267 -26
  13. data/lib/okf/bundle/writer.rb +1 -1
  14. data/lib/okf/bundle.rb +105 -4
  15. data/lib/okf/cli/catalog.rb +1 -1
  16. data/lib/okf/cli/command.rb +27 -8
  17. data/lib/okf/cli/files.rb +1 -1
  18. data/lib/okf/cli/index.rb +1 -1
  19. data/lib/okf/cli/lint.rb +70 -12
  20. data/lib/okf/cli/references.rb +97 -0
  21. data/lib/okf/cli/search.rb +2 -1
  22. data/lib/okf/cli/stats.rb +3 -39
  23. data/lib/okf/cli/tags.rb +6 -43
  24. data/lib/okf/cli/types.rb +1 -1
  25. data/lib/okf/cli/validate.rb +3 -3
  26. data/lib/okf/cli.rb +24 -3
  27. data/lib/okf/concept.rb +362 -10
  28. data/lib/okf/markdown/citations.rb +41 -4
  29. data/lib/okf/markdown/frontmatter.rb +1 -1
  30. data/lib/okf/markdown/links.rb +67 -7
  31. data/lib/okf/render/graph/template.html.erb +173 -41
  32. data/lib/okf/render/graph.rb +11 -3
  33. data/lib/okf/server/app.rb +47 -15
  34. data/lib/okf/server/hub.rb +1 -1
  35. data/lib/okf/skill/SKILL.md +28 -18
  36. data/lib/okf/skill/playbooks/curate.md +8 -3
  37. data/lib/okf/skill/playbooks/doctor.md +3 -1
  38. data/lib/okf/skill/playbooks/maintain.md +10 -7
  39. data/lib/okf/skill/playbooks/menu.md +5 -4
  40. data/lib/okf/skill/playbooks/migrate.md +31 -8
  41. data/lib/okf/skill/playbooks/produce.md +20 -11
  42. data/lib/okf/skill/playbooks/search.md +2 -2
  43. data/lib/okf/skill/reference/SPEC.md +739 -187
  44. data/lib/okf/skill/reference/authoring.md +164 -48
  45. data/lib/okf/skill/reference/cli/checks.md +171 -0
  46. data/lib/okf/skill/reference/cli/graph.md +49 -0
  47. data/lib/okf/skill/reference/cli/map.md +98 -0
  48. data/lib/okf/skill/reference/cli/registry.md +70 -0
  49. data/lib/okf/skill/reference/cli/search.md +130 -0
  50. data/lib/okf/skill/reference/cli/serve.md +83 -0
  51. data/lib/okf/skill/reference/cli/views.md +59 -0
  52. data/lib/okf/skill/reference/cli.md +44 -501
  53. data/lib/okf/skill/reference/spec-map.md +32 -0
  54. data/lib/okf/skill/templates/attested-computation.md +41 -0
  55. data/lib/okf/skill/templates/concept.md +13 -6
  56. data/lib/okf/skill/templates/root-index.md +1 -1
  57. data/lib/okf/version.rb +1 -1
  58. data/lib/okf.rb +22 -2
  59. metadata +13 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 84ee2cefa8e04f2358c9125ca00ccdf9de8a12ab0a78a96315ec846bab8d3e87
4
- data.tar.gz: 7a3186c5aa2924b4425a0c0a86c9f0812948427348d4fd3d0cd46852bd6dc575
3
+ metadata.gz: 047d30628ab36abdfe517a2722753dc8f4a6eb60cea9a41d6015508d64784af6
4
+ data.tar.gz: 637ce26fbb8375b9c4d0daeab6ef6bdc8b8cee0258838e73151944b772445c47
5
5
  SHA512:
6
- metadata.gz: b0c9f1002b408702c74f73be001eab10b6691419700d33b41e81bd001d21858f1d79289696edbb2ad6e7c6f4a76ccd4c7458cf211e82fded5224f5fcfa6328b4
7
- data.tar.gz: 0ed68e0095ab1aa912c5df3b6fd7b9f82e0e07f2cad0c182437cb70021beed8d5ecb054106185204b18d0f138303372bc62202c286aec75ce6c4a43434f2e546
6
+ metadata.gz: e30d74c10e269546cd81a433ff91b8f4f662540a81de9010de03c5e245d59eae3f7fff2743519379bbc2de53319501b2e517a6d7c613bf24d7e21a9ab447dd5d
7
+ data.tar.gz: 862d1454d20f6ff1f51e0f17829676becdf1503df42f2d6ca5bf9b0004437ba43e65f6db381bba14b0126f2f40bb3704a6f29c8e12bafc237d405612604610a8
data/CHANGELOG.md CHANGED
@@ -5,6 +5,227 @@ All notable changes to this project are documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [2.1.0] - 2026-08-17
9
+
10
+ ### Changed
11
+
12
+ - **The skill's CLI reference is an index and seven leaves.** `reference/cli.md`
13
+ answered every question about every verb in one 46,234-byte file, so an agent
14
+ asking about `search` paid for `serve`, `registry` and the graph too. The
15
+ shared contract — refs, exit codes, `--json`, the filters — stays in `cli.md`
16
+ and routes to one file per surface under `reference/cli/`. Measured on the
17
+ worst question rather than the mean: 9,084 bytes for a routing question,
18
+ 19,533 for the heaviest leaf. `reference/spec-map.md` is the other half, a
19
+ pointer from a spec clause to the file that answers it, so the vendored
20
+ `SPEC.md` keeps its index beside it instead of cut into it.
21
+ - **Cross-file citations in the skill name a `rule:` marker**, not a section
22
+ anchor: the key travels with the paragraph the next time anything moves.
23
+ - **`okf help` prints one row per extension**, plus a pointer to
24
+ `okf <verb> --help` for the rest. An addon with an umbrella verb and several
25
+ subcommands used to dwarf the built-ins the map exists to teach.
26
+
27
+ ### Added
28
+
29
+ - **The skill teaches what a log records.** `log.md` carries durable knowledge
30
+ and shipped behavior, never the process that produced them —
31
+ `rule:okf-log-durable-only` in `reference/authoring.md`, cited by the maintain
32
+ playbook and the Closeout gate. It was the okf-gem repository's own contract
33
+ before this, which an agent maintaining any other bundle never reads.
34
+
35
+ ## [2.0.0] - 2026-08-14
36
+
37
+ > Major, not minor. The Breaking entries below change public shapes a shipped
38
+ > consumer already reads: two lint check ids are renamed, the `stale` finding's
39
+ > JSON metric moves from `timestamp` to `generated_at`, and the `timestamp`
40
+ > catalog column is gone. A v0.1 *bundle* still validates with zero warnings —
41
+ > §13.1's fallbacks hold — but spec compatibility and API compatibility are
42
+ > different promises, and semver tracks the second. okf-mcp's open-ended
43
+ > `okf >= 1.13` is closed to `>= 2.0, < 3` in the same release.
44
+
45
+ ### Added
46
+
47
+ - **`okf references` — the §6.3 inventory.** Lists every file under
48
+ `references/`, the non-markdown ones included (the concept model carries
49
+ only markdown, so no other verb can see a `.py` attester or a `.sql`
50
+ computation), with which concepts cite each file through the §6.2
51
+ path-valued fields — plus every pointer into `references/` that resolves to
52
+ nothing. A bare `references/…` written from a subdirectory resolves relative
53
+ to the concept (the §6.2 trap the SPEC's own examples invite), and when the
54
+ leading-slash spelling would have hit, the dangling entry says so. Advisory
55
+ (exit 0); `--json` with `--fields`/`--except` like the other read views.
56
+ Backed by `OKF::Bundle::References` (pure) and
57
+ `OKF::Bundle::Folder#references` (the disk manifest) in the library, and by
58
+ `Markdown::Links.resolve_path` — `#resolve`'s path arithmetic without its
59
+ `.md` gate, since §6.2 fields accept any file.
60
+ - **The §5/§10 families across every surface.** The validator warns on their
61
+ shapes (raw keys only, warnings only — §11's three hard conditions are
62
+ untouched) and its warnings become machine-readable: each carries `check:`
63
+ (a stable id) and `source:` (`spec` | `convention`), with errors keeping
64
+ their exact two-key shape. A root `index.md`'s `okf_version` is actually
65
+ read now — an unknown version warns under §12 and the bundle is consumed
66
+ anyway; unquoted `okf_version: 0.2` (a Psych Float) is accepted.
67
+ - **Eight lint categories with pinned severities** (`Linter::SEVERITIES`, a
68
+ tested constant): Freshness gains `expired` (info, clock-gated — the CLI
69
+ passes today, `--today YYYY-MM-DD` pins it for reproducible CI reports, and
70
+ the pure library runs no clock check unless handed `today:`, confessing via
71
+ `stats[:skipped_checks]`); Provenance gains `broken_source`,
72
+ `unattributed_claim` (warn — only once a concept adopts keyed attribution,
73
+ with the label↔id join case-folded the way GFM resolves footnotes),
74
+ `unused_source`, and `unprefixed_actor` (info — a `verified[].by` outside
75
+ §7's three forms reads as machine-confirmed); a missing `generated.by` is
76
+ the validator's warning alone, never double-reported by lint. Attestation
77
+ gains `incomplete_computation` (warn, on
78
+ neither-or-both computation shapes) and `broken_attestation_ref` (warn — a
79
+ `computation`, `executor.resource` or `attester.resource` on an
80
+ `Attested Computation` naming an in-bundle `.md` that is not there; a contract whose parts are named but
81
+ absent is one no consumer can follow, so it warns rather than informs, with
82
+ the same URL/non-`.md` exemption `broken_source` carries); Migration's `legacy_timestamp` and
83
+ `legacy_citations` (info) name what a v0.1 bundle would change without ever
84
+ failing it. Reports gain `trust` and `status` distributions.
85
+ - **`--fail-on info`** joins `never | warn` — gateability without a severity
86
+ promotion; a migration campaign is
87
+ `okf lint <dir> --only legacy_timestamp,legacy_citations --fail-on info`.
88
+ - **`Linter.call(only:/except:)` refuses an unknown check id** with an
89
+ ArgumentError naming it, instead of silently intersecting to an empty run
90
+ that reports healthy — the library-side twin of the CLI's exit 2, and what
91
+ keeps a caller pinned to a renamed id from reading "checked and fine" over
92
+ a run that ran nothing.
93
+ - **`--status` and `--trust` filters** on catalog, files, search, tags and
94
+ types — `--status` matches the effective value (absent reads `stable`),
95
+ `--trust` folds either tier spelling.
96
+ - **Sources are searchable text** in both Ruby engines (weight 1 — the weight
97
+ the body text carried in v0.1) and snippet-eligible, so a migrated bundle
98
+ keeps its recall and a source-only hit keeps a snippet; the static page
99
+ bakes and indexes the same source text.
100
+ - **Trust as the graph page's third channel**: tier chips, status badges
101
+ (declared non-default only), a generated line, a client-clock stale marker,
102
+ and status/trust filter groups counted off the catalog.
103
+ - The skill teaches v0.2: the vendored SPEC is the published v0.2, a new
104
+ attested-computation template carries §10.3's MUST NOT, and the migrate
105
+ playbook walks the v0.1→v0.2 rewrite.
106
+ - **`Concept#shows_trust?` and `Bundle::RowFilter.shows_trust?` — §5.3's
107
+ display half, in one place.** The tier is always derivable; whether a surface
108
+ should *claim* it is a different question, and the answer is no for a concept
109
+ that declared no §5 family, which is every concept of every v0.1 bundle.
110
+ Claiming it there would paint a provenance verdict onto documents that never
111
+ made one — the false claim the trust system exists to prevent. The rule was
112
+ spelled twice, inline in `/node/meta` and in the page's `showsTrust`, and a
113
+ third consumer (okf-tui, which needs it for a chip, a facet gate, the facet
114
+ counts and the narrowing) is what a hand-copied predicate does not survive:
115
+ a gate disagreeing with the counts beside it reads "unverified 3" over two
116
+ chipped cards. One class method takes the two wire values so a Concept and a
117
+ catalog row ask the same rule; the server now routes through it, and the
118
+ page's client-side twin — unavoidable, since it runs over baked rows with no
119
+ Ruby to call — is pinned equal to it by `parity_test.rb`, truth table and
120
+ source literal both.
121
+ - **`Bundle#okf_version` — the spec version the root index declares (§12)**,
122
+ as the producer wrote it, or nil when it declares none, which §12 permits.
123
+ Stringified and stripped for the reason the validator compares that way: an
124
+ unquoted `okf_version: 0.2` is a Psych Float, and a consumer switching on it
125
+ must not be handed 0.2 the number. Public because the alternative is a
126
+ literal — a consumer naming the version on screen had no way to ask, which is
127
+ how a reader gets told "v0.1" about a bundle declaring 0.2. Unparseable
128
+ frontmatter stays the validator's error to report rather than this reader's
129
+ to raise.
130
+ - **`log_order` [info], the first log-side lint check.** §9 describes the log
131
+ as date-grouped entries, newest first — prose, not an RFC keyword, so
132
+ disorder is curation slack rather than a conformance error: exactly lint's
133
+ side of the split. Only shape-valid headings are compared; a malformed date
134
+ stays the validator's error, reported once.
135
+ - **`unprefixed_actor` now covers `generated.by` too.** §7 gives the actor
136
+ convention to both identity fields; only `verified[].by` was checked, on the
137
+ argument that §5.3 derives trust from nothing else — true, but a
138
+ `generated.by` no form can classify leaves a provenance reader unable to
139
+ tell a person from a process, so it earns the same info finding with its own
140
+ consequence. A *missing* `generated.by` stays the validator's warning.
141
+
142
+ - **`Bundle#stats` and `Bundle#tag_groups` join the library API** —
143
+ extracted from the CLI's `stats` and `tags --by` so the MCP shell reads
144
+ the same rollups instead of hand-copying them: the `by_dir` zero-keeping
145
+ subtlety and the within-group-beside-total tag counting each have one
146
+ home now (`Folder` delegates both). The CLI verbs are pure consumers of
147
+ the extraction; their output is unchanged.
148
+ - **Two long-standing behaviors are documented contract now, each with a
149
+ pinning test.** Hidden files are outside the bundle: the reader excludes
150
+ dot-prefixed files and everything under a dot-prefixed directory — the
151
+ Unix convention, kept so reading a project root cannot pull an installed
152
+ skill or `.github/` templates in as concepts. And a frontmatter `id:`
153
+ renames the concept, not its home — an extension beyond §2's path-derived
154
+ identity, with the recorded split stated where authors read it: the
155
+ identity views (catalog, hubs, `--dir`, search) follow the id, the
156
+ physical views (`index`, `dirs`, stats' `by_dir`) keep the file where it
157
+ lives. Both in the skill's authoring guide.
158
+
159
+ ### Changed
160
+
161
+ - **`validate` warns on a declared-but-blank `status`.** `status: ""` is a
162
+ producer typo, not an absence — §5.4's default belongs to a concept that
163
+ never declared the key — and reading the blank through the default turned it
164
+ into `stable` before the vocabulary check ran, so the one value §5.4 names
165
+ nowhere was the one that never warned.
166
+ - **`lint --stale-after` refuses the ISO spellings it used to reinterpret.**
167
+ It took whatever `Date.iso8601` parsed, so the basic (`20260101`) and week
168
+ (`2026-W01-1`) forms silently became a cutoff nobody asked for — `20260101`
169
+ read as the year 2026 day 01 of month 01 only by luck of the parser, and
170
+ `2026-W01-1` as 2025-12-29. Both now exit 2 naming the accepted shapes. A
171
+ `YYYY-MM-DD` date and a full `2026-01-01T09:00:00Z` timestamp (what a
172
+ concept's own `generated.at` looks like) are both still accepted, the
173
+ timestamp reduced to its date as before. The new `--today` takes the
174
+ narrower grammar — it names a calendar day, not a moment.
175
+ - **The gem targets OKF v0.2** and keeps reading v0.1 under §13.1's two
176
+ sanctioned fallbacks: a legacy `timestamp` reads as `generated.at` (per-key,
177
+ so a half-migrated `generated: { by: … }` beside a `timestamp` keeps its
178
+ date, and no actor is ever invented), and a body `# Citations` list reads as
179
+ `sources` whenever the native key yields zero mappings. One `OKF::Concept`
180
+ class, no version hierarchy; a pure v0.1 bundle still validates with zero
181
+ warnings.
182
+ - **Breaking: lint check ids renamed** — `missing_timestamp` is
183
+ `missing_generated`, `broken_citation` is `broken_source` — so `--only` /
184
+ `--except` lists naming the old ids exit 2. `uncited_external` is redefined
185
+ over `#sources` (a v0.1 `# Citations` still silences it — a prose-only
186
+ section included; so does a migrated `sources:` block). The `stale`
187
+ finding's JSON metric renames `timestamp:` to `generated_at:` (and its
188
+ message says "last updated <generated_at>") — a consumer reading
189
+ `finding.metric.timestamp` gets `null` and must move with it.
190
+ - **Breaking: the `timestamp` catalog column is removed** — `--fields
191
+ timestamp` exits 2 loudly, naming the valid fields. The `concepts` row gains
192
+ `generated_at`, `generated_by`, `generated` (the raw declared-key boolean
193
+ that tells hand-written apart from v0.1-with-timestamp), `trust` (the
194
+ hyphenated wire literals `unverified` | `machine-confirmed` |
195
+ `human-reviewed`), `stale_after`, and a `sources` count. `status` stays the
196
+ raw declared value, `null` when absent. Temporal values render ISO 8601.
197
+ - **Breaking: `Concept#citations` and `Markdown::Citations.targets` are
198
+ removed**, both subsumed by `Concept#sources` / `Citations.entries`.
199
+ - **The staleness boundary is `today >= stale_after`** — a concept is stale
200
+ **on** the day itself, per §5.5. A consumer replacing its own `<`-based
201
+ check shifts by one day and should know it.
202
+ - `GET /node/meta` returns JSON (`{ description, trust: { tier, generated_by,
203
+ generated_at, status, stale_after } }`, null-stripped) instead of an escaped
204
+ HTML fragment; the graph page composes the trust line client-side for served
205
+ and baked pages alike, and computes expiry against the viewer's own clock.
206
+ - The demo Open Graph card URL the graph template points at moved to
207
+ `og-demo-v5.png` (the site's card-art version bump); the old URL keeps
208
+ serving the current art, so nothing breaks in between.
209
+
210
+
211
+ ### Fixed
212
+
213
+ - **A calendar-invalid log heading is now a §11 error.** §9's MUST is ISO
214
+ 8601, and `## 2026-02-30` matched the digit shape while naming a day that
215
+ never existed — the validator now asks `Date.iso8601`, so a log.md is
216
+ conformant only around real dates.
217
+ - **`MAILTO:` is excluded case-insensitively**, like every other scheme (RFC
218
+ 3986). The guard sat inline and case-sensitive beside the case-insensitive
219
+ `SCHEME` regex in three places, so an uppercase mailto whose address ends in
220
+ `.md` resolved as a relative path — a link outside the bundle reported as a
221
+ file inside it. One `Links::MAILTO` now, beside `SCHEME`, for the same
222
+ reason `SCHEME` moved there.
223
+ - **`incomplete_computation` requires the fence, not the heading.** §10.3's
224
+ inline form is a fenced code block under `# Computation`; a heading over
225
+ prose used to count as provided, and a contract with nothing an executor
226
+ could run lint'd clean.
227
+
228
+
8
229
  ## [1.13.0] - 2026-08-07
9
230
 
10
231
  ### Added
@@ -1322,6 +1543,7 @@ Initial release.
1322
1543
 
1323
1544
  - Runs on Ruby >= 2.4 with two runtime dependencies: rack and webrick.
1324
1545
 
1546
+ [2.0.0]: https://github.com/serradura/okf-gem/compare/v1.13.0...v2.0.0
1325
1547
  [1.13.0]: https://github.com/serradura/okf-gem/compare/v1.12.0...v1.13.0
1326
1548
  [1.12.0]: https://github.com/serradura/okf-gem/compare/v1.11.0...v1.12.0
1327
1549
  [1.11.0]: https://github.com/serradura/okf-gem/compare/v1.10.0...v1.11.0
data/README.md CHANGED
@@ -67,9 +67,10 @@ okf validate <dir|@slug> # is this legal OKF?
67
67
  okf lint <dir|@slug> [--fail-on warn] # is it navigable, complete, fresh?
68
68
  okf loose <dir|@slug> # concepts with no links in or out
69
69
  okf search <dir|@slug…|@all> <term…> # ranked retrieval; @all spans every bundle
70
- okf index <dir|@slug> [--dir D] [--depth N] # the §6 map: index bodies, rollups, listings
70
+ okf index <dir|@slug> [--dir D] [--depth N] # the §8 map: index bodies, rollups, listings
71
71
  okf dirs <dir|@slug> [--dir D] [--depth N] # the shape: every directory and what it holds
72
72
  okf catalog | files | tags | types | stats <dir|@slug> # the browser views, on the CLI
73
+ okf references <dir|@slug> # the references/ inventory: files, citers, dangling pointers
73
74
  okf graph <dir|@slug> [--hubs] [--traffic] # the raw graph; --hubs ranks concepts, --traffic dirs
74
75
  okf server [DIR|@slug…] [-p PORT] [--bind ADDR] # the live graph: one bundle, or all of them
75
76
  okf render <dir|@slug> [-o FILE] # the same page as one static, self-contained file
@@ -89,7 +90,7 @@ behind one hub. `okf registry init` scopes one to a single project instead, and
89
90
  committed `.okf-registry.json` travels with the repo.
90
91
 
91
92
  **A big bundle is read a level at a time.** `okf index --depth 1 --except
92
- body,listing` is the map an agent orients on — on a 400-concept bundle, 2.8 KB
93
+ body,listing` is the map an agent orients on — on a 414-concept bundle, 2.8 KB
93
94
  against the full 313 KB — and `--dir` then opens one branch, bringing the
94
95
  ancestors that say what it is.
95
96
 
@@ -105,7 +106,7 @@ require "okf"
105
106
 
106
107
  folder = OKF::Bundle::Folder.load("docs")
107
108
  folder.concepts # => [OKF::Concept]
108
- folder.validate # => §9 conformance result
109
+ folder.validate # => §11 conformance result
109
110
  folder.lint # => curation report
110
111
  folder.graph # => nodes, edges, indexes
111
112
 
@@ -122,26 +123,47 @@ writer, and the lower-level pieces.
122
123
  ## validate and lint are two different questions
123
124
 
124
125
  `validate` asks *"is this legal OKF?"* and implements the spec's
125
- 9](lib/okf/skill/reference/SPEC.md#9-conformance) exactly — which means it is *forbidden* to
126
+ 11](lib/okf/skill/reference/SPEC.md#11-conformance) exactly — which means it is *forbidden* to
126
127
  reject a bundle for a broken link or a missing optional field.
127
128
 
128
129
  `lint` asks the complementary question, *"is this well-curated, navigable,
129
130
  trustworthy?"*, over exactly those tolerated things: reachability, backlog,
130
- completeness, freshness, provenance, hygiene. It is advisory and exits `0` even
131
- with findings unless you pass `--fail-on warn`.
131
+ completeness, freshness, provenance, attestation, migration, hygiene. It is
132
+ advisory and exits `0` even with findings unless you pass `--fail-on warn`, or
133
+ `--only legacy_timestamp,legacy_citations --fail-on info` to gate a migration
134
+ campaign on the two findings that name a bundle's leftover v0.1 spellings and
135
+ nothing else.
132
136
 
133
137
  Keeping them apart is what lets you gate CI on conformance without gating it on
134
138
  taste. `lint --json` is also the structured input an agent reads to reason about
135
139
  the two things no checker can compute — contradictions, and *semantic* staleness.
136
140
 
141
+ ## Trust is data, so you can filter on it
142
+
143
+ OKF v0.2 lets a bundle say where each concept came from and how far to trust
144
+ it — `generated` (who or what wrote it), `verified` (who confirmed it),
145
+ `status` (its lifecycle), `stale_after` (a declared expiry) — and this gem
146
+ reads the families everywhere: `--status` and `--trust` narrow `catalog`,
147
+ `files`, `search`, `tags` and `types`; the graph page shows each concept's
148
+ tier beside its type; `lint` reports what expired, against a clock you can
149
+ pin (`--today`) for a reproducible report. `okf references` closes the loop
150
+ for §10's attested computations, inventorying the `references/` files —
151
+ attester code, computation files — that back them, with every pointer that
152
+ resolves to nothing named. A v0.1 bundle needs none of this and stays
153
+ readable forever (§13); the two Migration findings tell it what to modernize
154
+ without ever failing it.
155
+
137
156
  ## Extending it
138
157
 
139
158
  Publish a gem named `okf-*` carrying an `okf/plugin.rb` and installing it is the
140
159
  whole installation: your verb answers to `okf` and behaves like a built-in.
141
160
  Nothing an addon registers can displace one, and a broken addon is skipped rather
142
- than taking the CLI down. [`okf-mcp`](https://rubygems.org/gems/okf-mcp) is the
143
- first one install it and `okf mcp` serves your bundles over the Model Context
144
- Protocol, with nothing in this gem naming it.
161
+ than taking the CLI down. Three ship alongside this one, with nothing in this
162
+ gem naming any of them: [`okf-mcp`](https://rubygems.org/gems/okf-mcp) serves
163
+ your bundles over the Model Context Protocol,
164
+ [`okf-tui`](https://rubygems.org/gems/okf-tui) browses them full-screen in a
165
+ terminal, and [`okf-pro`](https://rubygems.org/gems/okf-pro) writes an
166
+ agent's knowledge repository and enforces it at three doors.
145
167
 
146
168
  The graph page treats a bundle as untrusted content: inlined data is escaped and
147
169
  every concept body is sanitized before it reaches the DOM, so a script hidden in
@@ -152,8 +174,9 @@ do not know.
152
174
  ## More
153
175
 
154
176
  The [project README](https://github.com/serradura/okf-gem#readme) carries the
155
- diagrams, the comparison with `CLAUDE.md`, agent auto-memory and wikis, and the
156
- Claude Code plugin. The [docs](https://okfgem.com/docs/) are the manual. And the
177
+ diagrams, the comparison with `CLAUDE.md`, agent auto-memory and wikis, the
178
+ Claude Code plugin, and the way to install the skill into any agent without this
179
+ gem (`npx skills add serradura/okf-gem`). The [docs](https://okfgem.com/docs/) are the manual. And the
157
180
  repo documents *itself* in OKF — clone it and run `okf server .okf` to read this
158
181
  gem's own knowledge as a graph.
159
182
 
@@ -62,6 +62,22 @@ module OKF
62
62
  @bundle.directory_index
63
63
  end
64
64
 
65
+ def stats
66
+ @bundle.stats
67
+ end
68
+
69
+ def tag_groups(by:, entries: nil)
70
+ @bundle.tag_groups(by: by, entries: entries)
71
+ end
72
+
73
+ # The §6.3 inventory — this handle's half is the disk: the reader models
74
+ # only markdown, so listing `references/` here is how a .py attester or a
75
+ # .sql computation becomes visible at all. The pure model gets the
76
+ # manifest and answers the rest (citers, dangling pointers).
77
+ def references
78
+ Bundle::References.build(@bundle, files: reference_files)
79
+ end
80
+
65
81
  # Every log.md with its content, root scope first — read live from disk so a
66
82
  # just-appended entry shows without a reload; the reserved snapshot is the
67
83
  # fallback if the file has since vanished. Shared by `okf render`'s bake
@@ -119,7 +135,7 @@ module OKF
119
135
  Concept::File.new(root: @root, path: path).read
120
136
  end
121
137
 
122
- # Materialize the in-memory bundle to disk (Writer validates §9 before
138
+ # Materialize the in-memory bundle to disk (Writer validates §11 before
123
139
  # publishing, so a malformed bundle is never written).
124
140
  def save(overwrite: false)
125
141
  Writer.call(
@@ -139,6 +155,16 @@ module OKF
139
155
 
140
156
  private
141
157
 
158
+ # Bundle-relative paths of every file under references/, sorted. A glob
159
+ # on a directory that does not exist is simply empty — no references/ and
160
+ # an empty references/ are the same inventory.
161
+ def reference_files
162
+ Dir.glob(File.join(@root, "references", "**", "*"))
163
+ .select { |path| File.file?(path) }
164
+ .map { |path| Pathname.new(path).relative_path_from(Pathname.new(@root)).to_s }
165
+ .sort
166
+ end
167
+
142
168
  def reserved_hash(basename)
143
169
  @bundle.reserved
144
170
  .select { |entry| File.basename(entry.path) == basename }
@@ -20,7 +20,7 @@ module OKF
20
20
  attr_reader :nodes, :edges, :type_index, :tag_index
21
21
 
22
22
  def self.build(bundle, minimal: false, body: true)
23
- # Best-effort (§9): a malformed concept never reaches here — the reader keeps
23
+ # Best-effort (§11): a malformed concept never reaches here — the reader keeps
24
24
  # it in bundle.unparseable — so the rest of the bundle still renders. Inspect
25
25
  # bundle.unparseable to detect skips.
26
26
  concepts = bundle.concepts
@@ -51,10 +51,19 @@ module OKF
51
51
  # Edges resolve by *path* — a markdown link is a file path — then map that path
52
52
  # to the concept living there and use its id, so a frontmatter `id` that differs
53
53
  # from the path still lands the edge on the right node.
54
+ # Body links and sources[].resource entries feed the same resolver: §5.1
55
+ # says a `resource` naming another concept is a derivation edge that
56
+ # "already exists in the bundle graph", and this is what makes that true —
57
+ # it is also what keeps a migrated bundle's graph equal to its v0.1 twin's,
58
+ # since a `# Citations` in-bundle link stops being a body link the moment
59
+ # it is lifted into frontmatter. URLs and scope descriptors resolve to
60
+ # nothing; an unresolvable path is broken_source's to report.
54
61
  def self.edges_for(concepts, id_by_path, root)
55
62
  seen = Set.new
56
63
  concepts.each_with_object([]) do |concept, edges|
57
- Markdown::Links.extract(concept.body).each do |raw|
64
+ targets = Markdown::Links.extract(concept.body) +
65
+ concept.sources.map { |source| source["resource"] }.compact
66
+ targets.each do |raw|
58
67
  resolved = Markdown::Links.resolve(raw, from: concept.path, bundle: root)
59
68
  next if resolved.nil?
60
69
 
@@ -86,7 +95,7 @@ module OKF
86
95
  concept.tags.is_a?(Array) ? concept.tags : []
87
96
  end
88
97
 
89
- # Blank, not just nil: §9.2 makes a whitespace-only `type` as non-conformant
98
+ # Blank, not just nil: §11 condition 2 makes a whitespace-only `type` as non-conformant
90
99
  # as a missing one (the validator says so with the same OKF.blank?), so the
91
100
  # index must not sort them into different buckets. Otherwise `type: " "`
92
101
  # earns its own row, labelled with spaces, next to Untyped.