Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion CONFORMANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ v1.0.0). No spec MUST/SHOULD in §4–§16 is unimplemented.
| §6.1 Bearer auth scheme | yes | `lib/arcp/auth/bearer.rb` |
| §6.1 Pluggable AuthScheme | yes | `lib/arcp/auth/auth_scheme.rb` |
| §6.2 Capability negotiation (intersection) | yes | `lib/arcp/session/capability_set.rb#intersect` |
| §6.2 Feature names: heartbeat, ack, list_jobs, subscribe, lease_expires_at, cost.budget, progress, result_chunk, agent_versions | yes | `lib/arcp/session/feature.rb` |
| §6.2 Feature names: heartbeat, ack, list_jobs, subscribe, lease_expires_at, cost.budget, progress, result_chunk, agent_versions, model.use, provisioned_credentials | yes | `lib/arcp/session/feature.rb` |
| §6.3 session.welcome with resume_token + resume_window_sec | yes | `lib/arcp/session/welcome.rb`, `lib/arcp/runtime/session_actor.rb` |
| §6.3 Resume by last_event_seq | yes | `lib/arcp/runtime/event_log.rb` |
| §6.4 session.ping / session.pong heartbeats | yes | `lib/arcp/session/ping.rb`, `lib/arcp/session/pong.rb`, `lib/arcp/client.rb#start_heartbeat!` |
Expand Down Expand Up @@ -51,6 +51,9 @@ v1.0.0). No spec MUST/SHOULD in §4–§16 is unimplemented.
| §9.6 cost.budget capability (BigDecimal per currency) | yes | `lib/arcp/lease.rb#CostBudget` |
| §9.6 BudgetCounter try_decrement | yes | `lib/arcp/lease.rb#BudgetCounter` |
| §9.6 BUDGET_EXHAUSTED on overspend | yes | `lib/arcp/runtime/lease_manager.rb` |
| §9.7 model.use capability and lease checks | yes | `lib/arcp/lease.rb`, `lib/arcp/runtime/lease_manager.rb#check_model!` |
| §9.8 provisioned credential wire shape | yes | `lib/arcp/credential.rb`, `lib/arcp/job/accepted.rb` |
| §9.8 credential provisioner and revocation lifecycle | yes | `lib/arcp/credential_provisioner.rb`, `lib/arcp/runtime/credential_registry.rb`, `lib/arcp/runtime/job_manager.rb` |
| §10 Delegate event kind with child lease | yes | `lib/arcp/job/event_body/delegate.rb` |
| §10 LEASE_SUBSET_VIOLATION on excess | yes | `lib/arcp/lease.rb#Subsetting.bound` |
| §11 trace_id propagation on envelope | yes | `lib/arcp/envelope.rb`, `lib/arcp/client.rb#send_envelope` |
Expand Down
349 changes: 349 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,349 @@
# Contributing

## Development setup

```
git clone https://github.com/agentruntimecontrolprotocol/ruby-sdk
cd ruby-sdk
bundle install
bundle exec rake spec # run tests
bundle exec rubocop # lint
bundle exec steep check # type-check
```

## Pull requests

- One logical change per PR.
- All tests pass on the minimum and maximum supported Ruby versions.
- RuboCop exits 0.
- `CHANGELOG.md` updated. Breaking changes bump the major version and are
flagged prominently.

---

# Idiomatic Ruby Style Guide

Authoritative style for this gem. Optimized for readability, predictable
public API surface, and Claude Code consumption.

When this guide conflicts with personal taste, this guide wins. When it
conflicts with RuboCop defaults, this guide wins and the cop gets configured.

---

## Hard Limits (Non-Negotiable)

- Line length: aspire **80**, hard cap **100**.
- Method length: **10 lines** (excluding `def`/`end`).
- Class length: **100 lines** (excluding comments).
- Module length: **100 lines**.
- File length: **200 lines**.
- Cyclomatic complexity: **6** per method.
- Perceived complexity: **7** per method.
- ABC size: **17** per method.
- Block nesting: **3** levels.
- Method parameters: **4** (use keyword args or a `Data`/`Struct` beyond).
- Module nesting: **3** levels deep.

When a limit is breached, **refactor — do not configure the linter to allow
exceptions**. Excess size is a design smell, not a formatting problem.

---

## File Organization

- One class or module per file. No exceptions for "small helpers".
- Filename mirrors the constant: `MyGem::HTTPClient` lives at
`lib/my_gem/http_client.rb`.
- `lib/my_gem.rb` is the entry point and does nothing but `require` and
define the top-level module.
- `lib/my_gem/version.rb` holds `VERSION = "x.y.z"` and nothing else.
- Use Zeitwerk for autoloading in any non-trivial gem.
- Group by domain, not by type. Prefer `lib/my_gem/billing/invoice.rb` over
`lib/my_gem/models/invoice.rb`. Domain folders scale; type folders don't.

