Class: OKF::Bundle::Linter

Inherits:
Object
  • Object
show all
Defined in:
lib/okf/bundle/linter.rb,
lib/okf/bundle/linter/report.rb

Overview

Lints a bundle for curation quality — the deterministic subset of the ingest → query → lint loop (overview.md): reachability, backlog, completeness, freshness, provenance, attestation, migration, and hygiene. Pure — it reads nothing from disk and works entirely on the in-memory OKF::Bundle, mirroring OKF::Bundle::Validator.

Unlike OKF::Bundle::Validator (the §11 conformance gate, which MUST NOT reject for broken links or missing optional fields), lint never rejects a bundle: it reports :warn and :info findings the spec marks as tolerable, and emits them as structured data (OKF::Bundle::Linter::Report) for a human or agent to act on. Contradictions and semantic staleness are NOT detected here — they need meaning, not structure; the JSON report is the substrate an agent consumes for those passes.

Defined Under Namespace

Classes: Report

Constant Summary collapse

SEVERITIES =

Severity is API: machine consumers gate edits and CI on :warn and drop :info, so an id changing level changes its behavior for them — this map is pinned by a test, and a new gateable state gets a flag (--fail-on info), never a severity promotion. Two calls worth their one sentence: unattributed_claim warns while its join-twin unused_source informs, because a dangling footnote misattributes a claim — a correctness defect — while an uncited source is only slack. And expired informs rather than warns: a stale_after passes on the calendar, not on a change, so a warn would fail a --fail-on warn gate on a morning nobody chose.

{
  orphan: :warn, not_in_index: :warn, disconnected_component: :info, unlinked: :info,
  missing_concept: :info, broken_index_entry: :warn,
  stub: :info, missing_title: :info, missing_description: :info, missing_generated: :info,
  expired: :info, stale: :warn,
  uncited_external: :info, broken_source: :warn, unattributed_claim: :warn,
  unused_source: :info, unprefixed_actor: :info,
  incomplete_computation: :warn, broken_attestation_ref: :warn,
  legacy_timestamp: :info, legacy_citations: :info,
  duplicate_title: :info, unused_reference_def: :info, undefined_reference: :warn, self_link: :info,
  log_order: :info
}.freeze
CHECKS =

All checks, in display/registry order — derived from the severity map (insertion-ordered) rather than hand-listed twice: two parallel lists of the same 25 ids needed a test just to police their sync. --only/--except select from these.

SEVERITIES.keys.freeze
COMPUTATION_HEADING =

An ATX heading naming the §10.3 computation section.

/\A\#{1,6}\s+Computation\s*\z/i.freeze
ACTOR_FORMS =

§7's three actor forms: <producer>/<version>, human:<id>, process:<id>.

[ %r{\A\S+/\S+\z}, /\Ahuman:\S+\z/, /\Aprocess:\S+\z/ ].freeze
DEFAULT_MIN_BODY =
50
HUB_LIMIT =
5

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(bundle, min_body: DEFAULT_MIN_BODY, stale_before: nil, today: nil, only: nil, except: nil) ⇒ Linter

today is injected for the same reason stale_before is: the linter is pure and never reads the clock. Without it expired cannot know whether a stale_after has passed and does not run — and confesses, in stats, because a gate that is sometimes absent and does not say so converts "unchecked" into "checked and fine". The CLI always passes today; a library caller that wants the check passes today:.



65
66
67
68
69
70
71
72
73
# File 'lib/okf/bundle/linter.rb', line 65

def initialize(bundle, min_body: DEFAULT_MIN_BODY, stale_before: nil, today: nil, only: nil, except: nil)
  @bundle = bundle
  @min_body = min_body
  @stale_before = stale_before
  @today = coerce_today(today)
  @only = only
  @except = except
  @report = Report.new
end

Class Method Details

.call(bundle, **options) ⇒ Object



55
56
57
# File 'lib/okf/bundle/linter.rb', line 55

def self.call(bundle, **options)
  new(bundle, **options).call
end

Instance Method Details

#callObject



75
76
77
78
79
80
# File 'lib/okf/bundle/linter.rb', line 75

def call
  prepare
  selected_checks.each { |check| send("check_#{check}") }
  fill_stats
  @report
end