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 +7 -0
- data/CHANGELOG.md +48 -0
- data/LICENSE +21 -0
- data/README.md +135 -0
- data/docs/adoption.md +114 -0
- data/docs/gem-audit-first.md +76 -0
- data/docs/spinel-discipline.md +106 -0
- data/docs/spinelgems-issues.md +74 -0
- data/lib/spinel_kit/git.rb +73 -0
- data/lib/spinel_kit/json.rb +149 -0
- data/lib/spinel_kit/json_builder.rb +142 -0
- data/lib/spinel_kit/json_decoder.rb +394 -0
- data/lib/spinel_kit/log.rb +87 -0
- data/lib/spinel_kit/version.rb +6 -0
- data/lib/spinel_kit.rb +39 -0
- data/sig/spinel_kit/git.rbs +8 -0
- data/sig/spinel_kit/json.rbs +15 -0
- data/sig/spinel_kit/json_builder.rbs +19 -0
- data/sig/spinel_kit/json_decoder.rbs +21 -0
- data/sig/spinel_kit/log.rbs +19 -0
- data/sig/spinel_kit/version.rbs +3 -0
- data/spinel-ext.json +1 -0
- metadata +75 -0
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
|
+
[](https://rubygems.org/gems/spinel_kit)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+

|
|
6
|
+

|
|
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
|