spurline-deploy 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. checksums.yaml +7 -0
  2. data/lib/spurline/adapters/base.rb +17 -0
  3. data/lib/spurline/adapters/claude.rb +208 -0
  4. data/lib/spurline/adapters/open_ai.rb +213 -0
  5. data/lib/spurline/adapters/registry.rb +33 -0
  6. data/lib/spurline/adapters/scheduler/base.rb +15 -0
  7. data/lib/spurline/adapters/scheduler/sync.rb +15 -0
  8. data/lib/spurline/adapters/stub_adapter.rb +54 -0
  9. data/lib/spurline/agent.rb +433 -0
  10. data/lib/spurline/audit/log.rb +156 -0
  11. data/lib/spurline/audit/secret_filter.rb +121 -0
  12. data/lib/spurline/base.rb +130 -0
  13. data/lib/spurline/cartographer/analyzer.rb +71 -0
  14. data/lib/spurline/cartographer/analyzers/ci_config.rb +171 -0
  15. data/lib/spurline/cartographer/analyzers/dotfiles.rb +134 -0
  16. data/lib/spurline/cartographer/analyzers/entry_points.rb +145 -0
  17. data/lib/spurline/cartographer/analyzers/file_signatures.rb +55 -0
  18. data/lib/spurline/cartographer/analyzers/manifests.rb +217 -0
  19. data/lib/spurline/cartographer/analyzers/security_scan.rb +223 -0
  20. data/lib/spurline/cartographer/repo_profile.rb +140 -0
  21. data/lib/spurline/cartographer/runner.rb +88 -0
  22. data/lib/spurline/cartographer.rb +6 -0
  23. data/lib/spurline/channels/base.rb +41 -0
  24. data/lib/spurline/channels/event.rb +136 -0
  25. data/lib/spurline/channels/github.rb +205 -0
  26. data/lib/spurline/channels/router.rb +103 -0
  27. data/lib/spurline/cli/check.rb +88 -0
  28. data/lib/spurline/cli/checks/adapter_resolution.rb +81 -0
  29. data/lib/spurline/cli/checks/agent_loadability.rb +41 -0
  30. data/lib/spurline/cli/checks/base.rb +35 -0
  31. data/lib/spurline/cli/checks/credentials.rb +43 -0
  32. data/lib/spurline/cli/checks/permissions.rb +22 -0
  33. data/lib/spurline/cli/checks/project_structure.rb +48 -0
  34. data/lib/spurline/cli/checks/session_store.rb +97 -0
  35. data/lib/spurline/cli/console.rb +73 -0
  36. data/lib/spurline/cli/credentials.rb +181 -0
  37. data/lib/spurline/cli/generators/agent.rb +123 -0
  38. data/lib/spurline/cli/generators/migration.rb +62 -0
  39. data/lib/spurline/cli/generators/project.rb +331 -0
  40. data/lib/spurline/cli/generators/tool.rb +98 -0
  41. data/lib/spurline/cli/router.rb +121 -0
  42. data/lib/spurline/configuration.rb +23 -0
  43. data/lib/spurline/dsl/guardrails.rb +108 -0
  44. data/lib/spurline/dsl/hooks.rb +51 -0
  45. data/lib/spurline/dsl/memory.rb +39 -0
  46. data/lib/spurline/dsl/model.rb +23 -0
  47. data/lib/spurline/dsl/persona.rb +74 -0
  48. data/lib/spurline/dsl/suspend_until.rb +53 -0
  49. data/lib/spurline/dsl/tools.rb +176 -0
  50. data/lib/spurline/errors.rb +109 -0
  51. data/lib/spurline/lifecycle/deterministic_runner.rb +207 -0
  52. data/lib/spurline/lifecycle/runner.rb +456 -0
  53. data/lib/spurline/lifecycle/states.rb +47 -0
  54. data/lib/spurline/lifecycle/suspension_boundary.rb +82 -0
  55. data/lib/spurline/memory/context_assembler.rb +100 -0
  56. data/lib/spurline/memory/embedder/base.rb +17 -0
  57. data/lib/spurline/memory/embedder/open_ai.rb +70 -0
  58. data/lib/spurline/memory/episode.rb +56 -0
  59. data/lib/spurline/memory/episodic_store.rb +147 -0
  60. data/lib/spurline/memory/long_term/base.rb +22 -0
  61. data/lib/spurline/memory/long_term/postgres.rb +106 -0
  62. data/lib/spurline/memory/manager.rb +147 -0
  63. data/lib/spurline/memory/short_term.rb +57 -0
  64. data/lib/spurline/orchestration/agent_spawner.rb +151 -0
  65. data/lib/spurline/orchestration/judge.rb +109 -0
  66. data/lib/spurline/orchestration/ledger/store/base.rb +28 -0
  67. data/lib/spurline/orchestration/ledger/store/memory.rb +50 -0
  68. data/lib/spurline/orchestration/ledger.rb +339 -0
  69. data/lib/spurline/orchestration/merge_queue.rb +133 -0
  70. data/lib/spurline/orchestration/permission_intersection.rb +151 -0
  71. data/lib/spurline/orchestration/task_envelope.rb +201 -0
  72. data/lib/spurline/persona/base.rb +42 -0
  73. data/lib/spurline/persona/registry.rb +42 -0
  74. data/lib/spurline/secrets/resolver.rb +65 -0
  75. data/lib/spurline/secrets/vault.rb +42 -0
  76. data/lib/spurline/security/content.rb +76 -0
  77. data/lib/spurline/security/context_pipeline.rb +58 -0
  78. data/lib/spurline/security/gates/base.rb +36 -0
  79. data/lib/spurline/security/gates/operator_config.rb +22 -0
  80. data/lib/spurline/security/gates/system_prompt.rb +23 -0
  81. data/lib/spurline/security/gates/tool_result.rb +23 -0
  82. data/lib/spurline/security/gates/user_input.rb +22 -0
  83. data/lib/spurline/security/injection_scanner.rb +109 -0
  84. data/lib/spurline/security/pii_filter.rb +104 -0
  85. data/lib/spurline/session/resumption.rb +36 -0
  86. data/lib/spurline/session/serializer.rb +169 -0
  87. data/lib/spurline/session/session.rb +154 -0
  88. data/lib/spurline/session/store/base.rb +27 -0
  89. data/lib/spurline/session/store/memory.rb +45 -0
  90. data/lib/spurline/session/store/postgres.rb +123 -0
  91. data/lib/spurline/session/store/sqlite.rb +139 -0
  92. data/lib/spurline/session/suspension.rb +93 -0
  93. data/lib/spurline/session/turn.rb +98 -0
  94. data/lib/spurline/spur.rb +213 -0
  95. data/lib/spurline/streaming/buffer.rb +77 -0
  96. data/lib/spurline/streaming/chunk.rb +62 -0
  97. data/lib/spurline/streaming/stream_enumerator.rb +29 -0
  98. data/lib/spurline/testing.rb +245 -0
  99. data/lib/spurline/toolkit.rb +110 -0
  100. data/lib/spurline/tools/base.rb +209 -0
  101. data/lib/spurline/tools/idempotency.rb +220 -0
  102. data/lib/spurline/tools/permissions.rb +44 -0
  103. data/lib/spurline/tools/registry.rb +43 -0
  104. data/lib/spurline/tools/runner.rb +255 -0
  105. data/lib/spurline/tools/scope.rb +309 -0
  106. data/lib/spurline/tools/toolkit_registry.rb +63 -0
  107. data/lib/spurline/version.rb +5 -0
  108. data/lib/spurline.rb +56 -0
  109. metadata +161 -0
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spurline
4
+ module Memory
5
+ # Orchestrates short-term and long-term memory stores.
6
+ class Manager
7
+ attr_reader :short_term, :long_term, :episodic
8
+
9
+ def initialize(config: {})
10
+ window = config.fetch(:short_term, {}).fetch(:window, ShortTerm::DEFAULT_WINDOW)
11
+ @short_term = ShortTerm.new(window: window)
12
+ @long_term = build_long_term_store(config.fetch(:long_term, nil))
13
+ @episodic = build_episodic_store(config.fetch(:episodic, nil))
14
+ end
15
+
16
+ def add_turn(turn)
17
+ evicted_before = short_term.last_evicted
18
+ short_term.add_turn(turn)
19
+
20
+ evicted_turn = short_term.last_evicted
21
+ if long_term && evicted_turn && !evicted_turn.equal?(evicted_before)
22
+ persist_to_long_term!(evicted_turn)
23
+ end
24
+ end
25
+
26
+ def recent_turns(n = nil)
27
+ short_term.recent(n)
28
+ end
29
+
30
+ def turn_count
31
+ short_term.size
32
+ end
33
+
34
+ def recall(query:, limit: 5)
35
+ return [] unless long_term
36
+
37
+ long_term.retrieve(query: query, limit: limit)
38
+ end
39
+
40
+ def clear!
41
+ short_term.clear!
42
+ long_term&.clear!
43
+ episodic&.clear!
44
+ end
45
+
46
+ # Whether any turns have been evicted from the window.
47
+ # Useful for determining if summarization should kick in.
48
+ def window_overflowed?
49
+ !short_term.last_evicted.nil?
50
+ end
51
+
52
+ def record_episode(type:, content:, metadata: {}, turn_number: nil, parent_episode_id: nil, timestamp: Time.now)
53
+ return nil unless episodic
54
+
55
+ episodic.record(
56
+ type: type,
57
+ content: content,
58
+ metadata: metadata,
59
+ turn_number: turn_number,
60
+ parent_episode_id: parent_episode_id,
61
+ timestamp: timestamp
62
+ )
63
+ end
64
+
65
+ def restore_episodes(serialized_episodes)
66
+ return unless episodic
67
+
68
+ episodic.restore(serialized_episodes)
69
+ end
70
+
71
+ private
72
+
73
+ def build_long_term_store(config)
74
+ return nil unless config
75
+
76
+ adapter = config[:adapter]
77
+ case adapter
78
+ when :postgres
79
+ embedder = build_embedder(config)
80
+ LongTerm::Postgres.new(connection_string: config[:connection_string], embedder: embedder)
81
+ when nil
82
+ nil
83
+ else
84
+ return adapter if adapter.respond_to?(:store) && adapter.respond_to?(:retrieve)
85
+
86
+ raise Spurline::ConfigurationError,
87
+ "Unknown long-term memory adapter: #{adapter.inspect}."
88
+ end
89
+ end
90
+
91
+ def build_embedder(config)
92
+ model = config[:embedding_model] || config[:embedder]
93
+
94
+ case model
95
+ when :openai
96
+ Embedder::OpenAI.new
97
+ when nil
98
+ raise Spurline::ConfigurationError,
99
+ "Long-term memory requires an embedding_model. " \
100
+ "Example: memory :long_term, adapter: :postgres, embedding_model: :openai"
101
+ else
102
+ return model if model.respond_to?(:embed) && model.respond_to?(:dimensions)
103
+
104
+ raise Spurline::ConfigurationError,
105
+ "Unknown embedding model: #{model.inspect}."
106
+ end
107
+ end
108
+
109
+ def persist_to_long_term!(turn)
110
+ text_parts = []
111
+ text_parts << extract_text(turn.input) if turn.input
112
+ text_parts << extract_text(turn.output) if turn.output
113
+ content_text = text_parts.join("\n")
114
+ return if content_text.strip.empty?
115
+
116
+ long_term.store(content: content_text, metadata: { turn_number: turn.number })
117
+ end
118
+
119
+ def build_episodic_store(config)
120
+ enabled = case config
121
+ when nil
122
+ true
123
+ when true, false
124
+ config
125
+ when Hash
126
+ config.fetch(:enabled, true)
127
+ else
128
+ !!config
129
+ end
130
+
131
+ episodes = config.is_a?(Hash) ? config.fetch(:episodes, []) : []
132
+ EpisodicStore.new(enabled: enabled, episodes: episodes)
133
+ end
134
+
135
+ def extract_text(value)
136
+ case value
137
+ when Security::Content
138
+ value.text
139
+ when String
140
+ value
141
+ else
142
+ value.to_s
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spurline
4
+ module Memory
5
+ # Sliding window of recent turns. Holds Turn objects with their Content
6
+ # (input/output) carrying inherited trust levels.
7
+ #
8
+ # When the window overflows, oldest turns are evicted. The last evicted
9
+ # turn is available via #last_evicted for potential summarization.
10
+ class ShortTerm
11
+ DEFAULT_WINDOW = 20
12
+
13
+ attr_reader :window_size, :last_evicted
14
+
15
+ def initialize(window: DEFAULT_WINDOW)
16
+ @window_size = window
17
+ @turns = []
18
+ @last_evicted = nil
19
+ end
20
+
21
+ def add_turn(turn)
22
+ @turns << turn
23
+ trim!
24
+ end
25
+
26
+ # Returns recent turns as an array, most recent last.
27
+ def recent(n = nil)
28
+ n ? @turns.last(n) : @turns.dup
29
+ end
30
+
31
+ def size
32
+ @turns.length
33
+ end
34
+
35
+ def full?
36
+ @turns.length >= @window_size
37
+ end
38
+
39
+ def empty?
40
+ @turns.empty?
41
+ end
42
+
43
+ def clear!
44
+ @turns.clear
45
+ @last_evicted = nil
46
+ end
47
+
48
+ private
49
+
50
+ def trim!
51
+ while @turns.length > window_size
52
+ @last_evicted = @turns.shift
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spurline
4
+ module Orchestration
5
+ # Creates and runs child agents with permission-safe delegation.
6
+ #
7
+ # The setuid rule: child permissions are always <= parent permissions.
8
+ # Child scope inherits from the parent unless explicitly narrowed.
9
+ class AgentSpawner
10
+ def initialize(parent_agent:)
11
+ @parent_agent = parent_agent
12
+ @parent_session = parent_agent.session
13
+ @parent_scope = extract_scope(parent_agent)
14
+ @parent_permissions = extract_permissions(parent_agent)
15
+ end
16
+
17
+ # ASYNC-READY: spawns and runs a child agent, which is a blocking operation
18
+ def spawn(agent_class, input:, permissions: nil, scope: nil, &block)
19
+ validate_agent_class!(agent_class)
20
+
21
+ effective_permissions = compute_effective_permissions(permissions)
22
+ effective_scope = compute_effective_scope(scope)
23
+
24
+ child_agent = build_child_agent(
25
+ agent_class: agent_class,
26
+ permissions: effective_permissions,
27
+ scope: effective_scope
28
+ )
29
+
30
+ child_agent.session.metadata[:parent_session_id] = @parent_session.id
31
+ child_agent.session.metadata[:parent_agent_class] = @parent_agent.class.name
32
+
33
+ fire_parent_hook(:on_child_spawn, child_agent, agent_class)
34
+
35
+ begin
36
+ child_agent.run(input) do |chunk|
37
+ block&.call(chunk)
38
+ end
39
+
40
+ fire_parent_hook(:on_child_complete, child_agent, child_agent.session)
41
+ rescue Spurline::AgentError => e
42
+ fire_parent_hook(:on_child_error, child_agent, e)
43
+ raise Spurline::SpawnError,
44
+ "Child agent #{agent_class.name || agent_class} failed: #{e.message}. " \
45
+ "Parent session: #{@parent_session.id}, child session: #{child_agent.session.id}."
46
+ end
47
+
48
+ child_agent.session
49
+ end
50
+
51
+ private
52
+
53
+ def validate_agent_class!(agent_class)
54
+ unless agent_class.is_a?(Class) && agent_class <= Spurline::Agent
55
+ raise Spurline::ConfigurationError,
56
+ "spawn_agent requires a class that inherits from Spurline::Agent. " \
57
+ "Got: #{agent_class.inspect}"
58
+ end
59
+ end
60
+
61
+ def compute_effective_permissions(child_permissions)
62
+ return deep_copy(@parent_permissions) if child_permissions.nil?
63
+
64
+ PermissionIntersection.validate_no_escalation!(
65
+ @parent_permissions,
66
+ child_permissions
67
+ )
68
+
69
+ PermissionIntersection.compute(
70
+ @parent_permissions,
71
+ child_permissions
72
+ )
73
+ end
74
+
75
+ def compute_effective_scope(child_scope)
76
+ return @parent_scope if child_scope.nil?
77
+ return child_scope if @parent_scope.nil?
78
+
79
+ if child_scope.is_a?(Spurline::Tools::Scope)
80
+ validate_scope_subset!(child_scope) if @parent_scope
81
+ child_scope
82
+ elsif child_scope.is_a?(Hash)
83
+ @parent_scope.narrow(child_scope)
84
+ else
85
+ raise Spurline::ConfigurationError,
86
+ "scope must be a Spurline::Tools::Scope or a Hash of constraints. " \
87
+ "Got: #{child_scope.class} (#{child_scope.inspect})"
88
+ end
89
+ end
90
+
91
+ def validate_scope_subset!(child_scope)
92
+ return if child_scope.subset_of?(@parent_scope)
93
+
94
+ raise Spurline::ScopeViolationError,
95
+ "Child scope '#{child_scope.id}' is wider than parent scope '#{@parent_scope.id}'. " \
96
+ "A spawned agent cannot access resources outside the parent's scope. " \
97
+ "Narrow the child scope or widen the parent scope."
98
+ end
99
+
100
+ def build_child_agent(agent_class:, permissions:, scope:)
101
+ child_agent = agent_class.new(
102
+ user: @parent_session.user,
103
+ scope: scope
104
+ )
105
+
106
+ inject_effective_permissions!(child_agent, permissions)
107
+ child_agent
108
+ end
109
+
110
+ def inject_effective_permissions!(child_agent, permissions)
111
+ return if permissions.nil?
112
+
113
+ tool_runner = child_agent.instance_variable_get(:@tool_runner)
114
+ return unless tool_runner
115
+
116
+ existing_permissions = tool_runner.instance_variable_get(:@permissions) || {}
117
+ merged_permissions = existing_permissions.merge(permissions)
118
+ tool_runner.instance_variable_set(:@permissions, merged_permissions)
119
+ end
120
+
121
+ def extract_scope(agent)
122
+ agent.instance_variable_get(:@scope)
123
+ end
124
+
125
+ def extract_permissions(agent)
126
+ klass = agent.class
127
+ return {} unless klass.respond_to?(:permissions_config)
128
+
129
+ klass.permissions_config
130
+ end
131
+
132
+ def fire_parent_hook(hook_type, *args)
133
+ hooks = @parent_agent.class.hooks_config[hook_type] || []
134
+ hooks.each { |hook_block| hook_block.call(*args) }
135
+ end
136
+
137
+ def deep_copy(value)
138
+ case value
139
+ when Hash
140
+ value.each_with_object({}) do |(key, item), copy|
141
+ copy[key] = deep_copy(item)
142
+ end
143
+ when Array
144
+ value.map { |item| deep_copy(item) }
145
+ else
146
+ value
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spurline
4
+ module Orchestration
5
+ # Stateless evaluator that decides whether worker output satisfies a task.
6
+ class Judge
7
+ STRATEGIES = %i[structured llm_eval custom].freeze
8
+
9
+ Verdict = Struct.new(:decision, :reason, :feedback, keyword_init: true) do
10
+ def accepted?
11
+ decision == :accept
12
+ end
13
+
14
+ def rejected?
15
+ decision == :reject
16
+ end
17
+
18
+ def needs_revision?
19
+ decision == :revise
20
+ end
21
+ end
22
+
23
+ def initialize(strategy: :structured)
24
+ @strategy = strategy.to_sym
25
+ validate_strategy!(@strategy)
26
+ end
27
+
28
+ # ASYNC-READY: evaluate may call an LLM for :llm_eval strategy.
29
+ def evaluate(envelope:, output:, scheduler: Adapters::Scheduler::Sync.new, &custom_evaluator)
30
+ scheduler.run do
31
+ case @strategy
32
+ when :structured
33
+ evaluate_structured(envelope, output)
34
+ when :llm_eval
35
+ Verdict.new(
36
+ decision: :accept,
37
+ reason: "LLM evaluator stub",
38
+ feedback: "llm_eval strategy is a placeholder in M2.4"
39
+ )
40
+ when :custom
41
+ evaluate_custom(envelope, output, &custom_evaluator)
42
+ end
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def evaluate_structured(envelope, output)
49
+ output_text = normalize_output(output)
50
+ criteria = envelope.acceptance_criteria.map(&:to_s)
51
+
52
+ missing = criteria.reject do |criterion|
53
+ output_text.downcase.include?(criterion.downcase)
54
+ end
55
+
56
+ if missing.empty?
57
+ Verdict.new(decision: :accept, reason: "All acceptance criteria matched", feedback: nil)
58
+ else
59
+ Verdict.new(
60
+ decision: :reject,
61
+ reason: "Missing acceptance criteria",
62
+ feedback: "Missing: #{missing.join(", ")}"
63
+ )
64
+ end
65
+ end
66
+
67
+ def evaluate_custom(envelope, output, &block)
68
+ raise ArgumentError, "custom evaluator block is required" unless block
69
+
70
+ result = block.call(envelope, output)
71
+
72
+ case result
73
+ when Verdict
74
+ result
75
+ when true
76
+ Verdict.new(decision: :accept, reason: "custom evaluator accepted", feedback: nil)
77
+ when false
78
+ Verdict.new(decision: :reject, reason: "custom evaluator rejected", feedback: nil)
79
+ when Hash
80
+ decision = (result[:decision] || result["decision"] || :reject).to_sym
81
+ reason = result[:reason] || result["reason"] || "custom evaluator result"
82
+ feedback = result[:feedback] || result["feedback"]
83
+ Verdict.new(decision: decision, reason: reason, feedback: feedback)
84
+ else
85
+ raise ArgumentError, "custom evaluator must return Verdict, boolean, or hash"
86
+ end
87
+ end
88
+
89
+ def validate_strategy!(strategy)
90
+ return if STRATEGIES.include?(strategy)
91
+
92
+ raise Spurline::ConfigurationError, "invalid judge strategy: #{strategy.inspect}"
93
+ end
94
+
95
+ def normalize_output(output)
96
+ case output
97
+ when String
98
+ output
99
+ when Hash
100
+ output.map { |key, value| "#{key}: #{value}" }.join("\n")
101
+ when Array
102
+ output.join("\n")
103
+ else
104
+ output.to_s
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spurline
4
+ module Orchestration
5
+ class Ledger
6
+ module Store
7
+ # Abstract interface for ledger storage adapters.
8
+ class Base
9
+ def save_ledger(_ledger)
10
+ raise NotImplementedError, "#{self.class.name} must implement #save_ledger"
11
+ end
12
+
13
+ def load_ledger(_id)
14
+ raise NotImplementedError, "#{self.class.name} must implement #load_ledger"
15
+ end
16
+
17
+ def exists?(_id)
18
+ raise NotImplementedError, "#{self.class.name} must implement #exists?"
19
+ end
20
+
21
+ def delete(_id)
22
+ raise NotImplementedError, "#{self.class.name} must implement #delete"
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spurline
4
+ module Orchestration
5
+ class Ledger
6
+ module Store
7
+ # In-memory ledger store for tests and local development.
8
+ class Memory < Base
9
+ def initialize
10
+ @ledgers = {}
11
+ @mutex = Mutex.new
12
+ end
13
+
14
+ def save_ledger(ledger)
15
+ @mutex.synchronize do
16
+ @ledgers[ledger.id] = ledger.to_h
17
+ end
18
+ end
19
+
20
+ def load_ledger(id)
21
+ payload = @mutex.synchronize { @ledgers[id.to_s] }
22
+ raise Spurline::LedgerError, "ledger not found: #{id}" if payload.nil?
23
+
24
+ Spurline::Orchestration::Ledger.from_h(payload, store: self)
25
+ end
26
+
27
+ def exists?(id)
28
+ @mutex.synchronize { @ledgers.key?(id.to_s) }
29
+ end
30
+
31
+ def delete(id)
32
+ @mutex.synchronize { @ledgers.delete(id.to_s) }
33
+ end
34
+
35
+ def size
36
+ @mutex.synchronize { @ledgers.size }
37
+ end
38
+
39
+ def clear!
40
+ @mutex.synchronize { @ledgers.clear }
41
+ end
42
+
43
+ def ids
44
+ @mutex.synchronize { @ledgers.keys.dup }
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end