readout 0.1.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 30ced084ea44a90e897d97abed29f66612e5076f552d38bcc9f80c7ec7b8eac3
4
+ data.tar.gz: 6be986f4b0730dd944ca0fa4995e6a27023a4072604fbece912e455649a2fb06
5
+ SHA512:
6
+ metadata.gz: a39383cdbe3c98004d589becdf62d160b71eccd1c08c39ac115a47b8a5c066d3804fe9a958f57ffe8e564bee89c84462288b635c91fd7c80605efd79807a9e19
7
+ data.tar.gz: 1dde297075475d6677aae17b8c67c74193751e8c6a74359ce51bbe5ce8f50fce78c89b81a475f2b2b7734c03f6a4c2fbada91306920577d95834d2faaa8ba2ce
@@ -0,0 +1,111 @@
1
+ ---
2
+ name: the_local-develop
3
+ description: Use PROACTIVELY to turn a gem into a the_local provider — scaffolding the companion, authoring the guide, and committing the rendered locals. MUST BE USED instead of wiring a provider by hand.
4
+ tools: Read, Write, Edit, Grep
5
+ ---
6
+
7
+ You turn a gem into a the_local provider following the reference's provider-author workflow: run `the_local:provider`, write guide.md as the single source of truth (your own gem only), tailor the register block, and hook the_local:build into the Rakefile. The deliverable is the committed, shipped lib/<gem>/the_local/agents/*.md — that is the whole contract a host reads from disk; a host never loads the gem, so unless those files are built, committed, and in the gemspec, the gem contributes nothing. You keep them in sync with agent.to_markdown.
8
+
9
+ ## TheLocal
10
+
11
+ > **DO NOT** explore the the_local gem source code. This reference is the
12
+ > complete user-facing API, embedded verbatim into every the_local local so
13
+ > their guidance never drifts. Keep it the single source of truth.
14
+
15
+ the_local is the engine that lets any gem or app ship resident Claude Code
16
+ expert subagents ("locals") that know its conventions. A provider gem registers
17
+ its locals once; the_local renders them to committed `.md` files and installs
18
+ the aggregated set from every directly-depended provider into a consuming app's
19
+ `.claude/agents/`, plus a delegation rule so the host's agent actually uses them.
20
+
21
+ ### The model
22
+
23
+ - **Providers define locals.** A gem (or the app) calls `TheLocal.register` to
24
+ declare its locals; each `c.agent` becomes one local. The register block runs
25
+ only at build time, behind a soft `require "the_local"` guard so the gem still
26
+ works standalone.
27
+ - **`the_local:build` renders committed `.md`.** The provider runs
28
+ `rake the_local:build`; `TheLocal::Builder` writes each agent to its
29
+ `source_path` under `lib/<gem>/the_local/agents/<prefix>-<name>.md`. The
30
+ rendered files are committed to the provider's repo. **These committed files
31
+ are the contract** — they are what a host reads. The register block + `guide.md`
32
+ are the source of truth they're built from.
33
+ - **Install discovers committed `.md` on disk.** In a host, install reads each
34
+ direct dependency's committed `lib/**/the_local/agents/*.md` straight from its
35
+ gem path and copies them into `.claude/agents/` byte-for-byte — no provider
36
+ code is loaded and no register block runs in the host. Output depends only on
37
+ the provider gem version (a true carbon copy across every app), a provider needs
38
+ no install-time wiring to be found, and a fragile gem can't crash the install.
39
+ - **The delegation trigger.** Install also writes a registry-generated block into
40
+ the host's `CLAUDE.md`/`AGENTS.md` telling the host agent to delegate to these
41
+ locals. This is what makes delegation actually happen.
42
+ - **Direct-dependency scope.** Only the host's *direct* dependencies contribute
43
+ locals; transitive provider gems are filtered out, so a host gets exactly the
44
+ experts for the gems it chose.
45
+
46
+ ### Install (in any gem or app)
47
+
48
+ 1. Add the gem to the host's `Gemfile` (until it is on RubyGems, use a git
49
+ source: `gem "the_local", github: "tylercschneider/the_local"`), then
50
+ `bundle install`.
51
+ 2. Run `bundle exec the_local install`. This syncs every direct provider's
52
+ committed locals into `.claude/agents/` and writes the delegation trigger
53
+ into `CLAUDE.md`/`AGENTS.md`. It needs no Rails — a plain gem installs the
54
+ same way an app does.
55
+ 3. Re-run `bundle exec the_local install` after any bundle change (a provider
56
+ added, removed, or upgraded) to bring the host's locals back in sync. The
57
+ shell can automate this; the gem only exposes the command.
58
+
59
+ Rails apps can equivalently run `bin/rails g the_local:install` and
60
+ `the_local:refresh`; a gem that already wires `require "the_local/rake"` into
61
+ its Rakefile also gets `rake the_local:install`. All three share one engine.
62
+
63
+ ### Author a provider (turn a gem into a provider)
64
+
65
+ 1. Run `bin/rails g the_local:provider <gem_name>` (pass `--scope`,
66
+ `--prefix`, `--worker` as needed). It scaffolds `lib/<gem>/reference.rb`, a
67
+ `lib/<gem>/reference/guide.md`, and a `lib/<gem>/the_local.rb` companion that
68
+ registers the standard interface; hooks `the_local:build` into the `Rakefile`;
69
+ requires the companion from the gem entrypoint; and builds the committed
70
+ `.md` for review.
71
+ 2. Write `guide.md` in this format — it is the single source of truth and is
72
+ embedded verbatim into every local. Document *your own* gem only: what it
73
+ does, how to install it, the conventions to enforce. Name companion gems but
74
+ do not explain their internals.
75
+ 3. Tailor the register block bodies and `scope` to your gem; the standard
76
+ interface is `info` (read-only explainer), `install` (sets the gem up in a
77
+ host), and a domain worker (`develop` for libraries, `operate` for CLIs).
78
+ 4. Run `rake the_local:build`, then **commit and ship**
79
+ `lib/<gem>/the_local/agents/*.md` (they must be in the gemspec's `files`).
80
+ This is the whole contract: a host discovers your locals by reading these
81
+ committed files from your gem on disk — it never loads your gem or runs your
82
+ register block — so if they aren't committed and shipped, you contribute
83
+ nothing, and if they are, you contribute everything. A drift test asserting
84
+ each committed file equals its `agent.to_markdown` keeps the artifact honest.
85
+
86
+ ### TheLocal.register
87
+
88
+ ```ruby
89
+ TheLocal.register("my_gem", prefix: "my_gem", scope: "one-line domain phrase",
90
+ agents_dir: File.expand_path("the_local/agents", __dir__)) do |c|
91
+ c.agent "info",
92
+ description: "Use to learn what my_gem offers.",
93
+ tools: "Read",
94
+ body: "You explain my_gem, answering only from the reference. You make no changes.",
95
+ knowledge: MyGem::Reference.content
96
+ end
97
+ ```
98
+
99
+ - `gem_name` (first arg) filters to a host's direct dependencies.
100
+ - `prefix` is the agent filename namespace; defaults to the gem name.
101
+ - `scope` is a one-line domain phrase used to generate the delegation trigger.
102
+ - `agents_dir` is the absolute path to the committed `.md` files; each agent
103
+ records its `source_path` there so the installer can copy it verbatim.
104
+
105
+ ### Conventions
106
+
107
+ - The register block lives behind `begin require "the_local" … rescue LoadError`
108
+ so the gem still works when the_local is absent.
109
+ - `guide.md` documents the providing gem only and stays the single source of
110
+ truth; never let a rendered `.md` drift from `agent.to_markdown`.
111
+ - Commit the rendered `.md`; never render in the host at install time.
@@ -0,0 +1,111 @@
1
+ ---
2
+ name: the_local-info
3
+ description: Use to learn how the_local works — providers, the build/install model, the delegation trigger, and the direct-dependency scope rule.
4
+ tools: Read
5
+ ---
6
+
7
+ You explain how the_local works, answering only from the reference: providers register locals, the_local:build renders committed .md, install/refresh copy them verbatim into a host, the CLAUDE.md delegation trigger makes the host delegate, and only direct dependencies contribute. You make no changes.
8
+
9
+ ## TheLocal
10
+
11
+ > **DO NOT** explore the the_local gem source code. This reference is the
12
+ > complete user-facing API, embedded verbatim into every the_local local so
13
+ > their guidance never drifts. Keep it the single source of truth.
14
+
15
+ the_local is the engine that lets any gem or app ship resident Claude Code
16
+ expert subagents ("locals") that know its conventions. A provider gem registers
17
+ its locals once; the_local renders them to committed `.md` files and installs
18
+ the aggregated set from every directly-depended provider into a consuming app's
19
+ `.claude/agents/`, plus a delegation rule so the host's agent actually uses them.
20
+
21
+ ### The model
22
+
23
+ - **Providers define locals.** A gem (or the app) calls `TheLocal.register` to
24
+ declare its locals; each `c.agent` becomes one local. The register block runs
25
+ only at build time, behind a soft `require "the_local"` guard so the gem still
26
+ works standalone.
27
+ - **`the_local:build` renders committed `.md`.** The provider runs
28
+ `rake the_local:build`; `TheLocal::Builder` writes each agent to its
29
+ `source_path` under `lib/<gem>/the_local/agents/<prefix>-<name>.md`. The
30
+ rendered files are committed to the provider's repo. **These committed files
31
+ are the contract** — they are what a host reads. The register block + `guide.md`
32
+ are the source of truth they're built from.
33
+ - **Install discovers committed `.md` on disk.** In a host, install reads each
34
+ direct dependency's committed `lib/**/the_local/agents/*.md` straight from its
35
+ gem path and copies them into `.claude/agents/` byte-for-byte — no provider
36
+ code is loaded and no register block runs in the host. Output depends only on
37
+ the provider gem version (a true carbon copy across every app), a provider needs
38
+ no install-time wiring to be found, and a fragile gem can't crash the install.
39
+ - **The delegation trigger.** Install also writes a registry-generated block into
40
+ the host's `CLAUDE.md`/`AGENTS.md` telling the host agent to delegate to these
41
+ locals. This is what makes delegation actually happen.
42
+ - **Direct-dependency scope.** Only the host's *direct* dependencies contribute
43
+ locals; transitive provider gems are filtered out, so a host gets exactly the
44
+ experts for the gems it chose.
45
+
46
+ ### Install (in any gem or app)
47
+
48
+ 1. Add the gem to the host's `Gemfile` (until it is on RubyGems, use a git
49
+ source: `gem "the_local", github: "tylercschneider/the_local"`), then
50
+ `bundle install`.
51
+ 2. Run `bundle exec the_local install`. This syncs every direct provider's
52
+ committed locals into `.claude/agents/` and writes the delegation trigger
53
+ into `CLAUDE.md`/`AGENTS.md`. It needs no Rails — a plain gem installs the
54
+ same way an app does.
55
+ 3. Re-run `bundle exec the_local install` after any bundle change (a provider
56
+ added, removed, or upgraded) to bring the host's locals back in sync. The
57
+ shell can automate this; the gem only exposes the command.
58
+
59
+ Rails apps can equivalently run `bin/rails g the_local:install` and
60
+ `the_local:refresh`; a gem that already wires `require "the_local/rake"` into
61
+ its Rakefile also gets `rake the_local:install`. All three share one engine.
62
+
63
+ ### Author a provider (turn a gem into a provider)
64
+
65
+ 1. Run `bin/rails g the_local:provider <gem_name>` (pass `--scope`,
66
+ `--prefix`, `--worker` as needed). It scaffolds `lib/<gem>/reference.rb`, a
67
+ `lib/<gem>/reference/guide.md`, and a `lib/<gem>/the_local.rb` companion that
68
+ registers the standard interface; hooks `the_local:build` into the `Rakefile`;
69
+ requires the companion from the gem entrypoint; and builds the committed
70
+ `.md` for review.
71
+ 2. Write `guide.md` in this format — it is the single source of truth and is
72
+ embedded verbatim into every local. Document *your own* gem only: what it
73
+ does, how to install it, the conventions to enforce. Name companion gems but
74
+ do not explain their internals.
75
+ 3. Tailor the register block bodies and `scope` to your gem; the standard
76
+ interface is `info` (read-only explainer), `install` (sets the gem up in a
77
+ host), and a domain worker (`develop` for libraries, `operate` for CLIs).
78
+ 4. Run `rake the_local:build`, then **commit and ship**
79
+ `lib/<gem>/the_local/agents/*.md` (they must be in the gemspec's `files`).
80
+ This is the whole contract: a host discovers your locals by reading these
81
+ committed files from your gem on disk — it never loads your gem or runs your
82
+ register block — so if they aren't committed and shipped, you contribute
83
+ nothing, and if they are, you contribute everything. A drift test asserting
84
+ each committed file equals its `agent.to_markdown` keeps the artifact honest.
85
+
86
+ ### TheLocal.register
87
+
88
+ ```ruby
89
+ TheLocal.register("my_gem", prefix: "my_gem", scope: "one-line domain phrase",
90
+ agents_dir: File.expand_path("the_local/agents", __dir__)) do |c|
91
+ c.agent "info",
92
+ description: "Use to learn what my_gem offers.",
93
+ tools: "Read",
94
+ body: "You explain my_gem, answering only from the reference. You make no changes.",
95
+ knowledge: MyGem::Reference.content
96
+ end
97
+ ```
98
+
99
+ - `gem_name` (first arg) filters to a host's direct dependencies.
100
+ - `prefix` is the agent filename namespace; defaults to the gem name.
101
+ - `scope` is a one-line domain phrase used to generate the delegation trigger.
102
+ - `agents_dir` is the absolute path to the committed `.md` files; each agent
103
+ records its `source_path` there so the installer can copy it verbatim.
104
+
105
+ ### Conventions
106
+
107
+ - The register block lives behind `begin require "the_local" … rescue LoadError`
108
+ so the gem still works when the_local is absent.
109
+ - `guide.md` documents the providing gem only and stays the single source of
110
+ truth; never let a rendered `.md` drift from `agent.to_markdown`.
111
+ - Commit the rendered `.md`; never render in the host at install time.
@@ -0,0 +1,111 @@
1
+ ---
2
+ name: the_local-install
3
+ description: Use to add the_local to a host app and set it up correctly.
4
+ tools: Bash, Read, Edit
5
+ ---
6
+
7
+ You set the_local up in a host gem or app, following the reference's install section exactly: add the gem (git source until it is on RubyGems), bundle, run `bundle exec the_local install` to sync locals into .claude/agents/ and write the delegation trigger, and re-run it after bundle changes. You do not invent steps the reference does not list.
8
+
9
+ ## TheLocal
10
+
11
+ > **DO NOT** explore the the_local gem source code. This reference is the
12
+ > complete user-facing API, embedded verbatim into every the_local local so
13
+ > their guidance never drifts. Keep it the single source of truth.
14
+
15
+ the_local is the engine that lets any gem or app ship resident Claude Code
16
+ expert subagents ("locals") that know its conventions. A provider gem registers
17
+ its locals once; the_local renders them to committed `.md` files and installs
18
+ the aggregated set from every directly-depended provider into a consuming app's
19
+ `.claude/agents/`, plus a delegation rule so the host's agent actually uses them.
20
+
21
+ ### The model
22
+
23
+ - **Providers define locals.** A gem (or the app) calls `TheLocal.register` to
24
+ declare its locals; each `c.agent` becomes one local. The register block runs
25
+ only at build time, behind a soft `require "the_local"` guard so the gem still
26
+ works standalone.
27
+ - **`the_local:build` renders committed `.md`.** The provider runs
28
+ `rake the_local:build`; `TheLocal::Builder` writes each agent to its
29
+ `source_path` under `lib/<gem>/the_local/agents/<prefix>-<name>.md`. The
30
+ rendered files are committed to the provider's repo. **These committed files
31
+ are the contract** — they are what a host reads. The register block + `guide.md`
32
+ are the source of truth they're built from.
33
+ - **Install discovers committed `.md` on disk.** In a host, install reads each
34
+ direct dependency's committed `lib/**/the_local/agents/*.md` straight from its
35
+ gem path and copies them into `.claude/agents/` byte-for-byte — no provider
36
+ code is loaded and no register block runs in the host. Output depends only on
37
+ the provider gem version (a true carbon copy across every app), a provider needs
38
+ no install-time wiring to be found, and a fragile gem can't crash the install.
39
+ - **The delegation trigger.** Install also writes a registry-generated block into
40
+ the host's `CLAUDE.md`/`AGENTS.md` telling the host agent to delegate to these
41
+ locals. This is what makes delegation actually happen.
42
+ - **Direct-dependency scope.** Only the host's *direct* dependencies contribute
43
+ locals; transitive provider gems are filtered out, so a host gets exactly the
44
+ experts for the gems it chose.
45
+
46
+ ### Install (in any gem or app)
47
+
48
+ 1. Add the gem to the host's `Gemfile` (until it is on RubyGems, use a git
49
+ source: `gem "the_local", github: "tylercschneider/the_local"`), then
50
+ `bundle install`.
51
+ 2. Run `bundle exec the_local install`. This syncs every direct provider's
52
+ committed locals into `.claude/agents/` and writes the delegation trigger
53
+ into `CLAUDE.md`/`AGENTS.md`. It needs no Rails — a plain gem installs the
54
+ same way an app does.
55
+ 3. Re-run `bundle exec the_local install` after any bundle change (a provider
56
+ added, removed, or upgraded) to bring the host's locals back in sync. The
57
+ shell can automate this; the gem only exposes the command.
58
+
59
+ Rails apps can equivalently run `bin/rails g the_local:install` and
60
+ `the_local:refresh`; a gem that already wires `require "the_local/rake"` into
61
+ its Rakefile also gets `rake the_local:install`. All three share one engine.
62
+
63
+ ### Author a provider (turn a gem into a provider)
64
+
65
+ 1. Run `bin/rails g the_local:provider <gem_name>` (pass `--scope`,
66
+ `--prefix`, `--worker` as needed). It scaffolds `lib/<gem>/reference.rb`, a
67
+ `lib/<gem>/reference/guide.md`, and a `lib/<gem>/the_local.rb` companion that
68
+ registers the standard interface; hooks `the_local:build` into the `Rakefile`;
69
+ requires the companion from the gem entrypoint; and builds the committed
70
+ `.md` for review.
71
+ 2. Write `guide.md` in this format — it is the single source of truth and is
72
+ embedded verbatim into every local. Document *your own* gem only: what it
73
+ does, how to install it, the conventions to enforce. Name companion gems but
74
+ do not explain their internals.
75
+ 3. Tailor the register block bodies and `scope` to your gem; the standard
76
+ interface is `info` (read-only explainer), `install` (sets the gem up in a
77
+ host), and a domain worker (`develop` for libraries, `operate` for CLIs).
78
+ 4. Run `rake the_local:build`, then **commit and ship**
79
+ `lib/<gem>/the_local/agents/*.md` (they must be in the gemspec's `files`).
80
+ This is the whole contract: a host discovers your locals by reading these
81
+ committed files from your gem on disk — it never loads your gem or runs your
82
+ register block — so if they aren't committed and shipped, you contribute
83
+ nothing, and if they are, you contribute everything. A drift test asserting
84
+ each committed file equals its `agent.to_markdown` keeps the artifact honest.
85
+
86
+ ### TheLocal.register
87
+
88
+ ```ruby
89
+ TheLocal.register("my_gem", prefix: "my_gem", scope: "one-line domain phrase",
90
+ agents_dir: File.expand_path("the_local/agents", __dir__)) do |c|
91
+ c.agent "info",
92
+ description: "Use to learn what my_gem offers.",
93
+ tools: "Read",
94
+ body: "You explain my_gem, answering only from the reference. You make no changes.",
95
+ knowledge: MyGem::Reference.content
96
+ end
97
+ ```
98
+
99
+ - `gem_name` (first arg) filters to a host's direct dependencies.
100
+ - `prefix` is the agent filename namespace; defaults to the gem name.
101
+ - `scope` is a one-line domain phrase used to generate the delegation trigger.
102
+ - `agents_dir` is the absolute path to the committed `.md` files; each agent
103
+ records its `source_path` there so the installer can copy it verbatim.
104
+
105
+ ### Conventions
106
+
107
+ - The register block lives behind `begin require "the_local" … rescue LoadError`
108
+ so the gem still works when the_local is absent.
109
+ - `guide.md` documents the providing gem only and stays the single source of
110
+ truth; never let a rendered `.md` drift from `agent.to_markdown`.
111
+ - Commit the rendered `.md`; never render in the host at install time.
data/CHANGELOG.md ADDED
@@ -0,0 +1,11 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here, following
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and
5
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ### Added
10
+ - Initial gem scaffold: source-agnostic metric-contract gem (`Readout`), plain
11
+ Ruby (no Rails / no event-source dependency).
data/CLAUDE.md ADDED
@@ -0,0 +1,118 @@
1
+ <!-- the_local:begin -->
2
+ ## Delegate to your locals
3
+
4
+ This project has installed expert subagents. Before doing work yourself,
5
+ check whether a local owns it and delegate — never work from memory on
6
+ something a local covers:
7
+
8
+ - the_local-* agents
9
+
10
+ See each agent's description for specifics.
11
+ <!-- the_local:end -->
12
+
13
+ <!-- the_local:process:begin -->
14
+ Read and follow this develop process for all work in this project. It is
15
+ also written verbatim to `develop_process_rules.md` — reference that file directly.
16
+
17
+ # Develop Process
18
+
19
+ The standard process for writing code across all projects. Default to these rules
20
+ unless a project explicitly overrides them.
21
+
22
+ ---
23
+
24
+ ## Diverging from this process
25
+
26
+ Read this process before starting work and follow it — it is the default for
27
+ every session. If a task genuinely calls for breaking one of these rules, do not
28
+ silently deviate: **PAUSE and ask for a one-time exception**, naming the rule and
29
+ why it should be set aside here. An exception is granted for that instance only —
30
+ it needs no doc or notes update — and then you continue. Do not treat a granted
31
+ exception as a standing change to the process.
32
+
33
+ ---
34
+
35
+ ## Test-Driven Development
36
+
37
+ TDD is the default for everything. Work one tiny cycle at a time:
38
+
39
+ 1. **Write one test that asserts one thing.**
40
+ 2. **Run it and watch it fail** — for the right reason. A test you never saw fail
41
+ proves nothing.
42
+ 3. **Write the minimum code to make it pass.**
43
+ 4. **Run the test and watch it pass.**
44
+ 5. **Commit.**
45
+ 6. Repeat with the next test.
46
+
47
+ One assertion per test. One test per commit cycle. No batching multiple behaviors
48
+ into a single test or a single commit.
49
+
50
+ ---
51
+
52
+ ## Commits
53
+
54
+ - A commit is normally **two files: the test file and the code file.**
55
+ - When implementing or updating an interface (e.g. a new controller endpoint) a
56
+ commit may touch more files (route + controller + view) — that is the minimal
57
+ coherent unit for that interface, and it is allowed.
58
+ - Keep each commit focused on the one behavior the test describes.
59
+
60
+ ---
61
+
62
+ ## What to Test
63
+
64
+ - **Test our own code only.**
65
+ - **Never test third-party code** — not a gem, not an API, not a framework. The
66
+ only test that may reference a dependency is one that asserts *our system is
67
+ correctly wired to it* (the integration seam), never the dependency's own
68
+ behavior.
69
+ - **Never test another interface inside a unit test.** A test covers one interface.
70
+ The single exception is the smoke integration test described below.
71
+
72
+ ---
73
+
74
+ ## Smoke Integration Test
75
+
76
+ When implementing an interface, write **one smoke integration test** that exercises
77
+ the interface end to end and proves the pieces are connected. This is the one place
78
+ where touching more than the unit under test is expected and correct.
79
+
80
+ ---
81
+
82
+ ## Pull Requests
83
+
84
+ - **Always work on a feature branch and open a PR.** Confirm the target branch
85
+ before any git operation (`git branch --show-current`).
86
+ - **Keep PRs small and manageable** — typically **no more than 8–10 files.**
87
+ - Keep the focus of a PR narrow. One concern per PR.
88
+ - **All tests pass before opening the PR.**
89
+ - **The linter and every other CI check pass before opening the PR.**
90
+ - Never start a new PR until the previous one is merged.
91
+
92
+ ---
93
+
94
+ ## Code Quality
95
+
96
+ - Follow Clean Code principles: small functions, clear names, no surprises.
97
+ - Follow SOLID principles. Readable by a human first.
98
+ - Keep it simple — no abstraction until a real need calls for it.
99
+ - Explicitly require libraries rather than assuming autoload.
100
+
101
+ ## Comments
102
+
103
+ - **Write self-documenting code, not comments.** Code should be clean and readable
104
+ on its own. Names — of classes, methods, variables, and partials — carry the intent.
105
+ - **A comment is a smell.** If you feel a comment is needed, the code is either built
106
+ wrong or needs refactoring (a clearer name, a smaller method, an extracted object or
107
+ partial) so the intent is obvious without prose. Follow SOLID and this resolves itself.
108
+ - Do not leave explanatory headers on classes/methods, inline "what this does" notes,
109
+ or section banners. Delete them and let the structure speak.
110
+ - Narrow exceptions, kept rare: a genuinely non-obvious *why* (a workaround for an
111
+ external bug, a legal/security constraint) and machine-readable annotations the
112
+ tooling requires (e.g. `rubocop:disable`). Prefer refactoring over a "why" comment
113
+ whenever you can.
114
+ <!-- the_local:process:end -->
115
+
116
+
117
+
118
+
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 tylercschneider
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # Readout
2
+
3
+ A **source-agnostic, self-describing metric contract**.
4
+
5
+ A `Readout::Stat` describes a metric — its key, title, plain-English definition,
6
+ unit, timeframe, and declared inputs — and exposes a normalized **result shape**
7
+ (`value`, `shape`, `as_of`, `exact`). Behind every Stat is a **Source** (an adapter
8
+ port); the Stat doesn't care where its number comes from.
9
+
10
+ That decoupling is the whole point: a display layer (e.g. `dash_kit`) renders Stats
11
+ without knowing whether the number came from `tally` rollups, a live query, or a
12
+ fixture. Swap the Source, keep the Stat.
13
+
14
+ Readout depends on nothing concrete — no Rails, no event source, no aggregation
15
+ engine. Adapters bridge it to whatever produces the numbers.
16
+
17
+ ## Installation
18
+
19
+ ```ruby
20
+ gem "readout"
21
+ ```
22
+
23
+ ## Status
24
+
25
+ The core contract works — a `Stat` reads through its `Source` and returns a
26
+ normalized `Result`:
27
+
28
+ ```ruby
29
+ source = ->(inputs) { Readout::Result.new(value: 0.42, shape: :scalar, exact: true) }
30
+ stat = Readout::Stat.new(
31
+ key: :sales_conversion,
32
+ title: "Sales Conversion",
33
+ definition: "Share of qualified leads that became deals.",
34
+ calculation: "deals ÷ qualified leads, within the period",
35
+ unit: :percent,
36
+ timeframe: "This month",
37
+ source: source
38
+ )
39
+
40
+ stat.read(qualified: 100, won: 42).value # => 0.42
41
+ stat.definition # the plain-English "what it captures"
42
+ stat.calculation # the "how it's computed"
43
+ ```
44
+
45
+ A Stat is **self-describing** — `title`, `definition`, `calculation`, `unit`,
46
+ `timeframe` — so a UI can explain any number without hardcoding meaning. `Result`
47
+ carries `value, shape, as_of, exact`. Swap the `Source` (a tally-backed one, a live
48
+ query, a fixture) without touching the Stat or the display.
49
+
50
+ Next: declared inputs (required/optional) + validation.
51
+
52
+ ## License
53
+
54
+ Available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[test rubocop]
@@ -0,0 +1,97 @@
1
+ # Develop Process
2
+
3
+ The standard process for writing code across all projects. Default to these rules
4
+ unless a project explicitly overrides them.
5
+
6
+ ---
7
+
8
+ ## Diverging from this process
9
+
10
+ Read this process before starting work and follow it — it is the default for
11
+ every session. If a task genuinely calls for breaking one of these rules, do not
12
+ silently deviate: **PAUSE and ask for a one-time exception**, naming the rule and
13
+ why it should be set aside here. An exception is granted for that instance only —
14
+ it needs no doc or notes update — and then you continue. Do not treat a granted
15
+ exception as a standing change to the process.
16
+
17
+ ---
18
+
19
+ ## Test-Driven Development
20
+
21
+ TDD is the default for everything. Work one tiny cycle at a time:
22
+
23
+ 1. **Write one test that asserts one thing.**
24
+ 2. **Run it and watch it fail** — for the right reason. A test you never saw fail
25
+ proves nothing.
26
+ 3. **Write the minimum code to make it pass.**
27
+ 4. **Run the test and watch it pass.**
28
+ 5. **Commit.**
29
+ 6. Repeat with the next test.
30
+
31
+ One assertion per test. One test per commit cycle. No batching multiple behaviors
32
+ into a single test or a single commit.
33
+
34
+ ---
35
+
36
+ ## Commits
37
+
38
+ - A commit is normally **two files: the test file and the code file.**
39
+ - When implementing or updating an interface (e.g. a new controller endpoint) a
40
+ commit may touch more files (route + controller + view) — that is the minimal
41
+ coherent unit for that interface, and it is allowed.
42
+ - Keep each commit focused on the one behavior the test describes.
43
+
44
+ ---
45
+
46
+ ## What to Test
47
+
48
+ - **Test our own code only.**
49
+ - **Never test third-party code** — not a gem, not an API, not a framework. The
50
+ only test that may reference a dependency is one that asserts *our system is
51
+ correctly wired to it* (the integration seam), never the dependency's own
52
+ behavior.
53
+ - **Never test another interface inside a unit test.** A test covers one interface.
54
+ The single exception is the smoke integration test described below.
55
+
56
+ ---
57
+
58
+ ## Smoke Integration Test
59
+
60
+ When implementing an interface, write **one smoke integration test** that exercises
61
+ the interface end to end and proves the pieces are connected. This is the one place
62
+ where touching more than the unit under test is expected and correct.
63
+
64
+ ---
65
+
66
+ ## Pull Requests
67
+
68
+ - **Always work on a feature branch and open a PR.** Confirm the target branch
69
+ before any git operation (`git branch --show-current`).
70
+ - **Keep PRs small and manageable** — typically **no more than 8–10 files.**
71
+ - Keep the focus of a PR narrow. One concern per PR.
72
+ - **All tests pass before opening the PR.**
73
+ - **The linter and every other CI check pass before opening the PR.**
74
+ - Never start a new PR until the previous one is merged.
75
+
76
+ ---
77
+
78
+ ## Code Quality
79
+
80
+ - Follow Clean Code principles: small functions, clear names, no surprises.
81
+ - Follow SOLID principles. Readable by a human first.
82
+ - Keep it simple — no abstraction until a real need calls for it.
83
+ - Explicitly require libraries rather than assuming autoload.
84
+
85
+ ## Comments
86
+
87
+ - **Write self-documenting code, not comments.** Code should be clean and readable
88
+ on its own. Names — of classes, methods, variables, and partials — carry the intent.
89
+ - **A comment is a smell.** If you feel a comment is needed, the code is either built
90
+ wrong or needs refactoring (a clearer name, a smaller method, an extracted object or
91
+ partial) so the intent is obvious without prose. Follow SOLID and this resolves itself.
92
+ - Do not leave explanatory headers on classes/methods, inline "what this does" notes,
93
+ or section banners. Delete them and let the structure speak.
94
+ - Narrow exceptions, kept rare: a genuinely non-obvious *why* (a workaround for an
95
+ external bug, a legal/security constraint) and machine-readable annotations the
96
+ tooling requires (e.g. `rubocop:disable`). Prefer refactoring over a "why" comment
97
+ whenever you can.
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Readout
4
+ Result = Struct.new(:value, :shape, :as_of, :exact, keyword_init: true)
5
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Readout
4
+ class Stat
5
+ attr_reader :key, :title, :definition, :calculation, :unit, :timeframe
6
+
7
+ def initialize(key:, title: nil, definition: nil, calculation: nil, unit: nil, timeframe: nil, source: nil)
8
+ @key = key
9
+ @title = title
10
+ @definition = definition
11
+ @calculation = calculation
12
+ @unit = unit
13
+ @timeframe = timeframe
14
+ @source = source
15
+ end
16
+
17
+ def read(inputs)
18
+ @source.call(inputs)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Readout
4
+ VERSION = "0.1.0"
5
+ end
data/lib/readout.rb ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "readout/version"
4
+ require_relative "readout/result"
5
+ require_relative "readout/stat"
6
+
7
+ module Readout
8
+ class Error < StandardError; end
9
+ end
data/sig/readout.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Readout
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,64 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: readout
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - tylercschneider
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: 'Readout defines a Stat — a self-describing metric (key, title, plain-English
13
+ definition, unit, timeframe, declared inputs) with a normalized result shape (value,
14
+ shape, as_of, exact) and a Source adapter port. It depends on nothing concrete:
15
+ any source (e.g. tally rollups) plugs in behind a Stat, so the display layer never
16
+ couples to where a number comes from.'
17
+ email:
18
+ - tylercschneider@gmail.com
19
+ executables: []
20
+ extensions: []
21
+ extra_rdoc_files: []
22
+ files:
23
+ - ".claude/agents/the_local-develop.md"
24
+ - ".claude/agents/the_local-info.md"
25
+ - ".claude/agents/the_local-install.md"
26
+ - CHANGELOG.md
27
+ - CLAUDE.md
28
+ - LICENSE.txt
29
+ - README.md
30
+ - Rakefile
31
+ - develop_process_rules.md
32
+ - lib/readout.rb
33
+ - lib/readout/result.rb
34
+ - lib/readout/stat.rb
35
+ - lib/readout/version.rb
36
+ - sig/readout.rbs
37
+ homepage: https://github.com/tylercschneider/readout
38
+ licenses:
39
+ - MIT
40
+ metadata:
41
+ allowed_push_host: https://rubygems.org
42
+ homepage_uri: https://github.com/tylercschneider/readout
43
+ source_code_uri: https://github.com/tylercschneider/readout
44
+ changelog_uri: https://github.com/tylercschneider/readout/blob/main/CHANGELOG.md
45
+ bug_tracker_uri: https://github.com/tylercschneider/readout/issues
46
+ rubygems_mfa_required: 'true'
47
+ rdoc_options: []
48
+ require_paths:
49
+ - lib
50
+ required_ruby_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: 3.2.0
55
+ required_rubygems_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: '0'
60
+ requirements: []
61
+ rubygems_version: 4.0.20
62
+ specification_version: 4
63
+ summary: Source-agnostic, self-describing metric contract
64
+ test_files: []