Class: KairosMcp::DriftDetection::CorrespondenceChecker

Inherits:
Object
  • Object
show all
Defined in:
lib/kairos_mcp/drift_detection/correspondence_checker.rb

Overview

CorrespondenceChecker β€” INV-A detection floor (Cycle 1, toward by-construction).

Checks whether a live L0/L1 artifact still corresponds to its *current recorded provenance*: the content digest stored for that artifact at the head of the constitutive record (the hash chain). This is detection only β€”it surfaces divergence; it does not prevent edits or gate writes. Those are later cycles (single-source enforcement, record-as-gate).

Provenance is rooted in the hash chain, not in the SQLite knowledge_meta cache: INV-A names the chain head as the non-editable anchor. The chain is therefore the single source consulted here; the meta table (when present) is a derived view and is intentionally not used for the comparison.

The digest is computed over the *raw file content* (frontmatter included), matching exactly how it was recorded on create/update (a verbatim write, no normalization). Comparing the parsed/stripped body would never match.

Defined Under Namespace

Classes: Result

Class Method Summary collapse

Class Method Details

.check_l1(name:, md_file_path:, storage_backend: nil) ⇒ Result

Check an L1 knowledge artifact against its recorded provenance.

Parameters:

  • name (String)

    knowledge id (knowledge_id on the chain record)

  • md_file_path (String, nil)

    path to the live .md file relied upon

  • storage_backend (Storage::Backend, nil) (defaults to: nil)

    backend for chain access

Returns:



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/kairos_mcp/drift_detection/correspondence_checker.rb', line 55

def check_l1(name:, md_file_path:, storage_backend: nil)
  unless md_file_path && File.file?(md_file_path)
    # Relied upon but absent β€” a missing artifact is itself a
    # non-correspondence under INV-A (the expected set is recorded).
    return Result.new(
      status: :missing_artifact, name: name,
      active_digest: nil, recorded_digest: nil,
      message: "L1 '#{name}': artifact missing at the point of reliance"
    )
  end

  active = Digest::SHA256.hexdigest(File.read(md_file_path))
  recorded = recorded_digest_for(name, storage_backend)

  if recorded.nil?
    return Result.new(
      status: :missing_record, name: name,
      active_digest: active, recorded_digest: nil,
      message: "L1 '#{name}': live artifact has no recorded provenance on the chain"
    )
  end

  if active == recorded
    Result.new(
      status: :match, name: name,
      active_digest: active, recorded_digest: recorded, message: nil
    )
  else
    Result.new(
      status: :mismatch, name: name,
      active_digest: active, recorded_digest: recorded,
      message: "L1 '#{name}': live content diverged from recorded provenance " \
               "(active #{short(active)} β‰  recorded #{short(recorded)})"
    )
  end
rescue StandardError => e
  Result.new(
    status: :error, name: name,
    active_digest: nil, recorded_digest: nil,
    message: "L1 '#{name}': correspondence check could not complete: #{e.message}"
  )
end