Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]
Added
- GitHub Pages — P2 scaffolding (Jekyll + just-the-docs): stood up
the parallel docs site per
docs/v1/08-github-pages.md. Ships_config.yml(just-the-docs theme, Lunr search, dark-mode toggle,gh_edit_link,jekyll-seo-tag+jekyll-relative-linksplugins), an optional:docsBundler group inGemfilepinningjekyll ~> 4.3,just-the-docs ~> 0.10, andjekyll-relative-links ~> 0.7, the.github/workflows/docs.ymlworkflow (PR buildsbundle exec jekyll build --strict_front_matter; pushes tomaindeploy viaactions/deploy-pages@v4),docs:install/docs:build/docs:serveRake tasks driving the Jekyll toolchain, per-page front matter on every site page (Home, Getting started, Guides + 5 sub-pages, API reference, Deprecations, Examples- 7 sub-pages, Roadmap, Changelog), and a new
docs/guides/index.mdlanding for the Guides section. The site builds locally withrake docs:buildand deploys tohttps://ramongr.github.io/assistant/on every push tomain. Pages source =GitHub Actionswas enabled in repo settings after the original mkdocs PR (#177) merged; no further manual step is needed for the Jekyll cut-over.
- 7 sub-pages, Roadmap, Changelog), and a new
This entry supersedes the original mkdocs-based P2 scaffolding —
the mkdocs stack lived for one PR before being replaced with Jekyll
to match the gem's primary toolchain, drop the Python build
dependency, and avoid the Pygments==2.19.1 pin that worked around
a 2.20.0 HtmlFormatter regression.
[1.0.0.rc1] - 2026-06-15
Added
D2 (follow-up): four user-facing guides under
docs/guides/—inputs.md,validation.md,logging-and-results.md,composing-services.md. Each guide is mirrored by atest/docs/<guide>_examples_test.rbintegration test so the runnable examples can't silently drift from the actual behaviour.inputs.mdincludes the "Usingbin/assistant-rbsfor Steep users" subsection that closes the R1 user-facing-note item indocs/v1/05-quality-and-tooling.md..yardoptsextra-files list extended to include the four new pages so they ship with the rendered YARD output.bin/ smoke: new
bin-smokejob in.github/workflows/ci.ymlexercisesbin/setupagainst a cold bundle, syntax-checks the three developer scripts (bash -n bin/setup,ruby -c bin/{console,version}), runsbin/version --help, and pipes a short ruby snippet throughbin/consoleto confirmAssistant::VERSIONresolves. Closes thebin/smoke item indocs/v1/05-quality-and-tooling.md.CONTRIBUTING.mdgains abin/ developer scriptssection documenting each script's purpose and noting that none of the three ship in the packaged gem (onlyexe/assistant-rbsdoes).
Changed
- Release prep: gemspec polished for the 1.0 cut.
spec.summaryrewritten to match the README elevator pitch (Tiny, dependency-free soft-fail service objects for Ruby),spec.descriptionexpanded into a 3-sentence heredoc covering soft-fail semantics, the uniform result shape, the RBS / Steep posture, and the zero-runtime-deps guarantee. Addedspec.metadata['documentation_uri'](https://rubydoc.info/gems/assistant) andspec.metadata['bug_tracker_uri'](https://github.com/ramongr/assistant/issues). Thespec.filesglob now excludesexamples/,docs/v1/, anddocs/v1.x/from the packaged gem so internal planning material and runnable samples no longer ship to RubyGems (Q9 decision indocs/v1/07-risks-and-open-questions.md). No behaviour change;Assistant::VERSIONis unchanged.
Changed (Breaking)
- M12:
LogList#merge_logsand every internalAssistant::InputBuilderhelper now take their name / list parameter as a keyword argument (logs:/name:/names:) instead of a leading positional. The two public DSL entry pointsService.inputandService.inputsare deliberately exempt —input :foo, type: Xreads better as a class-body declaration thaninput name: :foo, type: X, so their leading positionalattr_name/attr_namesstays. Hard break for the rest, no runtime shim:Service.input(:foo, type: String)— unchangedService.inputs(%i[a b], type: Integer)— unchangedhost.merge_logs(other.logs)→host.merge_logs(logs: other.logs)The old positionalmerge_logsraisesArgumentErrorat call time ("wrong number of arguments ... required keyword: logs"). For users who don't compose log lists directly (i.e. who only useService#call_servicefor service composition), no source change is required. Migration is mechanical andgit grep-able; seedocs/v1/06-migration-0x-to-1.md. The full helper sweep also touches the M13-split per-concern modules:process_default_option,validate_default!,warn_on_mutable_default,process_optional_option,validate_optional!,register_input_definition,input_getter_meth,input_checker_meth,input_type_validator_meth,type_validator_body,type_mismatch_message_builder,input_require_validator_meth,input_require_conditional_meth, and the two privateRequireValidator#define_required_(conditional_)?validatorhelpers are all keyword-only. Internal-onlyService#input_supplied?keeps its positional shape (private, not part of the documented surface). RBS signatures acrosslib/assistant/input_builder/*.rbs(other thandsl.rbs) andlib/assistant/log_list.rbsupdated to match.
Added
- D2 (entry pages): shipped
docs/getting-started.mdanddocs/api-reference.md.docs/getting-started.mdwalks fromgem installto a workingCreateUserservice across three runs (one:ok, one:with_warnings, one:with_errors) and links out to the four follow-up guides.docs/api-reference.mdis the hand-written, curated reference for every Frozen symbol onAssistant,Assistant::Service,Assistant::LogItem,Assistant::LogList, the execute callbacks,#call_service, the notifier,#input_snapshot, and theassistant-rbsCLI;docs/v1/01-api-surface.mdremains the source of truth for stability labels.README.mddocumentation index and the.yardoptsextra-files list now include both new pages. The four topic guides (inputs.md,validation.md,logging-and-results.md,composing-services.md) ship in a follow-up D2 PR alongsidetest/docs/example tests. - D3: every public Frozen symbol enumerated in
docs/v1/01-api-surface.mdnow carries YARD documentation (@param,@return,@raise,@examplewhere meaningful). Internal helpers are documented too, sobundle exec yard stats --list-undocreports 100% documented public methods (52 / 52, plus 7 / 7 attributes and 9 / 9 constants). Shipped together with a top-level.yardopts(markdown markup,lib/**/*.rbas the source, README + repo-hygiene files as extra files), the newyarddevelopment dependency inassistant.gemspec, and arake yardtask that builds the site intodoc/and exits non-zero if coverage drops below 100%.rake cinow runstest + rubocop + steep + yard. - D4: shipped the repository-hygiene files called for in
docs/v1/03-documentation.md. NewCONTRIBUTING.mddocuments the clone /bin/setupflow, the local pipeline (rake test,rubocop,steep check,rake ci), branch naming, commit-tag conventions, and PR template expectations. NewSECURITY.mddeclares 1.x as the supported line, 0.x as EOL on the1.0.0release, givescerberus.ramon@gmail.comas the private report channel, and commits to a 7-day first-response / 30-day-fix-or-mitigation-plan SLA. New.github/PULL_REQUEST_TEMPLATE.mdenforces theScope / What ships / Verification / Out of scopebody shape and theCHANGELOG entry / tests added / docs updated / rake ci is greenchecklist on every pull request. D1: rewrote the top-level
README.md. Replaced the bundler-templateTODO:placeholders and[USERNAME]/assistantURLs with an elevator pitch, status badges (CI, gem version, downloads, Ruby version, license),bundle add/gem installinstructions, a runnable 60-secondCreateUserexample covering required inputs, defaults,allow_nil:,validate, and thelog_item_warning/log_item_errorshorthands, a "why another service-object gem?" comparison against Interactor and dry-transaction, a documentation index pointing atdocs/v1/01-api-surface.md, the migration guide, deprecations, examples, the changelog, and the roadmap, plus a refreshed Development section listingrake test,rubocop, andsteep check. (D1, v1 plan)Assistant::Service#input_snapshot— returns a frozenDatainstance whose members are the declared input names (viaService.input/Service.inputs), in declaration order, with values read from@inputsafterapply_input_defaultshas run. The snapshot therefore reflects post-default:and post-allow_nil:values, matching what the per-input getters expose. Only declared inputs appear; extra keyword arguments accepted by#initialize(which live in@inputsbut have noinput :foodeclaration) are intentionally excluded so the snapshot's shape mirrors the public DSL. A declared input with no default and no caller-supplied value surfaces asnil. The returnedDatais structurally immutable (no member reassignment); member values that are themselves mutable (e.g. anArray) keep their normal mutability — the snapshot does not deep-freeze. Each call returns a freshDatainstance backed by a per-subclassDataclass memoised onService.input_snapshot_class(rebuilt transparently if the subclass declares more inputs after the first snapshot call). Useful for passing a read-only view of inputs to helpers, collaborators, or test assertions without exposing the mutable@inputshash.Assistant::Service#call_service(klass, **inputs)— instance-level helper for composing services. Constructs an instance ofklass(asserted to be anAssistant::Servicesubclass; raisesArgumentErrorotherwise), invokesinner.run, merges the inner service's full log timeline (info + warning + error) onto the outer service viamerge_logs, and returns the inner instance. BecauseService#errors/#warnings/#statusare derived by filtering@logs, inner errors automatically downgrade the outer terminal status to:with_errorsand inner warnings surface as:with_warnings(when no errors are present), without any branching in the caller. Exceptions raised by the inner service's#executeor byAssistant.notifierare not rescued; they propagate to the caller, matching the baseService#runcontract. The inner service fires its own:service_started/:service_validated/:service_executed/:service_failedevents independently of the outer lifecycle. (M-S2, v1 plan)before_execute,after_execute { |result| }, andaround_execute { |&blk| ... }class-level DSL onAssistant::Servicefor wrapping#executewith reusable hooks. Hooks areinstance_exec'd on the service (soselfis the service instance) and execute after validation in declaration order; the first-declaredaround_executeis the outermost layer. Hooks are inherited at subclass-definition time via an array snapshot — later additions on the parent do not bleed into existing subclasses. Errors raised inside any hook are caught, never propagate out of#run, and are logged viaadd_log(level: :error, source: :hook, detail: <hook_type>, message: "<ErrorClass>: <message>", trace: backtrace). A hook-logged error downgrades the terminal lifecycle event to:service_failedand the run payload to{ errors:, result: nil, status: :with_errors }; the actual execute return value remains accessible viaservice.result. (M-S1, v1 plan)Assistant.notifierandAssistant.notifier=— module-level configuration accessor for an instrumentation callable. The default notifier is a frozen no-op lambda (Assistant::DEFAULT_NOTIFIER); the setter accepts any object responding to#call(event, payload)ornilto reset to the default. Passing anything else raisesArgumentErrorimmediately.Service#runnow fires four frozen events around its lifecycle::service_startedat entry,:service_validatedaftervalidate_inputs+validate, and exactly one of:service_executed(no logged errors) or:service_failed(errors present) before returning. Every payload carries{ service_class:, duration_s: };duration_sis aFloatmeasured againstProcess::CLOCK_MONOTONICfrom the start of#run. Notifier exceptions (StandardError) are caught and surfaced viaKernel.warn; subsequent events still fire. (M-S3, v1 plan)bin/assistant-rbs(shipped asexe/assistant-rbs) — a CLI that loads user-supplied Ruby paths and emits an.rbsfile perAssistant::Servicesubclass into a configurable output directory (defaultsig/). Each generated file declares the per-input getter (def <name>: () -> Type) and predicate (def <name>?: () -> bool) pairs derived fromService.input_definitions, including multi-type unions ((A | B)) andallow_nil:((A | B)?). Output is marked with a header sentinel and is idempotent: rerunning leaves unchanged files alone ([unchanged]) and refuses to overwrite hand-written.rbsfiles that lack the sentinel ([skipped]). Namespaced classes are emitted with nestedmoduledeclarations so the generated file is self-contained. Use--output DIR,--quiet, and--help. The generator only emits sigs forServicesubclasses introduced by the paths it was asked to load (snapshot diff viaObjectSpace). Anexamples/greeter.rb+ generatedsig/examples/greeter.rbsfixture is type-checked by Steep as the acceptance test. The CLI itself is Experimental; the generated.rbscontent tracks the FrozenService.inputsurface. (M11, v1 plan)Hand-written RBS signatures for the frozen public surface defined in
docs/v1/01-api-surface.md:Assistant::VERSION,Assistant::LogItem,Assistant::LogList,Assistant::Service(excluding the per-input methods generated byService.input),Assistant::InputBuilderplus itsRegistry,DefaultOption,OptionalOption,Accessors,RequireValidator,TypeValidator, andDslsubmodules, and a namespace shim forAssistant::Refinements::StringBlankness. Files live alongside the Ruby source aslib/**/*.rbsand ship with the gem (already covered bygit ls-files). ASteepfileadds a:libtarget type-checked by Steep in CI;steep checkruns against the subset of files that do not rely on Ruby refinements ordefine_method. The per-input surface generated byService.inputis documented in the RBS comments and will be emitted bybin/assistant-rbs(M11). Addssteepas a development dependency and asteepjob to.github/workflows/ci.yml. (M8, v1 plan)Assistant::Service.inputnow accepts adefault:option. The provider may be a literal value or a zero-arityProc/Lambda; anything else that responds to#call(e.g. aMethodobject) is rejected withArgumentErrorat class-definition time. Procs are invoked once perServiceinstance, with no arguments. A default fires when the input key is absent, or when the value is an explicitniland the input is not declaredallow_nil: true— withallow_nil: true, an explicitnilfrom the caller is honoured and the default is skipped. Defaulted values are subject to the same type,required:, andif:validation as caller-supplied values. Mutable literal defaults (unfrozenArray/Hash) emit aKernel.warnat class-definition time, since they are shared across every instance of theServicesubclass. (M1, v1 plan)Assistant::Service.input_definitions— per-subclass hash exposing the originalinputdeclaration options (including:default) for introspection. Experimental; subject to change before 1.0.0.Assistant::Service.inputnow acceptsallow_nil: true. When set, any supplied value for that key short-circuits bothvalid_type_<name>?andvalid_require_<name>?— i.e.nilis accepted, and type-checking is effectively disabled for the input. Whenallow_nil:is omitted (default), behaviour is unchanged from 0.1.0 — an absent ornilvalue silently passes type checks, and anilon arequired:input is still treated as missing. (M2, v1 plan)Assistant::Service.inputnow accepts an array fortype:, e.g.input :amount, type: [Integer, Float]. The generatedvalid_type_<name>?validator passes when the input matches any of the listed types. Single-type declarations keep the original"… is not a X but Y"error message; multi-type produces"… is not one of [A, B] but Y". (M3, v1 plan)Assistant::Service#logspublic reader exposing the full log timeline (info + warning + error) in insertion order. Callers no longer need to reach into@logsviainstance_variable_get. (M4, v1 plan)Assistant::LogList#log_item_info,#log_item_warning, and#log_item_errorshorthands. These wrapadd_log(level: ..., …)so service authors stop hand-rolling the level keyword on every call. (M5, v1 plan)Assistant::Service.inputnow accepts anoptional:flag.optional: trueis explicit sugar for the default behaviour (norequired:validator is generated);optional: falseis equivalent torequired: true. Declaringrequired: trueandoptional: truetogether raisesArgumentErrorat class-definition time, as does a non-booleanoptional:value. The flag is retained inService.input_definitionsfor introspection and composes withdefault:(M1) andallow_nil:(M2) without surprises. (M7, v1 plan)
Changed
Assistant::LogItem.newnow raisesArgumentErrorwhen constructed with invalid attributes instead of returning an invalid object. Validation runs at the end of initialization and reports every failing attribute in one message (level, source, detail, message). The#valid?predicate family remains for introspection and returnstruefor normally constructed instances.LogList#add_lognow inherits this fail-fast behaviour because it constructsLogIteminternally. (M10, v1 plan)Assistant::InputBuildersplit into per-concern submodules underlib/assistant/input_builder/(Registry,DefaultOption,OptionalOption,Accessors,RequireValidator,TypeValidator,Dsl). The umbrellaAssistant::InputBuilderincludes each submodule; the public surface (ServiceextendsAssistant::InputBuilder) is unchanged. Theusing Assistant::Refinements::StringBlanknessrefinement now activates only inside theAccessorssubmodule. Tests mirror the lib layout undertest/assistant/input_builder/. Removes the temporaryMetrics/ModuleLength: Max: 150override from.rubocop.yml. (M13, v1 plan)- For each
input :name, required: truedeclaration,Servicesubclasses now generate#valid_required_<name>?as the canonical requirement validator (and#valid_required_conditional_<name>?whenif:is also given). The pre-existing#valid_require_<name>?/#valid_require_conditional_<name>?predicates remain as deprecated aliases — they delegate to the canonical method and emit aKernel.warnonce per textual call site pointing at the canonical replacement.Service#validate_inputsinvokes only the canonical names, so internal framework code never triggers the deprecation warning. Seedocs/deprecations.md. (M9, v1 plan) lib/assistant.rbnow requires every core building block explicitly in dependency order (version,log_item,log_list,refinements/string_blankness,input_builder,service). After a barerequire "assistant",Assistant::LogList,Assistant::InputBuilder, andAssistant::Refinements::StringBlanknessare reachable without first loadingAssistant::Service. (M6, v1 plan)
Deprecated
Assistant::Service#valid_require_<name>?(use#valid_required_<name>?instead). Scheduled for removal inassistant 2.0. (M9, v1 plan)Assistant::Service#valid_require_conditional_<name>?(use#valid_required_conditional_<name>?instead). Scheduled for removal inassistant 2.0. (M9, v1 plan)
Migration
1.0.0 is a stabilisation release. Three small breaking changes have
to be addressed; every one is mechanical and git grep-able. The full
recipe lives in
docs/v1/06-migration-0x-to-1.md.
LogList#merge_logsis keyword-only (M12, B3) — rewrite everymerge_logs(other.logs)call site tomerge_logs(logs: other.logs). The two public DSL entry pointsService.inputandService.inputskeep their leading positionalattr_name/attr_names; onlymerge_logsand the internalInputBuilderhelpers changed.LogItem.newraises on invalid attrs (M10, B1) — audit any directLogItem.new(...)call sites. The gem's own call sites are already correct; fixtures that exercised the old "constructs butvalid? == false" path need updating. Prefer theadd_log/log_item_*helpers in regular code.valid_require_*?is deprecated (M9, B2) — rename direct calls to the newvalid_required_*?form. Users who don't call these predicates directly (driven internally byvalidate_inputs) need no source change; the old name still works in 1.x with a one-timeKernel.warnper call site, and is removed in 2.0.
Pin to ~> 1.0 in your Gemfile once the upgrade lands.
[0.1.0] - 2026-05-07
Added
LogList#log_item_error_initializehelper, used byInputBuilder-generated validators (previously redefined on everyinputdeclaration).- GitHub Actions CI workflow (
.github/workflows/ci.yml) running Minitest and RuboCop. - GitHub Actions release workflow (
.github/workflows/release.yml) using RubyGems trusted publishing (OIDC) onv*.*.*tags. - Direct test coverage for
LogList#warnings,#errors,#merge_logs,Service#success?,#failure?,#status,#resultmemoization, conditional requirement behavior, theinputs(...)plural DSL form, andLogItem#trace/#item.
Changed
- Standardized on Ruby 3.4 (
.ruby-version, gemspecrequired_ruby_version, RuboCopTargetRubyVersion). InputBuilderno longer requiresactive_support; the previous use ofObject#present?is replaced with plain Ruby checks. Whitespace-only strings continue to be treated as missing via a scopedAssistant::Refinements::StringBlanknessrefinement that addsString#whitespace?and is activated insideInputBuilder. The method is intentionally named to avoid colliding with ActiveSupport'sString#blank?.assistant.gemspecchangelog_urinow points atCHANGELOG.mdinstead ofCODE_OF_CONDUCT.md.- Migrated the test suite from RSpec to Minitest (
test/**/*_test.rb), exposed viarake test(the new default rake task). - Replaced the largely-dead RuboCop config (a fork of RuboCop's own internal
config) with a focused configuration for this gem;
rubocop-rspecis replaced withrubocop-minitest.
Removed
- CircleCI configuration (
.circleci/); replaced by GitHub Actions. - Dead
@keys = []instance variable inAssistant::Service#initialize. active_supportandactive_support/core_ext/objectrequires fromlib/assistant/input_builder.rb.- RSpec, FactoryBot, Faker,
rspec-collection_matchers,rspec_junit_formatter,rubocop-faker, andrubocop-rspecdevelopment dependencies; replaced byminitestandrubocop-minitest.
[0.0.2] - 2023-11-27
- Initial public release.