## Magic Comments

Every `.rb` file starts with:

```ruby
# frozen_string_literal: true
```

No exceptions. Add it to generators, templates, and rake tasks too.

---

## Naming

- `snake_case` for methods, variables, and files.
- `CamelCase` for classes and modules.
- `SCREAMING_SNAKE_CASE` for constants.
- Predicate methods end with `?` and return strict booleans where possible.
- Bang methods (`!`) signal mutation, danger, or raise-on-failure. Always
pair with a non-bang version unless raising is the only sensible behavior.
- Avoid `get_` and `set_` prefixes. Use attribute accessors.
- Spell names out. Abbreviate only when the abbreviation is more recognizable
than the word (`url`, `http`, `id`, `db`).
- Boolean attributes read as questions: `active?`, not `is_active`.
- Collection variables are plural: `users`, not `user_list`.

---

## Module & Class Design

- Wrap every public symbol in your top-level gem module.
- Prefer composition over inheritance. Inherit at most one level deep unless
modeling a genuine is-a hierarchy.
- Use `Module` for namespacing and stateless helpers. Use `Class` for objects
that carry state.
- Mix in `Comparable` / `Enumerable` instead of reimplementing their
contracts.
- Freeze public constants: `DEFAULT_TIMEOUT = 30` then `.freeze` mutable
literals (arrays, hashes, strings without the magic comment).
- For value objects, prefer `Data.define` (Ruby 3.2+) or `Struct` over
hand-rolled classes with `attr_reader` boilerplate.
- A class with only class methods is a module. Convert it.

---

## Method Design

- One method, one responsibility. If the name needs "and", split it.
- Required positional args come first, then keyword args.
- Use keyword arguments when a method has 3+ parameters, **or** any boolean
parameter (positional booleans are always wrong).
- Return meaningful values. Avoid returning `self` unless chaining is part
of the documented public API.
- No side effects in predicate methods.
- Methods that can fail in expected ways either return a result object or
raise a domain-specific error. Do not return `nil` ambiguously.

```ruby
# Good
def find_user(id:)
repo.fetch(id) or raise NotFoundError, "User #{id} not found"
end

# Bad
def find_user(id)
repo.fetch(id) # returns nil on miss — caller has to guess
end
```

---

## Error Handling

Define an error hierarchy rooted at one class so consumers can rescue a
single type:

```ruby
module MyGem
class Error < StandardError; end
class ConfigurationError < Error; end
class APIError < Error; end
class RateLimitError < APIError; end
class NotFoundError < APIError; end
end
```

- Every gem-raised error inherits from `MyGem::Error`.
- Never `rescue Exception`. Never `rescue` bare.
- Rescue the narrowest class that handles the case.
- Error messages include actionable context: IDs, URLs, expected vs got.
- Do not swallow errors silently. If suppression is required, log via a
configurable logger.
- Do not raise from initializers unless construction is genuinely
impossible.

---

## Public API Discipline

- Mark every method `public`, `private`, or `protected` explicitly in any
class that has non-public methods.
- Tag internal-but-reachable methods with `# @api private` (YARD).
- Public constants are frozen and documented.
- Keep the public surface small. Each new public method is a permanent
maintenance commitment.
- Do not monkey-patch core classes from a published gem. Use refinements
only when unavoidable, scoped to the smallest file possible.
- Never modify `Object`, `Kernel`, `Class`, or `Module`.

---

## Configuration

Single block-based entry point:

```ruby
MyGem.configure do |c|
c.api_key = ENV.fetch("MY_GEM_KEY")
c.timeout = 10
end
```

- Validate at configure time. Fail loudly on missing required keys.
- Freeze the config object after the block returns.
- Provide sensible defaults for every optional setting.
- Expose `MyGem.configuration` as a frozen reader, never a writer.

---

## Dependencies

- Minimize runtime dependencies. Each one constrains downstream users.
- Pin minimum versions (`~> 2.0`). Never pin maximum versions unless a known
break exists.
- Lazy-require optional dependencies inside the method that uses them and
raise a clear error if missing.
- Development dependencies go in the Gemfile, not the gemspec.

---

## Idioms to Prefer

