spinel_kit 0.1.0 → 0.2.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 +4 -4
- data/CHANGELOG.md +28 -0
- data/README.md +1 -0
- data/docs/adoption.md +6 -0
- data/lib/spinel_kit/git.rb +12 -3
- data/lib/spinel_kit/hex.rb +63 -0
- data/lib/spinel_kit/url.rb +164 -0
- data/lib/spinel_kit/version.rb +1 -1
- data/lib/spinel_kit.rb +6 -0
- data/sig/spinel_kit/hex.rbs +8 -0
- data/sig/spinel_kit/url.rbs +9 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a45aac9b6f91813e7d837ea8878d4dcee1731b5582f4a42e5fae4f73e9f764ce
|
|
4
|
+
data.tar.gz: f201acc7e5c64ba8f5666e4c9c454440c7d1cc6835a30dd8adfb07442b1b42e8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5a6f770a93f9975455637ed2fb2ffd8874dcc69d1e6fdf8547b7120c13ea341f2154a0427f2bf317851d0cb86072232649f6e1b011354c8ab68760f120a4bc2e
|
|
7
|
+
data.tar.gz: 45d722737fce1cce7d606765af43f405fa6f34c96fa8a0167c8794262dee1ffebaa034600d865399577aaf9e826491059f338a8e688502bfd52b00c1071157b3
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to SpinelKit are documented here.
|
|
4
4
|
|
|
5
|
+
## [0.2.0] - 2026-06-08
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **`SpinelKit::Hex`** (`lib/spinel_kit/hex.rb`) — hex digit/byte codec:
|
|
9
|
+
`nibble` (hex char → 0..15), `nibble_char` (0..15 → uppercase hex char),
|
|
10
|
+
`byte2` (byte → two lowercase hex chars), `to_int` (leading hex digits →
|
|
11
|
+
int). Consolidates a `hex_nibble` that was **byte-identical** across
|
|
12
|
+
`Tep::Url`, `Tep::Llm`, and `SpinelKit::Json`'s decoder.
|
|
13
|
+
- **`SpinelKit::Url`** (`lib/spinel_kit/url.rb`) — the `CGI`/`URI`-component
|
|
14
|
+
surface Spinel can't get from stdlib: `escape`/`unescape` (RFC 3986
|
|
15
|
+
percent-codec), `parse_query` (form-urlencoded → Hash), `split_url`
|
|
16
|
+
(scheme/host/port/path/query). Ported from `Tep::Url`; uses `SpinelKit::Hex`
|
|
17
|
+
and is self-contained (inline typed-hash seeding + substring search).
|
|
18
|
+
|
|
19
|
+
`SpinelKit::Json` keeps its own private `hex2`/`hex_nibble` so a JSON-only
|
|
20
|
+
consumer never compiles `Hex` (Spinel has no tree-shaking) — the small
|
|
21
|
+
duplication is the surface-isolation cost.
|
|
22
|
+
|
|
23
|
+
## [0.1.1] - 2026-06-08
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
- `SpinelKit::Git.read` no longer truncates branch names that contain a slash.
|
|
27
|
+
A branch like `feat/x` (HEAD → `ref: refs/heads/feat/x`) was reported as `x`
|
|
28
|
+
because the parser took the last `/`-segment. It now strips the `refs/heads/`
|
|
29
|
+
prefix, preserving the full branch path (`feat/x`, `user/feature/sub/thing`).
|
|
30
|
+
Non-`heads` refs fall back to the last segment. Caught by toy's run_start
|
|
31
|
+
provenance during the toy#44 migration. Covered by `test/git_test.rb`.
|
|
32
|
+
|
|
5
33
|
## [0.1.0] - 2026-06-08
|
|
6
34
|
|
|
7
35
|
First release. Establishes the gem and lands the three core shims, consolidated
|
data/README.md
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# SpinelKit
|
|
2
2
|
|
|
3
|
+
[](https://github.com/OriPekelman/spinelkit/actions/workflows/ci.yml)
|
|
3
4
|
[](https://rubygems.org/gems/spinel_kit)
|
|
4
5
|
[](LICENSE)
|
|
5
6
|

|
data/docs/adoption.md
CHANGED
|
@@ -12,6 +12,12 @@ introduce poly-degrade collisions in a consumer's whole-program inference — so
|
|
|
12
12
|
we migrate one consumer at a time and gate each on its poly-degrade scan. But
|
|
13
13
|
the end-state is clean call sites, one canonical surface, and no aliases.
|
|
14
14
|
|
|
15
|
+
**Tracking (each consumer migrates itself, in its own repo):**
|
|
16
|
+
[tep#202](https://github.com/OriPekelman/tep/pull/202) (codec + logger; in
|
|
17
|
+
review) and [toy#49](https://github.com/OriPekelman/toy/issues/49) (builder +
|
|
18
|
+
git). SpinelKit changes happen here; consumer changes happen in the consumer's
|
|
19
|
+
repo via those issues.
|
|
20
|
+
|
|
15
21
|
## Consumption mechanism (and the current interim)
|
|
16
22
|
|
|
17
23
|
The clean path is `gem "spinel_kit"` + `spinel-compat vendor`. But that flow
|
data/lib/spinel_kit/git.rb
CHANGED
|
@@ -49,9 +49,18 @@ module SpinelKit
|
|
|
49
49
|
end
|
|
50
50
|
if head.length > 5 && head[0...5] == "ref: "
|
|
51
51
|
ref_rel = head[5...head.length]
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
# The branch is the ref path minus the "refs/heads/" prefix (11
|
|
53
|
+
# chars). Strip the prefix rather than taking the last "/"-segment
|
|
54
|
+
# so slashes WITHIN a branch name survive — e.g. refs/heads/feat/x
|
|
55
|
+
# is the branch "feat/x", not "x". Non-heads refs (packed/remote/
|
|
56
|
+
# tag) fall back to the last segment.
|
|
57
|
+
if ref_rel.length > 11 && ref_rel[0...11] == "refs/heads/"
|
|
58
|
+
b = ref_rel[11...ref_rel.length]
|
|
59
|
+
else
|
|
60
|
+
pp = ref_rel.split("/")
|
|
61
|
+
if pp.length >= 1
|
|
62
|
+
b = pp[pp.length - 1]
|
|
63
|
+
end
|
|
55
64
|
end
|
|
56
65
|
ref_path = ".git/" + ref_rel
|
|
57
66
|
if File.exist?(ref_path)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# SpinelKit::Hex -- hex digit/byte encode + decode: the pieces every Spinel
|
|
2
|
+
# project re-rolls. The decode nibble appeared BYTE-IDENTICAL in Tep::Url,
|
|
3
|
+
# inside SpinelKit::Json's string decoder, and (as a multi-digit variant) in
|
|
4
|
+
# Tep::Llm's chunked-transfer size parser. Pure string/byte ops, Spinel-safe.
|
|
5
|
+
#
|
|
6
|
+
# NOTE on the Json overlap: SpinelKit::Json keeps its OWN private `hex2`/
|
|
7
|
+
# `hex_nibble` so a JSON-only consumer never compiles this file (Spinel has no
|
|
8
|
+
# tree-shaking — see json.rb). Hex is the shared surface for everyone else,
|
|
9
|
+
# e.g. SpinelKit::Url.
|
|
10
|
+
module SpinelKit
|
|
11
|
+
class Hex
|
|
12
|
+
# Hex digit char -> int 0..15, or -1 if not a hex digit (upper or lower).
|
|
13
|
+
def self.nibble(c)
|
|
14
|
+
if c >= "0" && c <= "9"
|
|
15
|
+
return c.getbyte(0) - "0".getbyte(0)
|
|
16
|
+
end
|
|
17
|
+
if c >= "a" && c <= "f"
|
|
18
|
+
return c.getbyte(0) - "a".getbyte(0) + 10
|
|
19
|
+
end
|
|
20
|
+
if c >= "A" && c <= "F"
|
|
21
|
+
return c.getbyte(0) - "A".getbyte(0) + 10
|
|
22
|
+
end
|
|
23
|
+
-1
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Int nibble 0..15 -> single UPPERCASE hex char ("0".."9","A".."F").
|
|
27
|
+
# (RFC 3986 percent-encoding uses uppercase.)
|
|
28
|
+
def self.nibble_char(n)
|
|
29
|
+
if n < 10
|
|
30
|
+
return ("0".getbyte(0) + n).chr
|
|
31
|
+
end
|
|
32
|
+
("A".getbyte(0) + n - 10).chr
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Int byte 0..255 -> two-char LOWERCASE hex (15 -> "0f"). (JSON \u00XX
|
|
36
|
+
# and similar use lowercase.)
|
|
37
|
+
def self.byte2(n)
|
|
38
|
+
hex = "0123456789abcdef"
|
|
39
|
+
out = ""
|
|
40
|
+
out = out + hex[(n / 16) % 16, 1]
|
|
41
|
+
out = out + hex[n % 16, 1]
|
|
42
|
+
out
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Parse the leading hex digits of `s` -> int ("1a3" -> 419). Stops at the
|
|
46
|
+
# first non-hex char; returns 0 if there is no leading hex digit. Useful
|
|
47
|
+
# for chunked-transfer sizes and the like.
|
|
48
|
+
def self.to_int(s)
|
|
49
|
+
n = 0
|
|
50
|
+
i = 0
|
|
51
|
+
len = s.length
|
|
52
|
+
while i < len
|
|
53
|
+
v = Hex.nibble(s[i])
|
|
54
|
+
if v < 0
|
|
55
|
+
return n
|
|
56
|
+
end
|
|
57
|
+
n = n * 16 + v
|
|
58
|
+
i += 1
|
|
59
|
+
end
|
|
60
|
+
n
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
require_relative "hex"
|
|
2
|
+
|
|
3
|
+
# SpinelKit::Url -- percent-encode/decode (the CGI / URI-component surface
|
|
4
|
+
# Spinel can't get from stdlib) plus a form-query parser and a small URL
|
|
5
|
+
# splitter. Ported from Tep::Url; the hex digits now come from SpinelKit::Hex.
|
|
6
|
+
#
|
|
7
|
+
# Self-contained: the empty str=>str hashes are seeded inline (the
|
|
8
|
+
# `{"" => ""}`-then-delete idiom that pins Spinel's value type), and substring
|
|
9
|
+
# search is a private `find_idx` (the `< 0` callsites can't narrow against
|
|
10
|
+
# String#index's int|nil under Spinel's current model). All pure string ops.
|
|
11
|
+
module SpinelKit
|
|
12
|
+
class Url
|
|
13
|
+
# "%41+b" -> "A b" (form-decode: `+` is space, `%XX` is a byte).
|
|
14
|
+
def self.unescape(s)
|
|
15
|
+
out = ""
|
|
16
|
+
i = 0
|
|
17
|
+
n = s.length
|
|
18
|
+
while i < n
|
|
19
|
+
c = s[i]
|
|
20
|
+
if c == "+"
|
|
21
|
+
out = out + " "
|
|
22
|
+
i += 1
|
|
23
|
+
elsif c == "%" && i + 2 < n
|
|
24
|
+
hi = Hex.nibble(s[i + 1])
|
|
25
|
+
lo = Hex.nibble(s[i + 2])
|
|
26
|
+
if hi >= 0 && lo >= 0
|
|
27
|
+
out = out + ((hi * 16 + lo).chr)
|
|
28
|
+
i += 3
|
|
29
|
+
else
|
|
30
|
+
out = out + c
|
|
31
|
+
i += 1
|
|
32
|
+
end
|
|
33
|
+
else
|
|
34
|
+
out = out + c
|
|
35
|
+
i += 1
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
out
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Percent-encode everything outside the RFC 3986 unreserved set
|
|
42
|
+
# (ALPHA / DIGIT / `-._~`); the rest becomes `%XX` with UPPERCASE hex.
|
|
43
|
+
# (Space -> `%20`, not `+` -- this is the URI-component, not form, encoder.)
|
|
44
|
+
#
|
|
45
|
+
# Byte-oriented: under Spinel `String#[]` indexes BYTES, so a multi-byte
|
|
46
|
+
# UTF-8 char is encoded byte-by-byte (correct %XX of each byte). Under CRuby,
|
|
47
|
+
# pass a binary string for the same behaviour (else `[]` splits on chars).
|
|
48
|
+
def self.escape(s)
|
|
49
|
+
out = ""
|
|
50
|
+
i = 0
|
|
51
|
+
while i < s.length
|
|
52
|
+
c = s[i]
|
|
53
|
+
if (c >= "a" && c <= "z") || (c >= "A" && c <= "Z") ||
|
|
54
|
+
(c >= "0" && c <= "9") || c == "-" || c == "." ||
|
|
55
|
+
c == "_" || c == "~"
|
|
56
|
+
out = out + c
|
|
57
|
+
else
|
|
58
|
+
b = c.getbyte(0)
|
|
59
|
+
out = out + "%" + Hex.nibble_char(b / 16) + Hex.nibble_char(b % 16)
|
|
60
|
+
end
|
|
61
|
+
i += 1
|
|
62
|
+
end
|
|
63
|
+
out
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# "a=1&b=2&c" -> {"a"=>"1","b"=>"2","c"=>""}. Keys + values are
|
|
67
|
+
# form-decoded (`unescape`).
|
|
68
|
+
def self.parse_query(s)
|
|
69
|
+
h = {"" => ""}
|
|
70
|
+
h.delete("")
|
|
71
|
+
if s.length == 0
|
|
72
|
+
return h
|
|
73
|
+
end
|
|
74
|
+
pairs = s.split("&")
|
|
75
|
+
pairs.each do |pair|
|
|
76
|
+
if pair.length > 0
|
|
77
|
+
eq = Url.find_idx(pair, "=", 0)
|
|
78
|
+
if eq < 0
|
|
79
|
+
h[Url.unescape(pair)] = ""
|
|
80
|
+
else
|
|
81
|
+
k = pair[0, eq]
|
|
82
|
+
v = pair[eq + 1, pair.length - eq - 1]
|
|
83
|
+
h[Url.unescape(k)] = Url.unescape(v)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
h
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Split `http(s)://host[:port]/path?query` into a str=>str hash keyed
|
|
91
|
+
# scheme / host / port / path / query. Without a scheme the input is
|
|
92
|
+
# treated as a path (host stays empty). Default ports follow the scheme
|
|
93
|
+
# (80 / 443); `query` is the raw substring after `?` (not decoded).
|
|
94
|
+
#
|
|
95
|
+
# One body on purpose: Spinel widens a Hash-typed value when a helper
|
|
96
|
+
# mutates it and the caller keeps reading, so `out` stays StrStrHash only
|
|
97
|
+
# if nothing factors the mutation out (find_idx returns an int, no mutate).
|
|
98
|
+
def self.split_url(u)
|
|
99
|
+
out = {"" => ""}
|
|
100
|
+
out.delete("")
|
|
101
|
+
out["scheme"] = ""
|
|
102
|
+
out["host"] = ""
|
|
103
|
+
out["port"] = ""
|
|
104
|
+
out["path"] = "/"
|
|
105
|
+
out["query"] = ""
|
|
106
|
+
|
|
107
|
+
rest = u
|
|
108
|
+
if rest.length >= 7 && rest[0, 7] == "http://"
|
|
109
|
+
out["scheme"] = "http"
|
|
110
|
+
out["port"] = "80"
|
|
111
|
+
rest = rest[7, rest.length - 7]
|
|
112
|
+
elsif rest.length >= 8 && rest[0, 8] == "https://"
|
|
113
|
+
out["scheme"] = "https"
|
|
114
|
+
out["port"] = "443"
|
|
115
|
+
rest = rest[8, rest.length - 8]
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
if out["scheme"].length > 0
|
|
119
|
+
slash = Url.find_idx(rest, "/", 0)
|
|
120
|
+
hostport = rest
|
|
121
|
+
tail = "/"
|
|
122
|
+
if slash >= 0
|
|
123
|
+
hostport = rest[0, slash]
|
|
124
|
+
tail = rest[slash, rest.length - slash]
|
|
125
|
+
end
|
|
126
|
+
colon = Url.find_idx(hostport, ":", 0)
|
|
127
|
+
if colon >= 0
|
|
128
|
+
out["host"] = hostport[0, colon]
|
|
129
|
+
out["port"] = hostport[colon + 1, hostport.length - colon - 1]
|
|
130
|
+
else
|
|
131
|
+
out["host"] = hostport
|
|
132
|
+
end
|
|
133
|
+
rest = tail
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
qi = Url.find_idx(rest, "?", 0)
|
|
137
|
+
if qi >= 0
|
|
138
|
+
out["path"] = rest[0, qi]
|
|
139
|
+
out["query"] = rest[qi + 1, rest.length - qi - 1]
|
|
140
|
+
else
|
|
141
|
+
out["path"] = rest
|
|
142
|
+
end
|
|
143
|
+
if out["path"].length == 0
|
|
144
|
+
out["path"] = "/"
|
|
145
|
+
end
|
|
146
|
+
out
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# First index of `needle` in `s` at/after `start`, or -1. Internal
|
|
150
|
+
# (Spinel-safe substring search; see the module comment).
|
|
151
|
+
def self.find_idx(s, needle, start)
|
|
152
|
+
nlen = needle.length
|
|
153
|
+
slen = s.length
|
|
154
|
+
pos = start
|
|
155
|
+
while pos <= slen - nlen
|
|
156
|
+
if s[pos, nlen] == needle
|
|
157
|
+
return pos
|
|
158
|
+
end
|
|
159
|
+
pos += 1
|
|
160
|
+
end
|
|
161
|
+
-1
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
data/lib/spinel_kit/version.rb
CHANGED
data/lib/spinel_kit.rb
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
# SpinelKit::Json -- JSON encoders (json.rb) + flat-key decoders
|
|
12
12
|
# (json_decoder.rb).
|
|
13
13
|
# SpinelKit::Json::Builder -- incremental ordered-object builder.
|
|
14
|
+
# SpinelKit::Hex -- hex digit/byte encode + decode.
|
|
15
|
+
# SpinelKit::Url -- percent-encode/decode + form-query + URL split.
|
|
14
16
|
# SpinelKit::Git -- git provenance from .git/HEAD (was Toy::Git).
|
|
15
17
|
# SpinelKit::Log -- minimal levelled logger (was Tep::Logger).
|
|
16
18
|
#
|
|
@@ -23,6 +25,8 @@
|
|
|
23
25
|
# require "spinel_kit/json" # encoders
|
|
24
26
|
# require "spinel_kit/json_decoder" # decoders (require alongside json if you decode)
|
|
25
27
|
# require "spinel_kit/json_builder" # builder
|
|
28
|
+
# require "spinel_kit/hex" # hex digit/byte codec
|
|
29
|
+
# require "spinel_kit/url" # percent-codec + query (pulls in hex)
|
|
26
30
|
# require "spinel_kit/git"
|
|
27
31
|
# require "spinel_kit/log"
|
|
28
32
|
#
|
|
@@ -32,6 +36,8 @@ require_relative "spinel_kit/version"
|
|
|
32
36
|
require_relative "spinel_kit/json"
|
|
33
37
|
require_relative "spinel_kit/json_decoder"
|
|
34
38
|
require_relative "spinel_kit/json_builder"
|
|
39
|
+
require_relative "spinel_kit/hex"
|
|
40
|
+
require_relative "spinel_kit/url"
|
|
35
41
|
require_relative "spinel_kit/git"
|
|
36
42
|
require_relative "spinel_kit/log"
|
|
37
43
|
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
module SpinelKit
|
|
2
|
+
class Hex
|
|
3
|
+
def self.nibble: (String c) -> Integer # hex digit -> 0..15, or -1
|
|
4
|
+
def self.nibble_char: (Integer n) -> String # 0..15 -> "0".."9","A".."F"
|
|
5
|
+
def self.byte2: (Integer n) -> String # 0..255 -> two lowercase hex chars
|
|
6
|
+
def self.to_int: (String s) -> Integer # leading hex digits -> int
|
|
7
|
+
end
|
|
8
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
module SpinelKit
|
|
2
|
+
class Url
|
|
3
|
+
def self.unescape: (String s) -> String
|
|
4
|
+
def self.escape: (String s) -> String
|
|
5
|
+
def self.parse_query: (String s) -> Hash[String, String]
|
|
6
|
+
def self.split_url: (String u) -> Hash[String, String]
|
|
7
|
+
def self.find_idx: (String s, String needle, Integer start) -> Integer
|
|
8
|
+
end
|
|
9
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: spinel_kit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ori Pekelman
|
|
@@ -33,16 +33,20 @@ files:
|
|
|
33
33
|
- docs/spinelgems-issues.md
|
|
34
34
|
- lib/spinel_kit.rb
|
|
35
35
|
- lib/spinel_kit/git.rb
|
|
36
|
+
- lib/spinel_kit/hex.rb
|
|
36
37
|
- lib/spinel_kit/json.rb
|
|
37
38
|
- lib/spinel_kit/json_builder.rb
|
|
38
39
|
- lib/spinel_kit/json_decoder.rb
|
|
39
40
|
- lib/spinel_kit/log.rb
|
|
41
|
+
- lib/spinel_kit/url.rb
|
|
40
42
|
- lib/spinel_kit/version.rb
|
|
41
43
|
- sig/spinel_kit/git.rbs
|
|
44
|
+
- sig/spinel_kit/hex.rbs
|
|
42
45
|
- sig/spinel_kit/json.rbs
|
|
43
46
|
- sig/spinel_kit/json_builder.rbs
|
|
44
47
|
- sig/spinel_kit/json_decoder.rbs
|
|
45
48
|
- sig/spinel_kit/log.rbs
|
|
49
|
+
- sig/spinel_kit/url.rbs
|
|
46
50
|
- sig/spinel_kit/version.rbs
|
|
47
51
|
- spinel-ext.json
|
|
48
52
|
homepage: https://github.com/OriPekelman/spinelkit
|