bundler-spinel 0.2.1 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b5a1d4be2ef6216a5cd1241e7ef27dd6ee5222f79f21e6924b8c52c0fe02f35b
4
- data.tar.gz: 0a36f37f19a5333e42515a23ec80524ee99bd2d81322e573bb7b3376b4a231b9
3
+ metadata.gz: 8518bfe1ec1303fb7116e33738abb8246bb3c7072afca1b70057523f824b4a2c
4
+ data.tar.gz: 5d945e9ca4fa4409f5ff325a6eae9d917469e5c0e7ceb2ef69c8c356551c0fda
5
5
  SHA512:
6
- metadata.gz: e9fd87e3769ca2246b536ef82c4e6c5c6a704021183008516ce7b41ebf10f0e310d03e297b06887c86cb9f97dc8ae388c6f45f01ce92f1a1a7af791763af0af9
7
- data.tar.gz: '0894f3ed497a5b7c2e226d5ec5b86ce8b60872775d12c72090df8a6da3f8eead78a93b8ced7e994769e0359a48850be22526192683351a1b16dfe49714744013'
6
+ metadata.gz: ff071af5cde1af69f32f92b509092ec6d535173dfbbdbe84c02940b5118abb1f7cafacc610cc26a4c3e20263426e4a7422560ba166c766ff296d391be1758e34
7
+ data.tar.gz: e704f7c8a825a0cdb278c604a86bb38fd9363ac0fe8fe7406c88c52e9b77e1a76cd2675036d503b3e80fe488f27f3ff1820c7803428989f7ac1930bdaff9a412
data/CHANGELOG.md CHANGED
@@ -4,6 +4,108 @@ All notable changes to `bundler-spinel` are documented here. The format
4
4
  follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the
5
5
  project aims to follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.4.0] — 2026-06-15