- `Array(value)` — nil-safe wrap.
- `Hash#dig` — nested access without nil checks.
- `Object#then` / `yield_self` — readable transformations.
- `Object#tap` — side effects mid-chain.
- Safe navigation `&.` — one level only. Chains of `&.` hide design
problems.
- Pattern matching (`case/in`) for structural conditionals on Ruby 3+.
- `Set` over `Array#include?` for membership when the collection grows past
~10 elements.
- `String#<<` over `+=` in loops.
- Heredocs with `<<~` (squiggly) for multiline strings.
- `each_with_object` over `inject` when accumulating into a mutable
collection.
- Memoize with `@x ||= compute` **only when** the value cannot legitimately
be `nil` or `false`. Otherwise use
`defined?(@x) ? @x : (@x = compute)`.

---

## Anti-Patterns (Forbidden)

- Class variables (`@@var`). Use class instance variables or a registry.
- Global variables (`$var`) outside genuine globals (`$stdout`, `$stderr`).
- `method_missing` without a paired `respond_to_missing?`.
- `eval`, `class_eval` with strings, `instance_eval` with strings.
- `rescue Exception` or bare `rescue`.
- Rescuing in initializers.
- `alias_method_chain`-style wrapping. Use `Module#prepend`.
- Monkey-patching core classes from a published gem.
- Long parameter lists hidden as `**opts` with no documentation.
- Returning different shapes from the same method (`String` or `nil` or
`Array`). Pick one return type and stick to it.
- `def self.method` scattered through a class. Group under
`class << self`.

---

## Documentation

- Every public class, module, and method has a YARD docstring.
- Document `@param`, `@return`, `@raise`, and provide at least one
`@example` for non-trivial methods.
- Keep `README.md` runnable: every snippet must execute against the current
version. CI should verify this where practical.
- Maintain `CHANGELOG.md` following the Keep a Changelog format.
- Document breaking changes prominently and bump major versions.

---

## Testing

- One test framework per gem. RSpec or Minitest, not both.
- Test the public API exclusively. Private methods are tested through their
callers.
- One logical assertion per test where practical. Group with
`aggregate_failures` (RSpec) when assertions are about one outcome.
- Stub external HTTP with WebMock or VCR.
- Run tests under the lowest and highest supported Ruby versions in CI.
- No `sleep` in tests. Use proper synchronization or time travel.

---

## RuboCop Baseline

Ship `.rubocop.yml` with:

```yaml
AllCops:
NewCops: enable
TargetRubyVersion: 3.1
SuggestExtensions: false

Layout/LineLength:
Max: 100

Metrics/MethodLength:
Max: 10
Metrics/ClassLength:
Max: 100
Metrics/ModuleLength:
Max: 100
Metrics/BlockLength:
Max: 15
Metrics/AbcSize:
Max: 17
Metrics/CyclomaticComplexity:
Max: 6
Metrics/PerceivedComplexity:
Max: 7
Metrics/ParameterLists:
Max: 4
CountKeywordArgs: false

Style/Documentation:
Enabled: true
Style/FrozenStringLiteralComment:
EnforcedStyle: always
```

Treat violations as build failures. Refactor first. Disable a cop only with
an inline comment justifying the exception.

---

## Reducing Complexity (Refactor Patterns)

When a method exceeds limits, apply these in order:

1. **Extract Method.** Pull cohesive lines into a named private method.
2. **Replace Conditional with Polymorphism.** A long `case` on a type
becomes classes implementing a shared interface.
3. **Introduce Parameter Object.** Group related params into `Data` or
`Struct`.
4. **Replace Temp with Query.** Turn intermediate variables into methods.
5. **Decompose Conditional.** Extract the predicate AND each branch into
named methods.
6. **Move Method.** If a method uses another object's data more than its
own, it belongs there.
7. **Replace Loop with Pipeline.** Chain `map` / `select` / `reduce`
instead of stateful loops.
8. **Guard Clauses.** Replace nested `if` with early returns.

If a class breaches 100 lines, look for a second class trying to escape.
Most overlong classes are hiding a collaborator. Names that signal this:
`*Manager`, `*Handler`, `*Processor`, `*Helper`, `*Utils`.

If a file breaches 200 lines, the class inside has already breached the
class limit. Fix the class, the file follows.

---

## Quick Checklist Before Merge

- [ ] `# frozen_string_literal: true` on every file.
- [ ] No file > 200 lines, no class > 100, no method > 10.
- [ ] No line > 100 chars; most lines ≤ 80.
- [ ] Every public method has a YARD docstring.
- [ ] Every gem-raised error inherits from `Arcp::Error`.
- [ ] No `@@`, no bare `rescue`, no `rescue Exception`.
- [ ] No monkey patches of core classes.
- [ ] RuboCop exits 0.
- [ ] All tests pass on min and max supported Ruby.
- [ ] CHANGELOG updated. Breaking changes flagged.
Loading
Loading