okf 1.13.0 → 2.0.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +195 -0
  3. data/README.md +25 -6
  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 +4 -1
  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 +14 -12
  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 +7 -6
  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 +16 -9
  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 +154 -35
  45. data/lib/okf/skill/reference/cli.md +155 -42
  46. data/lib/okf/skill/templates/attested-computation.md +41 -0
  47. data/lib/okf/skill/templates/concept.md +13 -6
  48. data/lib/okf/skill/templates/root-index.md +1 -1
  49. data/lib/okf/version.rb +1 -1
  50. data/lib/okf.rb +22 -2
  51. metadata +5 -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: 43476118366ed4610628f4692f1c1432e4babe4a9d5749f06eb69fc41738c4e5
4
+ data.tar.gz: 218144bd22483995d933cf9d89f44e9602c2c95255d9e1c95626700a4f5b2f2e
5
5
  SHA512:
6
- metadata.gz: b0c9f1002b408702c74f73be001eab10b6691419700d33b41e81bd001d21858f1d79289696edbb2ad6e7c6f4a76ccd4c7458cf211e82fded5224f5fcfa6328b4
7
- data.tar.gz: 0ed68e0095ab1aa912c5df3b6fd7b9f82e0e07f2cad0c182437cb70021beed8d5ecb054106185204b18d0f138303372bc62202c286aec75ce6c4a43434f2e546
6
+ metadata.gz: 40ccd9ca9a9fe8580856825a972753b0f58ff2158fec6262db84d0b9d504e24c071a78e5b05939c0c5648ac7da67083621085417547ae194a0343ae1de34694b
7
+ data.tar.gz: 506f9d1feb6c6428c9db8845b4880207e904901636b35411b8b85f37dc909abb08b96d82246f06e410c97f6991ccfd947a559eed1d9c5b8d8e429b4790bd5929
data/CHANGELOG.md CHANGED
@@ -5,6 +5,200 @@ 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.0.0] - 2026-08-14
9
+
10
+ > Major, not minor. The Breaking entries below change public shapes a shipped
11
+ > consumer already reads: two lint check ids are renamed, the `stale` finding's
12
+ > JSON metric moves from `timestamp` to `generated_at`, and the `timestamp`
13
+ > catalog column is gone. A v0.1 *bundle* still validates with zero warnings —
14
+ > §13.1's fallbacks hold — but spec compatibility and API compatibility are
15
+ > different promises, and semver tracks the second. okf-mcp's open-ended
16
+ > `okf >= 1.13` is closed to `>= 2.0, < 3` in the same release.
17
+
18
+ ### Added
19
+
20
+ - **`okf references` — the §6.3 inventory.** Lists every file under
21
+ `references/`, the non-markdown ones included (the concept model carries
22
+ only markdown, so no other verb can see a `.py` attester or a `.sql`
23
+ computation), with which concepts cite each file through the §6.2
24
+ path-valued fields — plus every pointer into `references/` that resolves to
25
+ nothing. A bare `references/…` written from a subdirectory resolves relative
26
+ to the concept (the §6.2 trap the SPEC's own examples invite), and when the
27
+ leading-slash spelling would have hit, the dangling entry says so. Advisory
28
+ (exit 0); `--json` with `--fields`/`--except` like the other read views.
29
+ Backed by `OKF::Bundle::References` (pure) and
30
+ `OKF::Bundle::Folder#references` (the disk manifest) in the library, and by
31
+ `Markdown::Links.resolve_path` — `#resolve`'s path arithmetic without its
32
+ `.md` gate, since §6.2 fields accept any file.
33
+ - **The §5/§10 families across every surface.** The validator warns on their
34
+ shapes (raw keys only, warnings only — §11's three hard conditions are
35
+ untouched) and its warnings become machine-readable: each carries `check:`
36
+ (a stable id) and `source:` (`spec` | `convention`), with errors keeping
37
+ their exact two-key shape. A root `index.md`'s `okf_version` is actually
38
+ read now — an unknown version warns under §12 and the bundle is consumed
39
+ anyway; unquoted `okf_version: 0.2` (a Psych Float) is accepted.
40
+ - **Eight lint categories with pinned severities** (`Linter::SEVERITIES`, a
41
+ tested constant): Freshness gains `expired` (info, clock-gated — the CLI
42
+ passes today, `--today YYYY-MM-DD` pins it for reproducible CI reports, and
43
+ the pure library runs no clock check unless handed `today:`, confessing via
44
+ `stats[:skipped_checks]`); Provenance gains `broken_source`,
45
+ `unattributed_claim` (warn — only once a concept adopts keyed attribution,
46
+ with the label↔id join case-folded the way GFM resolves footnotes),
47
+ `unused_source`, and `unprefixed_actor` (info — a `verified[].by` outside
48
+ §7's three forms reads as machine-confirmed); a missing `generated.by` is
49
+ the validator's warning alone, never double-reported by lint. Attestation
50
+ gains `incomplete_computation` (warn, on
51
+ neither-or-both computation shapes) and `broken_attestation_ref` (warn — a
52
+ `computation`, `executor.resource` or `attester.resource` on an
53
+ `Attested Computation` naming an in-bundle `.md` that is not there; a contract whose parts are named but
54
+ absent is one no consumer can follow, so it warns rather than informs, with
55
+ the same URL/non-`.md` exemption `broken_source` carries); Migration's `legacy_timestamp` and
56
+ `legacy_citations` (info) name what a v0.1 bundle would change without ever
57
+ failing it. Reports gain `trust` and `status` distributions.
58
+ - **`--fail-on info`** joins `never | warn` — gateability without a severity
59
+ promotion; a migration campaign is
60
+ `okf lint <dir> --only legacy_timestamp,legacy_citations --fail-on info`.
61
+ - **`Linter.call(only:/except:)` refuses an unknown check id** with an
62
+ ArgumentError naming it, instead of silently intersecting to an empty run
63
+ that reports healthy — the library-side twin of the CLI's exit 2, and what
64
+ keeps a caller pinned to a renamed id from reading "checked and fine" over
65
+ a run that ran nothing.
66
+ - **`--status` and `--trust` filters** on catalog, files, search, tags and
67
+ types — `--status` matches the effective value (absent reads `stable`),
68
+ `--trust` folds either tier spelling.
69
+ - **Sources are searchable text** in both Ruby engines (weight 1 — the weight
70
+ the body text carried in v0.1) and snippet-eligible, so a migrated bundle
71
+ keeps its recall and a source-only hit keeps a snippet; the static page
72
+ bakes and indexes the same source text.
73
+ - **Trust as the graph page's third channel**: tier chips, status badges
74
+ (declared non-default only), a generated line, a client-clock stale marker,
75
+ and status/trust filter groups counted off the catalog.
76
+ - The skill teaches v0.2: the vendored SPEC is the published v0.2, a new
77
+ attested-computation template carries §10.3's MUST NOT, and the migrate
78
+ playbook walks the v0.1→v0.2 rewrite.
79
+ - **`Concept#shows_trust?` and `Bundle::RowFilter.shows_trust?` — §5.3's
80
+ display half, in one place.** The tier is always derivable; whether a surface
81
+ should *claim* it is a different question, and the answer is no for a concept
82
+ that declared no §5 family, which is every concept of every v0.1 bundle.
83
+ Claiming it there would paint a provenance verdict onto documents that never
84
+ made one — the false claim the trust system exists to prevent. The rule was
85
+ spelled twice, inline in `/node/meta` and in the page's `showsTrust`, and a
86
+ third consumer (okf-tui, which needs it for a chip, a facet gate, the facet
87
+ counts and the narrowing) is what a hand-copied predicate does not survive:
88
+ a gate disagreeing with the counts beside it reads "unverified 3" over two
89
+ chipped cards. One class method takes the two wire values so a Concept and a
90
+ catalog row ask the same rule; the server now routes through it, and the
91
+ page's client-side twin — unavoidable, since it runs over baked rows with no
92
+ Ruby to call — is pinned equal to it by `parity_test.rb`, truth table and
93
+ source literal both.
94
+ - **`Bundle#okf_version` — the spec version the root index declares (§12)**,
95
+ as the producer wrote it, or nil when it declares none, which §12 permits.
96
+ Stringified and stripped for the reason the validator compares that way: an
97
+ unquoted `okf_version: 0.2` is a Psych Float, and a consumer switching on it
98
+ must not be handed 0.2 the number. Public because the alternative is a
99
+ literal — a consumer naming the version on screen had no way to ask, which is
100
+ how a reader gets told "v0.1" about a bundle declaring 0.2. Unparseable
101
+ frontmatter stays the validator's error to report rather than this reader's
102
+ to raise.
103
+ - **`log_order` [info], the first log-side lint check.** §9 describes the log
104
+ as date-grouped entries, newest first — prose, not an RFC keyword, so
105
+ disorder is curation slack rather than a conformance error: exactly lint's
106
+ side of the split. Only shape-valid headings are compared; a malformed date
107
+ stays the validator's error, reported once.
108
+ - **`unprefixed_actor` now covers `generated.by` too.** §7 gives the actor
109
+ convention to both identity fields; only `verified[].by` was checked, on the
110
+ argument that §5.3 derives trust from nothing else — true, but a
111
+ `generated.by` no form can classify leaves a provenance reader unable to
112
+ tell a person from a process, so it earns the same info finding with its own
113
+ consequence. A *missing* `generated.by` stays the validator's warning.
114
+
115
+ - **`Bundle#stats` and `Bundle#tag_groups` join the library API** —
116
+ extracted from the CLI's `stats` and `tags --by` so the MCP shell reads
117
+ the same rollups instead of hand-copying them: the `by_dir` zero-keeping
118
+ subtlety and the within-group-beside-total tag counting each have one
119
+ home now (`Folder` delegates both). The CLI verbs are pure consumers of
120
+ the extraction; their output is unchanged.
121
+ - **Two long-standing behaviors are documented contract now, each with a
122
+ pinning test.** Hidden files are outside the bundle: the reader excludes
123
+ dot-prefixed files and everything under a dot-prefixed directory — the
124
+ Unix convention, kept so reading a project root cannot pull an installed
125
+ skill or `.github/` templates in as concepts. And a frontmatter `id:`
126
+ renames the concept, not its home — an extension beyond §2's path-derived
127
+ identity, with the recorded split stated where authors read it: the
128
+ identity views (catalog, hubs, `--dir`, search) follow the id, the
129
+ physical views (`index`, `dirs`, stats' `by_dir`) keep the file where it
130
+ lives. Both in the skill's authoring guide.
131
+
132
+ ### Changed
133
+
134
+ - **`validate` warns on a declared-but-blank `status`.** `status: ""` is a
135
+ producer typo, not an absence — §5.4's default belongs to a concept that
136
+ never declared the key — and reading the blank through the default turned it
137
+ into `stable` before the vocabulary check ran, so the one value §5.4 names
138
+ nowhere was the one that never warned.
139
+ - **`lint --stale-after` refuses the ISO spellings it used to reinterpret.**
140
+ It took whatever `Date.iso8601` parsed, so the basic (`20260101`) and week
141
+ (`2026-W01-1`) forms silently became a cutoff nobody asked for — `20260101`
142
+ read as the year 2026 day 01 of month 01 only by luck of the parser, and
143
+ `2026-W01-1` as 2025-12-29. Both now exit 2 naming the accepted shapes. A
144
+ `YYYY-MM-DD` date and a full `2026-01-01T09:00:00Z` timestamp (what a
145
+ concept's own `generated.at` looks like) are both still accepted, the
146
+ timestamp reduced to its date as before. The new `--today` takes the
147
+ narrower grammar — it names a calendar day, not a moment.
148
+ - **The gem targets OKF v0.2** and keeps reading v0.1 under §13.1's two
149
+ sanctioned fallbacks: a legacy `timestamp` reads as `generated.at` (per-key,
150
+ so a half-migrated `generated: { by: … }` beside a `timestamp` keeps its
151
+ date, and no actor is ever invented), and a body `# Citations` list reads as
152
+ `sources` whenever the native key yields zero mappings. One `OKF::Concept`
153
+ class, no version hierarchy; a pure v0.1 bundle still validates with zero
154
+ warnings.
155
+ - **Breaking: lint check ids renamed** — `missing_timestamp` is
156
+ `missing_generated`, `broken_citation` is `broken_source` — so `--only` /
157
+ `--except` lists naming the old ids exit 2. `uncited_external` is redefined
158
+ over `#sources` (a v0.1 `# Citations` still silences it — a prose-only
159
+ section included; so does a migrated `sources:` block). The `stale`
160
+ finding's JSON metric renames `timestamp:` to `generated_at:` (and its
161
+ message says "last updated <generated_at>") — a consumer reading
162
+ `finding.metric.timestamp` gets `null` and must move with it.
163
+ - **Breaking: the `timestamp` catalog column is removed** — `--fields
164
+ timestamp` exits 2 loudly, naming the valid fields. The `concepts` row gains
165
+ `generated_at`, `generated_by`, `generated` (the raw declared-key boolean
166
+ that tells hand-written apart from v0.1-with-timestamp), `trust` (the
167
+ hyphenated wire literals `unverified` | `machine-confirmed` |
168
+ `human-reviewed`), `stale_after`, and a `sources` count. `status` stays the
169
+ raw declared value, `null` when absent. Temporal values render ISO 8601.
170
+ - **Breaking: `Concept#citations` and `Markdown::Citations.targets` are
171
+ removed**, both subsumed by `Concept#sources` / `Citations.entries`.
172
+ - **The staleness boundary is `today >= stale_after`** — a concept is stale
173
+ **on** the day itself, per §5.5. A consumer replacing its own `<`-based
174
+ check shifts by one day and should know it.
175
+ - `GET /node/meta` returns JSON (`{ description, trust: { tier, generated_by,
176
+ generated_at, status, stale_after } }`, null-stripped) instead of an escaped
177
+ HTML fragment; the graph page composes the trust line client-side for served
178
+ and baked pages alike, and computes expiry against the viewer's own clock.
179
+ - The demo Open Graph card URL the graph template points at moved to
180
+ `og-demo-v5.png` (the site's card-art version bump); the old URL keeps
181
+ serving the current art, so nothing breaks in between.
182
+
183
+
184
+ ### Fixed
185
+
186
+ - **A calendar-invalid log heading is now a §11 error.** §9's MUST is ISO
187
+ 8601, and `## 2026-02-30` matched the digit shape while naming a day that
188
+ never existed — the validator now asks `Date.iso8601`, so a log.md is
189
+ conformant only around real dates.
190
+ - **`MAILTO:` is excluded case-insensitively**, like every other scheme (RFC
191
+ 3986). The guard sat inline and case-sensitive beside the case-insensitive
192
+ `SCHEME` regex in three places, so an uppercase mailto whose address ends in
193
+ `.md` resolved as a relative path — a link outside the bundle reported as a
194
+ file inside it. One `Links::MAILTO` now, beside `SCHEME`, for the same
195
+ reason `SCHEME` moved there.
196
+ - **`incomplete_computation` requires the fence, not the heading.** §10.3's
197
+ inline form is a fenced code block under `# Computation`; a heading over
198
+ prose used to count as provided, and a contract with nothing an executor
199
+ could run lint'd clean.
200
+
201
+
8
202
  ## [1.13.0] - 2026-08-07
9
203
 
10
204
  ### Added
@@ -1322,6 +1516,7 @@ Initial release.
1322
1516
 
1323
1517
  - Runs on Ruby >= 2.4 with two runtime dependencies: rack and webrick.
1324
1518
 
1519
+ [2.0.0]: https://github.com/serradura/okf-gem/compare/v1.13.0...v2.0.0
1325
1520
  [1.13.0]: https://github.com/serradura/okf-gem/compare/v1.12.0...v1.13.0
1326
1521
  [1.12.0]: https://github.com/serradura/okf-gem/compare/v1.11.0...v1.12.0
1327
1522
  [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,18 +123,36 @@ 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
@@ -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.