Class: OKF::Concept
- Inherits:
-
Object
- Object
- OKF::Concept
- 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'sokf_versionis 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
status⇒stable." "stable"- ISO_DATE =
§5.5's date spelling, strict. Both ends of the
today >= stale_aftercomparison 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 MCPstale_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 owngenerated.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.Tonly: 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
-
#body ⇒ Object
readonly
Returns the value of attribute body.
-
#frontmatter ⇒ Object
readonly
Returns the value of attribute frontmatter.
-
#path ⇒ Object
readonly
Returns the value of attribute path.
Class Method Summary collapse
-
.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 stableand the MCP catalog once answered opposite things about one bundle). -
.fold_status(value) ⇒ Object
The case-fold without §5.4's default — what a filter's argument gets.
-
.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. -
.reserved?(path) ⇒ Boolean
Whether a bundle-relative path names a reserved file rather than a concept.
-
.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".
Instance Method Summary collapse
-
#attested_computation? ⇒ Boolean
── §10 attested computation ──.
-
#attester ⇒ Object
§10.2.
-
#citation_entries ⇒ Object
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.
-
#computation ⇒ Object
§10.3.
-
#declared_generated? ⇒ Boolean
Whether the document declares a
generatedmapping — raw-key detection, never the fallback. - #declared_status ⇒ Object
- #description ⇒ Object
-
#executor ⇒ Object
§10.2.
-
#external_links ⇒ Object
Body links that point outside the bundle — external URLs and mailto:.
-
#generated ⇒ Object
How the current content was produced, as { "by", "at" }.
-
#generated_at ⇒ Object
The content's last meaningful change (ISO 8601).
- #generated_by ⇒ Object
-
#id ⇒ Object
Stable node identity.
-
#initialize(path:, frontmatter:, body:) ⇒ Concept
constructor
A new instance of Concept.
-
#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.
-
#legacy_timestamp? ⇒ Boolean
── detection (lint's and the surfaces'; never reading's) ──.
-
#links ⇒ Object
Raw markdown cross-link targets in the body, in document order (spec §6.1).
-
#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).
-
#parameters ⇒ Object
The typed, named holes an agent may fill (§10.3: bind values for declared parameters only; never author or edit the computation).
- #reserved? ⇒ Boolean
-
#resource ⇒ Object
Canonical URI of the underlying asset (spec §4.1), when the concept is bound to one.
-
#runtime ⇒ Object
§10.2.
-
#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.
-
#sources ⇒ Object
The materials this concept derives from, as a list of mappings each carrying at least a
resource. -
#stale_after ⇒ Object
§5.5.
-
#stale_after_date ⇒ Object
The parsed
stale_after, or nil when absent or unparseable — strict YYYY-MM-DD, the one spelling §5.5 names. -
#stale_on?(today) ⇒ Boolean
Pure: it takes the day rather than reading the clock.
-
#status ⇒ Object
The effective status, defaulted per §5.4.
- #tags ⇒ Object
-
#timestamp ⇒ Object
The raw v0.1 field, kept readable because §13.1 keeps it consumable; what it means is #generated's business.
- #title ⇒ Object
-
#to_markdown ⇒ Object
Serialize back to a markdown document (frontmatter + body) — the inverse of Markdown::Frontmatter.parse.
-
#trust ⇒ Object
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.
-
#trust_tier ⇒ Object
§5.3 — derived, never stored.
- #type ⇒ Object
-
#usage_window ⇒ Object
§5.1.
-
#verified ⇒ Object
§5.2: "A single verifier MAY be written as one { by, at } mapping without the list dash.
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
#body ⇒ Object (readonly)
Returns the value of attribute body.
126 127 128 |
# File 'lib/okf/concept.rb', line 126 def body @body end |
#frontmatter ⇒ Object (readonly)
Returns the value of attribute frontmatter.
126 127 128 |
# File 'lib/okf/concept.rb', line 126 def frontmatter @frontmatter end |
#path ⇒ Object (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.
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.
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 ──
334 335 336 |
# File 'lib/okf/concept.rb', line 334 def attested_computation? type.to_s.strip == ATTESTED_COMPUTATION end |
#attester ⇒ Object
§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_entries ⇒ Object
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 |
#computation ⇒ Object
§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.
213 214 215 |
# File 'lib/okf/concept.rb', line 213 def declared_generated? frontmatter.key?("generated") end |
#declared_status ⇒ Object
294 295 296 |
# File 'lib/okf/concept.rb', line 294 def declared_status frontmatter["status"] end |
#description ⇒ Object
152 153 154 |
# File 'lib/okf/concept.rb', line 152 def description frontmatter["description"] end |
#executor ⇒ Object
§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 |
#external_links ⇒ Object
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 |
#generated ⇒ Object
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_at ⇒ Object
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) unless OKF.blank?() end |
#generated_by ⇒ Object
205 206 207 |
# File 'lib/okf/concept.rb', line 205 def generated_by generated && generated["by"] end |
#id ⇒ Object
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.
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) ──
378 379 380 |
# File 'lib/okf/concept.rb', line 378 def frontmatter.key?("timestamp") end |
#links ⇒ Object
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(**) Bundle.new(concepts: [ self ]).lint(only: CONCEPT_SCOPED_CHECKS, **) end |
#parameters ⇒ Object
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
172 173 174 |
# File 'lib/okf/concept.rb', line 172 def reserved? self.class.reserved?(path) end |
#resource ⇒ Object
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 |
#runtime ⇒ Object
§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.
249 250 251 |
# File 'lib/okf/concept.rb', line 249 def shows_trust? Concept.shows_trust?(trust, declared_generated?) end |
#sources ⇒ Object
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_after ⇒ Object
§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_date ⇒ Object
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.
327 328 329 330 |
# File 'lib/okf/concept.rb', line 327 def stale_on?(today) date = stale_after_date !date.nil? && today >= date end |
#status ⇒ Object
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 |
#tags ⇒ Object
162 163 164 |
# File 'lib/okf/concept.rb', line 162 def frontmatter["tags"] end |
#timestamp ⇒ Object
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 frontmatter["timestamp"] end |
#title ⇒ Object
148 149 150 |
# File 'lib/okf/concept.rb', line 148 def title frontmatter["title"] end |
#to_markdown ⇒ Object
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 |
#trust ⇒ Object
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_tier ⇒ Object
§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 |
#type ⇒ Object
144 145 146 |
# File 'lib/okf/concept.rb', line 144 def type frontmatter["type"] end |
#usage_window ⇒ Object
§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 |
#verified ⇒ Object
§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 |