Class: OKF::Bundle

Inherits:
Object
  • Object
show all
Defined in:
lib/okf/bundle.rb,
lib/okf/bundle/graph.rb,
lib/okf/bundle/folder.rb,
lib/okf/bundle/linter.rb,
lib/okf/bundle/reader.rb,
lib/okf/bundle/search.rb,
lib/okf/bundle/writer.rb,
lib/okf/bundle/validator.rb,
lib/okf/bundle/linter/report.rb,
lib/okf/bundle/validator/result.rb

Overview

A knowledge bundle held in memory (spec §2), Concept-first: parsed concepts plus the raw text of reserved index/log files and any files whose frontmatter failed to parse. Pure — it performs no disk access.

Build one straight from data (the Rails path — no markdown round-trip):

OKF::Bundle.new(concepts: [OKF::Concept.new(...)])

or let OKF::Bundle::Reader parse a directory into one. OKF::Bundle::Validator, OKF::Bundle::Linter, and OKF::Bundle::Graph consume it; the convenience methods below forward to them, so bundle.validate / bundle.lint / bundle.graph work on any in-memory bundle.

root is the bundle path kept purely as data — it seeds bundle-relative link resolution (§5.1) and report messages, never I/O. A bundle built in memory without a real directory gets VIRTUAL_ROOT so relative-link math still works.

Defined Under Namespace

Classes: Entry, Folder, Graph, Linter, Reader, Search, Validator, Writer

Constant Summary collapse

VIRTUAL_ROOT =

Stand-in absolute root for bundles built in memory (no directory). Only ever the base for pure path arithmetic in Links.resolve; the paths it yields are bundle-relative regardless of its value.

"/okf"

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(concepts: [], reserved: [], unparseable: [], root: nil) ⇒ Bundle

Returns a new instance of Bundle.



40
41
42
43
44
45
# File 'lib/okf/bundle.rb', line 40

def initialize(concepts: [], reserved: [], unparseable: [], root: nil)
  @concepts = concepts
  @reserved = reserved
  @unparseable = unparseable
  @root = root || VIRTUAL_ROOT
end

Instance Attribute Details

#conceptsObject (readonly)

Returns the value of attribute concepts.



38
39
40
# File 'lib/okf/bundle.rb', line 38

def concepts
  @concepts
end

#reservedObject (readonly)

Returns the value of attribute reserved.



38
39
40
# File 'lib/okf/bundle.rb', line 38

def reserved
  @reserved
end

#rootObject (readonly)

Returns the value of attribute root.



38
39
40
# File 'lib/okf/bundle.rb', line 38

def root
  @root
end

#unparseableObject (readonly)

Returns the value of attribute unparseable.



38
39
40
# File 'lib/okf/bundle.rb', line 38

def unparseable
  @unparseable
end

Instance Method Details

#catalogObject

Rich per-concept metadata the catalog / files / stats consumers want but the lean graph omits — the descriptive frontmatter fields plus in/out link degree taken from the graph edges. Pure: derived from the concepts and their links, sorted by id. Shared by the CLI views and the server's /catalog endpoint.



102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/okf/bundle.rb', line 102

def catalog
  out_degree = Hash.new(0)
  in_degree = Hash.new(0)
  graph(minimal: true).edges.each do |edge|
    out_degree[edge[:source]] += 1
    in_degree[edge[:target]] += 1
  end

  concepts.map do |concept|
    id = concept.id
    {
      id: id,
      title: (concept.title || id).to_s,
      type: concept.type.to_s,
      description: concept.description.to_s,
      tags: Array(concept.tags).map(&:to_s),
      timestamp: concept.timestamp&.to_s,
      status: concept.frontmatter["status"]&.to_s,
      backlog_ref: concept.frontmatter["backlog_ref"]&.to_s,
      dir: File.dirname("#{id}.md"),
      area: id.include?("/") ? id.split("/").first : "(root)",
      links_out: out_degree[id],
      links_in: in_degree[id]
    }
  end.sort_by { |entry| entry[:id] }
