Class: OKF::TUI::Model

Inherits:
Object
  • Object
show all
Defined in:
lib/okf/tui/model.rb

Overview

One bundle, and everything the views ask about it.

The split the gem enforces between its pure core and its shell is what makes this cheap: Bundle::Reader is the only thing here that touches disk, and every answer below is a pure call on the in-memory Bundle. The TUI is just another shell over the same core the CLI and the server use — it invents no analysis of its own.

Analysis is memoized rather than computed at load: with a whole registry open at once, switching bundles should not pay for a validate and a lint of every bundle nobody has looked at yet.

Constant Summary collapse

UNTYPED =

okf's own label for a concept whose type is missing or blank — the one its graph index uses, so the two agree about what the bundle contains.

"Untyped"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(dir, slug: nil) ⇒ Model

Returns a new instance of Model.



21
22
23
24
25
# File 'lib/okf/tui/model.rb', line 21

def initialize(dir, slug: nil)
  @dir = File.expand_path(dir)
  @slug = slug
  @bundle = OKF::Bundle::Reader.read(@dir)
end

Instance Attribute Details

#bundleObject (readonly)

Returns the value of attribute bundle.



19
20
21
# File 'lib/okf/tui/model.rb', line 19

def bundle
  @bundle
end

#dirObject (readonly)

Returns the value of attribute dir.



19
20
21
# File 'lib/okf/tui/model.rb', line 19

def dir
  @dir
end

#slugObject (readonly)

Returns the value of attribute slug.



19
20
21
# File 'lib/okf/tui/model.rb', line 19

def slug
  @slug
end

Class Method Details

.shows_trust?(row) ⇒ Boolean

§5.3's display half, asked of okf. Whether a tier is one this screen should claim is not the same question as what the tier is: a concept that declared no §5 family derives unverified and has claimed nothing, which is every concept of every v0.1 bundle. okf owns the rule — its server, its graph page and this all read one predicate — and the reason it is shared is that a gate disagreeing with the counts beside it reads "unverified 3" over two chipped rows.

Returns:

  • (Boolean)


84
85
86
# File 'lib/okf/tui/model.rb', line 84

def self.shows_trust?(row)
  OKF::Bundle::RowFilter.shows_trust?(row)
end

.type_label(type) ⇒ Object



230
231
232
# File 'lib/okf/tui/model.rb', line 230

def self.type_label(type)
  OKF.blank?(type) ? UNTYPED : type.to_s
end

.under_dir?(dir, ancestor) ⇒ Boolean

The same tallies over an arbitrary subset, so the graph view can count within a facet ("among Capability concepts, which tags?") rather than only over the whole bundle. okf's one rule for --dir: a directory names itself and everything beneath it. . needs no special case — nothing starts with "./", so the root selects only what lives directly in it, which is why okf's own dirs view reports a subtree of 1 for the root of a five-concept bundle.

Asked rather than re-spelled. This was a hand-written copy — byte-identical to okf's, which is the good case and still the wrong one: okf published the rule as Bundle::RowFilter.under_dir? precisely because three shells had spelled it separately and diverged three recorded times. Both sides are already okf's canonical spellings — a row's dir is OKF.dir_of, a facet's value came out of Bundle#directories — so none of okf's argument handling applies: no root alias to resolve, no trailing slash to trim, nothing here was typed by a user.

dirs_test.rb pins the result against okf's own subtree counts — agreement on answers, which is what actually matters and what would survive okf changing the rule underneath.

Returns:

  • (Boolean)


222
223
224
# File 'lib/okf/tui/model.rb', line 222

def self.under_dir?(dir, ancestor)
  OKF::Bundle::RowFilter.under_dir?(dir, ancestor)
end

Instance Method Details

#body_for(row) ⇒ Object



303
304
305
306
# File 'lib/okf/tui/model.rb', line 303

def body_for(row)
  concept = concept_by_id(row[:id])
  concept ? concept.body.to_s : ""
end

#catalogObject



31
32
33
# File 'lib/okf/tui/model.rb', line 31

def catalog
  @catalog ||= bundle.catalog
end

#concept_by_id(id) ⇒ Object



186
187
188
# File 'lib/okf/tui/model.rb', line 186

def concept_by_id(id)
  bundle.concept_by_id(id)
end

#concept_countObject



111
112
113
# File 'lib/okf/tui/model.rb', line 111

def concept_count
  bundle.concepts.length
end

#dir_arcsObject

The cross-directory link mass, as weighted arcs, narrowed to the cut okf suggests for this bundle.

The cut is fitted, not fixed — okf measured ten bundles at weight 3 and got anywhere from 2 arcs to 136 — so suggested_cut is asked rather than guessed. It narrows only the drawn picture: cohesion above is computed over every arc regardless, which is okf's own rule, and the reason the two can be shown together without the number moving when the list gets shorter.



177
178
179
180
181
182
183
184
# File 'lib/okf/tui/model.rb', line 177