8
+
9
+ ### Fixed
10
+ - **Probe: a require-only compile failure is the load-path limit, not
11
+ `analyze-failed` (spinel master b60fbd7 absorption).** Since matz/spinel's
12
+ #1383 fix, an unresolvable plain `require "gem/sub"` makes `spinel -c` exit
13
+ non-zero instead of warning and continuing. Because that idiom is
14
+ near-universal, the b60fbd7 corpus reprobe spuriously turned thousands of
15
+ previously-`clean` gems into `rejected:analyze-failed`. The probe now detects
16
+ a compile that failed *solely* on an unresolvable `require` (the
17
+ `CallNode \`require\`` form, with no real codegen error and no other
18
+ unsupported call) and classifies it `risky [load-path:require]` — the
19
+ documented no-load-path limitation a real Spinel project resolves by
20
+ vendoring. Real codegen errors still reject. +`test/probe_classify_test.rb`.
21
+ - **Engine rev detection for worktrees + frozen copies.** `Engine#rev` fell
22
+ back to an opaque `bin:<hash>` (a) in a git *worktree*, where `.git` is a
23
+ file not a directory, and (b) for a frozen/detached engine copy with no
24
+ `.git`. Both now resolve to `git:<short-sha>`: worktree `.git` files are
25
+ honored (`File.exist?`, not `File.directory?`), and a `.spinel_rev` stamp
26
+ (a bare HEAD sha written at freeze time) is read first. Keeps the ledger
27
+ key — and the history/diff tooling that joins on it — consistent across the
28
+ build layouts a reprobe actually uses. `bin/reprobe-corpus.sh`'s frozen
29
+ engines now carry the stamp.
30
+ - **cmake build-units on macOS (spinelgems#21).** The Vendorer now sets the
31
+ SDK libc++ include path (`CPLUS_INCLUDE_PATH` via `xcrun --show-sdk-path`)
32
+ for cmake build-unit invocations on Darwin — without it every C++ unit died
33
+ with `fatal error: 'array' file not found` (toy#27's Mac cold-start LIB
34
+ leg; fix verified on an M2). Caller-set `CPLUS_INCLUDE_PATH` is preserved
35
+ (SDK path appends); missing/failing `xcrun` degrades to the previous
36
+ behavior.
37
+
38
+ ### Added
39
+ - **Opt-in build-units + variant build dirs (spinelgems#20).** An `optional`
40
+ entry may declare `"default": "disabled"`: a plain `vendor` skips its build
41
+ entirely (placeholder ← `disabled_cflags`) until the consumer enables it via
42
+ `--with-ext NAME` / `SPINEL_EXT_ENABLE` — what a CUDA unit needs to not
43
+ attempt nvcc configures on a CUDA-less box. Explicit disable beats enable;
44
+ unknown enable names warn loud. cmake units take `"build_dir"` (default
45
+ `"build"`, relative-only) so two entries can build one shared source dir
46
+ into variant trees (`build/` vs `build-cuda/`), and source dirs are copied
47
+ once per vendor run — a second entry sharing `dir` no longer wipes the first
48
+ unit's just-built artifacts. Unblocks toy merging its staged
49
+ `spinel-ext-gpu.json` into `spinel-ext.json` (toy#45 Phase 3 → publish).
50
+ - **A gem's `sig/*.rbs` acts as the type root (spinelgems#13).** Spinel infers
51
+ param types from call sites (whole-program, closed-world), so an *uncalled*
52
+ public method widens to `int`/`poly` — the failure mode hand-written seed
53
+ blocks exist to patch. `--rbs` was verified to re-pin uncalled methods' param,
54
+ return, and ivar types, so the tooling now treats a shipped `sig/` tree as
55
+ authoritative: `spinel-compat verify` auto-passes `--rbs <gem>/sig` when the
56
+ gem ships signatures (`--rbs DIR` to override, `--no-rbs` to opt out), and
57
+ records an `rbs:sig` provenance tag in the verdict reasons. `vendor` copies
58
+ each gem's `sig/` alongside `lib/`, aggregates all of them under
59
+ `<into>/sig/<gem>/`, and advertises the single `--rbs <into>/sig` root in
60
+ `deps.rb` and the CLI output. A spinel-native gem (SpinelKit, the Tep
61
+ batteries) ships one standard Ruby artifact — no seed soup, no manifest keys.
62
+
63
+ ## [0.3.0] — 2026-06-08
64
+
65
+ ### Added
66
+ - **`spinel-compat vendor` build-units (spinelgems#14).** `spinel-ext.json` now
67
+ supports a `build` entry — a declared native build (`tool: cmake | make`, with
68
+ `dir`, `args`, `targets`, `artifacts`, and `patches`) run **inside the
69
+ consumer's vendor tree**, with link flags expanded relative to it (`{dir}` and
70
+ cross-entry `{dir:NAME}`). This lets a heavy-native gem — toy's vendored ggml
71
+ CMake build plus its tinynn shims — vendor **self-contained and relocatable**,
72
+ the same end state tep's small per-`.c` shims already had. A
73
+ `SPINEL_EXT_<PLACEHOLDER>` override substitutes prebuilt flags and skips the
74
+ build. Declared `patches` apply with stack-level already-applied detection, so
75
+ a `path:`-sourced dev checkout (already patched) vendors cleanly. Proven
76
+ end-to-end: a consumer vendors toy, the archives build in-tree, a
77
+ Spinel-compiled program links and runs, with zero absolute paths and survival
78
+ across a project move. Strictly narrower than `extconf.rb` (no free-form
79
+ shell; declared artifacts) — the Spinel analogue of a gemspec `extensions:`.
80
+ - **`spinel-compat vendor` handles transitive gem→gem dependencies
81
+ (spinelgems#19).** Vendoring a gem that depends on another vendored gem now
82
+ works: `deps.rb` is emitted in **topological order** (every gem's runtime
83
+ dependencies load before it, via a DFS over `spec.dependencies` with a stable
84
+ alphabetical tiebreak and a cycle guard) instead of the lockfile's alphabetical
85
+ order, and it prepends each vendored gem's `lib` to `$LOAD_PATH` so a
86
+ dependent's plain `require "<depgem>"` resolves under CRuby too. Spinel (no load
87
+ path) ignores both and relies on the topo-ordered `require_relative`s, so the
88
+ one `deps.rb` is correct under both runtimes — verified identical output on a
89
+ two-gem fixture. Unblocks `tep` → `spinel_kit` (the new stdlib-surface gem)
90
+ on the clean `gem "spinel_kit"` + vendor path.
91
+ - **`spinel-compat why <gem>` (spinelgems#12).** A legible "why doesn't this gem
92
+ work (yet)?" report: a plain-English cause, a category (native C-ext / Spinel
93
+ limitation / fixable compiler bug / dependency-blocked / metaprogramming), the
94
+ concrete evidence (the CRuby-vs-Spinel diff, the unresolved calls, the missing
95
+ require, the hard construct), and — most usefully — whether the verdict is
96
+ **terminal** (needs an upstream port or compiler feature) or **fixable** (a
97
+ tracked compiler bug that can graduate). Reads the dominant-rev ledger entry
98
+ like the catalog; `--probe` / `--dir` explains a fresh live probe instead.
99
+
100
+ ### Changed
101
+ - **`spinel-ext.json` wiring now warns on manifest drift.** A declared
102
+ placeholder that matches no vendored `.rb` emits a loud warning at vendor time
103
+ — replacing the per-gem "cflags canary" constants consumers maintained by hand.
104
+ - **Catalog/site rendering** advanced across several engine-rev reprobes (signals
105
+ lead every verdict tier; the download floor is off by default; human
106
+ attestations earn a verified rung; a load-bearing-gems roadmap page). These
107
+ affect the rendered spinelgems.org catalog, not the plugin's gating behaviour.
108
+
7
109
  ## [0.2.1] — 2026-06-01
8
110
 
9
111
  ### Fixed
@@ -21,6 +21,7 @@ module Bundler
21
21
  when "install-engine" then cmd_install_engine(argv)
22
22
  when "init" then cmd_init(argv)
23
23
  when "probe" then cmd_probe(argv)
24
+ when "why" then cmd_why(argv)
24
25
  when "verify" then cmd_verify(argv)
25
26
  when "vendor" then cmd_vendor(argv)
26
27
  when "check" then cmd_check(argv)
@@ -29,6 +30,7 @@ module Bundler
29
30
  when "build-site" then cmd_build_site(argv)
30
31
  when "build-db" then cmd_build_db(argv)
31
32
  when "build-history" then cmd_build_history(argv)
33
+ when "build-load-bearing" then cmd_build_load_bearing(argv)
32
34
  when "server" then cmd_server(argv)
33
35
  when "ledger" then cmd_ledger(argv)
34
36
  when "diff" then cmd_diff(argv)
@@ -91,12 +93,64 @@ module Bundler
91
93
  v.rejected? ? 1 : 0
92
94
  end
93
95
 
96
+ # why NAME [VERSION] [--rev REV] [--probe] [--dir PATH]
97
+ # Legible "why doesn't this gem work (yet)?" report (spinelgems#12).
98
+ # Reads the recorded verdict (dominant rev, like the catalog) by default;
99
+ # --probe / --dir explains a fresh live probe instead.
100
+ def cmd_why(argv)
101
+ require_relative "why"
102
+ dir = (i = argv.index("--dir")) ? argv.delete_at(i + 1).tap { argv.delete_at(i) } : nil
103
+ rev = (k = argv.index("--rev")) ? argv.delete_at(k + 1).tap { argv.delete_at(k) } : nil
104
+ live = !!argv.delete("--probe") || !!dir
105
+ name = argv.shift or raise Error, "usage: spinel-compat why NAME [VERSION] [--rev REV] [--probe] [--dir PATH]"
106
+ version = argv.shift
107
+
108
+ if live
109
+ engine = Engine.new
110
+ src = dir ? File.expand_path(dir) : GemFetcher.new.fetch(name, version || latest_version(name))
111
+ v = Probe.new(engine, Ledger.new).probe(name, version || "path", src)
112
+ Why.new(out: @out).report(v, source: "live probe")
113
+ else
114
+ v = ledger_pick(name, version: version, rev: rev)
115
+ unless v
116
+ @out.puts " no recorded verdict for #{name}#{version ? " #{version}" : ''}. " \
117
+ "Run `spinel-compat why #{name} --probe` to evaluate it now."
118
+ return 1
119
+ end
120
+ Why.new(out: @out).report(v, source: "ledger #{v.rev}")
121
+ end
122
+ 0
123
+ end
124
+
125
+ # The authoritative ledger entry for a gem: a specific --rev if asked,
126
+ # else the entry at the ledger's dominant rev (the rev most rows share —
127
+ # the same target the catalog renders), else the most recent seen.
128
+ def ledger_pick(name, version: nil, rev: nil)
129
+ entries = []
130
+ rev_counts = Hash.new(0)
131
+ Ledger.new.each do |v|
132
+ rev_counts[v.rev] += 1
133
+ next unless v.gem == name
134
+ next if version && v.version != version
135
+ entries << v
136
+ end
137
+ return nil if entries.empty?
138
+ return entries.select { |v| v.rev == rev }.last if rev
139
+
140
+ target = rev_counts.max_by { |_, c| c }&.first
141
+ entries.select { |v| v.rev == target }.last || entries.last
142
+ end
143
+
94
144
  def cmd_verify(argv)
95
145
  dir = (i = argv.index("--dir")) ? argv.delete_at(i + 1).tap { argv.delete_at(i) } : nil
96
146
  smoke = (j = argv.index("--smoke")) ? argv.delete_at(j + 1).tap { argv.delete_at(j) } : nil
97
147
  full = !!argv.delete("--full")
98
148
  tests = !!argv.delete("--tests")
99
- name = argv.shift or raise Error, "usage: spinel-compat verify NAME [VERSION] [--dir PATH] [--smoke FILE | --tests] [--full]"
149
+ # --rbs DIR / --no-rbs: explicit type root / opt out. Default :auto
150
+ # use the gem's own sig/*.rbs when shipped (spinelgems#13).
151
+ rbs = (k = argv.index("--rbs")) ? argv.delete_at(k + 1).tap { argv.delete_at(k) } : :auto
152
+ rbs = false if argv.delete("--no-rbs")
153
+ name = argv.shift or raise Error, "usage: spinel-compat verify NAME [VERSION] [--dir PATH] [--smoke FILE | --tests] [--full] [--rbs DIR | --no-rbs]"
100
154
  engine = Engine.new
101
155
  if dir
102
156
  version = argv.shift || "path"
@@ -114,7 +168,7 @@ module Bundler
114
168
  smoke = File.join(Dir.tmpdir, "__spinel_tests_#{name}.rb")
115
169
  File.write(smoke, runner)
116
170
  end
117
- v = Verifier.new(engine, Ledger.new).verify(name, version, gem_dir, smoke: smoke && File.expand_path(smoke), full: full)
171
+ v = Verifier.new(engine, Ledger.new).verify(name, version, gem_dir, smoke: smoke && File.expand_path(smoke), full: full, rbs: rbs)
118
172
  print_verdict(v)
119
173
  File.delete(smoke) if tests && smoke && File.exist?(smoke)
120
174
  (v.verified? || v.loaded?) ? 0 : 1
@@ -136,13 +190,26 @@ module Bundler
136
190
  ext_disable << argv.delete_at(d + 1).to_s
137
191
  argv.delete_at(d)
138
192
  end
193
+ # --with-ext NAME : opt IN to a default-disabled entry, e.g. a CUDA
194
+ # build-unit (repeatable; also SPINEL_EXT_ENABLE). spinelgems#20.
195
+ ext_enable = []
196
+ while (w = argv.index("--with-ext"))
197
+ ext_enable << argv.delete_at(w + 1).to_s
198
+ argv.delete_at(w)
199
+ end
139
200
  lock = argv.shift || "Gemfile.lock"
140
201
  raise Error, "no #{lock}; run `bundle lock` first" unless File.exist?(lock)
141
202
 
142
- res = Vendorer.new.vendor(lock, into: into, ext_overrides: ext_overrides, ext_disable: ext_disable)
203
+ res = Vendorer.new.vendor(lock, into: into, ext_overrides: ext_overrides,
204
+ ext_disable: ext_disable, ext_enable: ext_enable)
143
205
  ext = res[:extensions].to_i
144
206
  @out.puts "vendored #{res[:count]} gem(s)#{ext.positive? ? " (+#{ext} C ext)" : ''} -> #{res[:into]}"
145
207
  @out.puts " require_relative \"#{res[:into]}/deps\" from your Spinel entrypoint"
208
+ sigs = res[:sig_gems] || []
209
+ unless sigs.empty?
210
+ @out.puts " #{sigs.size} gem(s) ship sig/*.rbs type roots -> #{res[:into]}/sig"
211
+ @out.puts " compile with: spinel ... --rbs #{res[:into]}/sig (spinelgems#13)"
212
+ end
146
213
  0
147
214
  end
148
215
 
@@ -308,6 +375,13 @@ module Bundler
308
375
  0
309
376
  end
310
377
 
378
+ def cmd_build_load_bearing(argv)
379
+ out = (j = argv.index("--out")) ? argv[j + 1] : raise(Error, "build-load-bearing needs --out FILE")
380
+ f = LoadBearing.new.build_html(File.expand_path(out))
381
+ @out.puts "built load-bearing -> #{f}"
382
+ 0
383
+ end
384
+
311
385
  # Serve the static site + (with --store) the Compact Index from one process
312
386
  # — what the deploy host runs. Port defaults to $PORT (Upsun) then 9292.
313
387
  def cmd_server(argv)
@@ -410,8 +484,11 @@ module Bundler
410
484
  spinel-compat install-engine [REV] fetch+build the Spinel compiler -> ~/.cache/spinel
411
485
  spinel-compat init [DIR] scaffold a Spinel+Tep project (Gemfile, app.rb, bin/build)
412
486
  spinel-compat probe NAME [VERSION] probe one gem, record a verdict
487
+ spinel-compat why NAME [--probe] legible "why doesn't this work (yet)?" report
413
488
  spinel-compat verify NAME [--smoke F] differential CRuby-vs-Spinel run -> verified
414
- spinel-compat vendor [LOCK] [--into D] place deps where Spinel finds them + deps.rb
489
+ [--rbs DIR | --no-rbs] type root: gem's sig/*.rbs (auto when shipped)
490
+ spinel-compat vendor [LOCK] [--into D] place deps where Spinel finds them + deps.rb (+ sig/ roots)
491
+ [--with-ext NAME | --no-ext NAME] opt in/out of optional C-ext units
415
492
  spinel-compat check [LOCK] [--strict] gate a Gemfile.lock (exit 1 if rejected)
416
493
  spinel-compat survey GEM... | --list F wholesale review -> reason histogram
417
494
  spinel-compat serve --store DIR curated source (only vetted gems)
@@ -76,7 +76,17 @@ module Bundler
76
76
  private
77
77
 
78
78
  def compute_rev
79
- if File.directory?(File.join(@dir, ".git"))
79
+ # A frozen/detached engine copy (bin/reprobe-corpus.sh's
80
+ # spinel-frozen-<rev>, or a build whose .git was stripped) can carry a
81
+ # `.spinel_rev` stamp — a bare HEAD sha written at freeze time — so it
82
+ # still keys to its real git rev instead of an opaque binary hash.
83
+ stamp = File.join(@dir, ".spinel_rev")
84
+ if File.file?(stamp) && !(s = File.read(stamp).strip[0, 12].to_s).empty?
85
+ return "git:#{s}"
86
+ end
87
+ # `.git` is a *file* in a git worktree (a gitdir pointer), not a dir —
88
+ # File.exist? covers both so worktree checkouts report git:, not bin:.
89
+ if File.exist?(File.join(@dir, ".git"))
80
90
  sha = capture("git", "-C", @dir, "rev-parse", "--short", "HEAD")
81
91
  if sha && !sha.empty?
82
92
  dirty = capture("git", "-C", @dir, "status", "--porcelain")
@@ -26,6 +26,89 @@ module Bundler
26
26
  "(<code>e2e010c</code>); together with the new <code>instance_methods</code> const-fold " \
27
27
  "(<a href=\"https://github.com/matz/spinel/issues/1073\">#1073</a>) and <code>transpose</code>/map " \
28
28
  "specializations, the brass cluster and thousands more moved out of <code>rejected</code>." },
29
+ { rev: "95557f5", date: "2026-06-02", commit: "module/class-body side effects + lexical const refs (#1256), Regexp.last_match(n) (#1257), preserve Float-in-Hash (#1258), Struct typing, JSON.generate, alias, +14 more",
30
+ file: "survey-95557f5/compat.jsonl",
31
+ note: "<strong>The biggest single jump yet.</strong> 22 upstream commits — including fixes for three " \
32
+ "issues this harness filed (<a href=\"https://github.com/matz/spinel/issues/1256\">#1256</a> module-body, " \
33
+ "<a href=\"https://github.com/matz/spinel/issues/1257\">#1257</a> <code>Regexp.last_match</code>, " \
34
+ "<a href=\"https://github.com/matz/spinel/issues/1258\">#1258</a> Float-in-Hash) plus Struct typing, " \
35
+ "<code>alias</code>, <code>JSON.generate</code> for records and more — moved <strong>20,175</strong> gems " \
36
+ "out of <code>rejected</code>, among them <code>rspec</code>, <code>globalid</code>, " \
37
+ "<code>mini_portile2</code> and <code>coffee-rails</code>." },
38
+ { rev: "a782696", date: "2026-06-03", commit: "StringScanner unscan/check + Error, Time#to_s + puts-nil, Dir.exist? + alias_method dispatch, missing int-hash keys as nil, RBS extractor heterogeneous-union→poly, subclass-initialize poly unification (13 commits)",
39
+ file: "survey-a782696/compat.jsonl",
40
+ note: "<strong>A consolidation rev.</strong> The base verdict mix is essentially flat after the previous " \
41
+ "jump — 13 upstream commits of correctness fixes (<code>StringScanner</code>, <code>Time#to_s</code>, " \
42
+ "<code>Dir.exist?</code>/<code>alias_method</code>, and the RBS-extractor union→poly change) graduated a " \
43
+ "small set of gems — <code>google-adwords-api</code>, <code>libdatadog</code>, <code>random_user_agent</code>, " \
44
+ "<code>twitter_username_extractor</code> and the <code>redcar-*</code> cluster — while a couple regressed " \
45
+ "and were caught by the re-probe. The bigger story this rev was off the catalog: the harness found " \
46
+ "<code>spinel_analyze</code> consuming 100+ GB on a cluster of auto-generated API-SDK gems (a compiler " \
47
+ "memory blow-up, filed upstream)." },
48
+ { rev: "9c0a5f0", date: "2026-06-04", commit: "79 commits — incl. fixes for 6 harness-filed issues: stdlib-class-in-ivar (#1305), reopen-Object (#1306), lambda/proc branch-local (#1315), &blk+block_given? (#1316), inject(&:sym) (#1317), ignored-require constant (#1273)",
49
+ file: "survey-9c0a5f0/compat.jsonl",
50
+ note: "<strong>The harness loop paying off.</strong> matz landed fixes for <strong>six</strong> issues this " \
51
+ "harness filed the day before — all common idioms: <code>block_given?</code> with a named " \
52
+ "<code>&amp;blk</code> (<a href=\"https://github.com/matz/spinel/issues/1316\">#1316</a>), " \
53
+ "<code>inject(&amp;:+)</code> (<a href=\"https://github.com/matz/spinel/issues/1317\">#1317</a>), " \
54
+ "reopening <code>class Object</code> (<a href=\"https://github.com/matz/spinel/issues/1306\">#1306</a>), " \
55
+ "a stdlib class held in an instance variable " \
56
+ "(<a href=\"https://github.com/matz/spinel/issues/1305\">#1305</a>), and a branch-assigned local inside a " \
57
+ "lambda/proc (<a href=\"https://github.com/matz/spinel/issues/1315\">#1315</a>). <strong>3,487</strong> gems " \
58
+ "moved out of <code>rejected</code> (110.3k→106.8k). The one feature ruled out of scope — aliasing the " \
59
+ "regexp special globals (<a href=\"https://github.com/matz/spinel/issues/1307\">#1307</a>) — now fails with a " \
60
+ "clear diagnostic instead of bad C." },
61
+ { rev: "5c9790c", date: "2026-06-05", commit: "17 commits — fixes for 3 harness-filed typed-collection issues: Hash#fetch on int_int_hash (#1329), Array#join on poly_array (#1332), Class-in-collection→poly (#1337); plus regex line-anchoring/gsub-buffer + first-class string type",
62
+ file: "survey-5c9790c/compat.jsonl",
63
+ note: "<strong>Typed-collection coverage.</strong> matz fixed three issues this harness filed hours earlier — all " \
64
+ "the same shape: a method that exists on the generic path but was missing on a <em>specialized</em> " \
65
+ "collection. <code>Hash#fetch</code> on an int→int hash " \
66
+ "(<a href=\"https://github.com/matz/spinel/issues/1329\">#1329</a>), <code>Array#join</code> on a mixed " \
67
+ "<code>poly_array</code> (<a href=\"https://github.com/matz/spinel/issues/1332\">#1332</a>), and a " \
68
+ "<code>Class</code> value stored in a Hash/Array now typed as poly instead of int " \
69
+ "(<a href=\"https://github.com/matz/spinel/issues/1337\">#1337</a>, which had broken every options-hash " \
70
+ "carrying an exception class). The compile+scan base barely moves on fixes like these — they're " \
71
+ "full-surface/runtime, so the graduation shows in the behaviour-verified tier." },
72
+ { rev: "57af7f9", date: "2026-06-07", commit: "~40 commits — 9 harness-filed issues closed in one wave: the ecosystem-spine front doors (alias→attr_reader #1356, &:sym-after-positional parse #1359), unary operator mangling (#1357), sp_sym_intern link (#1355), plus typed-collection nil steps (#801/#1180) and #line / --emit-symbol-map diagnostics (the #1338 RFC direction)",
73
+ file: "survey-57af7f9/compat.jsonl",
74
+ note: "<strong>The spine-gems wave.</strong> Auditing why <code>bundler</code>/<code>rake</code>/" \
75
+ "<code>minitest</code>/<code>thor</code> reject found two shallow front doors — " \
76
+ "<code>alias</code> to an <code>attr_reader</code>-generated method " \
77
+ "(<a href=\"https://github.com/matz/spinel/issues/1356\">#1356</a>, rake + thor) and " \
78
+ "<code>&amp;:sym</code> after a positional argument mis-parsed as a hash literal " \
79
+ "(<a href=\"https://github.com/matz/spinel/issues/1359\">#1359</a>, bundler + minitest) — and matz closed " \
80
+ "both within a day, alongside 7 more harness filings. All four spine gems now compile past their old " \
81
+ "blockers into distinct second-tier issues " \
82
+ "(<a href=\"https://github.com/matz/spinel/issues/1368\">#1368</a> et al.). C compile errors now map back " \
83
+ "to Ruby source lines via <code>#line</code>, on by default — the " \
84
+ "<a href=\"https://github.com/matz/spinel/issues/1338\">#1338</a> RFC direction. The behaviour-verified " \
85
+ "tier reached 144 mechanical ★ this run." },
86
+ { rev: "cb23cc6", date: "2026-06-09", commit: "~23 commits — 12 harness-filed issues closed in one wave, incl. Mutex/Monitor#synchronize + Thread.new now running and carrying their block value (#1360), class-instance-vars in class methods (#1352), is_a?(IncludedModule)/ancestors (#1350), \\h/\\H regex (#1349), reopened-builtin optional defaults (#1348), bitwise/shift operator mangling (#1358/#1368), the captured-&block value/type family",
87
+ file: "survey-cb23cc6/compat.jsonl",
88
+ note: "<strong>Mutex/Thread come in from the cold.</strong> With matz/spinel#1360 landing " \
89
+ "(<code>Mutex#synchronize</code>/<code>Thread.new</code> now run single-threaded and carry their " \
90
+ "block's value), the static pre-filter no longer hard-rejects them — they're flagged " \
91
+ "<code>risky</code> (correct for defensive use, degenerate only for true concurrency). Re-probing " \
92
+ "the 8,833 gems that had been rejected on sight for <code>Mutex.new</code>/<code>Thread.new</code>, " \
93
+ "<strong>3,493 (39.5%) moved out of <code>rejected</code></strong> — they compile now; the static " \
94
+ "wall had been hiding it. The four spine gems (<code>bundler</code>/<code>rake</code>/" \
95
+ "<code>minitest</code>/<code>thor</code>) now reject for their <em>real</em> reason — the deep " \
96
+ "metaprogramming surface (<code>send</code>/<code>method_missing</code>/<code>cattr_accessor</code>) — " \
97
+ "not a misleading <code>hard:Mutex.new</code>. The differential re-audit also caught one regression in " \
98
+ "the wave: returning <code>self</code> from a reopened-builtin method " \
99
+ "(<a href=\"https://github.com/matz/spinel/issues/1386\">#1386</a>, <code>to-bool</code>), filed same day." },
100
+ { rev: "b60fbd7", date: "2026-06-15", commit: "636 commits — the largest wave yet. A type-inference + codegen rewrite (typed-array/poly ~222, inference/cast/box ~168, string ~124, block/yield ~101, module/singleton ~94) closing many harness-filed issues at once: reopened-builtin self (#1386), hash-block String type (#1382/#1394), circular require_relative (#1373), computed require (#1383), class-method yield (#1387), Scheduled-server boot + SIGTERM (#1369/#1384). New surfaces: value-type objects, --rbs/--emit-rbs/--emit-symbol-map.",
101
+ file: "survey-b60fbd7/compat.jsonl",
102
+ note: "<strong>The largest movement in the catalog's history.</strong> 636 upstream commits — a type-inference " \
103
+ "rewrite that resolved the entire <code>unresolved:&lt;method&gt;</code> reject family (~60k records → ~0): " \
104
+ "<strong>~49,000 gems moved out of <code>rejected</code></strong> (−46%), +22k to <code>clean</code>. The " \
105
+ "wave also changed unresolvable <code>require</code> to hard-fail the compile (post-#1383); because " \
106
+ "<code>require \"gem/version\"</code> is near-universal that spuriously rejected thousands, so the probe " \
107
+ "now classifies a require-only failure as the no-load-path limit (<code>risky [load-path:require]</code>, " \
108
+ "9,716 gems corrected). Cost side, caught by re-verifying every ★ at this rev: <strong>~34 of 175 " \
109
+ "behaviour-verified gems regressed</strong> (12 miscompiles incl. <code>after_commit_action</code>, 22 " \
110
+ "codegen incl. bare-<code>super</code>/<code>ForwardingSuperNode</code>) — the trust tier dropped 175→136. " \
111
+ "Fresh-verify overrode stickiness, so the ★ count is honest, not carried." },
29
112
  ].freeze
30
113
 
31
114
  ORDER = %w[clean risky rejected].freeze
@@ -133,7 +216,7 @@ module Bundler
133
216
  <title>#{h title}</title><link rel="stylesheet" href="/assets/style.css"></head>
134
217
  <body>
135
218
  <header><a class="brand" href="/">#{gem}SpinelGems</a>
136
- <nav><a href="/">Home</a> <a href="/catalog">Catalog</a> <a href="/history.html">History</a>
219
+ <nav><a href="/">Home</a> <a href="/catalog">Catalog</a> <a href="/load-bearing.html">Load-bearing</a> <a href="/history.html">History</a>
137
220
  <a href="https://github.com/OriPekelman/spinelgems">GitHub</a></nav></header>
138
221
  <main>
139
222
  #{body}
@@ -0,0 +1,116 @@
1
+ require "cgi"
2
+ require_relative "site"
3
+
4
+ module Bundler
5
+ module Spinel
6
+ # The "build-it-first" roadmap page: which gems, if made to compile, unblock
7
+ # the most of the ecosystem. Reads the committed
8
+ # harness/load-bearing/targets.tsv (transitive load-bearing + buildability
9
+ # impact, precomputed from the local dependency graph) and renders a clear,
10
+ # status-coloured table. Static, committed in site/ like the history page.
11
+ class LoadBearing
12
+ DATA = File.expand_path("../../../harness/load-bearing/targets.tsv", __dir__)
13
+ GLYPH = Site::GLYPH
14
+
15
+ # Buildability snapshot @ 95557f5 (from harness/load-bearing/buildability.rb).
16
+ BUILDABLE = 50_688
17
+ BLOCKED = 29_139
18
+ REJECTED = 110_256
19
+
20
+ def initialize(data = DATA) = (@data = data)
21
+
22
+ def build_html(out)
23
+ rows = File.exist?(@data) ? File.readlines(@data)[1..].map { |l| l.chomp.split("\t") } : []
24
+ compiler = rows.select { |r| r[7] == "compiler" }.sort_by { |r| -r[1].to_i }
25
+ File.write(out, page("Load-bearing gems — SpinelGems", body(compiler, rows)))
26
+ out
27
+ end
28
+
29
+ private
30
+
31
+ def body(compiler, all)
32
+ b = +""
33
+ b << "<h1>Load-bearing gems</h1>\n"
34
+ b << %(<p class="lede">A gem matters to the ecosystem by how many gems pull it in )
35
+ b << %(<em>transitively</em> — directly, or as a dependency of a dependency, turtles all )
36
+ b << %(the way down. If Spinel can't compile a load-bearing gem, nothing above it can ship )
37
+ b << %(either. This is the build-it-first roadmap.</p>\n)
38
+
39
+ b << %(<div class="stat-row">\n)
40
+ b << stat("buildable", BUILDABLE, "whole dependency tree compiles")
41
+ b << stat("blocked", BLOCKED, "compiles itself — but a dependency is rejected")
42
+ b << stat("rejected", REJECTED, "doesn't compile")
43
+ b << %(</div>\n)
44
+ b << %(<p class="note">~<strong>#{fmt(BLOCKED)}</strong> gems compile on their own but )
45
+ b << %(can't actually be used because something beneath them is rejected. Fixing a load-bearing )
46
+ b << %(blocker flows <em>up</em> the tree — its dependents become buildable too.</p>\n)
47
+
48
+ b << %(<h2>Build-it-first targets</h2>\n)
49
+ b << %(<p class="sub">Ranked by <strong>impact</strong> — how many blocked gems become )
50
+ b << %(buildable if this one alone is fixed. Filtered to <strong>compiler-fixable</strong> )
51
+ b << %(failures: the native (C-extension) and heavy-metaprogramming (Rails-shaped) clusters )
52
+ b << %(are deliberately set aside as not the first target here. Each row carries two ways to )
53
+ b << %(fix it — a focused Spinel issue, or, for a small library, a PR to the gem itself.</p>\n)
54
+
55
+ b << %(<table id="catalog"><thead><tr>)
56
+ b << %(<th class="num">impact</th><th>gem</th><th>status</th><th class="num">load-bearing</th>)
57
+ b << %(<th>failure</th><th class="num">lib size</th><th>fix</th></tr></thead><tbody>\n)
58
+ compiler.first(120).each do |r|
59
+ gem, sole, _reach, transit, _dl, verdict, ftype, _ach, files, loc = r
60
+ fi = files.to_i
61
+ strat = fi.positive? && fi <= 12 ? %(<span class="badge human">lib PR</span> small — #{fi} files) :
62
+ %(<span class="badge rubric">Spinel issue</span>)
63
+ b << %(<tr>)
64
+ b << %(<td class="num"><strong>#{fmt sole}</strong></td>)
65
+ b << %(<td class="g">#{h gem}</td>)
66
+ b << %(<td class="v #{verdict}">#{GLYPH[verdict] || '?'} #{h verdict}</td>)
67
+ b << %(<td class="num">#{fmt transit}</td>)
68
+ b << %(<td><span class="badge rubric">#{h ftype}</span></td>)
69
+ b << %(<td class="num">#{fi.positive? ? "#{fi}f / #{loc}L" : "—"}</td>)
70
+ b << %(<td>#{strat}</td>)
71
+ b << %(</tr>\n)
72
+ end
73
+ b << "</tbody></table>\n"
74
+
75
+ nat = all.count { |r| r[7] == "native" }; mp = all.count { |r| r[7] == "metaprog" }
76
+ b << %(<p class="meta">Set aside as not-first-target: <strong>#{nat}</strong> native )
77
+ b << %(C-extension blockers (need FFI/ext vendoring) and <strong>#{mp}</strong> heavy-metaprogramming )
78
+ b << %(blockers (the Rails ecosystem). Method + data: )
79
+ b << %(<a href="https://github.com/OriPekelman/spinelgems/blob/main/docs/load-bearing-gems.md">docs/load-bearing-gems.md</a> · )
80
+ b << %(<a href="https://github.com/OriPekelman/spinelgems/blob/main/harness/load-bearing/">harness/load-bearing/</a>. )
81
+ b << %(Built locally from the gem cache's dependency graph; impact is per engine revision.</p>\n)
82
+ b
83
+ end
84
+
85
+ def stat(cls, n, label)
86
+ %( <div class="stat #{cls}"><b>#{fmt n}</b><span>#{label}</span></div>\n)
87
+ end
88
+
89
+ def fmt(n)
90
+ n = n.to_i
91
+ n >= 1000 ? "#{(n / 1000.0).round(1)}k" : n.to_s
92
+ end
93
+
94
+ def h(s) = CGI.escapeHTML(s.to_s)
95
+
96
+ def page(title, body)
97
+ gem = %(<svg class="gem" viewBox="0 0 24 24" aria-hidden="true"><path d="M6 3h12l4 6-10 13L2 9z" fill="#7b2d8e"/><path d="M6 3 2 9l10 13z" fill="#5a1f6b" opacity=".55"/><path d="M18 3l4 6-10 13z" fill="#b14fc4"/><path d="M6 3h12l-6 6z" fill="#d98ee8"/></svg>)
98
+ <<~HTML
99
+ <!doctype html>
100
+ <html lang="en"><head><meta charset="utf-8">
101
+ <meta name="viewport" content="width=device-width, initial-scale=1">
102
+ <title>#{h title}</title><link rel="stylesheet" href="/assets/style.css"></head>
103
+ <body>
104
+ <header><a class="brand" href="/">#{gem}SpinelGems</a>
105
+ <nav><a href="/">Home</a> <a href="/catalog">Catalog</a> <a href="/load-bearing.html">Load-bearing</a>
106
+ <a href="/history.html">History</a> <a href="https://github.com/OriPekelman/spinelgems">GitHub</a></nav></header>
107
+ <main>
108
+ #{body}
109
+ </main>
110
+ #{Site::FOOTER_HTML}
111
+ </body></html>
112
+ HTML
113
+ end
114
+ end
115
+ end
116
+ end
@@ -65,17 +65,19 @@ module Bundler
65
65
  file.empty? ? "line #{v['line']}" : "#{file}:#{v['line']}"
66
66
  end
67
67
 
68
- # Resolve bisect.sh. It lives in spinel-dev (separate from the compiler
69
- # engine), so there's no fixed relation to the engine dir — probe a few
70
- # conventional spots, newest-intent first: an explicit override, a sibling
71
- # spinel-dev checkout next to the engine, the conventional ~/sites layout.
68
+ # Resolve bisect.sh. It lives in the spinel-dev tooling repo (separate from
69
+ # the compiler engine), so there's no fixed relation to the engine dir —
70
+ # probe a few conventional spots, newest-intent first: an explicit override,
71
+ # a sibling checkout next to the engine, the conventional ~/sites layout.
72
+ # The local checkout may be named spinel-tools (gx10) or spinel-dev (the
73
+ # GitHub repo name), so probe both.
72
74
  def bisect_script
73
75
  rel = "tools/value-bisect/bisect.sh"
74
- candidates = [
75
- ENV["SPINEL_BISECT"],
76
- File.expand_path(File.join(@engine.dir, "..", "spinel-dev", rel)),
77
- File.expand_path("~/sites/spinel-dev/#{rel}")
78
- ]
76
+ candidates = [ENV["SPINEL_BISECT"]]
77
+ %w[spinel-tools spinel-dev].each do |repo|
78
+ candidates << File.expand_path(File.join(@engine.dir, "..", repo, rel))
79
+ candidates << File.expand_path("~/sites/#{repo}/#{rel}")
80
+ end
79
81
  candidates.find { |c| c && File.exist?(c) }
80
82
  end
81
83
  end