From 7aea7b03299ca96b92b82b8f49bc564965a2ccd1 Mon Sep 17 00:00:00 2001 From: tylercschneider Date: Thu, 29 Oct 2026 14:18:02 -0400 Subject: [PATCH 1/5] Read a method exposed without an answer shape as having none Co-Authored-By: Claude Opus 5.5 --- lib/hub_kernel/exposes.rb | 4 ++-- test/hub_kernel/exposes_test.rb | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/hub_kernel/exposes.rb b/lib/hub_kernel/exposes.rb index f61e62e..43755c5 100644 --- a/lib/hub_kernel/exposes.rb +++ b/lib/hub_kernel/exposes.rb @@ -12,7 +12,7 @@ class NotAllowed < StandardError; end class Refused < StandardError; end module Exposes - Exposed = Data.define(:name, :takes, :writes, :earlier_answers) do + Exposed = Data.define(:name, :takes, :writes, :earlier_answers, :answer_shape) do def earlier_answer(major) = earlier_answers.find { |earlier| earlier.major == major } end EarlierAnswer = Data.define(:major, :translation) do @@ -21,7 +21,7 @@ def translate(answer) = translation.call(answer) def exposes(name, takes:, writes:, earlier_answers: {}) Interface::ExposingHubs.add(self) - exposed_methods[name.to_s] = Exposed.new(name: name, takes: takes, writes: writes, earlier_answers: earlier_answers.map { |major, translation| EarlierAnswer.new(major: major, translation: translation) }) + exposed_methods[name.to_s] = Exposed.new(name: name, takes: takes, writes: writes, earlier_answers: earlier_answers.map { |major, translation| EarlierAnswer.new(major: major, translation: translation) }, answer_shape: nil) end def exposed(name) = exposed_methods[name.to_s] diff --git a/test/hub_kernel/exposes_test.rb b/test/hub_kernel/exposes_test.rb index 4337cce..51f53e3 100644 --- a/test/hub_kernel/exposes_test.rb +++ b/test/hub_kernel/exposes_test.rb @@ -71,6 +71,10 @@ module Baker assert_equal({ price: 300 }, Baker.exposed("price_of").earlier_answer(2).translate({ cents: 300 })) end + test "a method exposed without an answer shape reads as having none" do + assert_nil Supplies.exposed("record_purchase").answer_shape + end + test "a hub that declares an exposed method is recorded as a hub that exposes methods" do assert_includes HubKernel::Interface::ExposingHubs.list, Supplies end From 196ad8c6f5f73d156fd08f84f22a61de00fd0e35 Mon Sep 17 00:00:00 2001 From: tylercschneider Date: Thu, 29 Oct 2026 14:18:14 -0400 Subject: [PATCH 2/5] Read an earlier answer given only a translation as having no answer shape Co-Authored-By: Claude Opus 5.5 --- lib/hub_kernel/exposes.rb | 4 ++-- test/hub_kernel/exposes_test.rb | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/hub_kernel/exposes.rb b/lib/hub_kernel/exposes.rb index 43755c5..44e3105 100644 --- a/lib/hub_kernel/exposes.rb +++ b/lib/hub_kernel/exposes.rb @@ -15,13 +15,13 @@ module Exposes Exposed = Data.define(:name, :takes, :writes, :earlier_answers, :answer_shape) do def earlier_answer(major) = earlier_answers.find { |earlier| earlier.major == major } end - EarlierAnswer = Data.define(:major, :translation) do + EarlierAnswer = Data.define(:major, :translation, :answer_shape) do def translate(answer) = translation.call(answer) end def exposes(name, takes:, writes:, earlier_answers: {}) Interface::ExposingHubs.add(self) - exposed_methods[name.to_s] = Exposed.new(name: name, takes: takes, writes: writes, earlier_answers: earlier_answers.map { |major, translation| EarlierAnswer.new(major: major, translation: translation) }, answer_shape: nil) + exposed_methods[name.to_s] = Exposed.new(name: name, takes: takes, writes: writes, earlier_answers: earlier_answers.map { |major, translation| EarlierAnswer.new(major: major, translation: translation, answer_shape: nil) }, answer_shape: nil) end def exposed(name) = exposed_methods[name.to_s] diff --git a/test/hub_kernel/exposes_test.rb b/test/hub_kernel/exposes_test.rb index 51f53e3..0e0dc89 100644 --- a/test/hub_kernel/exposes_test.rb +++ b/test/hub_kernel/exposes_test.rb @@ -75,6 +75,10 @@ module Baker assert_nil Supplies.exposed("record_purchase").answer_shape end + test "an earlier answer given only a translation reads as having no answer shape" do + assert_nil Grocer.exposed("price_of").earlier_answer(1).answer_shape + end + test "a hub that declares an exposed method is recorded as a hub that exposes methods" do assert_includes HubKernel::Interface::ExposingHubs.list, Supplies end From 82b8ee626390a2e4b54b5b6818219c30ea2cef6a Mon Sep 17 00:00:00 2001 From: tylercschneider Date: Thu, 29 Oct 2026 14:18:32 -0400 Subject: [PATCH 3/5] Expose a method with the JSON Schema of its answer as its answer shape Co-Authored-By: Claude Opus 5.5 --- lib/hub_kernel/exposes.rb | 4 ++-- test/hub_kernel/exposes_test.rb | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/hub_kernel/exposes.rb b/lib/hub_kernel/exposes.rb index 44e3105..98b3179 100644 --- a/lib/hub_kernel/exposes.rb +++ b/lib/hub_kernel/exposes.rb @@ -19,9 +19,9 @@ def earlier_answer(major) = earlier_answers.find { |earlier| earlier.major == ma def translate(answer) = translation.call(answer) end - def exposes(name, takes:, writes:, earlier_answers: {}) + def exposes(name, takes:, writes:, earlier_answers: {}, answer_shape: nil) Interface::ExposingHubs.add(self) - exposed_methods[name.to_s] = Exposed.new(name: name, takes: takes, writes: writes, earlier_answers: earlier_answers.map { |major, translation| EarlierAnswer.new(major: major, translation: translation, answer_shape: nil) }, answer_shape: nil) + exposed_methods[name.to_s] = Exposed.new(name: name, takes: takes, writes: writes, earlier_answers: earlier_answers.map { |major, translation| EarlierAnswer.new(major: major, translation: translation, answer_shape: nil) }, answer_shape: answer_shape) end def exposed(name) = exposed_methods[name.to_s] diff --git a/test/hub_kernel/exposes_test.rb b/test/hub_kernel/exposes_test.rb index 0e0dc89..eb508df 100644 --- a/test/hub_kernel/exposes_test.rb +++ b/test/hub_kernel/exposes_test.rb @@ -43,6 +43,12 @@ module Baker exposes :price_of, takes: %i[item], writes: false, earlier_answers: { 1 => ->(answer) { answer[:price] }, 2 => ->(answer) { { price: answer[:cents] } } } end + module Butcher + extend HubKernel::Exposes + + exposes :weigh_cut, takes: %i[cut], writes: false, answer_shape: { "type" => "integer" } + end + test "an exposed method is found by its name with the values it takes" do assert_equal %i[supplier_id bought_on lines], Supplies.exposed("record_purchase").takes end @@ -79,6 +85,10 @@ module Baker assert_nil Grocer.exposed("price_of").earlier_answer(1).answer_shape end + test "a method is exposed with the JSON Schema of its answer as its answer shape" do + assert_equal({ "type" => "integer" }, Butcher.exposed("weigh_cut").answer_shape) + end + test "a hub that declares an exposed method is recorded as a hub that exposes methods" do assert_includes HubKernel::Interface::ExposingHubs.list, Supplies end From db151e52a8dad41fbe0e83ab712820c11a2bc0d4 Mon Sep 17 00:00:00 2001 From: tylercschneider Date: Thu, 29 Oct 2026 14:18:55 -0400 Subject: [PATCH 4/5] Give an earlier answer the JSON Schema of the answer at its major Co-Authored-By: Claude Opus 5.5 --- lib/hub_kernel/exposes.rb | 8 +++++++- test/hub_kernel/exposes_test.rb | 10 ++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/hub_kernel/exposes.rb b/lib/hub_kernel/exposes.rb index 98b3179..151c57a 100644 --- a/lib/hub_kernel/exposes.rb +++ b/lib/hub_kernel/exposes.rb @@ -21,7 +21,7 @@ def translate(answer) = translation.call(answer) def exposes(name, takes:, writes:, earlier_answers: {}, answer_shape: nil) Interface::ExposingHubs.add(self) - exposed_methods[name.to_s] = Exposed.new(name: name, takes: takes, writes: writes, earlier_answers: earlier_answers.map { |major, translation| EarlierAnswer.new(major: major, translation: translation, answer_shape: nil) }, answer_shape: answer_shape) + exposed_methods[name.to_s] = Exposed.new(name: name, takes: takes, writes: writes, earlier_answers: earlier_answers.map { |major, declared| earlier_answer_from(major, declared) }, answer_shape: answer_shape) end def exposed(name) = exposed_methods[name.to_s] @@ -59,6 +59,12 @@ def exposed_methods = @exposed_methods ||= {} def exposing_hub = name.demodulize + def earlier_answer_from(major, declared) + return EarlierAnswer.new(major: major, translation: declared, answer_shape: nil) if declared.respond_to?(:call) + + EarlierAnswer.new(major: major, translation: declared[:translation], answer_shape: declared[:answer_shape]) + end + def refuse_without_caller(person, account) raise MissingArgumentError, "A call by name needs a person" if person.nil? raise MissingArgumentError, "A call by name needs an account" if account.nil? diff --git a/test/hub_kernel/exposes_test.rb b/test/hub_kernel/exposes_test.rb index eb508df..45bc49b 100644 --- a/test/hub_kernel/exposes_test.rb +++ b/test/hub_kernel/exposes_test.rb @@ -49,6 +49,12 @@ module Butcher exposes :weigh_cut, takes: %i[cut], writes: false, answer_shape: { "type" => "integer" } end + module Florist + extend HubKernel::Exposes + + exposes :price_of, takes: %i[item], writes: false, earlier_answers: { 1 => { translation: ->(answer) { answer.to_s }, answer_shape: { "type" => "string" } } } + end + test "an exposed method is found by its name with the values it takes" do assert_equal %i[supplier_id bought_on lines], Supplies.exposed("record_purchase").takes end @@ -89,6 +95,10 @@ module Butcher assert_equal({ "type" => "integer" }, Butcher.exposed("weigh_cut").answer_shape) end + test "an earlier answer is given the JSON Schema of the answer at its major beside its translation" do + assert_equal({ "type" => "string" }, Florist.exposed("price_of").earlier_answer(1).answer_shape) + end + test "a hub that declares an exposed method is recorded as a hub that exposes methods" do assert_includes HubKernel::Interface::ExposingHubs.list, Supplies end From bc139ccfd01acfbba15dd765793e53ee090cc830 Mon Sep 17 00:00:00 2001 From: tylercschneider Date: Thu, 29 Oct 2026 14:19:55 -0400 Subject: [PATCH 5/5] Translate an earlier answer given beside its answer shape with its own translation Co-Authored-By: Claude Opus 5.5 --- test/hub_kernel/exposes_test.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/hub_kernel/exposes_test.rb b/test/hub_kernel/exposes_test.rb index 45bc49b..ef761d6 100644 --- a/test/hub_kernel/exposes_test.rb +++ b/test/hub_kernel/exposes_test.rb @@ -99,6 +99,10 @@ module Florist assert_equal({ "type" => "string" }, Florist.exposed("price_of").earlier_answer(1).answer_shape) end + test "an earlier answer given a translation beside its answer shape translates the answer of the major after it" do + assert_equal "300", Florist.exposed("price_of").earlier_answer(1).translate(300) + end + test "a hub that declares an exposed method is recorded as a hub that exposes methods" do assert_includes HubKernel::Interface::ExposingHubs.list, Supplies end