def dir_arcs
  @dir_arcs ||= begin
    skeleton = bundle.skeleton
    cut = skeleton.suggested_cut
    [ OKF::Bundle::Skeleton.arcs_above(skeleton.arcs, cut).sort_by { |arc| -arc[:weight] },
      cut, skeleton.arcs.length ]
  end
end

#dir_trafficObject

The link graph one grain coarser: each directory with its internal, outbound and inbound traffic, and the internal share of that total as a cohesion — okf's graph --traffic.

Bundle::Skeleton is okf's pure model here; the arithmetic below is the aggregation its CLI view does, and only that. Cohesion is internal over total, and nil rather than 0% for a directory with no traffic at all, because a directory with nothing to weigh has not earned a number.

Counted over every arc, never a narrowed set: okf makes a point of this — the cut it suggests narrows the drawn picture and must never move the evidence. Sorted by cohesion ascending, so the directories with a case to answer come first rather than sitting under the ones nobody needed to read.



149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/okf/tui/model.rb', line 149

def dir_traffic
  @dir_traffic ||= begin
    skeleton = bundle.skeleton
    out = Hash.new(0)
    into = Hash.new(0)
    skeleton.arcs.each do |arc|
      out[arc[:source]] += arc[:weight]
      into[arc[:target]] += arc[:weight]
    end

    rows = skeleton.dirs.map do |row|
      total = row[:internal] + out[row[:dir]] + into[row[:dir]]
      row.merge(out: out[row[:dir]], in: into[row[:dir]],
        cohesion: total.zero? ? nil : (100.0 * row[:internal] / total).round)
    end

    rows.sort_by { |row| [ row[:cohesion] || 999, row[:dir] ] }
  end
end

#dirsObject

Every directory the bundle has — okf's own answer, not one derived here. Counting the catalog's directories instead gives a smaller set: the catalog knows only directories that hold concepts, so an intermediate that holds nothing but sub-directories, or one whose only file is a log, drops out. That is the disagreement okf 1.13.0 fixed by putting the question on the bundle, and asking it there is how the TUI cannot reintroduce it.

Not memoized: Bundle#directories already is, and a second cache over an immutable model is a second thing to invalidate for no gain.



107
108
109
# File 'lib/okf/tui/model.rb', line 107

def dirs
  bundle.directories
end

#dirs_of(subset) ⇒ Object

Each directory with the number of concepts at or below it — the same "subtree" okf's dirs view prints, and by construction exactly what narrowing to that directory will yield.

Kept in okf's order (root first, then alphabetically) rather than sorted by count like types and tags: a directory list sorted by size scrambles parents away from their children, and the shape of the tree is the thing full-path dirs exist to show.

A directory with nothing under it is dropped — history/, whose only file is its log, is a real directory that okf counts and --dir addresses, but as a facet it is a row that narrows to nothing. The header's dir count still includes it; the two answer different questions.



271
272
273
274
# File 'lib/okf/tui/model.rb', line 271

def dirs_of(subset)
  dirs.map { |dir| [ dir, subset.count { |row| Model.under_dir?(row[:dir], dir) } ] }
      .reject { |_dir, count| count.zero? }
end

#edge_countObject



115
116
117
# File 'lib/okf/tui/model.rb', line 115

def edge_count
  graph.edges.length
end

#findings_by_pathObject

Findings keyed by the concept path they were raised against, so the browse pane can badge a concept with its own problems.



278
279
280
# File 'lib/okf/tui/model.rb', line 278

def findings_by_path
  @findings_by_path ||= lint.findings.group_by { |finding| finding[:path] }
end

#findings_for(row) ⇒ Object



282
283
284
# File 'lib/okf/tui/model.rb', line 282

def findings_for(row)
  findings_by_path[row[:id]] || findings_by_path[row[:path]] || []
end

#graphObject



35
36
37
# File 'lib/okf/tui/model.rb', line 35

def graph
  @graph ||= bundle.graph(minimal: true)
end

#hubsObject

Concepts ranked by inbound link degree, each carrying where those links come from — okf's graph --hubs, unchanged.

The graph view already ranks by inbound degree, so what this adds is the by_top_dir breakdown, which is the whole point: it is the evidence for "is this hub well homed?". A hub whose inbound majority is foreign to its own top-level dir is a move candidate, and one with a single dominant foreign dir has already named its better home. That judgement is okf's, and this is the method that makes it.



132
133
134
# File 'lib/okf/tui/model.rb', line 132

def hubs
  @hubs ||= bundle.hubs
end

The outgoing cross-links of one document, in reading order and deduped by target — what the reader can follow out of the page it is on.

okf owns both halves. Markdown::Links is the same extraction Bundle::Graph builds its edges with and the validator warns on, so a link the TUI offers to follow is one okf already resolved; nothing here opens a file or parses markdown of its own. Memoized per path, like every other analysis.



315
316
317
318
319
320
# File 'lib/okf/tui/model.rb', line 315

