The Capability Autoloader
The Capability Autoloader: Zeitwerk's contract, ported. Drop a file at packs/text/summarize.rb defining Text::Summarize, and the capability "text.summarize" exists - no registration ceremony, no init file that lists everything twice. The convention IS the registry: file path <-> constant name <-> capability name, one bijection, three views. Lazy in development (load on first use), eager in production (load everything, and VERIFY the bijection - a file that defines the wrong …
Testing & Verification
Round 17
Xavier Noria
exit 0
bundle exec ruby examples/capability_autoloader.rb
a real captured run
THE CAPABILITY AUTOLOADER (the convention is the registry)
pack on disk: 3 files; capabilities loaded: 0 (laziness is a feature)
first use of text.summarize -> loaded 1/3 files, said: "Zeitwerk maps files to constants."
eager_load! (production parity): 2 loaded, 1 contract violation:
BOOT ERROR: expected math/percentile.rb to define Math::Percentile, but it doesn't
reload! then re-use -> "EDIT THE FILE!" (the edit, live, no restart)
one bijection, three views: the file path, the constant, the
capability name. lazy loading makes development start instantly;
eager loading makes production fail at BOOT when a file lies about
its constant (math/percentile.rb defining Math2 - caught, named,
fixable); reload makes the edit-run loop restart-free. none of it
required the framework's cooperation - though a registry miss-hook
(const_missing for capabilities) would make the loader invisible,
which is what a loader should be.
source
# frozen_string_literal: true # The Capability Autoloader: Zeitwerk's contract, ported. Drop a # file at packs/text/summarize.rb defining Text::Summarize, and the # capability "text.summarize" exists - no registration ceremony, no # init file that lists everything twice. The convention IS the # registry: file path <-> constant name <-> capability name, one # bijection, three views. Lazy in development (load on first use), # eager in production (load everything, and VERIFY the bijection - # a file that defines the wrong constant is a bug you want at boot, # not at 3am), reloadable in between. # # bundle exec ruby examples/capability_autoloader.rb # # Runs offline; a capability pack is written to a tmpdir, then # lazy-loaded, eager-verified, and hot-reloaded. require class="s">"bundler/setup" require class="s">"agentic" require class="s">"tmpdir" Agentic.logger.level = class="y">:fatal class CapabilityAutoloader attr_reader class="y">:loaded def initialize(root) @root = root @loaded = [] @generation = 0 end # "text.summarize" -> packs/text/summarize.rb -> Text::Summarize def ensure!(name) return if @loaded.include?(name) path = File.join(@root, *name.split(class="s">".")) raise class="s">"no file at #{path}.rb for capability #{name.inspect}" unless File.exist?(class="s">"#{path}.rb") load class="s">"#{path}.rb" constant = constant_for(name) register(name, constant) @loaded << name end # Production parity: load every file, and verify each one defines # the constant its path promises - the whole Zeitwerk contract def eager_load! errors = [] Dir[File.join(@root, class="s">"**", class="s">"*.rb")].sort.each do |file| name = file.delete_prefix(class="s">"#{@root}/").delete_suffix(class="s">".rb").tr(class="s">"/", class="s">".") begin ensure!(name) rescue NameError errors << class="s">"expected #{file.delete_prefix("#{@root}/class="s">")} to define #{camelize(name)}, but it doesn't" end end errors end # Development's third gear: forget everything, next use reloads # from disk - edits included, no process restart def reload! @loaded.each do |name| parts = camelize(name).split(class="s">"::") parent = parts[0..-2].inject(Object) { |m, c| m.const_get(c) } parent.send(class="y">:remove_const, parts.last.to_sym) end @loaded.clear # The registry resolves unversioned lookups to the LATEST version, # so each reload generation registers as 1.0.N and simply outranks # its predecessor - old providers age out instead of being mutated @generation += 1 end private def camelize(name) = name.split(class="s">".").map { |part| part.split(class="s">"_").map(&class="y">:capitalize).join }.join(class="s">"::") def constant_for(name) = Object.const_get(camelize(name)) def register(name, constant) spec = Agentic:class="y">:CapabilitySpecification.new( name: name, version: class="s">"1.0.#{@generation}", description: constant.const_defined?(class="y">:DESCRIPTION) ? constant:class="y">:DESCRIPTION : name, inputs: constant.const_defined?(class="y">:INPUTS) ? constant:class="y">:INPUTS : {}, outputs: constant.const_defined?(class="y">:OUTPUTS) ? constant:class="y">:OUTPUTS : {} ) Agentic.register_capability(spec, Agentic:class="y">:CapabilityProvider.new(capability: spec, implementation: ->(inputs) { constant.call(inputs) })) end end # --- the pack, written by convention (in real life: your repo) ---------------------- PACK = { class="s">"text/summarize.rb" => <<~RUBY, module Text module Summarize DESCRIPTION = class="s">"First sentence wins" def self.call(inputs) = {summary: inputs[class="y">:text].split(class="s">". ").first + class="s">"."} end end RUBY class="s">"text/word_count.rb" => <<~RUBY, module Text module WordCount def self.call(inputs) = {count: inputs[class="y">:text].split.size} end end RUBY class="s">"math/percentile.rb" => <<~RUBY module Math2 # <- 5pm strikes again: wrong constant for math/percentile.rb module Percentile def self.call(inputs) = {value: inputs[class="y">:samples].sort[(inputs[class="y">:samples].size * inputs[class="y">:p]).floor]} end end RUBY }.freeze failures = [] Dir.mktmpdir(class="s">"capability_pack") do |root| PACK.each { |rel, src| FileUtils.mkdir_p(File.dirname(File.join(root, rel))) File.write(File.join(root, rel), src) } loader = CapabilityAutoloader.new(root) puts class="s">"THE CAPABILITY AUTOLOADER (the convention is the registry)" puts puts class="s">" pack on disk: #{PACK.size} files; capabilities loaded: #{loader.loaded.size} (laziness is a feature)" # Lazy: first use loads exactly one file loader.ensure!(class="s">"text.summarize") agent = Agentic:class="y">:Agent.build { |a| a.name = class="s">"Reader" } agent.add_capability(class="s">"text.summarize") summary = agent.execute_capability(class="s">"text.summarize", {text: class="s">"Zeitwerk maps files to constants. The rest is commentary. Ask fxn."}) puts class="s">" first use of text.summarize -> loaded #{loader.loaded.size}/#{PACK.size} files, said: #{summary[class="y">:summary].inspect}" failures << class="s">"lazy load loaded too much" unless loader.loaded.size == 1 # Eager: production wants everything loaded AND the bijection verified errors = loader.eager_load! puts puts class="s">" eager_load! (production parity): #{loader.loaded.size} loaded, #{errors.size} contract violation:" errors.each { |e| puts class="s">" BOOT ERROR: #{e}" } failures << class="s">"eager load missed the misnamed constant" unless errors.any? { |e| e.include?(class="s">"Math:class="y">:Percentile") } # Reload: edit on disk, forget, next use sees the edit - no restart File.write(File.join(root, class="s">"text/summarize.rb"), PACK[class="s">"text/summarize.rb"].sub(class="s">"First sentence wins", class="s">"Now shouty").sub(class="s">"first + \".\class="s">"}", class="s">"first.upcase + \"!\class="s">"}")) loader.reload! loader.ensure!(class="s">"text.summarize") # Cached references are every reloader's one true enemy: the agent # snapshots its provider at add_capability time, so refresh the add. # (Zeitwerk fights the same war against `MyClass = SomeConstant`.) agent.add_capability(class="s">"text.summarize") shouty = agent.execute_capability(class="s">"text.summarize", {text: class="s">"Edit the file. See it live."}) puts puts class="s">" reload! then re-use -> #{shouty[class="y">:summary].inspect} (the edit, live, no restart)" failures << class="s">"reload didn't pick up the edit" unless shouty[class="y">:summary] == class="s">"EDIT THE FILE!" end puts puts class="s">" one bijection, three views: the file path, the constant, the" puts class="s">" capability name. lazy loading makes development start instantly;" puts class="s">" eager loading makes production fail at BOOT when a file lies about" puts class="s">" its constant (math/percentile.rb defining Math2 - caught, named," puts class="s">" fixable); reload makes the edit-run loop restart-free. none of it" puts class="s">" required the framework's cooperation - though a registry miss-hook" puts class="s">" (const_missing for capabilities) would make the loader invisible," puts class="s">" which is what a loader should be." exit(failures.empty? ? 0 : 1)