Class: OKF::TUI::Model
- Inherits:
-
Object
- Object
- OKF::TUI::Model
- 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
-
#bundle ⇒ Object
readonly
Returns the value of attribute bundle.
-
#dir ⇒ Object
readonly
Returns the value of attribute dir.
-
#slug ⇒ Object
readonly
Returns the value of attribute slug.
Class Method Summary collapse
-
.shows_trust?(row) ⇒ Boolean
§5.3's display half, asked of okf.
- .type_label(type) ⇒ Object
-
.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.
Instance Method Summary collapse
- #body_for(row) ⇒ Object
- #catalog ⇒ Object
- #concept_by_id(id) ⇒ Object
- #concept_count ⇒ Object
-
#dir_arcs ⇒ Object
The cross-directory link mass, as weighted arcs, narrowed to the cut okf suggests for this bundle.
-
#dir_traffic ⇒ Object
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. -
#dirs ⇒ Object
Every directory the bundle has — okf's own answer, not one derived here.
-
#dirs_of(subset) ⇒ Object
Each directory with the number of concepts at or below it — the same "subtree" okf's
dirsview prints, and by construction exactly what narrowing to that directory will yield. - #edge_count ⇒ Object
-
#findings_by_path ⇒ Object
Findings keyed by the concept path they were raised against, so the browse pane can badge a concept with its own problems.
- #findings_for(row) ⇒ Object
- #graph ⇒ Object
-
#hubs ⇒ Object
Concepts ranked by inbound link degree, each carrying where those links come from — okf's
graph --hubs, unchanged. -
#initialize(dir, slug: nil) ⇒ Model
constructor
A new instance of Model.
-
#links_for(path) ⇒ Object
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.
- #lint ⇒ Object
- #name ⇒ Object
-
#okf_version ⇒ Object
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.
- #orphan_ids ⇒ Object
-
#reserved ⇒ Object
index.md and log.md — structure rather than concepts, so the reader keeps them as raw text (§6).
-
#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.
- #row_by_id(id) ⇒ Object
-
#rows ⇒ Object
The catalog rows plus the one thing the catalog does not carry: the file path each concept came from.
-
#skipped_checks ⇒ Object
The checks lint did not run, because it was handed no clock.
- #status_posture ⇒ Object
-
#statuses_of(subset) ⇒ Object
§5.4, counted on the effective value — the same rule
--statusnarrows by, so a concept that declared nothing is counted as thestableit already means rather than dropped. - #tags ⇒ Object
- #tags_of(subset) ⇒ Object
-
#tiers_of(subset) ⇒ Object
§5.3, counted only over rows whose tier this screen is willing to claim.
-
#trust_posture ⇒ Object
The bundle's trust and status posture — the two distributions okf's own lint prints on its summary line.
- #types ⇒ Object
- #types_of(subset) ⇒ Object
- #validation ⇒ Object
Constructor Details
Instance Attribute Details
#bundle ⇒ Object (readonly)
Returns the value of attribute bundle.
19 20 21 |
# File 'lib/okf/tui/model.rb', line 19 def bundle @bundle end |
#dir ⇒ Object (readonly)
Returns the value of attribute dir.
19 20 21 |
# File 'lib/okf/tui/model.rb', line 19 def dir @dir end |
#slug ⇒ Object (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.
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.
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 |
#catalog ⇒ Object
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_count ⇒ Object
111 112 113 |
# File 'lib/okf/tui/model.rb', line 111 def concept_count bundle.concepts.length end |
#dir_arcs ⇒ Object
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_traffic ⇒ Object
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 |
#dirs ⇒ Object
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_count ⇒ Object
115 116 117 |
# File 'lib/okf/tui/model.rb', line 115 def edge_count graph.edges.length end |
#findings_by_path ⇒ Object
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 |
#graph ⇒ Object
35 36 37 |
# File 'lib/okf/tui/model.rb', line 35 def graph @graph ||= bundle.graph(minimal: true) end |
#hubs ⇒ Object
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 |
#links_for(path) ⇒ Object
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 |
#lint ⇒ Object
43 44 45 |
# File 'lib/okf/tui/model.rb', line 43 def lint @lint ||= bundle.lint end |
#name ⇒ Object
27 28 29 |
# File 'lib/okf/tui/model.rb', line 27 def name slug || File.basename(dir) end |
#okf_version ⇒ Object
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_ids ⇒ Object
119 120 121 |
# File 'lib/okf/tui/model.rb', line 119 def orphan_ids @orphan_ids ||= graph.unlinked_ids end |
#reserved ⇒ Object
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 |
#rows ⇒ Object
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_checks ⇒ Object
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_posture ⇒ Object
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 |
#tags ⇒ Object
198 199 200 |
# File 'lib/okf/tui/model.rb', line 198 def (rows) end |
#tags_of(subset) ⇒ Object
238 239 240 |
# File 'lib/okf/tui/model.rb', line 238 def (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_posture ⇒ Object
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 |
#types ⇒ Object
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 |
#validation ⇒ Object
39 40 41 |
# File 'lib/okf/tui/model.rb', line 39 def validation @validation ||= bundle.validate end |