@pi-in-go/pigpen-pi-typesafe-api 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.
package/json.go ADDED
@@ -0,0 +1,299 @@
1
+ package pitypesafe
2
+
3
+ import (
4
+ "bytes"
5
+ "encoding/json"
6
+ "errors"
7
+ "fmt"
8
+ "io"
9
+ "math"
10
+ "strconv"
11
+ "strings"
12
+ "unicode/utf8"
13
+ )
14
+
15
+ // The library moves JSON as a tree of nil, bool, float64, string, []any and
16
+ // *Object. Object remembers key order the way a JavaScript object does, because
17
+ // the order of questions, criteria and answers is observable: it decides how a
18
+ // large request is chunked, the order the model reads Choice options in, and the
19
+ // order answers are printed. Go maps cannot carry it.
20
+
21
+ // Object is a JSON object that keeps insertion order.
22
+ type Object struct {
23
+ keys []string
24
+ vals map[string]any
25
+ }
26
+
27
+ // NewObject returns an empty Object.
28
+ func NewObject() *Object { return &Object{vals: map[string]any{}} }
29
+
30
+ // Set stores a value. Setting an existing key keeps its position, as JavaScript does.
31
+ func (o *Object) Set(key string, value any) {
32
+ if _, ok := o.vals[key]; !ok {
33
+ o.keys = append(o.keys, key)
34
+ }
35
+ o.vals[key] = value
36
+ }
37
+
38
+ // Get returns the value for key and whether it is present.
39
+ func (o *Object) Get(key string) (any, bool) {
40
+ v, ok := o.vals[key]
41
+ return v, ok
42
+ }
43
+
44
+ // Has reports whether key is present.
45
+ func (o *Object) Has(key string) bool { _, ok := o.vals[key]; return ok }
46
+
47
+ // Delete removes a key.
48
+ func (o *Object) Delete(key string) {
49
+ if _, ok := o.vals[key]; !ok {
50
+ return
51
+ }
52
+ delete(o.vals, key)
53
+ for i, k := range o.keys {
54
+ if k == key {
55
+ o.keys = append(o.keys[:i:i], o.keys[i+1:]...)
56
+ break
57
+ }
58
+ }
59
+ }
60
+
61
+ // Keys returns the keys in insertion order. The slice is a copy.
62
+ func (o *Object) Keys() []string { return append([]string(nil), o.keys...) }
63
+
64
+ // Len is the number of keys.
65
+ func (o *Object) Len() int { return len(o.keys) }
66
+
67
+ // Clone copies the object and every value below it.
68
+ func (o *Object) Clone() *Object {
69
+ c := NewObject()
70
+ for _, k := range o.keys {
71
+ c.Set(k, cloneValue(o.vals[k]))
72
+ }
73
+ return c
74
+ }
75
+
76
+ func cloneValue(v any) any {
77
+ switch t := v.(type) {
78
+ case *Object:
79
+ return t.Clone()
80
+ case []any:
81
+ out := make([]any, len(t))
82
+ for i, e := range t {
83
+ out[i] = cloneValue(e)
84
+ }
85
+ return out
86
+ }
87
+ return v
88
+ }
89
+
90
+ // ParseJSON decodes JSON text into a tree that keeps object key order. Numbers are float64, like JavaScript's.
91
+ func ParseJSON(data []byte) (any, error) {
92
+ dec := json.NewDecoder(bytes.NewReader(data))
93
+ dec.UseNumber()
94
+ v, err := parseValue(dec)
95
+ if err != nil {
96
+ return nil, err
97
+ }
98
+ if _, err := dec.Token(); !errors.Is(err, io.EOF) {
99
+ return nil, errors.New("unexpected data after the JSON value")
100
+ }
101
+ return v, nil
102
+ }
103
+
104
+ func parseValue(dec *json.Decoder) (any, error) {
105
+ tok, err := dec.Token()
106
+ if err != nil {
107
+ return nil, err
108
+ }
109
+ switch t := tok.(type) {
110
+ case json.Delim:
111
+ switch t {
112
+ case '{':
113
+ obj := NewObject()
114
+ for dec.More() {
115
+ keyTok, err := dec.Token()
116
+ if err != nil {
117
+ return nil, err
118
+ }
119
+ key, _ := keyTok.(string)
120
+ v, err := parseValue(dec)
121
+ if err != nil {
122
+ return nil, err
123
+ }
124
+ obj.Set(key, v)
125
+ }
126
+ if _, err := dec.Token(); err != nil {
127
+ return nil, err
128
+ }
129
+ return obj, nil
130
+ case '[':
131
+ arr := []any{}
132
+ for dec.More() {
133
+ v, err := parseValue(dec)
134
+ if err != nil {
135
+ return nil, err
136
+ }
137
+ arr = append(arr, v)
138
+ }
139
+ if _, err := dec.Token(); err != nil {
140
+ return nil, err
141
+ }
142
+ return arr, nil
143
+ }
144
+ return nil, fmt.Errorf("unexpected delimiter %q", t)
145
+ case json.Number:
146
+ f, err := strconv.ParseFloat(string(t), 64)
147
+ if err != nil && !math.IsInf(f, 0) {
148
+ return nil, err
149
+ }
150
+ return f, nil
151
+ default:
152
+ return tok, nil // string, bool, nil
153
+ }
154
+ }
155
+
156
+ // FromGo converts any Go value to a tree: a tree passes through, everything else takes the
157
+ // encoding/json round trip. A value JSON cannot carry (a cycle, NaN, a function) is an error.
158
+ func FromGo(v any) (any, error) {
159
+ if isTree(v) {
160
+ return v, nil
161
+ }
162
+ data, err := json.Marshal(v)
163
+ if err != nil {
164
+ return nil, err
165
+ }
166
+ return ParseJSON(data)
167
+ }
168
+
169
+ func isTree(v any) bool {
170
+ switch t := v.(type) {
171
+ case nil, bool, string:
172
+ return true
173
+ case float64:
174
+ return !math.IsNaN(t) && !math.IsInf(t, 0)
175
+ case *Object:
176
+ for _, k := range t.keys {
177
+ if !isTree(t.vals[k]) {
178
+ return false
179
+ }
180
+ }
181
+ return t != nil
182
+ case []any:
183
+ for _, e := range t {
184
+ if !isTree(e) {
185
+ return false
186
+ }
187
+ }
188
+ return true
189
+ }
190
+ return false
191
+ }
192
+
193
+ // EncodeJSON serializes a tree the way JSON.stringify does: keys in insertion order, no
194
+ // insignificant whitespace, U+2028 and U+2029 unescaped, numbers in JavaScript's format. The
195
+ // byte length of this text is what the input limit measures.
196
+ func EncodeJSON(v any) ([]byte, error) {
197
+ var b bytes.Buffer
198
+ if err := encodeValue(&b, v); err != nil {
199
+ return nil, err
200
+ }
201
+ return b.Bytes(), nil
202
+ }
203
+
204
+ func encodeValue(b *bytes.Buffer, v any) error {
205
+ switch t := v.(type) {
206
+ case nil:
207
+ b.WriteString("null")
208
+ case bool:
209
+ b.WriteString(strconv.FormatBool(t))
210
+ case string:
211
+ writeJSString(b, t)
212
+ case float64:
213
+ if math.IsNaN(t) || math.IsInf(t, 0) {
214
+ return fmt.Errorf("cannot encode %v as JSON", t)
215
+ }
216
+ b.WriteString(jsNumber(t))
217
+ case *Object:
218
+ b.WriteByte('{')
219
+ for i, k := range t.keys {
220
+ if i > 0 {
221
+ b.WriteByte(',')
222
+ }
223
+ writeJSString(b, k)
224
+ b.WriteByte(':')
225
+ if err := encodeValue(b, t.vals[k]); err != nil {
226
+ return err
227
+ }
228
+ }
229
+ b.WriteByte('}')
230
+ case []any:
231
+ b.WriteByte('[')
232
+ for i, e := range t {
233
+ if i > 0 {
234
+ b.WriteByte(',')
235
+ }
236
+ if err := encodeValue(b, e); err != nil {
237
+ return err
238
+ }
239
+ }
240
+ b.WriteByte(']')
241
+ default:
242
+ tree, err := FromGo(v)
243
+ if err != nil {
244
+ return err
245
+ }
246
+ if isTree(tree) {
247
+ return encodeValue(b, tree)
248
+ }
249
+ return fmt.Errorf("cannot encode %T as JSON", v)
250
+ }
251
+ return nil
252
+ }
253
+
254
+ func writeJSString(b *bytes.Buffer, s string) {
255
+ b.WriteByte('"')
256
+ for _, r := range strings.ToValidUTF8(s, "\uFFFD") {
257
+ switch r {
258
+ case '"':
259
+ b.WriteString(`\"`)
260
+ case '\\':
261
+ b.WriteString(`\\`)
262
+ case '\b':
263
+ b.WriteString(`\b`)
264
+ case '\f':
265
+ b.WriteString(`\f`)
266
+ case '\n':
267
+ b.WriteString(`\n`)
268
+ case '\r':
269
+ b.WriteString(`\r`)
270
+ case '\t':
271
+ b.WriteString(`\t`)
272
+ default:
273
+ if r < 0x20 {
274
+ fmt.Fprintf(b, `\u%04x`, r)
275
+ } else {
276
+ var buf [utf8.UTFMax]byte
277
+ n := utf8.EncodeRune(buf[:], r)
278
+ b.Write(buf[:n])
279
+ }
280
+ }
281
+ }
282
+ b.WriteByte('"')
283
+ }
284
+
285
+ // jsNumber formats like JavaScript's Number.prototype.toString for the JSON range.
286
+ func jsNumber(f float64) string {
287
+ if f == 0 {
288
+ return "0" // also -0
289
+ }
290
+ abs := math.Abs(f)
291
+ if abs >= 1e21 || abs < 1e-6 {
292
+ s := strconv.FormatFloat(f, 'e', -1, 64) // 1e+21, 1.5e-07
293
+ mant, exp, _ := strings.Cut(s, "e")
294
+ sign := exp[0]
295
+ digits := strings.TrimLeft(exp[1:], "0")
296
+ return mant + "e" + string(sign) + digits
297
+ }
298
+ return strconv.FormatFloat(f, 'f', -1, 64)
299
+ }
package/json_test.go ADDED
@@ -0,0 +1,92 @@
1
+ package pitypesafe
2
+
3
+ import (
4
+ "math"
5
+ "testing"
6
+ )
7
+
8
+ func TestObjectKeepsInsertionOrderThroughParseAndEncode(t *testing.T) {
9
+ in := `{"z":1,"a":{"y":[true,null,"x"],"b":2},"m":"é\u2028\n"}`
10
+ v, err := ParseJSON([]byte(in))
11
+ if err != nil {
12
+ t.Fatal(err)
13
+ }
14
+ obj := v.(*Object)
15
+ if got := obj.Keys(); len(got) != 3 || got[0] != "z" || got[1] != "a" || got[2] != "m" {
16
+ t.Fatalf("keys = %v", got)
17
+ }
18
+ out, err := EncodeJSON(v)
19
+ if err != nil {
20
+ t.Fatal(err)
21
+ }
22
+ // U+2028 stays raw (JSON.stringify does not escape it), newline is escaped.
23
+ want := "{\"z\":1,\"a\":{\"y\":[true,null,\"x\"],\"b\":2},\"m\":\"é\u2028\\n\"}"
24
+ if string(out) != want {
25
+ t.Fatalf("got %q want %q", out, want)
26
+ }
27
+ }
28
+
29
+ func TestEncodeJSONNumbersFollowJavaScript(t *testing.T) {
30
+ cases := map[float64]string{0: "0", math.Copysign(0, -1): "0", 1: "1", 0.5: "0.5", 1e21: "1e+21", 1.5e-7: "1.5e-7", 123456789012: "123456789012", -2.25: "-2.25"}
31
+ for in, want := range cases {
32
+ out, err := EncodeJSON(in)
33
+ if err != nil || string(out) != want {
34
+ t.Errorf("EncodeJSON(%v) = %q, %v; want %q", in, out, err, want)
35
+ }
36
+ }
37
+ if _, err := EncodeJSON(math.NaN()); err == nil {
38
+ t.Error("NaN must not encode")
39
+ }
40
+ }
41
+
42
+ func TestSetKeepsPositionAndDeleteRemoves(t *testing.T) {
43
+ o := NewObject()
44
+ o.Set("a", 1.0)
45
+ o.Set("b", 2.0)
46
+ o.Set("a", 3.0)
47
+ o.Delete("missing")
48
+ if k := o.Keys(); k[0] != "a" || k[1] != "b" || o.Len() != 2 {
49
+ t.Fatalf("keys = %v", k)
50
+ }
51
+ o.Delete("a")
52
+ if k := o.Keys(); len(k) != 1 || k[0] != "b" {
53
+ t.Fatalf("keys after delete = %v", k)
54
+ }
55
+ }
56
+
57
+ func TestFromGoRejectsWhatJSONCannotCarry(t *testing.T) {
58
+ cyc := map[string]any{}
59
+ cyc["self"] = cyc
60
+ for name, v := range map[string]any{"cycle": cyc, "nan": map[string]any{"n": math.NaN()}, "inf": math.Inf(1), "func": func() {}, "chan": make(chan int)} {
61
+ if _, err := FromGo(v); err == nil {
62
+ t.Errorf("%s: want an error", name)
63
+ }
64
+ }
65
+ tree, err := FromGo(map[string]any{"b": 1, "a": []int{1, 2}})
66
+ if err != nil {
67
+ t.Fatal(err)
68
+ }
69
+ if k := tree.(*Object).Keys(); k[0] != "a" || k[1] != "b" {
70
+ t.Fatalf("a Go map has no order, so keys are sorted: %v", k)
71
+ }
72
+ }
73
+
74
+ func TestParseJSONRejectsTrailingData(t *testing.T) {
75
+ if _, err := ParseJSON([]byte(`{"a":1} x`)); err == nil {
76
+ t.Fatal("trailing data must fail")
77
+ }
78
+ if _, err := ParseJSON([]byte(`{"a":`)); err == nil {
79
+ t.Fatal("truncated input must fail")
80
+ }
81
+ }
82
+
83
+ func TestCloneIsDeep(t *testing.T) {
84
+ v, _ := ParseJSON([]byte(`{"a":{"b":[1,{"c":2}]}}`))
85
+ c := v.(*Object).Clone()
86
+ inner, _ := c.Get("a")
87
+ inner.(*Object).Set("b", "changed")
88
+ orig, _ := v.(*Object).Get("a")
89
+ if b, _ := orig.(*Object).Get("b"); b == "changed" {
90
+ t.Fatal("clone shares structure with the original")
91
+ }
92
+ }
@@ -0,0 +1,79 @@
1
+ package pitypesafe
2
+
3
+ import (
4
+ "context"
5
+ "errors"
6
+ "strings"
7
+ "testing"
8
+ "time"
9
+
10
+ "github.com/MichaelKinsy/pigpen/components/typesafe/libraries/typesafe"
11
+ )
12
+
13
+ type evalFunc func(ctx context.Context, req typesafe.SystemOneRequest) (*typesafe.SystemOneResult, error)
14
+
15
+ func (f evalFunc) SystemOne(ctx context.Context, req typesafe.SystemOneRequest, _ *typesafe.RequestOptions) (*typesafe.SystemOneResult, error) {
16
+ return f(ctx, req)
17
+ }
18
+
19
+ func TestOwnModelClientNeedsAnEvaluatorAndUsesNoKeyOrHost(t *testing.T) {
20
+ isolate(t)
21
+ if _, err := New(Options{Backend: "ownmodel"}); !hasCode(err, CodeConfiguration) || !strings.Contains(err.Error(), "Evaluator") {
22
+ t.Fatalf("err = %v", err)
23
+ }
24
+ c, err := New(Options{Backend: "ownmodel", Ledger: OpenUsageLedger(LedgerOptions{Path: t.TempDir() + "/u.json"}), Evaluator: evalFunc(func(_ context.Context, req typesafe.SystemOneRequest) (*typesafe.SystemOneResult, error) {
25
+ return &typesafe.SystemOneResult{Model: "m", Answers: map[string]typesafe.Answer{"yes": typesafe.NoulAnswer{Noul: 0.4}}, Usage: typesafe.Usage{InputTokens: 3}}, nil
26
+ })})
27
+ if err != nil {
28
+ t.Fatal(err)
29
+ }
30
+ ev, err := c.Evaluate(context.Background(), sampleRequest())
31
+ if err != nil || ev.Model != "m" || c.GetUsage().InputTokens != 3 {
32
+ t.Fatalf("ev=%v err=%v", ev, err)
33
+ }
34
+ // No key is involved, so nothing is written to the auth state.
35
+ if s := mustAuth(t, nil); s.Verified {
36
+ t.Error("an own-model success must not verify a TypeSafe key")
37
+ }
38
+ if _, err := c.ListModels(context.Background()); !hasCode(err, CodeConfiguration) {
39
+ t.Errorf("list models: %v", err)
40
+ }
41
+ }
42
+
43
+ func TestOwnModelTimeoutAndErrorsAreClassified(t *testing.T) {
44
+ isolate(t)
45
+ slow := evalFunc(func(ctx context.Context, _ typesafe.SystemOneRequest) (*typesafe.SystemOneResult, error) {
46
+ <-ctx.Done()
47
+ return nil, ctx.Err()
48
+ })
49
+ c, _ := New(Options{Backend: "ownmodel", Timeout: 10 * time.Millisecond, Evaluator: slow, Ledger: OpenUsageLedger(LedgerOptions{Path: t.TempDir() + "/u.json"})})
50
+ if _, err := c.Evaluate(context.Background(), sampleRequest()); !hasCode(err, CodeTimeout) || !strings.Contains(err.Error(), "The configured model") {
51
+ t.Fatalf("timeout: %v", err)
52
+ }
53
+ c2, _ := New(Options{Backend: "ownmodel", Evaluator: evalFunc(func(context.Context, typesafe.SystemOneRequest) (*typesafe.SystemOneResult, error) {
54
+ return nil, errors.New("provider body with sk-secret")
55
+ }), Ledger: OpenUsageLedger(LedgerOptions{Path: t.TempDir() + "/u.json"})})
56
+ _, err := c2.Evaluate(context.Background(), sampleRequest())
57
+ if !hasCode(err, CodeResponse) || strings.Contains(err.Error(), "sk-secret") {
58
+ t.Fatalf("plain error: %v", err)
59
+ }
60
+ }
61
+
62
+ func TestEvaluationJSONKeepsQuestionOrderAndAddsElapsed(t *testing.T) {
63
+ ev := &Evaluation{
64
+ SystemOneResult: &typesafe.SystemOneResult{Model: "m", Answers: map[string]typesafe.Answer{"b": typesafe.NoulAnswer{Noul: 0.5}, "a": typesafe.NoulAnswer{Noul: 0.25}}, Usage: typesafe.Usage{InputTokens: 2}},
65
+ ElapsedMs: 7, Order: []string{"b", "a"},
66
+ }
67
+ out, err := ev.MarshalJSON()
68
+ if err != nil {
69
+ t.Fatal(err)
70
+ }
71
+ s := string(out)
72
+ if !(strings.Index(s, `"b"`) < strings.Index(s, `"a"`)) || !strings.HasSuffix(s, `"elapsedMs":7}`) || strings.Index(s, `"model"`) > strings.Index(s, `"answers"`) {
73
+ t.Fatalf("json = %s", s)
74
+ }
75
+ d, _ := ev.DetailsJSON()
76
+ if !strings.Contains(string(d), `"order":["b","a"]`) {
77
+ t.Fatalf("details = %s", d)
78
+ }
79
+ }
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@pi-in-go/pigpen-pi-typesafe-api",
3
+ "version": "0.1.0",
4
+ "description": "Typed Go API for extension authors, a port of pi-typesafe's library half: one admission rule, request and spend caps, key store and auth state, judgment backends (TypeSafe, OpenRouter, Command Code, custom endpoint, own model), Ask, batching, calibration. Libraries only, no extension of its own.",
5
+ "keywords": [
6
+ "pig-package",
7
+ "typesafe",
8
+ "jev",
9
+ "library",
10
+ "pigpen"
11
+ ],
12
+ "license": "MIT",
13
+ "author": "Michael Kinsy",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/MichaelKinsy/pigpen.git",
17
+ "directory": "components/pi-typesafe-api"
18
+ },
19
+ "homepage": "https://github.com/MichaelKinsy/pigpen/tree/main/components/pi-typesafe-api#readme",
20
+ "bugs": {
21
+ "url": "https://github.com/MichaelKinsy/pigpen/issues"
22
+ },
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "files": [
27
+ "*.go",
28
+ "hostmodel",
29
+ "ui",
30
+ "testdata",
31
+ "evaluation_schema.json",
32
+ "go.mod",
33
+ "port",
34
+ "README.md",
35
+ "LICENSE",
36
+ "CREDITS.md",
37
+ "provenance.json",
38
+ "go.sum"
39
+ ]
40
+ }
package/port/PORT.md ADDED
@@ -0,0 +1,6 @@
1
+ # Port record: pi-typesafe-api
2
+
3
+ The library half of the pi-typesafe port. The unmodified original (pinned commit `ed439f834665ad6fc652787f5ba7c23852566d49`,
4
+ MIT, Ryan Gapac) and the complete record (file mapping, twins, scenarios, mutation results, deliberate differences) live in
5
+ [`components/pi-typesafe/port/`](../../pi-typesafe/port/PORT.md); the original is vendored there once, not twice. This Package's
6
+ Go twins of the library test files (`*_test.go` next to the code) are counted in that record.
@@ -0,0 +1,18 @@
1
+ {
2
+ "origin": "ported",
3
+ "authors": ["Michael Kinsy"],
4
+ "license": "MIT",
5
+ "licenseFile": "LICENSE",
6
+ "upstreams": [
7
+ {
8
+ "name": "pi-typesafe",
9
+ "authors": ["Ryan Gapac"],
10
+ "url": "https://github.com/DevMortimer/pi-typesafe",
11
+ "revision": "ed439f834665ad6fc652787f5ba7c23852566d49",
12
+ "path": "port/PORT.md",
13
+ "license": "MIT",
14
+ "licenseFile": "LICENSE",
15
+ "attributionFile": "CREDITS.md"
16
+ }
17
+ ]
18
+ }
package/review_test.go ADDED
@@ -0,0 +1,23 @@
1
+ package pitypesafe
2
+
3
+ import "testing"
4
+
5
+ // Cases added by the review (rev-pigpen-typesafe).
6
+
7
+ // Plain http: is allowed only for a loopback address. The original's WHATWG URL parser rejects a dotted
8
+ // address with an octet above 255 (new URL throws), so 127.999.0.1 never passes its 127.0.0.0/8 rule; Go's
9
+ // url.Parse accepts it as a host name, which the resolver would then look up in DNS and reach over plain
10
+ // http: with the endpoint's key.
11
+ func TestLoopbackHTTPNeedsARealLoopbackAddress(t *testing.T) {
12
+ for _, host := range []string{"http://127.999.0.1", "http://127.0.0.256:8080"} {
13
+ _, err := ResolveBackend(map[string]any{"label": "Local", "host": host, "keyEnv": "GATEWAY_JEV_KEY"})
14
+ if !hasCode(err, CodeConfiguration) || err.Error() != hostMessage {
15
+ t.Errorf("%s: %v", host, err)
16
+ }
17
+ }
18
+ for _, host := range []string{"http://127.0.0.1:8787", "http://127.255.255.254", "http://localhost:1", "http://[::1]:9"} {
19
+ if _, err := ResolveBackend(map[string]any{"label": "Local", "host": host, "keyEnv": "GATEWAY_JEV_KEY"}); err != nil {
20
+ t.Errorf("%s: %v", host, err)
21
+ }
22
+ }
23
+ }