end

#concept_by_id(id) ⇒ Object

The Concept with this id, or nil. Last wins on a (rare) duplicate id.



75
76
77
# File 'lib/okf/bundle.rb', line 75

def concept_by_id(id)
  concepts_by_id[id]
end

#directory_indexObject

The progressive-disclosure map (spec §6): one entry per directory that holds concepts or carries an index.md, sorted with the root (".") first. Each entry gives the authored index body (frontmatter stripped) when an index.md is present, a type/tag rollup over the concepts that live directly in the directory, its immediate child directories, and the concept listing an index.md there would enumerate. A directory with concepts but no index.md has present: false and still carries the listing, so a consumer can synthesize the map on the fly (§6 permits exactly that). Grouped by the concept's file path — index files are physical directory listings, so a custom frontmatter id must not move a concept out of the directory it lives in. Pure: derived from the concepts and the reserved index text, no disk. Shared by the okf index view and the server's Index panel (/index).



141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/okf/bundle.rb', line 141

def directory_index
  by_dir = concepts.group_by { |concept| File.dirname(concept.path) }
  dirs = directory_set(by_dir.keys)

  dirs.map do |dir|
    here = (by_dir[dir] || []).sort_by(&:id)
    index_path = dir == "." ? "index.md" : "#{dir}/index.md"
    present = index_files.include?(index_path)
    {
      dir: dir,
      index_path: index_path,
      present: present,
      synthesized: !present,
      body: present ? strip_frontmatter(reserved_content(index_path)) : nil,
      count: here.size,
      types: tally(here.map { |concept| concept.type.to_s }),
      tags: tally(here.flat_map { |concept| Array(concept.tags).map(&:to_s) }),
      subdirs: dirs.select { |other| other != dir && File.dirname(other) == dir },
      listing: here.map do |concept|
        {
          id: concept.id,
          title: (concept.title || concept.id).to_s,
          description: concept.description.to_s,
          type: concept.type.to_s,
          tags: Array(concept.tags).map(&:to_s)
        }
      end
    }
  end
end

#graph(minimal: false, body: true) ⇒ Object



94
95
96
# File 'lib/okf/bundle.rb', line 94

def graph(minimal: false, body: true)
  Graph.build(self, minimal: minimal, body: body)
end

#index_filesObject



53
54
55
# File 'lib/okf/bundle.rb', line 53

def index_files
  reserved_paths("index.md")
end

#lint(**options) ⇒ Object



90
91
92
# File 'lib/okf/bundle.rb', line 90

def lint(**options)
  Linter.call(self, **options)
end

#log_filesObject



57
58
59
# File 'lib/okf/bundle.rb', line 57

def log_files
  reserved_paths("log.md")
end

#pathsObject

Bundle-relative paths of every markdown file — concepts, reserved, and unparseable — sorted.



49
50
51
# File 'lib/okf/bundle.rb', line 49

def paths
  (@concepts.map(&:path) + @reserved.map(&:path) + @unparseable.map(&:path)).sort
end

#paths_by_idObject

{ id => bundle-relative path }.



80
81
82
# File 'lib/okf/bundle.rb', line 80

def paths_by_id
  @paths_by_id ||= @concepts.map { |concept| [ concept.id, concept.path ] }.to_h
end

#reserved_content(path) ⇒ Object

Raw content of a reserved file (index.md/log.md) by bundle-relative path, or "" when absent. Reserved structure is validated as text, and index links are extracted from it, so its raw form is retained.



64
65
66
67
# File 'lib/okf/bundle.rb', line 64

def reserved_content(path)
  entry = @reserved.find { |candidate| candidate.path == path }
  entry ? entry.content.to_s : ""
end

#validateObject

── analysis (pure; forwards to the core analyzers) ──



86
87
88
# File 'lib/okf/bundle.rb', line 86

def validate
  Validator.call(self)
end