Class: OKF::Concept

Inherits:
Object
  • Object
show all
Defined in:
lib/okf/concept.rb,
lib/okf/concept/file.rb

Defined Under Namespace

Classes: File

Constant Summary collapse

RESERVED_FILENAMES =

Reserved filenames (spec §3.1): defined at any level of the hierarchy and never concept documents. The single source of truth for "concept vs reserved" — OKF::Bundle and OKF::Bundle::Validator ask through Concept.reserved?.

%w[index.md log.md].freeze
CONCEPT_SCOPED_CHECKS =

The lint checks that apply to a single concept out of bundle context. The rest (orphan, backlog, duplicate_title, broken_source, …) need the whole bundle. Linter#selected_checks intersects silently, so a stale id here quietly stops Concept#lint running the check — a test pins the list against Linter::CHECKS.

%i[
  stub missing_title missing_description missing_generated
  expired uncited_external unattributed_claim unused_source
  unprefixed_actor incomplete_computation
  legacy_timestamp legacy_citations
  self_link unused_reference_def undefined_reference
].freeze
KNOWN_SPEC_VERSIONS =

The spec versions this gem has a reader for, newest first — what a root index.md's okf_version is checked against (§12). A document is always read as the newest version, without sniffing its shape: §13.1 makes the legacy fallbacks part of v0.2's own reading rule, so a v0.2 reader handed a v0.1 document is the correct reader for it.

%w[0.2 0.1].freeze
HUMAN_ACTOR =

The prefix §7 reserves for a person, and the whole of what §5.3's tier classifier keys off — which is why §7 makes producers MUST use it for hand-authored or human-confirmed content.

"human:"
STATUSES =

§5.4. The three values the spec names. A producer MAY use another (§4.1), and consumers MUST tolerate it, so this is what the validator warns against — never what a reader rejects.

%w[draft stable deprecated].freeze
DEFAULT_STATUS =

§5.4: "Absent statusstable."

"stable"
ISO_DATE =

§5.5's date spelling, strict. Both ends of the today >= stale_after comparison read it: #stale_after_date below, and the clock the linter is handed. Date.iso8601 alone also parses the basic (20260101) and week (2026-W01-1) forms, which would put the two ends on different grammars.

/\A\d{4}-\d{2}-\d{2}\z/.freeze
ISO_CUTOFF =

The grammar for a cutoff a reader supplies (--stale-after, the MCP stale_after). Wider than ISO_DATE on purpose: a cutoff is a moment rather than a calendar day, and the value a caller has to hand is a concept's own generated.at — a full timestamp, which Date.iso8601 reduces to its date. Narrow enough to still refuse the basic (20260101) and week (2026-W01-1) spellings, which are the ones a reader never means and the parser would silently reinterpret. T only: Date.iso8601 raises on the space-separated form, so admitting it here described a grammar one branch wider than the parser behind it — a value that matched the rule and was refused anyway.

/\A\d{4}-\d{2}-\d{2}(?:T.+)?\z/.freeze
ATTESTED_COMPUTATION =

§10.1. The type that carries a sanctioned computation.

"Attested Computation"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(path:, frontmatter:, body:) ⇒ Concept

Returns a new instance of Concept.



128
129
130
131
132
# File 'lib/okf/concept.rb', line 128

def initialize(path:, frontmatter:, body:)
  @path = Path.normalize_relative!(path)
  @frontmatter = Markdown::Frontmatter.stringify_keys(frontmatter)
  @body = body.to_s
end

Instance Attribute Details

#bodyObject (readonly)

Returns the value of attribute body.



126
127
128
# File 'lib/okf/concept.rb', line 126

def body
  @body
end

#frontmatterObject (readonly)

Returns the value of attribute frontmatter.



126
127
128
# File 'lib/okf/concept.rb', line 126

def frontmatter
  @frontmatter
end

#pathObject (readonly)

Returns the value of attribute path.



