Class: OKF::Bundle::Graph
- Inherits:
-
Object
- Object
- OKF::Bundle::Graph
- Defined in:
- lib/okf/bundle/graph.rb
Overview
An in-memory knowledge graph of a bundle: concepts become nodes and bundle-relative markdown links become directed edges. Pure — built from an OKF::Bundle (already in memory), does no I/O, and carries no presentation concerns (sizing/colour belong to a renderer).
Node fidelity is a build option, so the graph can ship only what a consumer needs and let a server serve the rest on demand:
* default (minimal: false, body: true) — full: id, type, title, description,
tags, body.
* body: false — everything but the body.
* minimal: true — just id and title (the leanest payload to draw the graph).
Regardless of node fidelity, #type_index and #tag_index expose compact inverted indexes ({ value => [id, …] }) computed from every concept, so a minimal client can still colour by type and filter by tag.
Instance Attribute Summary collapse
-
#edges ⇒ Object
readonly
Returns the value of attribute edges.
-
#nodes ⇒ Object
readonly
Returns the value of attribute nodes.
-
#tag_index ⇒ Object
readonly
Returns the value of attribute tag_index.
-
#type_index ⇒ Object
readonly
Returns the value of attribute type_index.
Class Method Summary collapse
- .build(bundle, minimal: false, body: true) ⇒ Object
- .default(value, fallback) ⇒ Object
-
.edges_for(concepts, id_by_path, root) ⇒ Object
Edges resolve by path — a markdown link is a file path — then map that path to the concept living there and use its id, so a frontmatter
idthat differs from the path still lands the edge on the right node. - .node_for(concept, minimal: false, body: true) ⇒ Object
-
.tag_index_for(concepts) ⇒ Object
{ tag => [id, …] } over every concept (a concept contributes 0..n tags).
- .tags_of(concept) ⇒ Object
-
.type_index_for(concepts) ⇒ Object
{ type => [id, …] } over every concept (one type each, "Untyped" if blank).
Instance Method Summary collapse
-
#initialize(nodes:, edges:, type_index: {}, tag_index: {}) ⇒ Graph
constructor
A new instance of Graph.
- #to_h ⇒ Object
-
#unlinked_ids ⇒ Object
Ids of nodes with graph degree 0 — no cross-links in or out.
Constructor Details
#initialize(nodes:, edges:, type_index: {}, tag_index: {}) ⇒ Graph
Returns a new instance of Graph.
93 94 95 96 97 98 |
# File 'lib/okf/bundle/graph.rb', line 93 def initialize(nodes:, edges:, type_index: {}, tag_index: {}) @nodes = nodes @edges = edges @type_index = type_index @tag_index = tag_index end |
Instance Attribute Details
#edges ⇒ Object (readonly)
Returns the value of attribute edges.
20 21 22 |
# File 'lib/okf/bundle/graph.rb', line 20 def edges @edges end |
#nodes ⇒ Object (readonly)
Returns the value of attribute nodes.
20 21 22 |
# File 'lib/okf/bundle/graph.rb', line 20 def nodes @nodes end |
#tag_index ⇒ Object (readonly)
Returns the value of attribute tag_index.
20 21 22 |
# File 'lib/okf/bundle/graph.rb', line 20 def tag_index @tag_index end |
#type_index ⇒ Object (readonly)
Returns the value of attribute type_index.
20 21 22 |
# File 'lib/okf/bundle/graph.rb', line 20 def type_index @type_index end |
Class Method Details
.build(bundle, minimal: false, body: true) ⇒ Object
22 23 24 25 26 27 28 29 30 31 32 33 34 |
# File 'lib/okf/bundle/graph.rb', line 22 def self.build(bundle, minimal: false, body: true) # Best-effort (§9): a malformed concept never reaches here — the reader keeps # it in bundle.unparseable — so the rest of the bundle still renders. Inspect # bundle.unparseable to detect skips. concepts = bundle.concepts id_by_path = concepts.map { |concept| [ concept.path, concept.id ] }.to_h new( nodes: concepts.map { |concept| node_for(concept, minimal: minimal, body: body) }, edges: edges_for(concepts, id_by_path, bundle.root), type_index: type_index_for(concepts), tag_index: tag_index_for(concepts) ) end |
.default(value, fallback) ⇒ Object
89 90 91 |
# File 'lib/okf/bundle/graph.rb', line 89 def self.default(value, fallback) value.nil? ? fallback : value.to_s end |
.edges_for(concepts, id_by_path, root) ⇒ Object
Edges resolve by path — a markdown link is a file path — then map that path
to the concept living there and use its id, so a frontmatter id that differs
from the path still lands the edge on the right node.
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
# File 'lib/okf/bundle/graph.rb', line 54 def self.edges_for(concepts, id_by_path, root) seen = Set.new concepts.each_with_object([]) do |concept, edges| Markdown::Links.extract(concept.body).each do |raw| resolved = Markdown::Links.resolve(raw, from: concept.path, bundle: root) next if resolved.nil? target_id = id_by_path[resolved] next if target_id.nil? next if target_id == concept.id next unless seen.add?([ concept.id, target_id ]) edges << { source: concept.id, target: target_id } end end end |
.node_for(concept, minimal: false, body: true) ⇒ Object
36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
# File 'lib/okf/bundle/graph.rb', line 36 def self.node_for(concept, minimal: false, body: true) title = default(concept.title, File.basename(concept.id)) return { id: concept.id, title: title } if minimal node = { id: concept.id, type: default(concept.type, "Untyped"), title: title, description: concept.description.to_s, tags: (concept) } node[:body] = concept.body.to_s.strip if body node end |
.tag_index_for(concepts) ⇒ Object
{ tag => [id, …] } over every concept (a concept contributes 0..n tags).
79 80 81 82 83 |
# File 'lib/okf/bundle/graph.rb', line 79 def self.tag_index_for(concepts) concepts.each_with_object({}) do |concept, index| (concept).each { |tag| (index[tag.to_s] ||= []) << concept.id } end end |
.tags_of(concept) ⇒ Object
85 86 87 |
# File 'lib/okf/bundle/graph.rb', line 85 def self.(concept) concept..is_a?(Array) ? concept. : [] end |
.type_index_for(concepts) ⇒ Object
{ type => [id, …] } over every concept (one type each, "Untyped" if blank).
72 73 74 75 76 |
# File 'lib/okf/bundle/graph.rb', line 72 def self.type_index_for(concepts) concepts.each_with_object({}) do |concept, index| (index[default(concept.type, "Untyped")] ||= []) << concept.id end end |
Instance Method Details
#to_h ⇒ Object
100 101 102 |
# File 'lib/okf/bundle/graph.rb', line 100 def to_h { nodes: nodes, edges: edges } end |
#unlinked_ids ⇒ Object
Ids of nodes with graph degree 0 — no cross-links in or out. These are the "loose" files: they float in a rendered graph, reachable (if at all) only via an index.md listing, which is not an edge. Distinct from an orphan, a reachability notion that an index listing satisfies — a concept can be indexed (not an orphan) yet still be unlinked here. Preserves node order.
109 110 111 112 113 114 115 |
# File 'lib/okf/bundle/graph.rb', line 109 def unlinked_ids @unlinked_ids ||= begin linked = Set.new edges.each { |edge| linked << edge[:source] << edge[:target] } nodes.map { |node| node[:id] }.reject { |id| linked.include?(id) } end end |