def links_for(path)
  @links_for ||= {}
  return @links_for[path] if @links_for.key?(path)

  @links_for[path] = build_links(path)
end

#lintObject



43
44
45
# File 'lib/okf/tui/model.rb', line 43

def lint
  @lint ||= bundle.lint
end

#nameObject



27
28
29
# File 'lib/okf/tui/model.rb', line 27

def name
  slug || File.basename(dir)
end

#okf_versionObject

The spec version the bundle declares (§12), or nil where it declares none — which §12 permits, so nil is an answer and the view says "conformant" rather than guessing a number. Asked of okf rather than assumed: this screen told every reader "legal OKF v0.1" for a release, including about a bundle that had migrated.



52
53
54
# File 'lib/okf/tui/model.rb', line 52

def okf_version
  bundle.okf_version
end

#orphan_idsObject



119
120
121
# File 'lib/okf/tui/model.rb', line 119

def orphan_ids
  @orphan_ids ||= graph.unlinked_ids
end

#reservedObject

index.md and log.md — structure rather than concepts, so the reader keeps them as raw text (§6). The browse list shows them because they are part of what is in the bundle, and often the first thing worth reading.



289
290
291
# File 'lib/okf/tui/model.rb', line 289

def reserved
  bundle.reserved
end

#reserved_text(path) ⇒ Object

The body without its frontmatter, exactly as Bundle#directory_index does it — the reader keeps reserved files as raw text, and the header is metadata rather than something to read.



296
297
298
299
300
301
# File 'lib/okf/tui/model.rb', line 296

def reserved_text(path)
  content = bundle.reserved_content(path)
  OKF::Markdown::Frontmatter.parse(content).last
rescue OKF::Markdown::Frontmatter::ParseError
  content
end

#row_by_id(id) ⇒ Object



190
191
192
# File 'lib/okf/tui/model.rb', line 190

def row_by_id(id)
  rows.find { |row| row[:id] == id }
end

#rowsObject

The catalog rows plus the one thing the catalog does not carry: the file path each concept came from. Everything else the browse list needs — the area, the in/out link degree — the catalog already computed.



91
92
93
94
95
96
# File 'lib/okf/tui/model.rb', line 91

def rows
  @rows ||= begin
    paths = bundle.paths_by_id
    catalog.map { |entry| entry.merge(path: paths[entry[:id]].to_s) }
  end
end

#skipped_checksObject

The checks lint did not run, because it was handed no clock. §5.5's freshness pair is clock-gated, and the pure library confesses the omission here rather than reporting a verdict it did not earn — so the view says so too. Reporting "lint clean" over a check that never ran is the one way a health screen can be worse than no health screen.



61
62
63
# File 'lib/okf/tui/model.rb', line 61

def skipped_checks
  Array(lint.stats[:skipped_checks])
end

#status_postureObject



73
74
75
# File 'lib/okf/tui/model.rb', line 73

def status_posture
  lint.stats[:status] || {}
end

#statuses_of(subset) ⇒ Object

§5.4, counted on the effective value — the same rule --status narrows by, so a concept that declared nothing is counted as the stable it already means rather than dropped. Without that the group would hide the majority the one deprecated concept is measured against.



246
247
248
# File 'lib/okf/tui/model.rb', line 246

def statuses_of(subset)
  tally(subset.map { |row| OKF::Concept.effective_status(row[:status]) })
end

#tagsObject



198
199
200
# File 'lib/okf/tui/model.rb', line 198

def tags
  tags_of(rows)
end

#tags_of(subset) ⇒ Object



238
239
240
# File 'lib/okf/tui/model.rb', line 238

def tags_of(subset)
  tally(subset.flat_map { |row| row[:tags] })
end

#tiers_of(subset) ⇒ Object

§5.3, counted only over rows whose tier this screen is willing to claim. Counting the rest would make the facet promise more concepts than selecting it returns — okf hit exactly that and describes it as "unverified 3" over two chipped cards.



254
255
256
# File 'lib/okf/tui/model.rb', line 254

def tiers_of(subset)
  tally(subset.select { |row| Model.shows_trust?(row) }.map { |row| row[:trust] })
end

#trust_postureObject

The bundle's trust and status posture — the two distributions okf's own lint prints on its summary line. Hashes, so stats_block (which keeps scalars) drops them; they are named here because the standing pane is exactly where they belong, being short by construction and carrying no path.



69
70
71
# File 'lib/okf/tui/model.rb', line 69

def trust_posture
  lint.stats[:trust] || {}
end

#typesObject



194
195
196
# File 'lib/okf/tui/model.rb', line 194

def types
  types_of(rows)
end

#types_of(subset) ⇒ Object



234
235
236
# File 'lib/okf/tui/model.rb', line 234

def types_of(subset)
  tally(subset.map { |row| Model.type_label(row[:type]) })
end

#validationObject



39
40
41
# File 'lib/okf/tui/model.rb', line 39

def validation
  @validation ||= bundle.validate
end