126
127
128
# File 'lib/okf/concept.rb', line 126

def path
  @path
end

Class Method Details

.effective_status(value) ⇒ Object

The narrowing semantics every surface shares — the CLI's --status/--trust and the MCP shell's filters both fold through here, so a tweak to either rule cannot land on one surface and not the other (which is exactly how --status stable and the MCP catalog once answered opposite things about one bundle).

--status matches the EFFECTIVE status: absent (or blank) reads stable per §5.4, and the value folds through the same serialization #status keeps, so a YAML-boolean status: no is "false" everywhere.



73
74
75
76
# File 'lib/okf/concept.rb', line 73

def self.effective_status(value)
  text = fold_status(value)
  text.empty? ? DEFAULT_STATUS : text
end

.fold_status(value) ⇒ Object

The case-fold without §5.4's default — what a filter's argument gets. The default belongs to a concept that declared no status; a caller who asked for one and supplied "" asked for a status no concept has, and answering stable there made the CLI's one empty filter that matches something (--tag "" and --trust "" both match nothing).



83
84
85
# File 'lib/okf/concept.rb', line 83

def self.fold_status(value)
  value.nil? ? "" : value.to_s.strip.downcase
end

.fold_tier(value) ⇒ Object

A tier prints hyphenated (machine-confirmed); a caller may echo that back or type the underscore form — both fold to the wire spelling.



89
90
91
# File 'lib/okf/concept.rb', line 89

def self.fold_tier(value)
  value.to_s.downcase.tr("_", "-")
end

.reserved?(path) ⇒ Boolean

Whether a bundle-relative path names a reserved file rather than a concept. ::File is explicit: OKF::Concept::File (the on-disk handle) shadows Ruby's File inside this namespace.

Returns:

  • (Boolean)


122
123
124
# File 'lib/okf/concept.rb', line 122

def self.reserved?(path)
  RESERVED_FILENAMES.include?(::File.basename(path))
end

.shows_trust?(tier, declared_generated) ⇒ Boolean

Whether a surface should claim a tier — the display half of §5.3, and deliberately not the same question as "what is the tier".

§5.3 derives unverified for every concept that declares no verification, which is every concept of every v0.1 bundle. Displaying that unconditionally would paint a provenance verdict onto documents that never made one — the false claim the trust system exists to prevent — so the tier is computed for filtering and withheld from display. A concept that declared generated has opted into §5, and its unverified is a real answer worth showing; one that declared nothing is silent, and so is every surface reading this.

Takes the two wire values so one rule serves both shapes: a Concept asks through #shows_trust?, a catalog row through Bundle::RowFilter.shows_trust?. Shared rather than re-spelled because the gate, the counts and the narrowing have to agree — a gate disagreeing with the counts beside it reads "unverified 3" over two chipped cards.

Returns:

  • (Boolean)


109
110
111
112
113
114
115
116
117
# File 'lib/okf/concept.rb', line 109

def self.shows_trust?(tier, declared_generated)
  folded = fold_tier(tier)
  # A blank tier is nothing to claim. #trust never returns one, so this is
  # the client-side twin's `!!(c.trust && …)` guard kept in step rather than
  # a case the Ruby can reach on its own — and the two are asserted equal.
  return false if folded.empty?

  !(folded == "unverified" && !declared_generated)
end

Instance Method Details

#attested_computation?Boolean

── §10 attested computation ──

Returns:

  • (Boolean)


334
335
336
# File 'lib/okf/concept.rb', line 334

def attested_computation?
  type.to_s.strip == ATTESTED_COMPUTATION
end

#attesterObject

§10.2. The deterministic (no-LLM) check that takes a receipt and returns a verdict. Meant to run consumer-side.



365
366
367
# File 'lib/okf/concept.rb', line 365

def attester
  mapping("attester")
end

#citation_entriesObject

The §13.1 lifted entries, parsed once for however many readers ask — #sources' fallback and the linter's broken_source (which checks the section's targets even beside a native list) share this parse.



