spinel_kit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: cf881dd39764d4001bad8e3a8df70eab2063d4083e8a2641ceec8ea6c299bbbd
4
+ data.tar.gz: b91ccadce9e0058db8df0e1fc82e64d912d60958b31b6f0c23986ed99ccdf662
5
+ SHA512:
6
+ metadata.gz: 15b02b65083c60e8d9501d7c27e86c740f6cd62b814bcf4fc83c4a614b3f82fb434695cc8213e46f772cce441c51a48148658b4679ee8f4191213bef74345e43
7
+ data.tar.gz: 634f583e51bfa029ffe15a1a0b102e7e10f2916acd3632d85770002d91634d9a2bd5666a40c9deb11cf42b18e94ae1f2ff46b3475c1e4385692063a60990d356
data/CHANGELOG.md ADDED
@@ -0,0 +1,48 @@
1
+ # Changelog
2
+
3
+ All notable changes to SpinelKit are documented here.
4
+
5
+ ## [0.1.0] - 2026-06-08
6
+
7
+ First release. Establishes the gem and lands the three core shims, consolidated
8
+ from toy and tep. See [OriPekelman/toy#44](https://github.com/OriPekelman/toy/issues/44)
9
+ for the rationale.
10
+
11
+ ### Added
12
+ - `SpinelKit::Json` encoders (`lib/spinel_kit/json.rb`) — from `Tep::Json`:
13
+ `escape`/`quote`/`encode_pair_*`/`from_*`.
14
+ - `SpinelKit::Json` decoders (`lib/spinel_kit/json_decoder.rb`) — from
15
+ `Tep::Json`: flat-key `get_str`/`get_int`/`get_float`/`get_int_array`/
16
+ `has_key?` + the hand-rolled walker.
17
+ - `SpinelKit::Json::Builder` (`lib/spinel_kit/json_builder.rb`) — the
18
+ incremental ordered-object builder from `Toy::Json`, now
19
+ `add_str`/`add_num`/`add_bool`/`add_raw`/`add_obj`/`dump`, with its own
20
+ byte-identical escapers.
21
+ - Encoders / decoders / builder are split across three files on purpose:
22
+ Spinel has no tree-shaking, so a consumer that loaded an unused half compiled
23
+ (and degraded) it — concretely, dead decoder walkers widened `escape`'s
24
+ string arg to `int` and silently emitted `""` keys from `from_*_hash`. With
25
+ the split, each real consumer shape compiles 0-warning and correct (verified
26
+ through the Spinel binary on rev `57af7f9`): builder-only (toy), and
27
+ encode+decode (tep).
28
+ - `SpinelKit::Git` — `.git/HEAD` provenance (`sha`/`branch`), ported from
29
+ `Toy::Git`. tep gains it.
30
+ - `SpinelKit::Log` — minimal levelled logger, ported from `Tep::Logger`. toy
31
+ gains it.
32
+
33
+ ### Naming
34
+ - Dropped the donor `j_`/`tj_`/`gi_` prefixes in favour of plain, standard
35
+ names. Those prefixes worked around a Spinel name-keyed inference bug that
36
+ has since been fixed upstream (`ac7720e` #684, `23ba632` #1043), verified on
37
+ rev `57af7f9` via toy's `gate-poly-degrade` and its landmine-#16 probe.
38
+ - `docs/gem-audit-first.md` — the spinelgems catalog audit justifying
39
+ implement-don't-reuse for each surface, the methodology, and links to the
40
+ filed verification-request issues.
41
+ - `docs/spinel-discipline.md` — the poly-degrade naming rules and the
42
+ `Hash[missing]==0` / SafeHash gotcha.
43
+ - `docs/adoption.md` — the deferred 3-way move (tep adopts, toy adopts).
44
+
45
+ ### Not yet done (deferred — see docs/adoption.md)
46
+ - Consumer adoption: tep and toy still ship their own `*::Json`/`Git`/`Logger`.
47
+ The alias-and-vendor migration sequences behind this frozen API, gated by
48
+ each consumer's poly-degrade scan.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ori Pekelman
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 all
13
+ 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 THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,135 @@
1
+ # SpinelKit
2
+
3
+ [![Gem Version](https://img.shields.io/gem/v/spinel_kit)](https://rubygems.org/gems/spinel_kit)
4
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
5
+ ![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.2-CC342D)
6
+ ![Pure Ruby](https://img.shields.io/badge/native%20ext-none-brightgreen)
7
+
8
+ **The Spinel stdlib-surface gem.** A pure-Ruby, Spinel-safe toolkit holding
9
+ the generic "stdlib substitute" shims that every Spinel-compiled project would
10
+ otherwise hand-roll.
11
+
12
+ [Spinel](https://github.com/matz/spinel) is a Ruby→native AOT compiler. It
13
+ cannot lower large chunks of the CRuby standard library — the `json` gem's
14
+ C-extension fast path and its metaprogrammed pure-Ruby fallback, stdlib
15
+ `Logger`, C-extension git bindings, and more. So every Spinel project re-derives
16
+ the same shims. [toy](https://github.com/OriPekelman/toy) and
17
+ [tep](https://github.com/OriPekelman/tep) grew theirs independently, and the
18
+ JSON escape/quote/hex code came out *byte-identical*. SpinelKit is that code,
19
+ consolidated once.
20
+
21
+ ## Installation
22
+
23
+ ```sh
24
+ gem install spinel_kit
25
+ ```
26
+
27
+ Or in a Gemfile:
28
+
29
+ ```ruby
30
+ gem "spinel_kit"
31
+ ```
32
+
33
+ Pure Ruby, no native extension, no runtime dependencies — so it also vendors
34
+ cleanly into a Spinel build via [bundler-spinel](https://github.com/OriPekelman/spinelgems).
35
+
36
+ ## Why a gem instead of reusing one?
37
+
38
+ The first thing we did was audit the
39
+ [spinelgems](https://github.com/OriPekelman/spinelgems) compatibility catalog
40
+ (verdict ladder: `verified > loaded > clean > risky > rejected`) for an existing
41
+ gem to reuse — that would have been the biggest win. **There wasn't one.** These
42
+ shims *are* the ecosystem's gaps:
43
+
44
+ | Surface | Catalog finding | Decision |
45
+ |---------|-----------------|----------|
46
+ | JSON | `json` **rejected** (C-ext + metaprogrammed fallback); `oj` **risky** (C-ext) | implement |
47
+ | Log | `logger` **rejected** (unresolved calls) | implement |
48
+ | Git | `rugged` **rejected** (C); `gitkite`/`git_manager` only **clean**, unmet `needs:` | implement (read `.git/HEAD`) |
49
+ | Path | `hike` **verified** at an older rev, only **loaded** now; overkill for basename/join | deferred |
50
+ | Bytes | `unicode_utils`/`utf8-cleaner` **clean** only; toy's need is tokenizer-specific | deferred |
51
+
52
+ See [`docs/gem-audit-first.md`](docs/gem-audit-first.md) for the full audit and
53
+ the verification-request issues we filed on spinelgems.
54
+
55
+ ## What's in it
56
+
57
+ ```ruby
58
+ require "spinel_kit" # everything (CRuby / convenience)
59
+ ```
60
+
61
+ For a **Spinel-compiled** consumer, require only the surface you use — Spinel
62
+ has no tree-shaking, so every loaded method is compiled (and an uncalled one
63
+ can degrade). The Json surface is split into three files for exactly this
64
+ reason: `spinel_kit/json` (encoders), `spinel_kit/json_decoder` (decoders), and
65
+ `spinel_kit/json_builder` (the builder). e.g. tep requires `json` + `json_decoder`;
66
+ toy requires `json_builder`.
67
+
68
+ - **`SpinelKit::Json`** — a JSON-over-HTTP codec: encoders
69
+ (`escape`/`quote`/`encode_pair_*`/`from_*`, in `spinel_kit/json`) and flat-key
70
+ decoders (`get_str`/`get_int`/`get_float`/`get_int_array`/`has_key?`, in
71
+ `spinel_kit/json_decoder`).
72
+
73
+ ```ruby
74
+ SpinelKit::Json.get_int('{"age":33}', "age") # => 33
75
+ SpinelKit::Json.from_int_hash({"a" => 1, "b" => 2}) # => {"a":1,"b":2}
76
+ ```
77
+
78
+ - **`SpinelKit::Json::Builder`** — an incremental ordered-object builder
79
+ (`add_str`/`add_num`/`add_bool`/`add_raw`/`add_obj`/`dump`), in its own file
80
+ so a builder-only consumer never compiles the codec, and vice versa.
81
+
82
+ ```ruby
83
+ j = SpinelKit::Json::Builder.new
84
+ j.add_str("kind", "run_start")
85
+ j.add_num("t", 1715000000)
86
+ j.dump # => {"kind":"run_start","t":1715000000}
87
+ ```
88
+
89
+ - **`SpinelKit::Git`** — git provenance from `.git/HEAD`.
90
+
91
+ ```ruby
92
+ g = SpinelKit::Git.read
93
+ g.sha # => "a1b2c3..." (or "unknown" outside a repo)
94
+ g.branch # => "main"
95
+ ```
96
+
97
+ - **`SpinelKit::Log`** — a minimal levelled logger (CRuby `Logger` doesn't
98
+ compile under Spinel).
99
+
100
+ ```ruby
101
+ log = SpinelKit::Log.new
102
+ log.set_level("info")
103
+ log.info("server up")
104
+ ```
105
+
106
+ ## Design constraints (read before editing)
107
+
108
+ SpinelKit is **pure Ruby, no native extension** (`spinel-ext.json` is `[]`) and
109
+ has **no runtime dependencies**, so it vendors cleanly via `bundler-spinel`. The
110
+ surface uses plain, standard names; the `j_`/`tj_`/`gi_` prefixes the donor
111
+ copies carried were a workaround for a Spinel whole-program-inference bug that
112
+ has since been fixed upstream (verified with toy's `gate-poly-degrade` on the
113
+ current compiler). One numeric caveat remains for `add_num` — see
114
+ [`docs/spinel-discipline.md`](docs/spinel-discipline.md).
115
+
116
+ ## Status
117
+
118
+ Pre-alpha (`0.1.0`). The shims are implemented, CRuby-verified, and the surface
119
+ is the single canonical one (the donor prefixes and duplicated escapers are
120
+ gone — the Spinel inference bug that motivated them is fixed). Consumer adoption
121
+ is the next phase: because we author every repo in this set, tep and toy
122
+ **migrate to `SpinelKit::*` directly and delete their donor modules** — we
123
+ standardize and clean rather than leave compatibility aliases behind. See
124
+ [`docs/adoption.md`](docs/adoption.md). Tracking issue:
125
+ [OriPekelman/toy#44](https://github.com/OriPekelman/toy/issues/44).
126
+
127
+ ## Development
128
+
129
+ ```sh
130
+ rake test # CRuby-side parity tests (never compiled)
131
+ rake rbs:validate # syntax-check the advisory RBS in sig/
132
+ gem build spinel_kit.gemspec
133
+ ```
134
+
135
+ MIT licensed.
data/docs/adoption.md ADDED
@@ -0,0 +1,114 @@
1
+ # Adoption — standardize and clean (not shim-and-alias)
2
+
3
+ Bootstrap landed the gem and its three shims. Adoption by tep and toy is the
4
+ next phase. **Because we author every repo in this set — toy, tep, spinelgems,
5
+ and Spinel itself — the goal is to standardize and clean, not to bolt
6
+ compatibility aliases on top of the old duplication.** Each consumer migrates
7
+ to `SpinelKit::*` directly and **deletes** its donor module. No permanent
8
+ `Tep::Json = SpinelKit::Json` shims left lying around.
9
+
10
+ This is still a sequenced move — moving shared code into a new namespace can
11
+ introduce poly-degrade collisions in a consumer's whole-program inference — so
12
+ we migrate one consumer at a time and gate each on its poly-degrade scan. But
13
+ the end-state is clean call sites, one canonical surface, and no aliases.
14
+
15
+ ## Consumption mechanism (and the current interim)
16
+
17
+ The clean path is `gem "spinel_kit"` + `spinel-compat vendor`. But that flow
18
+ does **not yet support transitive gem→gem dependencies** (no topo-sorted
19
+ `deps.rb`, no inter-gem require resolution), so a consumer that is *itself* a
20
+ vendored gem — like tep — can't yet pull spinel_kit through it. Tracked at
21
+ [OriPekelman/spinelgems#19](https://github.com/OriPekelman/spinelgems/issues/19).
22
+
23
+ **Interim:** the consumer vendors spinel_kit's lib (the surface it uses)
24
+ **committed under its own `lib/spinel_kit/`**, re-synced from the published gem
25
+ (e.g. tep's `make vendor-spinelkit`). Committed-under-`lib/` is what makes it
26
+ travel correctly when the consumer is itself vendored. Once spinelgems#19 lands,
27
+ switch to the gitignored `spinel-compat vendor` flow.
28
+
29
+ ## Migration per consumer
30
+
31
+ For each of tep and toy:
32
+
33
+ 1. Add `gem "spinel_kit"` (path/git during dev) and `spinel-compat vendor`.
34
+ 2. **Rewrite the call sites** to the canonical names:
35
+ - tep: ~157 `Tep::Json.*` → `SpinelKit::Json.*`; `Tep::Logger` →
36
+ `SpinelKit::Log`.
37
+ - toy: the `Toy::Json.new` builder sites → `SpinelKit::Json.new`;
38
+ `Toy::Git.read` → `SpinelKit::Git.read`.
39
+ A mechanical find/replace per symbol; the names below are chosen so the
40
+ replacement is 1:1 (see "Canonical surface").
41
+ 3. **Delete the donor files** (`lib/tep/json.rb`, `lib/tep/logger.rb`,
42
+ `lib/toy/io/toy_json.rb`, `lib/toy/io/toy_git.rb`) — no alias, no subclass.
43
+ 4. Build + run that consumer's suite.
44
+ 5. **toy only:** `make gate-poly-degrade` must stay byte-identical to the
45
+ frozen baseline (`prep/poly_degrade_gate.rb`). toy is where the
46
+ name-collision corruption originally bit. If an emit-0 appears, fix the
47
+ colliding name in SpinelKit (then re-vendor) rather than re-baselining.
48
+
49
+ Sequence tep first (simpler, no training landmine), then toy.
50
+
51
+ ## Canonical surface — already converged
52
+
53
+ The kit exposes ONE clean surface, not the union of two prefixed donor copies:
54
+
55
+ - **No `j_*`/`tj_*`/`gi_*` prefixes.** Those existed only to dodge a Spinel
56
+ name-keyed inference bug, which is now fixed upstream (`ac7720e` #684,
57
+ `23ba632` #1043; verified on rev `57af7f9` with toy's gate-poly-degrade — see
58
+ [`spinel-discipline.md`](spinel-discipline.md)). The builder uses
59
+ `add_str`/`add_num`/`add_bool`/`add_raw`/`add_obj`/`dump`; Git uses
60
+ `sha`/`branch`.
61
+ - **No duplicated escapers.** `escape`/`quote`/`hex2` are single canonical
62
+ methods the builder calls.
63
+
64
+ The Json surface is split across **three files** so each consumer compiles only
65
+ what it uses — Spinel has no tree-shaking, so loading code a consumer never
66
+ calls would compile (and degrade) it, which both trips the poly-degrade gate
67
+ and, worse, was observed to silently miscompile (dead decoder walkers widened
68
+ `escape`'s string arg to int, emitting `""` keys from `from_*_hash`). The split:
69
+ `spinel_kit/json` (encoders), `spinel_kit/json_decoder` (decoders),
70
+ `spinel_kit/json_builder` (builder). So:
71
+
72
+ - **tep** `require "spinel_kit/json"` + `"spinel_kit/json_decoder"` (it both
73
+ encodes responses and decodes request bodies) + `spinel_kit/log`. Verified:
74
+ the full encode+decode surface compiles **0 warnings, correct** on rev
75
+ `57af7f9`.
76
+ - **toy** `require "spinel_kit/json_builder"` + `spinel_kit/git`. Verified:
77
+ builder-only compiles **0 warnings, correct** (integers preserved, no
78
+ cross-module `value` poisoning).
79
+
80
+ A consumer must actually exercise the surface it loads (real ones do). A
81
+ program that loads decoders but never calls them — or encodes via `from_*_hash`
82
+ without any other string-`quote` call — can still see the dead-method
83
+ degradation; the poly-degrade gate is what catches that.
84
+
85
+ The per-symbol replacement is then a clean rename:
86
+
87
+ | donor call | canonical call |
88
+ |--------------------------|---------------------------------|
89
+ | `Toy::Json.new` | `SpinelKit::Json::Builder.new` |
90
+ | `j.j_str(k, v)` | `j.add_str(k, v)` |
91
+ | `j.j_num(k, v)` | `j.add_num(k, v)` |
92
+ | `j.j_dump` | `j.dump` |
93
+ | `Toy::Git.read.gi_sha` | `SpinelKit::Git.read.sha` |
94
+ | `Tep::Json.get_str(s,k)` | `SpinelKit::Json.get_str(s,k)` |
95
+ | `Tep::Json.quote(s)` | `SpinelKit::Json.quote(s)` |
96
+ | `Tep::Logger.new` | `SpinelKit::Log.new` |
97
+
98
+ tep's encoder/decoder spellings (`escape`/`quote`/`encode_pair_*`/`from_*`/
99
+ `get_*`) are unchanged — only the namespace moves. toy's builder is the one set
100
+ of call sites that changes method names. Each consumer's compile should be
101
+ warning-clean (no new emit-0) because it loads only its own surface — verify
102
+ with the poly-degrade gate after the move.
103
+
104
+ ## Git/Log
105
+
106
+ Once tep and toy are both on `SpinelKit::Json`, tep gains `SpinelKit::Git` and
107
+ toy gains `SpinelKit::Log` — each was previously single-consumer. No aliasing
108
+ needed; just use the canonical names at the new call sites.
109
+
110
+ ## Out of scope until a catalog change
111
+
112
+ `Path` and `Bytes` stay where they are (see
113
+ [`gem-audit-first.md`](gem-audit-first.md)). Revisit `Path` only if the filed
114
+ `hike` re-verification flips it back to `verified`.
@@ -0,0 +1,76 @@
1
+ # Gem-audit-first methodology
2
+
3
+ **Rule: before implementing any SpinelKit lib, audit the
4
+ [spinelgems](../../spinelgems) catalog for an existing gem that already
5
+ provides the capability. Reusing a verified gem is always the biggest win.
6
+ Only implement when the catalog says nothing fit exists — and record why.**
7
+
8
+ This is not a one-time check. Every new surface added to SpinelKit must pass
9
+ through the same gate and append a row to the audit table below.
10
+
11
+ ## How to query the catalog
12
+
13
+ The catalog is an append-only JSONL ledger keyed by gem name, with a verdict
14
+ ladder. Records live at `../../spinelgems/ledger/compat.jsonl`:
15
+
16
+ ```
17
+ {"gem":"X","version":"1.2.3","rev":"git:8d88ebe/aarch64-linux-gnu",
18
+ "verdict":"rejected|risky|clean|loaded|verified","reasons":[...],
19
+ "risks":[...],"probe":"...","at":"..."}
20
+ ```
21
+
22
+ | Verdict | Meaning |
23
+ |-------------|---------|
24
+ | `verified` | Full-surface compile + load + behaviour smoke matches CRuby. **Only tier to trust for production.** (~159 gems) |
25
+ | `loaded` | Require-only differential load matches; logic untested. |
26
+ | `clean` | Static lower bound — compiles; **no behaviour run**. Overstates compatibility. |
27
+ | `risky` | Compiles but uses constructs Spinel degrades silently (`eval`, `define_method`, …). |
28
+ | `rejected` | Doesn't compile, or a detected silent no-op/miscompile. `reasons` names the missing feature. |
29
+
30
+ Verdicts are scoped to an **engine revision** (the Spinel git SHA + platform).
31
+ A `rejected` is "rejected *as of this rev, because of these features*," never
32
+ forever — upgrading Spinel triggers an automatic re-probe.
33
+
34
+ Query examples:
35
+
36
+ ```sh
37
+ LEDGER=../../spinelgems/ledger/compat.jsonl
38
+ grep '"verdict":"verified"' "$LEDGER" | jq -r '.gem' # all verified
39
+ grep -E '"gem":"(json|oj|logger|rugged)"' "$LEDGER" | jq -c # specific gems
40
+ ```
41
+
42
+ Human attestations (highest-trust, version-pinned) live separately in
43
+ `../../spinelgems/attestations.jsonl`.
44
+
45
+ ## The audit (as of engine rev `git:8d88ebe`, aarch64-linux-gnu)
46
+
47
+ | SpinelKit surface | Candidate gems | Verdict | Decision | Notes |
48
+ |-------------------|----------------|---------|----------|-------|
49
+ | **Json** | `json` | rejected | **implement** | C-ext fast path + `define_method`/`class_eval` pure fallback — unlowerable. |
50
+ | | `oj`, `yajl-ruby`, `multi_json` | risky / rejected | | All C-ext or thin wrappers thereof. |
51
+ | **Log** | `logger` (stdlib) | rejected | **implement** | Metaprogrammed severity dispatch + formatter API; ~35 unresolved calls. |
52
+ | **Git** | `rugged` | rejected | **implement** | libgit2 C bindings — needs `dlopen`. We read `.git/HEAD` as a plain file instead. |
53
+ | | `gitkite`, `git_manager` | clean (unmet `needs:`) | | Only the cheap static tier; load-path-terminal, never behaviour-verified. |
54
+ | **Path** *(deferred)* | `hike` | verified@`2183a92`, only `loaded` now | **re-verify, then maybe reuse** | A path-*search* lib (Trail#find) — overkill for basename/join/expand. Filed a re-verify issue; revisit if it re-verifies. |
55
+ | | `pathname` (stdlib) | rejected | | C-ext + eval. |
56
+ | **Bytes** *(deferred)* | `unicode_utils`, `utf8-cleaner` | clean only | **defer** | toy's actual need (`cp_to_utf8` + GPT-2 byte tables) is tokenizer-specific; no general reuse. |
57
+ | **SafeHash** | — | n/a | **document, don't implement** | Not a gem — a coding *pattern* (always `has_key?` before `Hash[]`, because Spinel returns `0` for a missing key). See [`spinel-discipline.md`](spinel-discipline.md). |
58
+
59
+ ## Verification-request issues filed on spinelgems
60
+
61
+ Filed on `OriPekelman/spinelgems` (see [`spinelgems-issues.md`](spinelgems-issues.md)
62
+ for the exact bodies; issue numbers backfilled here once created):
63
+
64
+ 1. **Re-verify `hike`** at the current engine rev — if it re-verifies,
65
+ SpinelKit::Path becomes a *reuse* instead of an implement.
66
+ → [spinelgems#16](https://github.com/OriPekelman/spinelgems/issues/16)
67
+ 2. **Rubric clarification: `gitkite` / `git_manager`** — confirm their `clean`
68
+ verdict is load-path-terminal, so SpinelKit::Git's implement decision is
69
+ catalog-blessed.
70
+ → [spinelgems#17](https://github.com/OriPekelman/spinelgems/issues/17)
71
+ 3. *(optional)* **`oj` closure** — confirm `risky` is C-ext-terminal (no
72
+ pure-Ruby path), closing the JSON-reuse question on the record.
73
+ → [spinelgems#18](https://github.com/OriPekelman/spinelgems/issues/18)
74
+
75
+ We did **not** file issues for `json`, `logger`, or `rugged`: their rejections
76
+ are unambiguous (C-ext / metaprogramming) and a re-probe wouldn't flip them.
@@ -0,0 +1,106 @@
1
+ # Spinel discipline — naming, poly-degrade, and the Hash gotcha
2
+
3
+ SpinelKit ships code that gets compiled into *other* programs by Spinel's
4
+ whole-program AOT compiler. That changes the rules for how it must be written.
5
+ This doc is the contract for anyone editing `lib/spinel_kit/*.rb`.
6
+
7
+ ## 1. The name-keyed inference bug — FIXED, prefixes dropped
8
+
9
+ The donor copies (`Toy::Json`, `Toy::Git`) carried `j_`/`tj_`/`gi_` prefixes on
10
+ every method and parameter to dodge a Spinel bug:
11
+
12
+ > A same-named method or attr-reader across unrelated classes, reached through
13
+ > an unresolved receiver, could commit a wrong type and widen an unrelated
14
+ > value to `poly` — degrading or corrupting compute **even when the offending
15
+ > module was merely `require`d and never called.**
16
+
17
+ This actually bit toy (landmines #12/#16 in the gx10-side memory note
18
+ `feedback_spinel_type_inference_landmines.md`; #16 was filed as matz/spinel
19
+ #1043). **It has since been fixed in the compiler** — the relevant commits are
20
+ `ac7720e` ("same-named attr_accessor on unrelated classes no longer widens
21
+ reader to poly", #684) and `23ba632` ("Don't let a Struct member name globally
22
+ type-merge unrelated code", fixes #1043), part of a sustained campaign that
23
+ made inference receiver-aware / method-scoped instead of name-keyed. Each ships
24
+ a regression test in Spinel's CI.
25
+
26
+ **Verified, not assumed.** On the current compiler (rev `57af7f9`) we compiled
27
+ two-module reproducers — a builder whose `value` param legitimately goes poly
28
+ alongside an unrelated method with its own `value` param — and confirmed no
29
+ cross-method leakage; toy's own `gate-poly-degrade` and its landmine-#16 probe
30
+ both pass. So **the prefixes are gone** and SpinelKit uses plain, standard
31
+ names (`escape`/`quote`/`add_str`/`sha`/`branch`/plain `value`).
32
+
33
+ **Remaining rules:**
34
+
35
+ - **Split the surface so consumers don't compile dead code.** Spinel has no
36
+ tree-shaking: every loaded method is compiled, and a *set* of uncalled
37
+ methods can degrade each other's (and nearby live methods') param types. We
38
+ saw this concretely — with the encoders and decoders in one class, an
39
+ encode-only program left the 9 decoder walkers dead; their `s` params
40
+ collapsed to `int` and dragged `escape`'s `s` to `int` too, silently
41
+ emitting `""` keys from `from_*_hash`. The fix was structural: encoders
42
+ (`json.rb`), decoders (`json_decoder.rb`), and builder (`json_builder.rb`)
43
+ live in separate files, so a consumer loads only a coherent surface it
44
+ actually exercises. Verified clean (0 emit-0, correct output) for the real
45
+ consumer shapes — builder-only (toy) and encode+decode (tep) — on rev
46
+ `57af7f9`. A consumer that loads a surface but leaves part of it uncalled can
47
+ still trip the gate; that's expected, and the gate is what flags it.
48
+ - **`escape`/`quote`/`hex2` are single canonical methods** — the builder
49
+ carries its own byte-identical copies (so a builder-only compile pulls in no
50
+ codec); there is no `tj_*` prefix any more.
51
+ - **Keep `get_float` inlined** (it does NOT delegate to a `parse_float_value`
52
+ helper). This is unrelated to the name bug — it's a value-walk *indirection*
53
+ issue where Spinel mis-widened the string arg `s` to int through the helper
54
+ call. Until that's separately confirmed fixed, leave it inlined.
55
+ - **Never override `to_s`** — it merges across the whole program.
56
+ - The stale cautionary comments in toy's `toy_json.rb`/`toy_git.rb` and the
57
+ landmine memory note should be annotated "fixed upstream by `ac7720e`/
58
+ `23ba632`, verified on `57af7f9`" when those repos are next touched.
59
+
60
+ ## 2. The poly-degrade gate (how consumers verify SpinelKit is safe)
61
+
62
+ Each consumer has a poly-degrade scan that compiles a canonical entrypoint and
63
+ counts Spinel's `cannot resolve call to '<x>' on <type> (emitting 0)` warnings,
64
+ comparing against a frozen baseline of known-benign dead paths. A **new**
65
+ warning is a regression.
66
+
67
+ - toy: `make gate-poly-degrade` → `prep/poly_degrade_gate.rb`
68
+ (baseline-compare; `--record` to re-baseline).
69
+
70
+ When toy/tep migrate to SpinelKit (see [`adoption.md`](adoption.md)), the gate
71
+ must stay **byte-identical to baseline** after the move. If rewriting toy's call
72
+ sites to `SpinelKit::Json.*` introduces an emit-0, treat it as a real
73
+ regression — fix the cause, don't re-baseline. (This gate guards a broader
74
+ failure mode than the old name bug — unresolved hot-path calls and missing
75
+ requires that emit a literal `0` — so it stays in CI regardless.)
76
+
77
+ ## 3. The `Hash[missing] == 0` gotcha (a.k.a. "SafeHash")
78
+
79
+ This is **not a class** SpinelKit ships — it's a coding rule, because under
80
+ Spinel a missing hash key reads back as integer `0`, not `nil`:
81
+
82
+ ```ruby
83
+ # WRONG under Spinel — absent key looks like rank 0 (highest priority):
84
+ r = @merge_rank[key]
85
+
86
+ # RIGHT — guard with has_key? first:
87
+ if @merge_rank.has_key?(key)
88
+ r = @merge_rank[key]
89
+ ...
90
+ end
91
+ ```
92
+
93
+ In toy's tokenizer this exact bug made BPE apply spurious merges (every absent
94
+ merge looked like rank 0). **Always `has_key?` before `Hash[]`** in any code
95
+ destined for Spinel. SpinelKit's own decoders follow this rule.
96
+
97
+ ## 4. Other Spinel limits relevant here
98
+
99
+ - No `STDERR` as a general writable destination in all contexts — `Log` prefers
100
+ a file path; the `$stderr.puts` fallback is the donor's and works where tep
101
+ runs, but file output is the portable path.
102
+ - `Time.now` exposes integer seconds only (no rich `strftime`) — `Log` formats
103
+ with `Time.now.to_i`.
104
+ - `Integer#chr` is not uniform for arbitrary bytes — `Json.byte_to_chr` uses a
105
+ printable-ASCII table with a `"?"` fallback.
106
+ - No C extensions — `spinel-ext.json` is `[]`.
@@ -0,0 +1,74 @@
1
+ # spinelgems verification-request issues
2
+
3
+ Issues filed on `OriPekelman/spinelgems` as part of the gem-audit-first pass
4
+ (see [`gem-audit-first.md`](gem-audit-first.md)):
5
+ [#16](https://github.com/OriPekelman/spinelgems/issues/16) (hike re-verify),
6
+ [#17](https://github.com/OriPekelman/spinelgems/issues/17) (gitkite/git_manager
7
+ rubric), [#18](https://github.com/OriPekelman/spinelgems/issues/18) (oj
8
+ closure). Bodies below.
9
+
10
+ ---
11
+
12
+ ## 1. Re-verify `hike` at the current engine rev
13
+
14
+ **Title:** Re-verify `hike` — was `verified`@2183a92, now only `loaded`
15
+
16
+ **Body:**
17
+
18
+ `hike` (path search / `Trail#find`) is the one candidate that could let a
19
+ downstream consumer *reuse* a gem instead of hand-rolling path resolution
20
+ (tracking: SpinelKit / toy#44). It shows `verified` at engine rev `2183a92`
21
+ but only `loaded` at the current dominant rev — i.e. its require-only load
22
+ still matches CRuby, but no behaviour smoke has run at this rev.
23
+
24
+ - Gem: `hike` (latest 2.x)
25
+ - Current verdict: `loaded` (please confirm with `spinel-compat probe hike`)
26
+ - Ask: run a full `verify` so it returns to `verified` (or surfaces the
27
+ regression).
28
+ - Proposed smoke: build a `Hike::Trail`, append two roots, assert
29
+ `trail.find("x.rb")` resolves the same path under CRuby and Spinel; assert a
30
+ miss returns `nil`/empty identically.
31
+
32
+ If it re-verifies, `SpinelKit::Path` becomes a reuse instead of an implement.
33
+
34
+ ---
35
+
36
+ ## 2. Rubric clarification: `gitkite` / `git_manager`
37
+
38
+ **Title:** Confirm `gitkite` / `git_manager` `clean` verdicts are load-path-terminal
39
+
40
+ **Body:**
41
+
42
+ For SpinelKit (toy#44) we decided to implement `.git/HEAD` provenance directly
43
+ rather than depend on a git gem, because the only non-rejected candidates sit
44
+ at the `clean` tier with unmet `needs:`. Before we lock that decision in, can
45
+ you confirm the `clean` verdict for `gitkite` and `git_manager` is
46
+ **load-path-terminal** (the `needs:<x>` cannot be satisfied by vendoring),
47
+ i.e. they will not advance to `loaded`/`verified` without upstream changes?
48
+
49
+ - Gems: `gitkite`, `git_manager`
50
+ - Current verdict: `clean` with `needs:` reasons (please paste the `reasons`
51
+ array from the ledger)
52
+ - Ask: confirm terminal, or tell us what would unblock them.
53
+
54
+ This just makes the "implement, don't reuse" call catalog-blessed rather than
55
+ assumed.
56
+
57
+ ---
58
+
59
+ ## 3. (optional) `oj` closure
60
+
61
+ **Title:** Confirm `oj` `risky` is C-ext-terminal (no pure-Ruby path)
62
+
63
+ **Body:**
64
+
65
+ Closing the JSON-reuse question on the record for SpinelKit (toy#44). `json`
66
+ is cleanly rejected (C-ext + metaprogrammed fallback). `oj` shows `risky` —
67
+ can you confirm that's C-extension-terminal (the fast path is native, and the
68
+ `method_missing` mimic path Spinel degrades), so there is no pure-Ruby
69
+ configuration of `oj` that could reach `verified`?
70
+
71
+ - Gem: `oj`
72
+ - Current verdict: `risky`
73
+ - Ask: confirm terminal for our purposes, or point at a pure-Ruby mode we
74
+ missed.
@@ -0,0 +1,73 @@
1
+ # SpinelKit::Git -- git provenance read from .git/HEAD.
2
+ #
3
+ # WHY THIS EXISTS. Spinel-compiled tooling that stamps a `git:{sha,branch}`
4
+ # provenance field (toy's run_start events; any reproducible-build banner)
5
+ # can't reach for a git gem: `rugged` is a C extension (rejected by the
6
+ # spinelgems catalog), and `gitkite`/`git_manager` only reach the `clean`
7
+ # tier with unmet `needs:` (load-path-terminal). Reading `.git/HEAD` as a
8
+ # plain file is a dozen lines of pure Ruby with no FFI -- so this is that.
9
+ # Ported from Toy::Git; tep gains it for free.
10
+ #
11
+ # Behaviour: reads .git/HEAD; if it's a `ref: refs/heads/<branch>` pointer,
12
+ # the branch is the last path segment and the 40-char sha is read from the
13
+ # pointed-at ref file; if HEAD is detached (a raw sha), sha = that, branch =
14
+ # "HEAD". Anything missing/short -> "unknown". Caller-facing default stays
15
+ # "unknown"/"unknown" so a non-repo checkout is non-fatal.
16
+ #
17
+ # NAMING. The Toy::Git copy carried a `gi_` prefix on every member/local to
18
+ # dodge a Spinel whole-program-inference bug; that bug was fixed upstream
19
+ # (see docs/spinel-discipline.md), so this uses plain `sha`/`branch`.
20
+ #
21
+ # USAGE:
22
+ # g = SpinelKit::Git.read
23
+ # git_sha = g.sha
24
+ # git_branch = g.branch
25
+ module SpinelKit
26
+ class Git
27
+ def initialize(sha, branch)
28
+ @sha = sha
29
+ @branch = branch
30
+ end
31
+
32
+ def sha
33
+ @sha
34
+ end
35
+
36
+ def branch
37
+ @branch
38
+ end
39
+
40
+ # Read provenance from ./.git/HEAD. Returns a SpinelKit::Git
41
+ # (sha/branch).
42
+ def self.read
43
+ s = "unknown"
44
+ b = "unknown"
45
+ if File.exist?(".git/HEAD")
46
+ head = File.read(".git/HEAD")
47
+ if head.length > 0 && head[head.length - 1...head.length] == "\n"
48
+ head = head[0...head.length - 1]
49
+ end
50
+ if head.length > 5 && head[0...5] == "ref: "
51
+ ref_rel = head[5...head.length]
52
+ pp = ref_rel.split("/")
53
+ if pp.length >= 3
54
+ b = pp[pp.length - 1]
55
+ end
56
+ ref_path = ".git/" + ref_rel
57
+ if File.exist?(ref_path)
58
+ sha_raw = File.read(ref_path)
59
+ if sha_raw.length >= 40
60
+ s = sha_raw[0...40]
61
+ end
62
+ end
63
+ else
64
+ if head.length >= 40
65
+ s = head[0...40]
66
+ b = "HEAD"
67
+ end
68
+ end
69
+ end
70
+ SpinelKit::Git.new(s, b)
71
+ end
72
+ end
73
+ end