siding 0.0.1

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: cc7055b7dfa4eed27f2595530e7deb5046c8fbd53e524f9a0574e1bcae70b369
4
+ data.tar.gz: 1f9aa47e9f32bda598a5b7b4ed74238f737b3f3fdc3159d1a69431c4da64964c
5
+ SHA512:
6
+ metadata.gz: fca7785f2b8b84617ebd45de63bd66761c6e828f7ef4529adb346a0643c182a68d68d1a203cfb291381184cc386b260de23465a3df3d57493b92e64745329776
7
+ data.tar.gz: c8be2d9e4a69711d3c6cafb492f34b5c86931f680bbc8797f66760b85cb87894d2fa63972cafd0fd0ade5f8ab2216bdf515c453f95aeab6c865992da83dd34e3
data/CLAUDE.md ADDED
@@ -0,0 +1,248 @@
1
+ # Siding — working notes
2
+
3
+ A Rails application preloader whose design point is a single guarantee: **it never serves stale
4
+ code**. Everything below exists to keep that true. When a change would trade the guarantee for
5
+ speed, convenience, or a configuration option, the change is wrong — that is Principle I, and it is
6
+ not negotiable.
7
+
8
+ This file is binding, not advisory. Code comments cite the principles and invariants below by
9
+ number, and those citations resolve to this file. Every guarantee stated here must have a test that
10
+ fails when it stops being true; adding or moving a guarantee means adding or moving that test.
11
+
12
+ ## Principles
13
+
14
+ ### I. Correctness Over Speed (NON-NEGOTIABLE)
15
+
16
+ Siding exists to make development faster, not faster than it is correct. Where the two conflict,
17
+ correctness wins — every time, without a configuration option to decide otherwise.
18
+
19
+ - MUST NOT execute a command against application state that predates the developer's most recent
20
+ saved edit.
21
+ - MUST NOT offer a setting, flag, or environment variable that trades staleness for speed. A knob
22
+ that disables a correctness check will be found, enabled, and then blamed on us.
23
+ - Correctness mechanisms MUST be structural, not procedural. A guarantee that depends on a
24
+ developer maintaining a list, or a reviewer remembering to check, is not a guarantee. Prefer
25
+ designs where the incorrect state is unrepresentable.
26
+ - When correctness forces a slow path, the system MUST say so within a bounded time. Silent
27
+ slowness is indistinguishable from a hang.
28
+ - Optimizations that reduce the cost of a correct path are welcome. Optimizations that skip a
29
+ correctness check are rejected regardless of measured benefit.
30
+
31
+ **Rationale**: the established tools in this space are fast but not trustworthy, so developers
32
+ preemptively disable them. A preloader that serves stale code has negative value — it costs more
33
+ debugging time than it saves in boot time, and trust is not recovered by a benchmark.
34
+
35
+ ### II. Executable Guarantees
36
+
37
+ Any claim the project makes about its own behavior MUST be enforced by an automated test in CI. A
38
+ guarantee that is not executed is a hope.
39
+
40
+ - Every MUST-level requirement, and every absolute success criterion ("zero stale results", "no
41
+ orphaned processes", "byte-identical output"), MUST map to a test that fails if it breaks.
42
+ - Absolute claims MUST be tested against the real thing. Where fidelity to a real environment is
43
+ the property under test, mocks are not acceptable evidence.
44
+ - Failure and degraded paths MUST be tested, not merely implemented — that code otherwise runs
45
+ unobserved on other people's machines.
46
+ - A skipped, quarantined, or pending test is a guarantee that no longer holds. Fix it, or withdraw
47
+ the claim from the documentation.
48
+
49
+ ### III. Code Quality
50
+
51
+ Code MUST be written to be read by the next person to debug it under time pressure. This is systems
52
+ code involving processes, signals, and file descriptors, where bugs are intermittent and expensive
53
+ to reproduce.
54
+
55
+ - New code MUST match the surrounding conventions — naming, structure, comment density, idiom.
56
+ Consistency outranks personal preference.
57
+ - Public API MUST be minimal and explicitly declared. Anything not documented as public is private
58
+ and may change freely.
59
+ - Every configuration option MUST justify itself against the cost of another code path to keep
60
+ correct. Absence of an option is the default.
61
+ - Dependencies MUST be justified; prefer the standard library. A dependency added here is imposed
62
+ on every project that installs the tool (see Invariant 9).
63
+ - Comments MUST explain why, not what. Code needing a comment to explain what it does should be
64
+ rewritten.
65
+ - Error messages MUST identify what failed, what the system did about it, and what the user can do
66
+ next. An error the user cannot act on is a bug in the error.
67
+
68
+ ### IV. User Experience Consistency
69
+
70
+ The tool MUST behave the same way every time, and the way the tools it wraps already behave.
71
+ Surprise is the failure mode. A tool sitting between a developer and their command inherits blame
72
+ for everything downstream; the only defense is being ruled out quickly, and being trivially
73
+ removable when it cannot be.
74
+
75
+ - Accelerated execution MUST be observationally indistinguishable from unaccelerated execution,
76
+ except faster (Invariant 5).
77
+ - Logger MUST NOT reach stdout, stderr, or any stream the user redirected; they go to the
78
+ controlling terminal, visually distinct from application output (Invariant 4). With no
79
+ controlling terminal there is no developer to inform and the notice is dropped, but it MUST
80
+ remain recoverable from `siding status`.
81
+ - MUST NOT require a ritual. Any reachable state MUST recover automatically on the next
82
+ invocation — no manual cleanup, restart, or stop command.
83
+ - MUST be disableable for a single invocation and for a whole session, without editing files
84
+ committed to the repository.
85
+ - MUST be able to explain its own behavior on request: whether acceleration was used, against what
86
+ state, and why or why not.
87
+ - When the tool cannot do its job, the user's command MUST still run (Invariant 6).
88
+
89
+ Naming stderr as the logger destination reads as the obvious answer and is wrong: stderr
90
+ belongs to the command, not to the tool. Once a developer redirects it, "write to stderr" and "never
91
+ pollute redirected output" cannot both hold, and the only way to obey both is silence — which
92
+ Principle I prohibits. The controlling terminal is the one channel that belongs to the developer
93
+ rather than to their command.
94
+
95
+ ## Constraints
96
+
97
+ **Platform**: Linux and macOS. WSL is supported and treated as Linux. Windows is not supported, and
98
+ this MUST be stated explicitly rather than manifest as degraded behavior.
99
+
100
+ **Runtime dependencies**: code on the latency-critical path MUST NOT load Bundler or the application
101
+ framework. What may be declared is governed by Principle III and Invariant 9.
102
+
103
+ **Isolation**: runtime state MUST be per-user and user-owned. Never shared across users, project
104
+ checkouts, tool versions, or language runtime versions.
105
+
106
+ **Compatibility**: the supported Ruby floor is declared in the gemspec and MUST NOT be raised in a
107
+ patch release. EOL Ruby and framework versions are out of scope.
108
+
109
+ ## Invariants
110
+
111
+ Break one of these and the tool becomes the thing it was written to replace. They are the
112
+ codebase-specific shape of the principles above; where an invariant looks arbitrary, the principle
113
+ it serves is the reason.
114
+
115
+ 1. **Validation precedes fork.** Every invocation revalidates the manifest before a worker exists.
116
+ Filesystem watching (`Restarter`) is an optimization on the *rebuild*, never on the *decision*.
117
+ 2. **No knob turns validation off.** No flag, no environment variable, no config file.
118
+ `test/unit/platform_test.rb` scans `lib/**/*.rb` for per-project config file reads and fails.
119
+ 3. **The watch set is derived, never declared.** `LoadManifest` is the `$LOADED_FEATURES` delta
120
+ across boot, plus autoload/eager-load roots, `config/initializers`, `Gemfile.lock`, the resolved
121
+ gem set, and boot-component environment. `Siding.boot_component` is a residue escape hatch — if
122
+ ordinary application code needs it, the derivation has a bug.
123
+ 4. **The tool never writes to stdout or stderr.** Everything goes to `/dev/tty` via `Logger`.
124
+ `SIDING_LOG` changes verbosity, never destination. Application boot errors are the exception,
125
+ because an unaccelerated run would put them on stderr too.
126
+ 5. **An accelerated run is observationally identical to an unaccelerated one.** Same bytes on each
127
+ stream in the same order, same exit status including death by signal, same tty behaviour.
128
+ 6. **The tool is never the reason a command fails.** Unsupported platform, unusable runtime
129
+ directory, a boot that will not finish, a protocol version mismatch — each degrades to running
130
+ the command unaccelerated.
131
+ 7. **Runtime state is per-user and owner-only.** `XDG_RUNTIME_DIR` or `~/.local/state/siding`, mode
132
+ `0700`, never a shared temp directory. A warm application reachable by another user is the same
133
+ class of failure as stale code. Every file under there is a hint: confirm a pid is live before
134
+ using it, and recover from any leftover combination without asking a developer to clean up.
135
+ 8. **No orphaned processes.** `ProcessHelpers#assert_no_surviving_processes` runs in the teardown of
136
+ *every* integration test, not just the ones about life cycle.
137
+ 9. **`watchcat` is the one runtime gem dependency; standard library otherwise.** `siding.gemspec`
138
+ depends on it directly, so `bundle install` resolves it the moment `gem "siding"` is added — no
139
+ separate step, no silent gap. It backs *both* `SIDING_WATCH` backends (`events`, native OS
140
+ notification; `poll`, watchcat's `force_polling`), so `Restarter`'s wake source is watchcat or
141
+ nothing — never a home-grown poll loop that behaves differently depending on what happened to be
142
+ installed, which is what made `SIDING_WATCH=poll` and "watchcat isn't installed" the same code
143
+ path before the dependency was declared. Reimplementing cross-platform file watching in the
144
+ standard library is a worse trade than depending on the one gem written for this project's own
145
+ fork-safety needs (see `restarter.rb`'s `around_fork`). Every other dependency remains
146
+ disallowed.
147
+
148
+ ## Layout
149
+
150
+ | File | Role |
151
+ |---|---|
152
+ | `exe/siding` | Entry point. Requires exactly one file — the client's load time is a floor on the speedup |
153
+ | `lib/siding.rb` | The public API surface, and *not* what the client loads |
154
+ | `lib/siding/client.rb` | The process the developer runs. Loads no gems, no Bundler, no application. `DEFAULT_BOOT_TIMEOUT = 90.0` (`SIDING_TIMEOUT`), `NOTICE_AFTER = 0.75` |
155
+ | `lib/siding/cli.rb` | Argument handling, management commands, the `doctor` / `status` reports |
156
+ | `lib/siding/server.rb` | The long-lived booted application. `setsid` first: no controlling terminal, own process group. Validates, forks, hands off — never relays I/O |
157
+ | `lib/siding/worker.rb` | One invocation in a fork. Takes the developer's descriptors, env, cwd, signals; leaves the server's process group. Cannot be constructed without a verdict |
158
+ | `lib/siding/load_manifest.rb` | What the boot actually loaded. The central design decision |
159
+ | `lib/siding/staleness.rb` | `fresh` / `reloadable` / `reboot`, with reasons and trigger paths |
160
+ | `lib/siding/restarter.rb` | Speculative reboot while idle. Allowed to be wrong; can only cost speed. Wakes on filesystem events or a poll backoff (`Watch`, `SIDING_WATCH`) — the wake source, never what a boot is validated against |
161
+ | `lib/siding/watch.rb` | Backend selection and the watchcat life cycle for `Restarter`'s wake source only — no staleness logic. `SIDING_WATCH=poll` drives watchcat with `force_polling: true`. watchcat is a declared dependency (Invariant 9), so its *absence* is not a case to handle; a watcher that cannot *start* — an environment problem — degrades to `Restarter`'s own interval backoff, because Principle I only asks watchcat to cost speed, never correctness |
162
+ | `lib/siding/runtime.rb` | The on-disk layout and its permissions |
163
+ | `lib/siding/project_key.rb` | app_root + uid + tool version + ruby version + app env. Every field names a way a warm application could serve a command it has no business serving |
164
+ | `lib/siding/protocol.rb` | Wire protocol. `VERSION = 1`, versioned independently of the gem |
165
+ | `lib/siding/logger.rb` | The `/dev/tty` rule, in code |
166
+ | `lib/siding/life cycle.rb` | `before_fork` / `after_fork`. The only configuration hook in the project |
167
+ | `lib/siding/platform.rb` | Support detection. Deliberately has **no** Rails version check — resolution already makes a below-floor install unreachable |
168
+ | `lib/siding/invocation.rb` | Reads the `SIDING_RESOLUTION` / `_REVISION` / `_BOOT_SECONDS` env keys back out, so introspection is truthful unaccelerated too |
169
+ | `lib/siding/boot_component.rb` | The `Siding.boot_component` registry (Invariant 3) |
170
+
171
+ `reloadable` vs `reboot` is decided from the booted application's own configuration — a file under
172
+ an autoload path *when reloading is actually enabled* is repaired by Rails' reloader in the worker.
173
+ Directory layout alone does not decide it.
174
+
175
+ ## Tests
176
+
177
+ ```bash
178
+ bundle exec rake test # unit + integration; green on every commit
179
+ bundle exec rake test:unit # in-process only; fastest loop
180
+ bundle exec rake test:integration # drives exe/siding as a subprocess
181
+ bundle exec rake test:pty # interactive fidelity; needs a real pty
182
+
183
+ SIDING_SOAK=1 bundle exec ruby -Ilib -Itest test/integration/soak_test.rb # opt-in, ~2 min
184
+ bundle exec ruby -Ilib -Itest test/unit/staleness_events_test.rb -n /pattern/ # one file or test
185
+ ```
186
+
187
+ The suites split by *character*, not by mirrored source path: `test/unit/`, `test/integration/`,
188
+ `test/pty/`.
189
+
190
+ Things that will bite you:
191
+
192
+ - **The fixture application is a prerequisite, not a fixture file.** `test/fixtures/rails_app` has
193
+ its own Gemfile and sqlite databases, neither in version control. `rake fixture:prepare` is a
194
+ prerequisite of the test tasks and is cheap when there is nothing to do. Skipping it fails
195
+ quietly: with no bundle, accelerated and unaccelerated runs fail the same way, and every
196
+ output-identity comparison passes by comparing two identical failures.
197
+ - **Integration tests run the real executable** through `TestSupport#siding_invoke` /
198
+ `#unaccelerated_invoke`. Byte-identity, exit status, and process cleanup are properties of a
199
+ process and cannot fail in a way an in-process call would notice.
200
+ - **`siding_env` nils out** `SIDING_DISABLE`, `SIDING_LOG`, and the bundler variables.
201
+ - **Anything asserting on the tool's own output needs a pty.** `include TerminalTests`, then
202
+ `open_siding_terminal` + `expect_on(session, pattern, timeout:)` + `session.screen`. A piped
203
+ `siding doctor` or `siding status` produces nothing by contract; the exit status is the part a
204
+ script reads, and that is checked through the pipe.
205
+ - **Tests inspect the runtime directory directly** (`siding_state_dir`, `siding_server_info`,
206
+ `warm_application?`, `siding_server_pid`) rather than asking the tool, because "is something
207
+ warm?" has to be answerable when the tool is what is under suspicion.
208
+ - **A timing bound is proved by squeezing the bound, not by slowing the subject** — see the
209
+ boot-timeout test in `test/integration/degradation_test.rb`, which sets `SIDING_TIMEOUT=0.05`
210
+ rather than waiting 90 seconds, then waits for the abandoned boot to finish publishing so it does
211
+ not leak into the next test.
212
+ - **`SIDING_WATCH` gets both modes run for real.** `test/unit/restarter_test.rb` parameterizes the
213
+ decision tests over `events` and `poll`; `test/integration/restarter_test.rb` does the same end to
214
+ end. `bundle exec rake test:integration` alone only proves the default; add
215
+ `SIDING_WATCH=poll bundle exec rake test:integration` for the poll backend. The fixture lists
216
+ `watchcat` directly in its own Gemfile because it never adds `gem "siding"` itself
217
+ (`test/test_helper.rb` invokes `exe/siding` by absolute path, outside Bundler, so siding's gemspec
218
+ dependencies never reach the fixture's lockfile).
219
+
220
+ There is no performance suite. Wall-clock budgets and the benchmarks enforcing them were removed;
221
+ what remains are the correctness properties, which is why `test/integration/` asserts on *where*
222
+ code ran rather than how long it took. A change made for speed is currently unmeasured — if that
223
+ becomes a problem, the honest fix is to bring the benchmarks back, not to reason about it.
224
+
225
+ ## Environment surface
226
+
227
+ `SIDING_DISABLE`, `SIDING_TIMEOUT`, `SIDING_IDLE_TIMEOUT`, `SIDING_LOG`, `SIDING_SERVER`,
228
+ `SIDING_RESOLUTION`, `SIDING_REVISION`, `SIDING_BOOT_SECONDS`, `SIDING_WATCH`. That list is asserted
229
+ by an allowlist scan in `test/unit/config_test.rb` — adding a variable without adding it there fails
230
+ the build, which is the point.
231
+
232
+ ## Before merge
233
+
234
+ 1. All tests pass, with none skipped or quarantined to achieve it.
235
+ 2. Every new MUST-level requirement has an automated test (Principle II).
236
+ 3. Public API changes are documented, including what is deliberately not public. Public API is the
237
+ life cycle hooks, `boot_component`, and introspection — what `lib/siding.rb` exposes and documents
238
+ as public. Everything else under `Siding::` may change.
239
+ 4. Any new configuration option carries a written justification (Principle III).
240
+ 5. Comments name the principle or invariant that forced the shape, at the density already in the
241
+ surrounding file — it is high, deliberately.
242
+
243
+ Repository artifacts — code, comments, README, commit messages — are written in English.
244
+
245
+ A deviation from a principle MUST name the simpler alternative and why it was insufficient, in the
246
+ commit that introduces it. Amending a principle to weaken or remove it MUST document what problem it
247
+ was preventing and why that problem no longer applies; rewording that does not change what is
248
+ permitted needs no justification.
@@ -0,0 +1,10 @@
1
+ # Code of Conduct
2
+
3
+ "siding" follows [The Ruby Community Conduct Guideline](https://www.ruby-lang.org/en/conduct) in all "collaborative space", which is defined as community communications channels (such as mailing lists, submitted patches, commit comments, etc.):
4
+
5
+ * Participants will be tolerant of opposing views.
6
+ * Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks.
7
+ * When interpreting the words and actions of others, participants should always assume good intentions.
8
+ * Behaviour which can be reasonably considered harassment will not be tolerated.
9
+
10
+ If you have any concerns about behaviour within this project, please contact us at ["yuuji.yaginuma@gmail.com"](mailto:"yuuji.yaginuma@gmail.com").
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Yuji Yaginuma
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,182 @@
1
+ # Siding
2
+
3
+ An alternative Rals application preloader that focuses to never serves state code.
4
+
5
+ Siding's design point is that guarantee. It derives the set of files it watches from what the boot
6
+ actually loaded, rather than from a maintained list, and it revalidates before every invocation. So
7
+ "you never run against stale code" is structural rather than best-effort, and when it cannot
8
+ accelerate, your command still runs — unaccelerated, never refused.
9
+
10
+ ## Design
11
+
12
+ **The watch set is the whole argument.** A hand-maintained list is incomplete by construction: it is
13
+ maintained by a person while the boot process is free to load anything. All files are loaded at boot,
14
+ none is watched, and all of them persist across invocations after you change them. You then debug
15
+ behavior that does not match the source in front of you. Deriving the set from what boot *actually loaded*
16
+ makes it complete instead, because the program is observing itself rather than trusting a list.
17
+
18
+ **Checking on every invocation is what makes it a guarantee.** Filesystem events are missable sometimes.
19
+ So a watcher can only ever be best-effort, so Siding uses one solely to get a head start on the rebuild,
20
+ never to decide whether the application is current. That decision is made by revalidating before the fork,
21
+ on the path that cannot be skipped. Siding uses filesystem events by default, but you can switch it
22
+ by `SIDING_WATCH`. If you want to use polling, please specify `poll`.
23
+
24
+ ## Supported versions and platforms
25
+
26
+ | | Supported | Below the line |
27
+ |---|---|---|
28
+ | Platform | Linux, macOS (WSL counts as Linux) | Commands run unaccelerated |
29
+ | Commands | `rails`, `rake`, `rspec`, `test` | Commands run unaccelerated |
30
+
31
+ `test` is Rails' own `bin/test` (minitest); invoke it as `siding test ...`. `siding init` does not
32
+ generate a `bin/test` binstub, since `bin/rails test` already covers the same entry point — prefix
33
+ `test` explicitly when you use it directly.
34
+
35
+ `rails server` (and its alias `rails s`) are accelerated. But, two rails subcommands
36
+ stay below the line: `rails server -d`/`--daemon` (daemonizing detaches from the process siding
37
+ manages, which would leave nothing for it to signal or clean up) and `rails dev:cache` (a
38
+ one-shot toggle of the running application's caching mode, not something to accelerate). Below the
39
+ line means passed through unaccelerated, never refused.
40
+
41
+ `siding status` and `siding doctor` describe the warm *application* process siding manages — not the
42
+ Rails web server you get from `rails server`. Once `rails server` is itself accelerated, it's easy
43
+ to conflate the two: "server" in siding's own output always means the former.
44
+
45
+ ## Installation
46
+
47
+ Add it to the `development` and `test` groups of your application's `Gemfile`:
48
+
49
+ ```ruby
50
+ group :development, :test do
51
+ gem "siding"
52
+ end
53
+ ```
54
+
55
+ Then:
56
+
57
+ ```bash
58
+ bundle install
59
+ ```
60
+
61
+ That's enough to use it — prefix any command with `siding`, as shown below. Optionally, run
62
+ `bundle exec siding init` to generate `bin/rails`, `bin/rake`, and `bin/rspec` binstub shims, so
63
+ that the plain, unprefixed commands you already run are accelerated automatically. It reports
64
+ exactly which files it wrote.
65
+
66
+ `init` exists purely to remove the friction of remembering the `siding` prefix. Each shim it writes
67
+ is a one-line `exec("siding", ...)` wrapper — nothing else depends on it, deleting the file reverts
68
+ to the plain, unaccelerated command, and an existing `bin/rails` you've edited by hand is never
69
+ overwritten. `siding <command>` behaves identically whether or not you've ever run `init`.
70
+
71
+ ## Usage
72
+
73
+ Prefix any command you would normally run:
74
+
75
+ ```bash
76
+ siding rspec test/models/user_test.rb
77
+ siding rails runner 'puts User.count'
78
+ siding rails db:migrate
79
+ ```
80
+
81
+ The accelerated run is observationally indistinguishable from the unaccelerated one: same stdout and
82
+ stderr, byte for byte and in the same order; same exit status, including death by signal; the same
83
+ interactive behavior, including debuggers and a real tty.
84
+
85
+ Siding's own logger never appear on stdout or stderr. They go to your controlling terminal, so
86
+ a redirected or piped stream contains exactly what your command wrote and nothing else.
87
+
88
+ ### Other commands
89
+
90
+ | Command | What it does |
91
+ |---------|--------------|
92
+ | `siding start` | Boots a warm application without running a command. Idempotent, and never a prerequisite — the first accelerated command boots the same thing |
93
+ | `siding status` | Whether a warm application exists, when it booted, what it has served, and recent staleness events |
94
+ | `siding stop` | Stops everything belonging to this project. Idempotent |
95
+ | `siding restart` | `stop` followed by a fresh boot |
96
+ | `siding doctor` | Why a given invocation was or was not accelerated: platform support, runtime directory state, recent boot failures |
97
+ | `siding init` | Generates binstub shims |
98
+
99
+ ### Environment variables
100
+
101
+ | Variable | Effect |
102
+ |----------|--------|
103
+ | `SIDING_DISABLE` | Truthy value runs the invocation unaccelerated. Works per-command and per-shell |
104
+ | `SIDING_TIMEOUT` | Maximum wait for a boot before surfacing the situation rather than hanging |
105
+ | `SIDING_IDLE_TIMEOUT` | Idle period after which the warm application exits (default 15 minutes) |
106
+ | `SIDING_LOG` | Raises diagnostic verbosity. Changes how much is said, never where |
107
+ | `SIDING_WATCH` | `events` (default) or `poll`. Chooses how watchcat wakes a speculative reboot while idle, never what it is validated against |
108
+
109
+ Siding is active in `development` and `test`, and stays inactive in production-like environments.
110
+ The gate is an allowlist: an unrecognized environment name defaults to inactive.
111
+
112
+ There is no option that turns staleness validation off. That is the guarantee the tool is for, and a
113
+ tool that can be put into an unsafe state on purpose will be found in one by accident.
114
+
115
+ ## Ruby API
116
+
117
+ A correctly-behaving application needs none of this. It exists for the cases automatic derivation
118
+ cannot reach on its own, and for tooling that wants to know what state a run happened against.
119
+
120
+ **Fork life cycle hooks.** Forking leaves a child with dead background threads and sockets shared
121
+ with its parent. A gem that holds either releases it before the fork and re-establishes it after:
122
+
123
+ ```ruby
124
+ Siding.before_fork { MyConnectionPool.disconnect! }
125
+ Siding.after_fork { MyConnectionPool.reconnect! }
126
+ ```
127
+
128
+ **Boot components declaration.** Siding derives what it watches from what your boot actually loaded,
129
+ so ordinary code needs no declaration. Use this only for what that cannot see — a data file read at
130
+ boot, a generated artifact:
131
+
132
+ ```ruby
133
+ Siding.boot_component "config/feature_flags.yml"
134
+ ```
135
+
136
+ If you find yourself needing this for ordinary application code, the derivation has a gap. That is a
137
+ bug worth reporting, not a line worth adding.
138
+
139
+ **Introspection.** Truthful in an unaccelerated run too, so no guard is needed:
140
+
141
+ ```ruby
142
+ Siding.accelerated? # => true / false
143
+ Siding.resolution # => "fresh", "reloaded_in_worker", "rebuild", or nil
144
+ Siding.revision # => a label for the source state this run served
145
+ Siding.boot_seconds # => how long the warm application took to boot
146
+ Siding.invocation # => all of the above, as a Hash
147
+ ```
148
+
149
+ ## Development
150
+
151
+ After checking out the repo, run `bin/setup` to install dependencies. Then:
152
+
153
+ ```bash
154
+ bundle exec rake test # unit + integration; expected green on every commit
155
+ bundle exec rake test:pty # interactive fidelity, needs a real pty
156
+ ```
157
+
158
+ The integration suite drives the real executable against the fixture application in
159
+ `test/fixtures/rails_app/`. Its bundle and databases are a prerequisite of those tasks and are
160
+ prepared automatically on a fresh clone, so there is nothing to install by hand.
161
+
162
+ Before changing behavior, read `CLAUDE.md` — its principles and invariants are binding, not
163
+ advisory.
164
+
165
+ To install the gem onto your local machine, run `bundle exec rake install`. To release a new
166
+ version, update the version number in `version.rb`, then run `bundle exec rake release`.
167
+
168
+ ## Contributing
169
+
170
+ Bug reports and pull requests are welcome on GitHub at https://github.com/y-yagi/siding. This
171
+ project is intended to be a safe, welcoming space for collaboration, and contributors are expected
172
+ to adhere to the [code of conduct](https://github.com/y-yagi/siding/blob/main/CODE_OF_CONDUCT.md).
173
+
174
+ ## License
175
+
176
+ The gem is available as open source under the terms of the
177
+ [MIT License](https://opensource.org/licenses/MIT).
178
+
179
+ ## Code of Conduct
180
+
181
+ Everyone interacting in the Siding project's codebases, issue trackers, chat rooms and mailing lists
182
+ is expected to follow the [code of conduct](https://github.com/y-yagi/siding/blob/main/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ UNIT_GLOBS = ["test/test_*.rb", "test/unit/**/*_test.rb"].freeze
7
+ INTEGRATION_GLOBS = ["test/integration/**/*_test.rb"].freeze
8
+ PTY_GLOBS = ["test/pty/**/*_test.rb"].freeze
9
+
10
+ FIXTURE_APP = File.expand_path("test/fixtures/rails_app", __dir__)
11
+
12
+ Minitest::TestTask.create(:"test:unit") { |t| t.test_globs = UNIT_GLOBS }
13
+ Minitest::TestTask.create(:"test:integration") { |t| t.test_globs = INTEGRATION_GLOBS }
14
+ Minitest::TestTask.create(:"test:pty") { |t| t.test_globs = PTY_GLOBS }
15
+ Minitest::TestTask.create(:test) { |t| t.test_globs = UNIT_GLOBS + INTEGRATION_GLOBS }
16
+
17
+ # The soak exercise, as a task rather than a documented incantation.
18
+ SOAK_GLOBS = ["test/integration/soak_test.rb"].freeze
19
+ Minitest::TestTask.create(:"test:soak") { |t| t.test_globs = SOAK_GLOBS }
20
+
21
+ task :"soak:enable" do
22
+ ENV["SIDING_SOAK"] ||= "1"
23
+ end
24
+
25
+ desc "Install the fixture application's bundle and prepare its databases"
26
+ task :"fixture:prepare" do
27
+ Bundler.with_unbundled_env do
28
+ unless system({}, "bundle", "check", chdir: FIXTURE_APP, out: File::NULL, err: File::NULL)
29
+ run_in_fixture({}, "bundle", "install")
30
+ end
31
+
32
+ %w[development test].each do |rails_env|
33
+ next if File.exist?(File.join(FIXTURE_APP, "storage", "#{rails_env}.sqlite3"))
34
+
35
+ run_in_fixture({ "RAILS_ENV" => rails_env }, "bundle", "exec", "rails", "db:prepare")
36
+ end
37
+ end
38
+ end
39
+
40
+ def run_in_fixture(env, *command)
41
+ return if system(env, *command, chdir: FIXTURE_APP)
42
+
43
+ raise "#{command.join(' ')} failed in #{FIXTURE_APP}"
44
+ end
45
+
46
+ task test: :"fixture:prepare"
47
+ task "test:integration" => :"fixture:prepare"
48
+ task "test:pty" => :"fixture:prepare"
49
+ task "test:soak" => [:"soak:enable", :"fixture:prepare"]
50
+
51
+ task default: :test
data/exe/siding ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/siding/client"
5
+ exit Siding::Client.new(ARGV).run
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Siding
4
+ module BootComponent
5
+ class << self
6
+ def add(*adding_paths)
7
+ adding_paths.flatten.each do |path|
8
+ expanded = File.expand_path(path.to_s)
9
+ paths << expanded unless paths.include?(expanded)
10
+ end
11
+ paths
12
+ end
13
+
14
+ def paths = @paths ||= []
15
+ def reset! = @paths = []
16
+ end
17
+ end
18
+ end