372
373
374
# File 'lib/okf/concept.rb', line 372

def citation_entries
  @citation_entries ||= Markdown::Citations.entries(body)
end

#computationObject

§10.3. A path to a file holding the computation, used instead of an inline body fence. Absent ⇒ the # Computation fence is the computation.



353
354
355
# File 'lib/okf/concept.rb', line 353

def computation
  frontmatter["computation"]
end

#declared_generated?Boolean

Whether the document declares a generated mapping — raw-key detection, never the fallback. The one predicate that distinguishes hand-written (no provenance at all) from v0.1-with-timestamp, which #generated_at alone conflates.

Returns:

  • (Boolean)


213
214
215
# File 'lib/okf/concept.rb', line 213

def declared_generated?
  frontmatter.key?("generated")
end

#declared_statusObject



294
295
296
# File 'lib/okf/concept.rb', line 294

def declared_status
  frontmatter["status"]
end

#descriptionObject



152
153
154
# File 'lib/okf/concept.rb', line 152

def description
  frontmatter["description"]
end

#executorObject

§10.2. How the computation is run: resource names run instructions, receipt declares the fields a run must return.



359
360
361
# File 'lib/okf/concept.rb', line 359

def executor
  mapping("executor")
end

Body links that point outside the bundle — external URLs and mailto:.



398
399
400
# File 'lib/okf/concept.rb', line 398

def external_links
  links.select { |raw| raw.match?(Markdown::Links::SCHEME) || raw.match?(Markdown::Links::MAILTO) }
end

#generatedObject

How the current content was produced, as { "by", "at" }. A lifted timestamp yields no by: the v0.1 field never recorded an actor, and inventing one — the running user, the gem — is exactly the false provenance claim §5 exists to prevent. A non-mapping generated is ignored rather than rejected (§11) and falls back like an absent one.



183
184
185
186
187
188
189
190
191
192
193
# File 'lib/okf/concept.rb', line 183

def generated
  # Memoized like #sources, on the same premise — the model is immutable
  # once built — and for the same reason: one catalog row asks four times
  # over (#generated_at and #generated_by each read this twice), and every
  # call re-stringifies and re-allocates. `defined?` rather than `||=`
  # because nil is the answer for a whole bundle mid-migration, and `||=`
  # would recompute exactly there.
  return @generated if defined?(@generated)

  @generated = compute_generated
end

#generated_atObject

The content's last meaningful change (ISO 8601). The fallback is per-key, not per-mapping: a half-migrated document carrying generated: { by: … } plus a legacy timestamp must not lose its date.



198
199
200
201
202
203
# File 'lib/okf/concept.rb', line 198

def generated_at
  at = generated && generated["at"]
  return at unless OKF.blank?(at)

  timestamp unless OKF.blank?(timestamp)
end

#generated_byObject



205
206
207
# File 'lib/okf/concept.rb', line 205

def generated_by
  generated && generated["by"]
end

#idObject

Stable node identity. A concept may pin an explicit id in its frontmatter (any scalar; blank is ignored); otherwise it is the bundle-relative path with the .md suffix stripped — i.e. "folder/filename". Because cross-links are file paths, OKF::Bundle maps a resolved link path back to the concept there and uses its id, so a custom id still resolves edges correctly.



139
140
141
142
# File 'lib/okf/concept.rb', line 139

def id
  explicit = frontmatter["id"].to_s.strip
  explicit.empty? ? path.sub(/\.md\z/, "") : explicit
end

#legacy_citations?Boolean

Memoized with defined? because the answer may be false: the linter asks per check, and each un-memoized ask was a full body scan.

Returns:

  • (Boolean)


384
385
386
387
388
# File 'lib/okf/concept.rb', line 384

def legacy_citations?
  return @legacy_citations if defined?(@legacy_citations)

  @legacy_citations = !Markdown::Citations.section(body).nil?
