hames 0.1.0 → 0.1.1
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.
- checksums.yaml +4 -4
- data/lib/hames/context.rb +36 -8
- data/lib/hames/events.rb +5 -1
- data/lib/hames/loader.rb +81 -6
- data/lib/hames/schema.rb +154 -0
- data/lib/hames.rb +2 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9f376372b744ef6dac48af0a0577f32e8bb82d3834c13f0d2416927f2558c22a
|
|
4
|
+
data.tar.gz: a554420a27aa0850da074e9d9bb5a9069b75060d57a68b56e2fd064119548e64
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 273414c4bc70159631dfb9c9d7239c97f8fab9fe0d370e1c4aec29ae89c4c550e88e06933ddb9d63c0e17bf5345051060bf9fd910b7fe81f195706a957622dfb
|
|
7
|
+
data.tar.gz: 659bd79c726c33ba9764df5b04fd76efeff316f1f157e1901ddc8980a6224e74778e08bdd418f51f1469054f93649bb03c93ff12bddb402221628ad59eeaadfb
|
data/lib/hames/context.rb
CHANGED
|
@@ -49,10 +49,28 @@ module Hames
|
|
|
49
49
|
|
|
50
50
|
# Runs the block now; the block must return a disposer callable (or nil).
|
|
51
51
|
# Disposal happens in reverse registration order, per owner, on unload.
|
|
52
|
+
# The disposer handed back is self-removing and idempotent: calling it
|
|
53
|
+
# runs the teardown once and drops its own entry from @effects, so a
|
|
54
|
+
# long-lived context does not pin every disposed registration (and
|
|
55
|
+
# whatever its closure captured). The done flag lives on the frame (not
|
|
56
|
+
# presence in @effects) because dispose_owner! removes frames from
|
|
57
|
+
# @effects BEFORE running them — a presence check would silently skip
|
|
58
|
+
# the real teardown there.
|
|
52
59
|
def effect(&block)
|
|
53
60
|
disposer = block.call
|
|
54
|
-
|
|
55
|
-
|
|
61
|
+
return disposer unless disposer
|
|
62
|
+
|
|
63
|
+
frame = [@owner, nil, false]
|
|
64
|
+
wrapped = lambda do
|
|
65
|
+
next if frame[2]
|
|
66
|
+
|
|
67
|
+
frame[2] = true
|
|
68
|
+
@effects.delete(frame)
|
|
69
|
+
disposer.call
|
|
70
|
+
end
|
|
71
|
+
frame[1] = wrapped
|
|
72
|
+
@effects << frame
|
|
73
|
+
wrapped
|
|
56
74
|
end
|
|
57
75
|
|
|
58
76
|
def with_owner(owner)
|
|
@@ -68,12 +86,16 @@ module Hames
|
|
|
68
86
|
doomed = []
|
|
69
87
|
@effects.each { |fr| (fr[0] == owner ? doomed : kept) << fr }
|
|
70
88
|
@effects = kept
|
|
89
|
+
# each wrapped disposer re-scans @effects for a frame that the
|
|
90
|
+
# partition above already removed — an O(doomed·kept) miss. Negligible
|
|
91
|
+
# at current roster sizes; if owner disposal ever gets hot, carry the
|
|
92
|
+
# original disposer on the frame and call it directly here.
|
|
71
93
|
doomed.reverse_each { |(_o, d)| d.call }
|
|
72
94
|
end
|
|
73
95
|
|
|
74
96
|
# Dispose the whole context (child scopes call this when they end).
|
|
75
97
|
def dispose!
|
|
76
|
-
@effects.reverse_each { |(_o, d)| d.call }
|
|
98
|
+
@effects.dup.reverse_each { |(_o, d)| d.call }
|
|
77
99
|
@effects.clear
|
|
78
100
|
end
|
|
79
101
|
|
|
@@ -88,9 +110,7 @@ module Hames
|
|
|
88
110
|
l = Listener.new(name:, block:, prepend:, owner: @owner)
|
|
89
111
|
bucket = @listeners[name]
|
|
90
112
|
prepend ? bucket.unshift(l) : bucket.push(l)
|
|
91
|
-
|
|
92
|
-
@effects << [@owner, disposer]
|
|
93
|
-
disposer
|
|
113
|
+
effect { -> { bucket.delete(l) } }
|
|
94
114
|
end
|
|
95
115
|
|
|
96
116
|
# Listeners visible to this context: parent chain first (registration
|
|
@@ -100,10 +120,18 @@ module Hames
|
|
|
100
120
|
parent ? parent.listeners_for(name) + own : own.dup
|
|
101
121
|
end
|
|
102
122
|
|
|
103
|
-
# emit: fire-and-forget, registration order, no return value.
|
|
123
|
+
# emit: fire-and-forget, registration order, no return value. Listener
|
|
124
|
+
# failures are isolated (warned, not raised): by the time listeners run,
|
|
125
|
+
# the producer's fact is already committed — a durable append emits after
|
|
126
|
+
# the store write — so a consumer bug must not un-happen it. The other
|
|
127
|
+
# modes raise through: their results are load-bearing.
|
|
104
128
|
def emit(name, *args)
|
|
105
129
|
Hames.assert_mode!(name, :emit)
|
|
106
|
-
listeners_for(name).each
|
|
130
|
+
listeners_for(name).each do |l|
|
|
131
|
+
l.block.call(*args)
|
|
132
|
+
rescue StandardError => e
|
|
133
|
+
warn "hames: emit(#{name}): listener isolated: #{e.class}: #{e.message}"
|
|
134
|
+
end
|
|
107
135
|
nil
|
|
108
136
|
end
|
|
109
137
|
|
data/lib/hames/events.rb
CHANGED
|
@@ -18,7 +18,11 @@ module Hames
|
|
|
18
18
|
# Hames.event "tools/pre_execute", mode: :waterfall, payload: Tools::Call
|
|
19
19
|
def event(name, mode: nil, durable: false, payload: nil, doc: nil)
|
|
20
20
|
name = name.to_s
|
|
21
|
-
|
|
21
|
+
if mode.nil?
|
|
22
|
+
return events.fetch(name) do
|
|
23
|
+
raise Hames::ContractError, "lookup of undeclared event #{name}"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
22
26
|
|
|
23
27
|
raise ArgumentError, "unknown dispatch mode #{mode.inspect}" unless MODES.include?(mode)
|
|
24
28
|
if (existing = events[name]) && existing.mode != mode
|
data/lib/hames/loader.rb
CHANGED
|
@@ -4,16 +4,27 @@ module Hames
|
|
|
4
4
|
# Base class for service plugins. A plugin may instead be any object that
|
|
5
5
|
# responds to apply(ctx) (the functional form), with optional #inject.
|
|
6
6
|
class Service
|
|
7
|
+
# config_schema (and its inheritance) comes from the DSL mixin, so a
|
|
8
|
+
# functional plugin that is not a Service can extend the same module and be
|
|
9
|
+
# schema'd identically (docs/composition.md §9).
|
|
10
|
+
extend Hames::Schema::DSL
|
|
11
|
+
|
|
7
12
|
class << self
|
|
13
|
+
# Both class-level declarations are inherited: a subclass (a test
|
|
14
|
+
# double, a provider variant) mounts exactly like its parent unless it
|
|
15
|
+
# redeclares. inject accumulates down the chain; service_key overrides.
|
|
8
16
|
def service_key(key = nil)
|
|
9
17
|
@service_key = key.to_sym if key
|
|
10
|
-
@service_key
|
|
18
|
+
return @service_key if @service_key
|
|
19
|
+
|
|
20
|
+
superclass <= Hames::Service ? superclass.service_key : nil
|
|
11
21
|
end
|
|
12
22
|
|
|
13
23
|
def inject(*keys)
|
|
14
24
|
@inject ||= []
|
|
15
25
|
@inject.concat(keys.map(&:to_sym)) unless keys.empty?
|
|
16
|
-
|
|
26
|
+
inherited = superclass <= Hames::Service ? superclass.inject : []
|
|
27
|
+
inherited + @inject
|
|
17
28
|
end
|
|
18
29
|
end
|
|
19
30
|
|
|
@@ -32,6 +43,18 @@ module Hames
|
|
|
32
43
|
|
|
33
44
|
def start(ctx); end
|
|
34
45
|
def stop(ctx); end
|
|
46
|
+
|
|
47
|
+
# Hot-reload hook (plan §M6): the loader replaces config wholesale, then
|
|
48
|
+
# calls this. Override to re-derive whatever start captured. The default
|
|
49
|
+
# warns — a service running on stale knobs should say so, not hide it.
|
|
50
|
+
def reconfigure(_config)
|
|
51
|
+
warn "hames: #{self.class} does not support hot-reconfigure; remount the row to apply"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Loader-only: swap the config object under the reader.
|
|
55
|
+
def replace_config!(config)
|
|
56
|
+
@config = config
|
|
57
|
+
end
|
|
35
58
|
end
|
|
36
59
|
|
|
37
60
|
Row = Data.define(:id, :plugin, :config, :disabled) do
|
|
@@ -46,7 +69,12 @@ module Hames
|
|
|
46
69
|
# row with an existing id replaces that row's config wholesale (never a
|
|
47
70
|
# deep merge); unknown ids append.
|
|
48
71
|
class Loader
|
|
49
|
-
|
|
72
|
+
# `mounted` is insertion-ordered by boot!, which mounts in dependency
|
|
73
|
+
# order — so its reverse is exact reverse-dependency order, and that is
|
|
74
|
+
# what a teardown wants: a consumer comes down before what it injects.
|
|
75
|
+
# Row order cannot stand in for it, since an inserted row's position in
|
|
76
|
+
# the tree need not match where its dependencies put it.
|
|
77
|
+
attr_reader :ctx, :rows, :mounted
|
|
50
78
|
|
|
51
79
|
def initialize(ctx = Context.new)
|
|
52
80
|
@ctx = ctx
|
|
@@ -88,10 +116,44 @@ module Hames
|
|
|
88
116
|
ctx
|
|
89
117
|
end
|
|
90
118
|
|
|
119
|
+
# Hot config swap — wholesale, like layering, never a merge. Replaces the
|
|
120
|
+
# row's config, swaps the mounted service's, invokes its reconfigure hook
|
|
121
|
+
# under with_owner(id) (so hook-registered effects are owned by the row,
|
|
122
|
+
# not left ownerless), and emits config/updated when the app vocabulary
|
|
123
|
+
# declares it (hames itself declares no events and stays app-agnostic). A
|
|
124
|
+
# raising hook rolls the row and the service's config back to what they
|
|
125
|
+
# were before the call and re-raises — the swap is atomic, never split-brain.
|
|
126
|
+
def reconfigure!(id, config)
|
|
127
|
+
id = id.to_s
|
|
128
|
+
old_row = @rows.fetch(id) { raise KeyError, "no row #{id.inspect}" }
|
|
129
|
+
service = @mounted.fetch(id) { raise KeyError, "no mounted plugin #{id.inspect}" }
|
|
130
|
+
old_config = old_row.config
|
|
131
|
+
@rows[id] = Hames::Row.new(id: old_row.id, plugin: old_row.plugin, config: config, disabled: old_row.disabled)
|
|
132
|
+
service.replace_config!(config)
|
|
133
|
+
begin
|
|
134
|
+
ctx.with_owner(id) { service.reconfigure(config) }
|
|
135
|
+
rescue StandardError
|
|
136
|
+
@rows[id] = old_row
|
|
137
|
+
service.replace_config!(old_config)
|
|
138
|
+
raise
|
|
139
|
+
end
|
|
140
|
+
ctx.emit("config/updated", id, config) if Hames.declared?("config/updated")
|
|
141
|
+
config
|
|
142
|
+
end
|
|
143
|
+
|
|
91
144
|
def unload!(id)
|
|
92
|
-
plugin = @mounted.
|
|
93
|
-
|
|
94
|
-
|
|
145
|
+
plugin = @mounted.fetch(id) { raise ArgumentError, "no mounted plugin #{id}" }
|
|
146
|
+
# Owner effects are disposed even when stop raises, so a wedged stop hook
|
|
147
|
+
# cannot strand this row's service registration and listeners; and the
|
|
148
|
+
# @mounted entry is dropped only AFTER a clean teardown, so a stop that
|
|
149
|
+
# raised keeps the row unloadable (a retry re-runs stop) rather than
|
|
150
|
+
# vanishing behind a misleading "no mounted plugin".
|
|
151
|
+
begin
|
|
152
|
+
plugin.stop(ctx) if plugin.respond_to?(:stop)
|
|
153
|
+
ensure
|
|
154
|
+
ctx.dispose_owner!(id)
|
|
155
|
+
end
|
|
156
|
+
@mounted.delete(id)
|
|
95
157
|
end
|
|
96
158
|
|
|
97
159
|
# Resolved tree, layer-agnostic view (for --dump-config).
|
|
@@ -118,6 +180,19 @@ module Hames
|
|
|
118
180
|
def mount(row, plugin)
|
|
119
181
|
ctx.with_owner(row.id) { plugin.apply(ctx) }
|
|
120
182
|
@mounted[row.id] = plugin
|
|
183
|
+
rescue Exception # rubocop:disable Lint/RescueException
|
|
184
|
+
# apply raised partway through: whatever it registered before the raise is
|
|
185
|
+
# owned by this row, and this row is NOT in @mounted, so no unload! or
|
|
186
|
+
# shutdown could ever find those registrations to dispose them. Roll them
|
|
187
|
+
# back here so a failed mount leaves nothing live, then let the boot
|
|
188
|
+
# failure out unchanged. A raising disposer during rollback is warned
|
|
189
|
+
# rather than allowed to mask the failure that started it.
|
|
190
|
+
begin
|
|
191
|
+
ctx.dispose_owner!(row.id)
|
|
192
|
+
rescue StandardError => e
|
|
193
|
+
warn "hames: mount rollback for #{row.id}: a disposer raised: #{e.class}: #{e.message}"
|
|
194
|
+
end
|
|
195
|
+
raise
|
|
121
196
|
end
|
|
122
197
|
end
|
|
123
198
|
end
|
data/lib/hames/schema.rb
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hames
|
|
4
|
+
# The kernel's own tiny config validator (docs/composition.md §9): a plain
|
|
5
|
+
# description of a service's config keys — for each one a type:, whether it is
|
|
6
|
+
# required:, an optional enum: of legal values, a default:, and a doc: string
|
|
7
|
+
# — plus the code that checks a config hash against it and reports what does
|
|
8
|
+
# not fit.
|
|
9
|
+
#
|
|
10
|
+
# Pure stdlib rather than dry-schema, because the kernel's zero-runtime-
|
|
11
|
+
# dependency rule is a design constraint rather than a coincidence (CLAUDE.md).
|
|
12
|
+
# The plan's §10 dry-schema mention is superseded by that rule.
|
|
13
|
+
class Schema
|
|
14
|
+
# Distinguishes a read of config_schema from a declaration: with no argument
|
|
15
|
+
# config_schema() reads the stored schema, config_schema({}) declares an
|
|
16
|
+
# empty one (a service that takes no config), and config_schema(key: {...})
|
|
17
|
+
# declares a populated one.
|
|
18
|
+
UNSET = Object.new.freeze
|
|
19
|
+
private_constant :UNSET
|
|
20
|
+
|
|
21
|
+
# One key's rules. `type` is a Class or an array of Classes (a union);
|
|
22
|
+
# [TrueClass, FalseClass] is how a boolean is spelled, since Ruby has no
|
|
23
|
+
# Boolean class. `default` documents what the service reads when the key is
|
|
24
|
+
# missing — it fills a missing key at read time, it does NOT merge into the
|
|
25
|
+
# config, because config layering is wholesale-replace (CLAUDE.md).
|
|
26
|
+
KeySpec = Data.define(:name, :type, :required, :enum, :default, :doc)
|
|
27
|
+
|
|
28
|
+
Result = Data.define(:errors, :warnings) do
|
|
29
|
+
def ok? = errors.empty?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Class-level DSL. Hames::Service extends it so every service is schema-able;
|
|
33
|
+
# a functional plugin (one that only responds to apply, not a Service) can
|
|
34
|
+
# `extend Hames::Schema::DSL` to be schema'd too — that is how the OpenRouter
|
|
35
|
+
# plugin, which is not a Service, still carries a schema doctor can read.
|
|
36
|
+
module DSL
|
|
37
|
+
def config_schema(specs = UNSET)
|
|
38
|
+
return stored_config_schema if specs.equal?(UNSET)
|
|
39
|
+
|
|
40
|
+
@config_schema = Hames::Schema.build(specs)
|
|
41
|
+
Hames::Schema.register(self, @config_schema)
|
|
42
|
+
@config_schema
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Inherited like service_key and inject: a subclass that does not redeclare
|
|
46
|
+
# validates exactly like its parent.
|
|
47
|
+
def stored_config_schema
|
|
48
|
+
return @config_schema if defined?(@config_schema) && @config_schema
|
|
49
|
+
|
|
50
|
+
parent = superclass if respond_to?(:superclass)
|
|
51
|
+
parent.respond_to?(:config_schema) ? parent.config_schema : nil
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Every declared schema, class => schema, in declaration order. The catalog
|
|
56
|
+
# generator (rake config:catalog) walks this once the code is required, the
|
|
57
|
+
# way events:catalog walks Hames.catalog.
|
|
58
|
+
def self.declared = (@declared ||= {})
|
|
59
|
+
def self.register(klass, schema) = declared[klass] = schema
|
|
60
|
+
def self.reset_declared! = declared.clear # test hook
|
|
61
|
+
|
|
62
|
+
def self.build(specs)
|
|
63
|
+
keys = (specs || {}).to_h do |name, opts|
|
|
64
|
+
opts ||= {}
|
|
65
|
+
key = name.to_sym
|
|
66
|
+
[key, KeySpec.new(name: key, type: opts[:type], required: opts.fetch(:required, false),
|
|
67
|
+
enum: opts[:enum], default: opts[:default], doc: opts[:doc])]
|
|
68
|
+
end
|
|
69
|
+
new(keys)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
attr_reader :keys
|
|
73
|
+
|
|
74
|
+
def initialize(keys)
|
|
75
|
+
@keys = keys
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Validates a config hash without mutating it. `subject` names the row (or
|
|
79
|
+
# class) so an error reads as "sandbox: image is required but was not set" —
|
|
80
|
+
# a refusal that cannot say which of thirty rows it is about is one an
|
|
81
|
+
# operator cannot act on.
|
|
82
|
+
#
|
|
83
|
+
# A key that is absent OR present-but-nil counts as UNSET: a required unset
|
|
84
|
+
# key is an error, a non-required one is fine — its default applies, and an
|
|
85
|
+
# unset !env resolving to nil is an ordinary state (§5) rather than a fault.
|
|
86
|
+
# A non-nil value is checked against `type` and `enum`. Extra keys WARN
|
|
87
|
+
# rather than fail, because config rows grow.
|
|
88
|
+
#
|
|
89
|
+
# `redact:` governs whether a rejected value's CONTENT may appear in the
|
|
90
|
+
# message. It stays false for programmatic callers (which hold plain config,
|
|
91
|
+
# and a message naming the value is more useful), and doctor passes true
|
|
92
|
+
# because by the time it validates, a value is a materialized !env/!setting/
|
|
93
|
+
# !ruby result and may be a secret — so its message names the type only,
|
|
94
|
+
# never the content.
|
|
95
|
+
def validate(config, subject: nil, redact: false)
|
|
96
|
+
config ||= {}
|
|
97
|
+
prefix = subject ? "#{subject}: " : ""
|
|
98
|
+
errors = []
|
|
99
|
+
|
|
100
|
+
@keys.each_value do |spec|
|
|
101
|
+
value = config[spec.name]
|
|
102
|
+
if value.nil?
|
|
103
|
+
errors << "#{prefix}#{spec.name} is required but was not set" if spec.required
|
|
104
|
+
next
|
|
105
|
+
end
|
|
106
|
+
if spec.type && Array(spec.type).none? { |t| value.is_a?(t) }
|
|
107
|
+
got = redact ? Schema.article(value.class.name) : Schema.describe(value)
|
|
108
|
+
errors << "#{prefix}#{spec.name} must be #{Schema.type_desc(spec.type)}, got #{got}"
|
|
109
|
+
end
|
|
110
|
+
if spec.enum && !spec.enum.include?(value)
|
|
111
|
+
allowed = spec.enum.map(&:inspect).join(", ")
|
|
112
|
+
# Redacted: the allowed set is schema-defined and safe to print; the
|
|
113
|
+
# configured value is not, so it is named as out-of-set, not echoed.
|
|
114
|
+
errors << if redact
|
|
115
|
+
"#{prefix}#{spec.name} must be one of #{allowed} (the configured value is not)"
|
|
116
|
+
else
|
|
117
|
+
"#{prefix}#{spec.name} must be one of #{allowed}, got #{value.inspect}"
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
warnings = (config.keys - @keys.keys).map { |extra| "#{prefix}#{extra} is not a known config key" }
|
|
123
|
+
Result.new(errors: errors, warnings: warnings)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# A boolean is the TrueClass/FalseClass union, so it reads as "a boolean"
|
|
127
|
+
# rather than naming two classes nobody wrote in a config.
|
|
128
|
+
def self.type_desc(type)
|
|
129
|
+
types = Array(type)
|
|
130
|
+
return "a boolean" if types.sort_by(&:name) == [FalseClass, TrueClass]
|
|
131
|
+
return article(types.first.name) if types.length == 1
|
|
132
|
+
|
|
133
|
+
"one of #{types.map(&:name).join(', ')}"
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# "an Integer", "a String" — the article chosen by the leading sound, close
|
|
137
|
+
# enough on class names, so a message never reads "a Integer".
|
|
138
|
+
def self.article(word)
|
|
139
|
+
word = word.to_s
|
|
140
|
+
/\A[aeiou]/i.match?(word) ? "an #{word}" : "a #{word}"
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# A scalar renders as itself ("got 5", "got nil"); anything larger renders
|
|
144
|
+
# as its class, so a wrong Hash where a String was wanted does not paste a
|
|
145
|
+
# whole tree into a one-line status. Only reached when redact is false —
|
|
146
|
+
# doctor never lets a rejected value's content into a message.
|
|
147
|
+
def self.describe(value)
|
|
148
|
+
case value
|
|
149
|
+
when String, Numeric, Symbol, true, false then value.inspect
|
|
150
|
+
else value.class.to_s
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
data/lib/hames.rb
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
# four dispatch modes, reversible effects, and dependency-driven boot. It has
|
|
5
5
|
# no knowledge of LLMs and is reusable for any plugin-composed application.
|
|
6
6
|
module Hames
|
|
7
|
-
VERSION = "0.1.
|
|
7
|
+
VERSION = "0.1.1"
|
|
8
8
|
end
|
|
9
9
|
|
|
10
10
|
require_relative "hames/events"
|
|
11
11
|
require_relative "hames/context"
|
|
12
|
+
require_relative "hames/schema"
|
|
12
13
|
require_relative "hames/loader"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: hames
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Obie Fernandez
|
|
@@ -24,6 +24,7 @@ files:
|
|
|
24
24
|
- lib/hames/context.rb
|
|
25
25
|
- lib/hames/events.rb
|
|
26
26
|
- lib/hames/loader.rb
|
|
27
|
+
- lib/hames/schema.rb
|
|
27
28
|
homepage: https://terret.org
|
|
28
29
|
licenses:
|
|
29
30
|
- MIT
|