namespaced-gem 0.1.0.pre

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: b72c048d303c91b9d305add224623f46281534c5fa44c44b866a2ac3809cf40c
4
+ data.tar.gz: 7cd113090909aded453c6e04ebf5bfe7603a223311ec1125a5910c3190a41130
5
+ SHA512:
6
+ metadata.gz: c7e733db83c4900d393ea6b80e32dda174b8d8db7f77fdd9533de40d79526f9f270b865be00c6f9c3715df342fe467433d21eace29cabb5144815cc4aa393d2b
7
+ data.tar.gz: 92d026b40e287882e94e54dc29398ec4f48e39cdf94e05ffad72cb6b5ef972d03971e21d86850a0160e5afbf3e076bfb8fc2f5a452f7c990df8169aa2062174e
data/.idea/.gitignore ADDED
@@ -0,0 +1,46 @@
1
+ # Default ignored files
2
+ /shelf/
3
+ /workspace.xml
4
+ # Ignored default folder with query files
5
+ /queries/
6
+ # Datasource local storage ignored files
7
+ /dataSources/
8
+ /dataSources.local.xml
9
+ # Editor-based HTTP Client requests
10
+ /httpRequests/
11
+
12
+ # Zencoder local files
13
+ /zencoder/chats
14
+ /zencoder-chat-index.xml
15
+ /zencoder-chats-dedicated.xml
16
+ # Local project config
17
+ *.iml
18
+
19
+ # Added: Ignore plugin state artifacts (machine-specific)
20
+ /copilot*.xml
21
+ /GitLink.xml
22
+
23
+ # Added: Ignore task & usage statistics (contain local timestamps / paths)
24
+ /tasks.xml
25
+ /usage.statistics.xml
26
+
27
+ # Added: Local inspection profiles (developer-specific customizations)
28
+ /inspectionProfiles/
29
+
30
+ # Added: Library & index caches (regenerated per machine)
31
+ /libraries/
32
+ /indexLayout.xml
33
+ /indexes/
34
+
35
+ # Added: Terminal, SSH, and other per-user runtime config
36
+ /terminal/
37
+ /sshConfigs/
38
+
39
+ # Added: RubyMine misc SDK version drift (optional). Comment to share SDK config.
40
+ # /misc.xml
41
+
42
+ # Commit: VCS local mappings can be regenerated (optional). Comment to share settings.
43
+ /vcs.xml
44
+
45
+ # Commit: Dictionary files (local spellcheck customizations)
46
+ # /dictionaries/
@@ -0,0 +1,15 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="GitToolBoxProjectSettings">
4
+ <option name="commitMessageIssueKeyValidationOverride">
5
+ <BoolValueOverride>
6
+ <option name="enabled" value="true" />
7
+ </BoolValueOverride>
8
+ </option>
9
+ <option name="commitMessageValidationEnabledOverride">
10
+ <BoolValueOverride>
11
+ <option name="enabled" value="true" />
12
+ </BoolValueOverride>
13
+ </option>
14
+ </component>
15
+ </project>
data/.idea/modules.xml ADDED
@@ -0,0 +1,8 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="ProjectModuleManager">
4
+ <modules>
5
+ <module fileurl="file://$PROJECT_DIR$/.idea/namespaced-gem.iml" filepath="$PROJECT_DIR$/.idea/namespaced-gem.iml" />
6
+ </modules>
7
+ </component>
8
+ </project>
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0.pre] - 2026-03-07
4
+
5
+ - Initial release
@@ -0,0 +1,10 @@
1
+ # Code of Conduct
2
+
3
+ "namespaced-gem" 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 ["peter.boling@gmail.com"](mailto:"peter.boling@gmail.com").
data/HOT_HOOK.md ADDED
@@ -0,0 +1,160 @@
1
+ # Hookah::Gem
2
+
3
+ Investigation into RubyGems plugin hot-loading — specifically whether a
4
+ `rubygems_plugin.rb` carried by a **previously-uninstalled dependency gem** can
5
+ be live-loaded into the running `gem install` process.
6
+
7
+ **TL;DR — Yes, it works, via `Gem::Installer#load_plugin`.**
8
+
9
+ ---
10
+
11
+ ## How RubyGems Loads Plugins
12
+
13
+ ### Path 1 — at `gem` startup (already-installed gems only)
14
+
15
+ `GemRunner#run` calls two methods before executing any command:
16
+
17
+ ```ruby
18
+ Gem.load_env_plugins # scans $LOAD_PATH for rubygems_plugin.rb
19
+ Gem.load_plugins # scans $GEM_HOME/plugins/ for *_plugin.rb stubs
20
+ ```
21
+
22
+ These only reach gems that are **already on disk**.
23
+
24
+ ### Path 2 — hot-load during `gem install` (the interesting one)
25
+
26
+ `Gem::Installer#install` runs this sequence for **every gem** it installs:
27
+
28
+ ```
29
+ pre_install_checks
30
+ run_pre_install_hooks ← Gem.pre_install hooks fire here
31
+ extract_files
32
+ build_extensions
33
+ run_post_build_hooks ← Gem.post_build hooks fire here
34
+ generate_plugins ← writes $GEM_HOME/plugins/<name>_plugin.rb stub
35
+ write_spec
36
+ load_plugin ← HOT-LOADS the plugin into the running process
37
+ run_post_install_hooks ← Gem.post_install hooks fire here
38
+ ```
39
+
40
+ `load_plugin` (installer.rb ~line 986):
41
+
42
+ ```ruby
43
+ def load_plugin
44
+ specs = Gem::Specification.find_all_by_name(spec.name)
45
+ # Only hot-load on first install — avoids loading two versions at once.
46
+ return unless specs.size == 1
47
+
48
+ plugin_files = spec.plugins.map do |plugin|
49
+ File.join(@plugins_dir, "#{spec.name}_plugin#{File.extname(plugin)}")
50
+ end
51
+ Gem.load_plugin_files(plugin_files)
52
+ end
53
+ ```
54
+
55
+ `spec.plugins` (basic_specification.rb) discovers files via:
56
+
57
+ ```ruby
58
+ def plugins
59
+ matches_for_glob("rubygems#{Gem.plugin_suffix_pattern}")
60
+ # expands to lib/rubygems_plugin{,.rb,.so,...}
61
+ end
62
+ ```
63
+
64
+ So any gem with `lib/rubygems_plugin.rb` in its `require_paths` is a plugin gem.
65
+
66
+ ---
67
+
68
+ ## The Dependency Hot-Load Trick
69
+
70
+ `RequestSet#install` (request_set.rb) installs gems in **topological order**
71
+ (`sorted_requests` uses `TSort` / `strongly_connected_components`):
72
+ dependencies are installed **before** the gems that require them.
73
+
74
+ This creates the following opportunity:
75
+
76
+ ```
77
+ gem install hookah-gem
78
+
79
+ ├─ 1. Resolve graph: hookah-gem → hookah-core (has rubygems_plugin.rb)
80
+
81
+ ├─ 2. Install hookah-core first (leaf node)
82
+ │ └─ Gem::Installer#load_plugin fires
83
+ │ └─ hookah-core's rubygems_plugin.rb is require'd into THIS process
84
+ │ └─ Gem.pre_install / post_install / done_installing hooks register
85
+
86
+ └─ 3. Install hookah-gem
87
+ └─ run_pre_install_hooks ← hooks from hookah-core are already active!
88
+ └─ run_post_install_hooks ← same
89
+ ```
90
+
91
+ To use this pattern, add the plugin-carrying gem as a runtime dependency:
92
+
93
+ ```ruby
94
+ # hookah-gem.gemspec
95
+ spec.add_dependency "hookah-core" # hookah-core ships lib/rubygems_plugin.rb
96
+ ```
97
+
98
+ ### Critical caveat
99
+
100
+ The hot-load only fires when `Gem::Specification.find_all_by_name(spec.name).size == 1`,
101
+ i.e. **this is the very first version of the dependency on the system**. If the
102
+ user already has any version of `hookah-core` installed the plugin stub is
103
+ regenerated but the file is **not** `require`'d into the live process. The
104
+ next time the `gem` command runs (a fresh process) `Gem.load_plugins` will pick
105
+ it up from the stubs directory.
106
+
107
+ ---
108
+
109
+ ## Available Hooks (registered inside `lib/rubygems_plugin.rb`)
110
+
111
+ | Hook | Fires | Abort? |
112
+ |------|-------|--------|
113
+ | `Gem.pre_install { \|installer\| }` | Before files are extracted | `return false` raises `Gem::InstallError` |
114
+ | `Gem.post_build { \|installer\| }` | After native exts compile | `return false` removes gem dir + raises |
115
+ | `Gem.post_install { \|installer\| }` | After gem fully installed | No |
116
+ | `Gem.done_installing { \|dep_installer, specs\| }` | After entire batch done | No |
117
+
118
+ `installer` is a `Gem::Installer`; `installer.spec` is the `Gem::Specification`
119
+ being installed. `dep_installer` is the `Gem::DependencyInstaller` that drove
120
+ the whole `gem install` run.
121
+
122
+ See `lib/rubygems_plugin.rb` in this repo for an annotated skeleton.
123
+
124
+ ## Installation
125
+
126
+ TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
127
+
128
+ Install the gem and add to the application's Gemfile by executing:
129
+
130
+ ```bash
131
+ bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
132
+ ```
133
+
134
+ If bundler is not being used to manage dependencies, install the gem by executing:
135
+
136
+ ```bash
137
+ gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
138
+ ```
139
+
140
+ ## Usage
141
+
142
+ TODO: Write usage instructions here
143
+
144
+ ## Development
145
+
146
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
147
+
148
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
149
+
150
+ ## Contributing
151
+
152
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/hookah-gem. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/hookah-gem/blob/main/CODE_OF_CONDUCT.md).
153
+
154
+ ## License
155
+
156
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
157
+
158
+ ## Code of Conduct
159
+
160
+ Everyone interacting in the Hookah::Gem project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/hookah-gem/blob/main/CODE_OF_CONDUCT.md).
@@ -0,0 +1,325 @@
1
+ # Hot-Load Analysis: Can `Gem::Installer#load_plugin` Solve the Chicken-and-Egg Problem?
2
+
3
+ **TL;DR — The hot-load alone cannot solve the cold-start resolution problem
4
+ (resolution runs before installation), but it enables a metadata-based
5
+ two-phase approach via `MetadataDepsHook`. More importantly, when the plugin is
6
+ pre-installed (the common case), all `gem install` paths work today — including
7
+ direct `gem install @kaspth/oaken`.**
8
+
9
+ ---
10
+
11
+ ## The Chicken-and-Egg Problem (recap)
12
+
13
+ When a user runs `gem install my-gem` and `my-gem`'s gemspec contains:
14
+
15
+ ```ruby
16
+ spec.add_dependency "namespaced-gem"
17
+ spec.add_dependency "https://beta.gem.coop/@myspace/foo", "~> 1.0"
18
+ ```
19
+
20
+ …and `namespaced-gem` is **not yet installed**, the patches from
21
+ `rubygems_plugin.rb` are not loaded. RubyGems sees the URI string as a literal
22
+ gem name, queries rubygems.org for it, finds nothing, and aborts.
23
+
24
+ ---
25
+
26
+ ## Exact `gem install` Execution Sequence
27
+
28
+ ```
29
+ gem install my-gem
30
+
31
+ ├─ 1. BOOT — Gem.load_plugins / Gem.load_env_plugins
32
+ │ └─ Only loads rubygems_plugin.rb from ALREADY-INSTALLED gems
33
+ │ └─ namespaced-gem NOT installed → NO patches loaded
34
+
35
+ ├─ 2. RESOLVE — DependencyInstaller#resolve_dependencies
36
+ │ ├─ Fetches my-gem's spec from rubygems.org
37
+ │ ├─ Sees deps: ["namespaced-gem", "https://beta.gem.coop/@myspace/foo"]
38
+ │ ├─ Tries to look up "https://beta.gem.coop/@myspace/foo" on rubygems.org
39
+ │ ├─ No match found
40
+ │ └─ ❌ ABORTS — resolution fails BEFORE any gem is installed
41
+
42
+ └─ 3. INSTALL (never reached)
43
+ ├─ Would install namespaced-gem first (leaf dep, via TSort)
44
+ ├─ Gem::Installer#load_plugin would fire
45
+ ├─ rubygems_plugin.rb would be require'd into running process
46
+ ├─ All patches would activate
47
+ └─ my-gem install would proceed with patches active
48
+ ```
49
+
50
+ **The hot-load fires at step 3, but the failure is at step 2.**
51
+
52
+ The resolution phase (`RequestSet#resolve`) runs in its entirety before
53
+ `RequestSet#install` begins. The `InstallerSet#find_all` method encounters the
54
+ URI dependency string, has no patch to intercept it, queries the default source
55
+ (rubygems.org), finds no gem by that name, and the resolver aborts with
56
+ `Gem::UnsatisfiableDependencyError`.
57
+
58
+ ---
59
+
60
+ ## Impact on Each Use Case
61
+
62
+ ### Use Case 1: Gem Authors (`gem install my-gem`, first time)
63
+
64
+ **Hot-load: too late for cold-start.** When the plugin is NOT pre-installed,
65
+ resolution fails before install begins.
66
+
67
+ However, when the plugin IS pre-installed (Use Cases 2–4), `gem install my-gem`
68
+ with URI deps in the gemspec **works today** — `GemResolverPatch` intercepts
69
+ the resolver, `ApiSpecPatch` synthesizes specs from Compact Index data, and
70
+ `DownloadPatch` handles namespace download errors.
71
+
72
+ **For the Bundler path** (`bundle install` with `gemspec`), the hot-load is
73
+ **irrelevant** — the existing `BundlerIntegration` TracePoint approach already
74
+ handles this. Bundler loads `Bundler::Dsl`, the TracePoint fires, patches
75
+ activate, and URI deps are remapped before Bundler's own resolution begins.
76
+ This path works today.
77
+
78
+ ### Use Case 2: Application Developers (pre-installed)
79
+
80
+ **Hot-load: irrelevant.** The plugin is already installed (via
81
+ `gem install namespaced-gem`), so `rubygems_plugin.rb` loads at boot via
82
+ `Gem.load_plugins`. The patches are active before any gemspec is evaluated.
83
+
84
+ ### Use Case 3: Global Installation (pre-installed)
85
+
86
+ **Same as Use Case 2.** Plugin already in gem path. Hot-load not involved.
87
+
88
+ ---
89
+
90
+ ## What the Hot-Load CAN Do
91
+
92
+ While the hot-load can't fix the resolution-phase failure, it **does** enable:
93
+
94
+ 1. **`post_install` / `done_installing` hooks** — After `namespaced-gem` is
95
+ installed as a leaf dependency (in a separate install invocation, or as part
96
+ of a successful resolution that didn't include URI deps), hooks registered
97
+ by `rubygems_plugin.rb` fire for all subsequent gems in the same batch.
98
+
99
+ 2. **Mid-batch patch activation** — If `namespaced-gem` is installed as part
100
+ of a batch (e.g. `gem install namespaced-gem other-gem`), the hot-load
101
+ activates all patches before `other-gem`'s install phase. However, the
102
+ **resolution** for the entire batch still happened before any installs, so
103
+ this only helps if `other-gem` doesn't have URI deps that need resolving
104
+ (or if they were resolved via other means).
105
+
106
+ ---
107
+
108
+ ## Viable Approaches Leveraging the Hot-Load Discovery
109
+
110
+ ### Approach A: Metadata-Based Two-Phase Resolution (recommended)
111
+
112
+ Instead of putting URI deps directly in `add_dependency` (which must survive
113
+ resolution on rubygems.org), encode them in `spec.metadata`:
114
+
115
+ ```ruby
116
+ # Published gemspec on rubygems.org
117
+ Gem::Specification.new do |spec|
118
+ spec.name = "my-gem"
119
+ spec.version = "1.0.0"
120
+
121
+ # namespaced-gem is a normal runtime dep — resolves fine on rubygems.org
122
+ spec.add_dependency "namespaced-gem"
123
+
124
+ # URI deps stored in metadata — invisible to RubyGems' resolver
125
+ spec.metadata["namespaced_dependencies"] = [
126
+ "https://beta.gem.coop/@myspace/foo ~> 1.0",
127
+ "@myorg/bar >= 2.0"
128
+ ].join("\n")
129
+
130
+ # Normal deps work as usual
131
+ spec.add_dependency "rack", "~> 3.0"
132
+ end
133
+ ```
134
+
135
+ The flow becomes:
136
+
137
+ ```
138
+ gem install my-gem
139
+
140
+ ├─ 1. BOOT — no namespaced-gem installed → no patches
141
+
142
+ ├─ 2. RESOLVE — sees deps: ["namespaced-gem", "rack"]
143
+ │ └─ ✅ All plain names — resolves fine on rubygems.org
144
+
145
+ ├─ 3. INSTALL (topological order)
146
+ │ ├─ Install namespaced-gem (leaf dep)
147
+ │ │ └─ load_plugin fires → rubygems_plugin.rb loaded
148
+ │ │ └─ All patches activate
149
+ │ │ └─ done_installing hook registers
150
+ │ ├─ Install rack
151
+ │ └─ Install my-gem
152
+ │ └─ post_install hook fires
153
+ │ └─ Reads spec.metadata["namespaced_dependencies"]
154
+ │ └─ Parses URI deps
155
+ │ └─ Triggers second resolution pass for URI deps
156
+ │ (patches are now active!)
157
+ │ └─ Installs namespace-sourced gems
158
+
159
+ └─ 4. DONE — all gems installed ✅
160
+ ```
161
+
162
+ **This is the only approach that enables single-command `gem install my-gem`
163
+ without pre-installation of the plugin.**
164
+
165
+ #### Implementation sketch
166
+
167
+ A new `MetadataDepsHook` module, loaded by `rubygems_plugin.rb`, would:
168
+
169
+ 1. Register a `Gem.done_installing` hook (or `Gem.post_install` per-gem).
170
+ 2. After each gem installs, check `spec.metadata["namespaced_dependencies"]`.
171
+ 3. If present, parse the URI dep strings and trigger a
172
+ `Gem::DependencyInstaller` for each, with all patches now active.
173
+
174
+ #### Trade-offs
175
+
176
+ - **Pro:** Single-command install works. No pre-installation required.
177
+ - **Pro:** Compatible with the hot-load mechanism — patches activate before
178
+ the hook fires.
179
+ - **Con:** URI deps are not visible in the standard `add_dependency` list.
180
+ Tools that inspect gemspec dependencies won't see them.
181
+ - **Con:** Requires gem authors to use `metadata` instead of (or in addition
182
+ to) `add_dependency` for URI deps — a less intuitive API.
183
+ - **Con:** The second resolution pass is a separate install; version conflicts
184
+ between the first and second pass must be handled.
185
+
186
+ ### Approach B: Dual Encoding (best of both worlds)
187
+
188
+ Gem authors use **both** `add_dependency` (for Bundler, which handles URI deps
189
+ via `BundlerIntegration`) **and** `metadata` (for `gem install`, via the
190
+ hot-load hook):
191
+
192
+ ```ruby
193
+ Gem::Specification.new do |spec|
194
+ spec.add_dependency "namespaced-gem"
195
+
196
+ # For Bundler (handled by BundlerIntegration patch):
197
+ spec.add_dependency "https://beta.gem.coop/@myspace/foo", "~> 1.0"
198
+
199
+ # For gem install (handled by done_installing hook after hot-load):
200
+ spec.metadata["namespaced_dependencies"] = \
201
+ "https://beta.gem.coop/@myspace/foo ~> 1.0"
202
+ end
203
+ ```
204
+
205
+ When `gem install my-gem` runs:
206
+ - Resolution ignores the URI dep (rubygems.org returns no match, but the gem
207
+ itself still resolves because the URI dep is not "required" for resolution to
208
+ complete — **this needs verification**; if the resolver hard-fails on
209
+ unresolvable deps, this won't work).
210
+
211
+ **⚠️ Problem:** RubyGems' resolver will try to resolve ALL `add_dependency`
212
+ entries. An unresolvable URI dep causes `Gem::UnsatisfiableDependencyError`
213
+ and aborts the entire resolution. Dual encoding only works if we can teach
214
+ the resolver to **skip** URI deps during resolution and defer them.
215
+
216
+ This could be done with a minimal boot-time shim (see Approach C).
217
+
218
+ ### Approach C: Minimal Boot-Time Shim (no full plugin needed)
219
+
220
+ Ship a **second, tiny gem** (`namespaced-gem-shim`) that contains only a
221
+ `rubygems_plugin.rb` with a single patch: teach `InstallerSet#find_all` to
222
+ **silently skip** URI-named deps during resolution (returning an empty array
223
+ instead of failing). The full `namespaced-gem` plugin then handles actual
224
+ installation via the `done_installing` hot-load hook.
225
+
226
+ ```ruby
227
+ # namespaced-gem-shim/lib/rubygems_plugin.rb
228
+ # This gem is tiny and can be pre-installed globally, or it can be the
229
+ # gem that gets hot-loaded.
230
+
231
+ # Teach the resolver to not crash on URI dep names.
232
+ module NamespacedGemShim
233
+ module InstallerSetSkipUri
234
+ def find_all(req)
235
+ return [] if req.name.match?(%r{\Ahttps?://|^@[^/]+/|^pkg:gem/})
236
+ super
237
+ end
238
+ end
239
+ end
240
+
241
+ Gem::Resolver::InstallerSet.prepend(NamespacedGemShim::InstallerSetSkipUri)
242
+ ```
243
+
244
+ With this shim installed:
245
+ 1. Resolution encounters the URI dep, `find_all` returns `[]`, resolver
246
+ treats it as an optional/unsatisfiable dep (depending on `type`).
247
+ 2. All other deps resolve normally.
248
+ 3. `namespaced-gem` installs as a leaf dep, hot-loads full patches.
249
+ 4. `done_installing` hook processes the URI deps that were skipped.
250
+
251
+ **⚠️ Problem:** Runtime deps are not optional — the resolver will still fail
252
+ if it can't satisfy a required dep. The shim would need to also patch the
253
+ resolver's conflict-handling to treat URI deps as "deferred" rather than
254
+ "missing."
255
+
256
+ ---
257
+
258
+ ## Verdict
259
+
260
+ | Approach | Single-command install? | Gem author API | Complexity |
261
+ |----------|----------------------|----------------|------------|
262
+ | **A: Metadata only** | ✅ Yes | `metadata` field (non-standard) | Medium |
263
+ | **B: Dual encoding** | ❌ Resolver aborts on URI dep | Both `add_dependency` + `metadata` | High |
264
+ | **C: Shim gem** | ⚠️ Depends on resolver skip | Standard `add_dependency` | Very High |
265
+ | **Pre-install** (status quo) | Requires 2 commands | Standard `add_dependency` | None |
266
+
267
+ ### Recommendation
268
+
269
+ **For `gem install` (Use Cases 2–4, plugin pre-installed):** This **already
270
+ works**. `GemResolverPatch` intercepts URI deps during resolution,
271
+ `ApiSpecPatch` synthesizes specs from Compact Index data (bypassing the missing
272
+ Marshal endpoint), and the namespace server serves `/gems/` for downloads.
273
+
274
+ **For `gem install my-gem` (Use Case 1, cold-start):** **Approach A** has been
275
+ implemented as `MetadataDepsHook` (metadata-based deps + `done_installing`
276
+ hook). This enables single-command `gem install my-gem` via the hot-load
277
+ mechanism when gem authors encode URI deps in
278
+ `spec.metadata["namespaced_dependencies"]`.
279
+
280
+ **For Bundler (all use cases):** The current architecture is already correct.
281
+ The `BundlerIntegration` TracePoint-based deferred patching handles URI deps
282
+ in `add_dependency` seamlessly. **No changes needed.**
283
+
284
+ **For `gem install` of direct URI names** (`gem install @kaspth/oaken`):
285
+ This **works today** (plugin must be pre-installed). The namespace server
286
+ serves both the Compact Index and `/gems/` endpoints.
287
+
288
+ ### Implementation Status (completed)
289
+
290
+ 1. ✅ `lib/namespaced/gem/metadata_deps_hook.rb` — `Gem.done_installing` hook
291
+ that reads `namespaced_dependencies` from installed specs' metadata and
292
+ triggers a second install pass.
293
+ 2. ✅ Wired into `rubygems_plugin.rb` alongside existing patches (step 6).
294
+ 3. ✅ `Namespaced::Gem.add_namespaced_dependency(spec, uri, version)` helper
295
+ that writes both `add_dependency` and `metadata` automatically.
296
+ 4. ✅ `add_dependency` URI support works for both the Bundler and
297
+ `gem install` paths (when plugin is pre-installed).
298
+
299
+ ---
300
+
301
+ ## The Hot-Load Mechanism IS Valuable
302
+
303
+ The discovery in HOT_HOOK.md is genuinely important. It confirms that:
304
+
305
+ 1. `rubygems_plugin.rb` files hot-load during `gem install` via
306
+ `Gem::Installer#load_plugin` — this is a documented, reliable RubyGems
307
+ feature.
308
+ 2. TSort-based topological install order guarantees the plugin gem installs
309
+ before dependents.
310
+ 3. Hooks registered by the hot-loaded plugin (`pre_install`, `post_install`,
311
+ `done_installing`) are active for all subsequent gems in the batch.
312
+ 4. The hot-load fires only on first install (`find_all_by_name.size == 1`) —
313
+ upgrades don't re-trigger it (the next boot picks up the new version).
314
+
315
+ The key insight is that **hooks fire post-install, not post-resolve**. The
316
+ hot-load enables a **second-phase install** pattern where URI deps are deferred
317
+ past the initial resolution and handled by hooks after the plugin is live.
318
+ This pattern is implemented in `MetadataDepsHook` and enables the cold-start
319
+ `gem install my-gem` path (when the gem author uses
320
+ `spec.metadata["namespaced_dependencies"]`).
321
+
322
+ For the pre-installed plugin case (Use Cases 2–4), the hot-load is not needed —
323
+ the plugin loads at boot and all patches are active for the entire `gem install`
324
+ pipeline, including resolution. **Both `gem install @kaspth/oaken` and
325
+ `gem install my-gem` (with URI deps in the gemspec) work today.**