Class: RosettAi::Provenance::Store

Inherits:
Object
  • Object
show all
Defined in:
lib/rosett_ai/provenance/store.rb

Overview

Append-only YAML store for .ai-provenance.yml.

Manages the provenance file lifecycle: creation, appending entries, reading, querying, and hash chain integrity. Entries are never modified or deleted. Each entry includes a SHA-256 hash of the previous entry to create a tamper-evident chain.

Author:

  • hugo

  • claude

Constant Summary collapse

FILENAME =
'.ai-provenance.yml'
MAX_FILE_SIZE =

1 MB

1_048_576
SCHEMA_VERSION =
1
ZERO_HASH =
('0' * 64).freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(root:) ⇒ Store

Returns a new instance of Store.

Parameters:

  • root (Pathname, String)

    project root directory



29
30
31
# File 'lib/rosett_ai/provenance/store.rb', line 29

def initialize(root:)
  @root = Pathname.new(root)
end

Instance Attribute Details

#rootObject (readonly)

Returns the value of attribute root.



26
27
28
# File 'lib/rosett_ai/provenance/store.rb', line 26

def root
  @root
end

Instance Method Details

#append(entry) ⇒ Pathname

Appends an entry to the provenance file with hash chain linkage.

Parameters:

  • entry (Entry)

    provenance entry to append

Returns:

  • (Pathname)

    path to the updated file

Raises:



60
61
62
63
64
65
66
67
68
69
70
# File 'lib/rosett_ai/provenance/store.rb', line 60

def append(entry)
  raise RosettAi::ProvenanceError, "Provenance file not found: #{path}" unless exist?

  warn_file_size if file_over_limit?
  data = read_data
  entry_hash = chain_hash(data['entries'])
  serialized = entry.to_h.merge('chain_hash' => entry_hash)
  data['entries'] << serialized
  path.write(YAML.dump(data))
  path
end

#entries_for_commit(commit_sha) ⇒ Array<Hash>

Returns all entries for a specific commit.

Parameters:

  • commit_sha (String)

    full or partial commit SHA

Returns:

  • (Array<Hash>)

    matching entries



86
87
88
89
90
# File 'lib/rosett_ai/provenance/store.rb', line 86

def entries_for_commit(commit_sha)
  return [] unless exist?

  read['entries'].select { |entry| entry['commit']&.start_with?(commit_sha) }
end

#entries_for_file(file_path) ⇒ Array<Hash>

Returns all entries that reference a specific file.

Parameters:

  • file_path (String)

    relative file path

Returns:

  • (Array<Hash>)

    matching entries



96
97
98
99
100
101
102
# File 'lib/rosett_ai/provenance/store.rb', line 96

def entries_for_file(file_path)
  return [] unless exist?

  read['entries'].select do |entry|
    Array(entry['files']).any? { |file| file['path'] == file_path }
  end
end

#entry_countInteger

Returns number of entries in the provenance file.

Returns:

  • (Integer)

    number of entries in the provenance file



105
106
107
108
109
# File 'lib/rosett_ai/provenance/store.rb', line 105

def entry_count
  return 0 unless exist?

  read['entries'].size
end

#exist?Boolean

Returns true if the provenance file exists.

Returns:

  • (Boolean)

    true if the provenance file exists



39
40
41
# File 'lib/rosett_ai/provenance/store.rb', line 39

def exist?
  path.exist?
end

#initPathname

Creates a new provenance file with version header.

Returns:

  • (Pathname)

    path to the created file

Raises:



47
48
49
50
51
52
53
# File 'lib/rosett_ai/provenance/store.rb', line 47

def init
  raise RosettAi::ProvenanceError, "Provenance file already exists: #{path}" if exist?

  data = { 'provenance_version' => SCHEMA_VERSION, 'entries' => [] }
  path.write(YAML.dump(data))
  path
end

#pathPathname

Returns full path to the provenance file.

Returns:

  • (Pathname)

    full path to the provenance file



34
35
36
# File 'lib/rosett_ai/provenance/store.rb', line 34

def path
  @root.join(FILENAME)
end

#readHash

Reads and parses the provenance file.

Returns:

  • (Hash)

    parsed provenance data

Raises:



76
77
78
79
80
# File 'lib/rosett_ai/provenance/store.rb', line 76

def read
  raise RosettAi::ProvenanceError, "Provenance file not found: #{path}" unless exist?

  read_data
end

#verify_chainArray<String>

Verifies the integrity of the hash chain.

Returns:

  • (Array<String>)

    list of integrity violations (empty if valid)



114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/rosett_ai/provenance/store.rb', line 114

def verify_chain
  return [] unless exist?

  entries = read['entries']
  violations = []
  entries.each_with_index do |entry, index|
    expected = chain_hash(entries[0...index])
    actual = entry['chain_hash']
    next if actual == expected

    violations << "Entry ##{index + 1} (#{entry['commit']}): " \
                  "expected #{expected[0, 16]}..., got #{(actual || 'nil')[0, 16]}..."
  end
  violations
end