end

#legacy_timestamp?Boolean

── detection (lint's and the surfaces'; never reading's) ──

Returns:

  • (Boolean)


378
379
380
# File 'lib/okf/concept.rb', line 378

def legacy_timestamp?
  frontmatter.key?("timestamp")
end

Raw markdown cross-link targets in the body, in document order (spec §6.1).



393
394
395
# File 'lib/okf/concept.rb', line 393

def links
  Markdown::Links.extract(body)
end

#lint(**options) ⇒ Object

Lint this concept in isolation: the concept-scoped checks only (a lone concept has no bundle to judge reachability, backlog, or duplicate titles).



410
411
412
# File 'lib/okf/concept.rb', line 410

def lint(**options)
  Bundle.new(concepts: [ self ]).lint(only: CONCEPT_SCOPED_CHECKS, **options)
end

#parametersObject

The typed, named holes an agent may fill (§10.3: bind values for declared parameters only; never author or edit the computation).



346
347
348
349
# File 'lib/okf/concept.rb', line 346

def parameters
  Array(frontmatter["parameters"]).grep(Hash)
                                  .map { |entry| Markdown::Frontmatter.stringify_keys(entry) }
end

#reserved?Boolean

Returns:

  • (Boolean)


172
173
174
# File 'lib/okf/concept.rb', line 172

def reserved?
  self.class.reserved?(path)
end

#resourceObject

Canonical URI of the underlying asset (spec §4.1), when the concept is bound to one. Absent for concepts describing purely abstract ideas.



158
159
160
# File 'lib/okf/concept.rb', line 158

def resource
  frontmatter["resource"]
end

#runtimeObject

§10.2. REQUIRED for the type — but §11's conformance conditions are only three, so its absence is a warning and never an error.



340
341
342
# File 'lib/okf/concept.rb', line 340

def runtime
  frontmatter["runtime"]
end

#shows_trust?Boolean

Whether this concept's tier is one a surface should show — see .shows_trust? for why the display question is separate from the derivation.

Returns:

  • (Boolean)


249
250
251
# File 'lib/okf/concept.rb', line 249

def shows_trust?
  Concept.shows_trust?(trust, declared_generated?)
end

#sourcesObject

The materials this concept derives from, as a list of mappings each carrying at least a resource. The fallback to a legacy # Citations body list fires when the native value yields zero mappings — absent, non-list, or a list with no mapping entries — not merely when the key is absent: sources: [prod-db, warehouse] has always been a legal free-form key (§4.1), and it must not silently mask a document's real provenance.



261
262
263
264
265
266
# File 'lib/okf/concept.rb', line 261

def sources
  # Memoized like the bundle's graph and for the same reason: the model is
  # immutable once built, several lint checks and the row builder each ask,
  # and the v0.1 fallback re-parses the body on every call.
  @sources ||= compute_sources
end

#stale_afterObject

§5.5. An absolute date, not a relative TTL — which is what keeps staleness a plain date comparison with no reference to when the concept was read.



300
301
302
# File 'lib/okf/concept.rb', line 300

def stale_after
  frontmatter["stale_after"]
end

#stale_after_dateObject

The parsed stale_after, or nil when absent or unparseable — strict YYYY-MM-DD, the one spelling §5.5 names. Psych may already have yielded a Date; an unparseable string is a validator warning, never a read failure.



307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
# File 'lib/okf/concept.rb', line 307

def stale_after_date
  value = stale_after
  # DateTime < Date, so a bare Date check would admit the one temporal
  # class the strict-YYYY-MM-DD contract excludes — the same exclusion the
  # validator's date check makes, kept in step so lint, validate and the
  # page cannot answer three ways about one value.
  return value if value.is_a?(Date) && !value.is_a?(DateTime)

  text = value.to_s.strip
  return nil unless text.match?(ISO_DATE)

  begin
    Date.iso8601(text)
  rescue ArgumentError
    nil
  end
end

#stale_on?(today) ⇒ Boolean

Pure: it takes the day rather than reading the clock. §5.5 puts the boundary on the day itself: stale when today >= stale_after.

Returns:

  • (Boolean)


327
328
329
330
# File 'lib/okf/concept.rb', line 327

def stale_on?(today)
  date = stale_after_date
  !date.nil? && today >= date
end

#statusObject

The effective status, defaulted per §5.4. #declared_status keeps the raw value for the surfaces that must not fabricate frontmatter a concept never declared. Defaulted off the same serialization the row prints — not OKF.blank? — because Psych reads status: no as false, blank? folds false into "absent", and the row's &.to_s prints "false": one concept, two answers. Serializing first keeps every surface on the same string.



283
284
285
286
287
288
289
290
291
292
# File 'lib/okf/concept.rb', line 283

def status
  # Defaulted, *not* folded — and the split is the point. Every surface
  # that displays a status prints what the producer wrote: the catalog row
  # (`declared_status&.to_s`), the card chip, the inspector line. Only
  # comparison folds, which is `.effective_status`'s job and why it is a
  # separate method. Folding here made the library accessor the one place
  # answering `deprecated` where the whole CLI and page say `Deprecated`.
  text = declared_status.nil? ? "" : declared_status.to_s.strip
  text.empty? ? DEFAULT_STATUS : text
end

#tagsObject



162
163
164
# File 'lib/okf/concept.rb', line 162

def tags
  frontmatter["tags"]
end

#timestampObject

The raw v0.1 field, kept readable because §13.1 keeps it consumable; what it means is #generated's business.



168
169
170
# File 'lib/okf/concept.rb', line 168

def timestamp
  frontmatter["timestamp"]
end

#titleObject



148
149
150
# File 'lib/okf/concept.rb', line 148

def title
  frontmatter["title"]
end

#to_markdownObject

Serialize back to a markdown document (frontmatter + body) — the inverse of Markdown::Frontmatter.parse.



404
405
406
# File 'lib/okf/concept.rb', line 404

def to_markdown
  Markdown::Frontmatter.dump(frontmatter, body)
end

#trustObject

The wire spelling of #trust_tier — the hyphenated string every surface prints (rows, lint's trust stat, the page), pinned so a consumer comparing against a literal knows which form arrives.



243
244
245
# File 'lib/okf/concept.rb', line 243

def trust
  trust_tier.to_s.tr("_", "-")
end

#trust_tierObject

§5.3 — derived, never stored. A stored tier would be subjective, unportable between consumers, and stale the moment a verification lands, so the spec has consumers infer it and OKF record only the events.



229
230
231
232
233
234
235
236
237
238
# File 'lib/okf/concept.rb', line 229

def trust_tier
  events = verified
  return :unverified if events.empty?
  # Stripped, because the linter strips before matching §7's forms: an
  # unstripped compare here let one report call a padded `  human:…` actor
  # human-reviewed in its message and machine-confirmed in its stat.
  return :human_reviewed if events.any? { |event| event["by"].to_s.strip.start_with?(HUMAN_ACTOR) }

  :machine_confirmed
end

#typeObject



144
145
146
# File 'lib/okf/concept.rb', line 144

def type
  frontmatter["type"]
end

#usage_windowObject

§5.1. Written once as a sibling of sources, framing every usage_count with a { from, to } range; an entry MAY override it (validated for shape, deliberately consumed by nothing — see model/concept.md).



271
272
273
# File 'lib/okf/concept.rb', line 271

def usage_window
  mapping("usage_window")
end

#verifiedObject

§5.2: "A single verifier MAY be written as one { by, at } mapping without the list dash. Consumers MUST treat a bare mapping as a one-element list." Entries that are not mappings are dropped here and warned about by the validator; verified: [] and all-entries-dropped fold into the key-absent case — every degenerate shape reads as unverified.



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

def verified
  @verified ||= compute_verified
end