@pi-in-go/pigpen-pi-typesafe 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.
Files changed (189) hide show
  1. package/CREDITS.md +14 -0
  2. package/LICENSE +22 -0
  3. package/README.md +45 -0
  4. package/extensions/pi-typesafe/branches_test.go +185 -0
  5. package/extensions/pi-typesafe/command.go +319 -0
  6. package/extensions/pi-typesafe/export_test.go +9 -0
  7. package/extensions/pi-typesafe/extension.go +188 -0
  8. package/extensions/pi-typesafe/extension_test.go +321 -0
  9. package/extensions/pi-typesafe/fakehost_test.go +548 -0
  10. package/extensions/pi-typesafe/format.go +191 -0
  11. package/extensions/pi-typesafe/format_test.go +75 -0
  12. package/extensions/pi-typesafe/go.mod +10 -0
  13. package/extensions/pi-typesafe/go.sum +2 -0
  14. package/extensions/pi-typesafe/go.work +11 -0
  15. package/extensions/pi-typesafe/harness_test.go +200 -0
  16. package/extensions/pi-typesafe/ownmodel_test.go +100 -0
  17. package/extensions/pi-typesafe/review_test.go +134 -0
  18. package/extensions/pi-typesafe/tool.go +193 -0
  19. package/extensions/pi-typesafe/twin_test.go +28 -0
  20. package/libs/pi-typesafe-api/CREDITS.md +14 -0
  21. package/libs/pi-typesafe-api/LICENSE +22 -0
  22. package/libs/pi-typesafe-api/README.md +30 -0
  23. package/libs/pi-typesafe-api/ask.go +62 -0
  24. package/libs/pi-typesafe-api/ask_test.go +76 -0
  25. package/libs/pi-typesafe-api/auth.go +249 -0
  26. package/libs/pi-typesafe-api/auth_test.go +131 -0
  27. package/libs/pi-typesafe-api/backends.go +336 -0
  28. package/libs/pi-typesafe-api/backends_test.go +404 -0
  29. package/libs/pi-typesafe-api/batch.go +202 -0
  30. package/libs/pi-typesafe-api/batch_test.go +202 -0
  31. package/libs/pi-typesafe-api/battery_test.go +41 -0
  32. package/libs/pi-typesafe-api/calibrate.go +354 -0
  33. package/libs/pi-typesafe-api/calibrate_test.go +186 -0
  34. package/libs/pi-typesafe-api/client.go +615 -0
  35. package/libs/pi-typesafe-api/client_test.go +490 -0
  36. package/libs/pi-typesafe-api/credentials.go +252 -0
  37. package/libs/pi-typesafe-api/credentials_test.go +216 -0
  38. package/libs/pi-typesafe-api/doc.go +14 -0
  39. package/libs/pi-typesafe-api/errors.go +143 -0
  40. package/libs/pi-typesafe-api/evaluation.go +86 -0
  41. package/libs/pi-typesafe-api/evaluation_schema.json +264 -0
  42. package/libs/pi-typesafe-api/gaps_test.go +77 -0
  43. package/libs/pi-typesafe-api/go.mod +9 -0
  44. package/libs/pi-typesafe-api/go.sum +2 -0
  45. package/libs/pi-typesafe-api/helpers_test.go +169 -0
  46. package/libs/pi-typesafe-api/hostmodel/hostmodel.go +87 -0
  47. package/libs/pi-typesafe-api/json.go +299 -0
  48. package/libs/pi-typesafe-api/json_test.go +92 -0
  49. package/libs/pi-typesafe-api/ownmodel_test.go +79 -0
  50. package/libs/pi-typesafe-api/package.json +40 -0
  51. package/libs/pi-typesafe-api/provenance.json +18 -0
  52. package/libs/pi-typesafe-api/review_test.go +23 -0
  53. package/libs/pi-typesafe-api/schema.go +473 -0
  54. package/libs/pi-typesafe-api/schema_test.go +262 -0
  55. package/libs/pi-typesafe-api/testdata/tools/typebox-messages.mts +5 -0
  56. package/libs/pi-typesafe-api/testdata/typebox-messages.json +285 -0
  57. package/libs/pi-typesafe-api/twin_test.go +28 -0
  58. package/libs/pi-typesafe-api/ui/fakehost_test.go +548 -0
  59. package/libs/pi-typesafe-api/ui/keyprompt.go +115 -0
  60. package/libs/pi-typesafe-api/ui/login.go +106 -0
  61. package/libs/pi-typesafe-api/ui/twin_test.go +28 -0
  62. package/libs/pi-typesafe-api/ui/ui_test.go +285 -0
  63. package/libs/pi-typesafe-api/usage.go +366 -0
  64. package/libs/pi-typesafe-api/usage_test.go +139 -0
  65. package/libs/typesafe/CONTRACT.md +125 -0
  66. package/libs/typesafe/CREDITS.md +37 -0
  67. package/libs/typesafe/LICENSE +23 -0
  68. package/libs/typesafe/README.md +19 -0
  69. package/libs/typesafe/go.mod +3 -0
  70. package/libs/typesafe/libraries/ownmodel/backend_test.go +496 -0
  71. package/libs/typesafe/libraries/ownmodel/canon.go +190 -0
  72. package/libs/typesafe/libraries/ownmodel/convert.go +199 -0
  73. package/libs/typesafe/libraries/ownmodel/doc.go +15 -0
  74. package/libs/typesafe/libraries/ownmodel/equivalence_test.go +199 -0
  75. package/libs/typesafe/libraries/ownmodel/helpers_test.go +155 -0
  76. package/libs/typesafe/libraries/ownmodel/mutation_test.go +31 -0
  77. package/libs/typesafe/libraries/ownmodel/ownmodel.go +225 -0
  78. package/libs/typesafe/libraries/ownmodel/plan.go +442 -0
  79. package/libs/typesafe/libraries/ownmodel/run.go +288 -0
  80. package/libs/typesafe/libraries/ownmodel/schema_test.go +254 -0
  81. package/libs/typesafe/libraries/ownmodel/twins_test.go +169 -0
  82. package/libs/typesafe/libraries/ownmodel/utils_test.go +125 -0
  83. package/libs/typesafe/libraries/pigmodel/pigmodel.go +264 -0
  84. package/libs/typesafe/libraries/pigmodel/pigmodel_test.go +410 -0
  85. package/libs/typesafe/libraries/typesafe/answers.go +268 -0
  86. package/libs/typesafe/libraries/typesafe/api_response_test.go +113 -0
  87. package/libs/typesafe/libraries/typesafe/batch.go +80 -0
  88. package/libs/typesafe/libraries/typesafe/batch_test.go +133 -0
  89. package/libs/typesafe/libraries/typesafe/bench_test.go +71 -0
  90. package/libs/typesafe/libraries/typesafe/client.go +561 -0
  91. package/libs/typesafe/libraries/typesafe/client_test.go +495 -0
  92. package/libs/typesafe/libraries/typesafe/crosscheck_test.go +464 -0
  93. package/libs/typesafe/libraries/typesafe/crosscheck_workflowevals_test.go +219 -0
  94. package/libs/typesafe/libraries/typesafe/doc.go +27 -0
  95. package/libs/typesafe/libraries/typesafe/entry.go +142 -0
  96. package/libs/typesafe/libraries/typesafe/env.go +11 -0
  97. package/libs/typesafe/libraries/typesafe/errors.go +310 -0
  98. package/libs/typesafe/libraries/typesafe/errors_test.go +175 -0
  99. package/libs/typesafe/libraries/typesafe/helpers_test.go +294 -0
  100. package/libs/typesafe/libraries/typesafe/live_test.go +96 -0
  101. package/libs/typesafe/libraries/typesafe/logging.go +160 -0
  102. package/libs/typesafe/libraries/typesafe/logging_test.go +259 -0
  103. package/libs/typesafe/libraries/typesafe/marshal_test.go +112 -0
  104. package/libs/typesafe/libraries/typesafe/mutation_test.go +39 -0
  105. package/libs/typesafe/libraries/typesafe/questions.go +490 -0
  106. package/libs/typesafe/libraries/typesafe/questions_test.go +166 -0
  107. package/libs/typesafe/libraries/typesafe/regressions_test.go +159 -0
  108. package/libs/typesafe/libraries/typesafe/reliability_test.go +649 -0
  109. package/libs/typesafe/libraries/typesafe/retry.go +350 -0
  110. package/libs/typesafe/libraries/typesafe/retry_test.go +297 -0
  111. package/libs/typesafe/libraries/typesafe/runtime_test.go +26 -0
  112. package/libs/typesafe/libraries/typesafe/transport_test.go +163 -0
  113. package/libs/typesafe/libraries/typesafe/twins_test.go +127 -0
  114. package/libs/typesafe/libraries/typesafe/types_test.go +165 -0
  115. package/libs/typesafe/libraries/typesafe/version.go +10 -0
  116. package/libs/typesafe/package.json +37 -0
  117. package/libs/typesafe/provenance.json +49 -0
  118. package/package.json +42 -0
  119. package/port/PORT.md +98 -0
  120. package/port/accepted-gaps.json +3 -0
  121. package/port/golden/enable-confirm.jsonl +11 -0
  122. package/port/golden/enable-decline.jsonl +20 -0
  123. package/port/golden/enable-missing-key.jsonl +4 -0
  124. package/port/golden/login-shadow.jsonl +4 -0
  125. package/port/golden/logout-env-key.jsonl +6 -0
  126. package/port/golden/playground-cancel.jsonl +4 -0
  127. package/port/golden/playground-invalid-json.jsonl +5 -0
  128. package/port/golden/playground-invalid-questions.jsonl +5 -0
  129. package/port/golden/status-env-key.jsonl +6 -0
  130. package/port/golden/status-no-key.jsonl +6 -0
  131. package/port/golden/tool-disabled.jsonl +18 -0
  132. package/port/golden/trailing-words.jsonl +10 -0
  133. package/port/library-mutations.py +44 -0
  134. package/port/mutations.json +302 -0
  135. package/port/oracle/.env.example +4 -0
  136. package/port/oracle/CHANGELOG.md +91 -0
  137. package/port/oracle/CONTRIBUTING.md +35 -0
  138. package/port/oracle/LICENSE +21 -0
  139. package/port/oracle/README.md +159 -0
  140. package/port/oracle/docs/api.md +143 -0
  141. package/port/oracle/docs/ci-cd.md +97 -0
  142. package/port/oracle/examples/decision-extension.ts +41 -0
  143. package/port/oracle/extensions/index.js +2 -0
  144. package/port/oracle/package.json +89 -0
  145. package/port/oracle/scripts/dev-pi.mjs +23 -0
  146. package/port/oracle/scripts/live-smoke.mjs +35 -0
  147. package/port/oracle/src/ask.ts +42 -0
  148. package/port/oracle/src/auth.ts +171 -0
  149. package/port/oracle/src/backends.ts +196 -0
  150. package/port/oracle/src/batch.ts +170 -0
  151. package/port/oracle/src/calibrate.ts +237 -0
  152. package/port/oracle/src/client.ts +310 -0
  153. package/port/oracle/src/credentials.ts +136 -0
  154. package/port/oracle/src/errors.ts +53 -0
  155. package/port/oracle/src/extension.ts +204 -0
  156. package/port/oracle/src/index.ts +31 -0
  157. package/port/oracle/src/key-prompt.ts +51 -0
  158. package/port/oracle/src/login.ts +60 -0
  159. package/port/oracle/src/schema.ts +158 -0
  160. package/port/oracle/src/ui.ts +4 -0
  161. package/port/oracle/src/usage.ts +258 -0
  162. package/port/oracle/tests/ask.test.ts +63 -0
  163. package/port/oracle/tests/auth.test.ts +141 -0
  164. package/port/oracle/tests/backends.test.ts +380 -0
  165. package/port/oracle/tests/batch.test.ts +156 -0
  166. package/port/oracle/tests/calibrate.test.ts +144 -0
  167. package/port/oracle/tests/client.test.ts +499 -0
  168. package/port/oracle/tests/credentials.test.ts +144 -0
  169. package/port/oracle/tests/extension.test.ts +276 -0
  170. package/port/oracle/tests/key-prompt.test.ts +47 -0
  171. package/port/oracle/tests/login.test.ts +101 -0
  172. package/port/oracle/tests/schema.test.ts +85 -0
  173. package/port/oracle/tests/usage.test.ts +106 -0
  174. package/port/oracle/tsconfig.build.json +10 -0
  175. package/port/oracle/tsconfig.json +14 -0
  176. package/port/scenarios/enable-confirm.json +5 -0
  177. package/port/scenarios/enable-decline.json +3 -0
  178. package/port/scenarios/enable-missing-key.json +2 -0
  179. package/port/scenarios/login-shadow.json +2 -0
  180. package/port/scenarios/logout-env-key.json +3 -0
  181. package/port/scenarios/playground-cancel.json +2 -0
  182. package/port/scenarios/playground-invalid-json.json +2 -0
  183. package/port/scenarios/playground-invalid-questions.json +2 -0
  184. package/port/scenarios/status-env-key.json +3 -0
  185. package/port/scenarios/status-no-key.json +3 -0
  186. package/port/scenarios/tool-disabled.json +2 -0
  187. package/port/scenarios/trailing-words.json +5 -0
  188. package/port/upstream-tests.json +160 -0
  189. package/provenance.json +18 -0
@@ -0,0 +1,163 @@
1
+ package typesafe
2
+
3
+ import (
4
+ "context"
5
+ "net/http"
6
+ "net/http/httptest"
7
+ "sync/atomic"
8
+ "testing"
9
+ "time"
10
+ )
11
+
12
+ // These tests use a real HTTP server: fakes do not reproduce the headers/body lifecycle.
13
+
14
+ func withServer(t *testing.T, h http.HandlerFunc, run func(baseURL string)) {
15
+ t.Helper()
16
+ s := httptest.NewServer(h)
17
+ defer func() {
18
+ s.CloseClientConnections()
19
+ s.Close()
20
+ }()
21
+ run(s.URL)
22
+ }
23
+
24
+ // countingDoer wraps the real client and counts responses whose headers arrived.
25
+ type countingDoer struct {
26
+ inner *http.Client
27
+ headers atomic.Int32
28
+ after func()
29
+ }
30
+
31
+ func (c *countingDoer) Do(req *http.Request) (*http.Response, error) {
32
+ resp, err := c.inner.Do(req)
33
+ if err == nil {
34
+ c.headers.Add(1)
35
+ if c.after != nil {
36
+ c.after()
37
+ }
38
+ }
39
+ return resp, err
40
+ }
41
+
42
+ func TestNativeTransport_SendsOneAuthorizationContentTypeAndOverridingCustomHeaderOnTheWire(t *testing.T) {
43
+ twin(t, "native-transport.test.ts | native response transport sends one authorization, content type and overriding custom header on the wire")
44
+ var observed http.Header
45
+ withServer(t, func(w http.ResponseWriter, r *http.Request) {
46
+ observed = r.Header.Clone()
47
+ _, _ = w.Write([]byte("{}"))
48
+ }, func(baseURL string) {
49
+ c, err := NewClient(Config{APIKey: "test", BaseURL: baseURL, Getenv: noEnv, DefaultHeaders: map[string]string{"authorization": "bad", "X-Team": "default", "content-type": "bad"}})
50
+ noErr(t, err)
51
+ _, err = c.SystemOne(ctxBG(), SystemOneRequest{State: Text("s"), Questions: Questions{Ask("q", Noul("?"))}}, &RequestOptions{Headers: map[string]string{"x-team": "call"}})
52
+ noErr(t, err)
53
+ })
54
+ eq(t, observed.Values("Authorization"), []string{"Bearer test"})
55
+ eq(t, observed.Values("X-Team"), []string{"call"})
56
+ eq(t, observed.Values("Content-Type"), []string{"application/json"})
57
+ }
58
+
59
+ func TestNativeTransport_TimesOutAStalledBodyOnParsedWithResponseAndRawPaths(t *testing.T) {
60
+ twin(t,
61
+ "native-transport.test.ts | native response transport times out a stalled 200 body on parsed, withResponse and raw paths",
62
+ "native-transport.test.ts | native response transport times out a stalled 503 body on parsed, withResponse and raw paths")
63
+ for _, status := range []int{200, 503} {
64
+ withServer(t, func(w http.ResponseWriter, r *http.Request) {
65
+ w.Header().Set("Content-Type", "application/json")
66
+ w.WriteHeader(status)
67
+ w.(http.Flusher).Flush()
68
+ <-r.Context().Done()
69
+ }, func(baseURL string) {
70
+ doer := &countingDoer{inner: &http.Client{}}
71
+ c, err := NewClient(Config{APIKey: "k", BaseURL: baseURL, Getenv: noEnv, Timeout: 200 * time.Millisecond, Retry: RetryOverrides{MaxRetries: Ptr(0)}, HTTPClient: doer})
72
+ noErr(t, err)
73
+ _, err = c.Models().List(ctxBG(), nil)
74
+ mustAs[*APITimeoutError](t, err)
75
+ _, err = c.Models().ListWithResponse(ctxBG(), nil)
76
+ mustAs[*APITimeoutError](t, err)
77
+ _, err = c.Models().ListRaw(ctxBG(), nil)
78
+ mustAs[*APITimeoutError](t, err)
79
+ eq(t, int(doer.headers.Load()), 3)
80
+ })
81
+ }
82
+ }
83
+
84
+ func TestNativeTransport_HonorsCallerCancellationAfterHeadersAndNeverRetries(t *testing.T) {
85
+ twin(t,
86
+ "native-transport.test.ts | native response transport honors caller cancellation after 200 headers and never retries",
87
+ "native-transport.test.ts | native response transport honors caller cancellation after 503 headers and never retries")
88
+ for _, status := range []int{200, 503} {
89
+ var attempts atomic.Int32
90
+ withServer(t, func(w http.ResponseWriter, r *http.Request) {
91
+ attempts.Add(1)
92
+ w.Header().Set("Content-Type", "application/json")
93
+ w.WriteHeader(status)
94
+ w.(http.Flusher).Flush()
95
+ <-r.Context().Done()
96
+ }, func(baseURL string) {
97
+ ctx, cancel := context.WithCancel(context.Background())
98
+ defer cancel()
99
+ doer := &countingDoer{inner: &http.Client{}, after: func() { time.AfterFunc(10*time.Millisecond, cancel) }}
100
+ c, err := NewClient(Config{APIKey: "k", BaseURL: baseURL, Getenv: noEnv, HTTPClient: doer})
101
+ noErr(t, err)
102
+ _, err = c.Models().List(ctx, nil)
103
+ mustAs[*APIUserAbortError](t, err)
104
+ eq(t, int(attempts.Load()), 1)
105
+ })
106
+ }
107
+ }
108
+
109
+ func TestNativeTransport_RetriesABrokenBodyAndExposesTheSuccessfulResponseMetadata(t *testing.T) {
110
+ twin(t,
111
+ "native-transport.test.ts | native response transport retries a broken 200 body and exposes the successful response metadata",
112
+ "native-transport.test.ts | native response transport retries a broken 503 body and exposes the successful response metadata")
113
+ for _, status := range []int{200, 503} {
114
+ var attempts atomic.Int32
115
+ withServer(t, func(w http.ResponseWriter, r *http.Request) {
116
+ n := attempts.Add(1)
117
+ w.Header().Set("Content-Type", "application/json")
118
+ w.Header().Set("X-Typesafe-Request-Id", "req_"+string(rune('0'+n)))
119
+ if n == 1 {
120
+ w.WriteHeader(status)
121
+ _, _ = w.Write([]byte("["))
122
+ w.(http.Flusher).Flush()
123
+ time.Sleep(10 * time.Millisecond)
124
+ panic(http.ErrAbortHandler) // closes the connection mid-body
125
+ }
126
+ _, _ = w.Write([]byte(`{"models":[]}`))
127
+ }, func(baseURL string) {
128
+ c, err := NewClient(Config{APIKey: "k", BaseURL: baseURL, Getenv: noEnv, Retry: RetryOverrides{MaxRetries: Ptr(1), BackoffInitial: Ptr(time.Duration(0))}})
129
+ noErr(t, err)
130
+ res, err := c.Models().ListWithResponse(ctxBG(), nil)
131
+ noErr(t, err)
132
+ eq(t, res.Data, []ModelCard{})
133
+ eq(t, int(attempts.Load()), 2)
134
+ eq(t, res.RequestID, "req_2")
135
+ eq(t, res.URL, baseURL+"/v1/models")
136
+ })
137
+ }
138
+ }
139
+
140
+ func TestNativeTransport_BuffersDelayedChunksBeforeRawHandoffAndPreservesAReadableBodyAfterCancellation(t *testing.T) {
141
+ twin(t, "native-transport.test.ts | native response transport buffers delayed chunks before raw handoff and preserves a readable body after cancellation")
142
+ var completed atomic.Bool
143
+ withServer(t, func(w http.ResponseWriter, r *http.Request) {
144
+ w.Header().Set("Content-Type", "application/json")
145
+ _, _ = w.Write([]byte("["))
146
+ w.(http.Flusher).Flush()
147
+ time.Sleep(20 * time.Millisecond)
148
+ completed.Store(true)
149
+ _, _ = w.Write([]byte("]"))
150
+ }, func(baseURL string) {
151
+ ctx, cancel := context.WithCancel(context.Background())
152
+ c, err := NewClient(Config{APIKey: "k", BaseURL: baseURL, Getenv: noEnv})
153
+ noErr(t, err)
154
+ raw, err := c.Models().ListRaw(ctx, nil)
155
+ noErr(t, err)
156
+ if !completed.Load() {
157
+ t.Fatal("the raw response was handed over before the body finished")
158
+ }
159
+ eq(t, raw.URL, baseURL+"/v1/models")
160
+ cancel()
161
+ eq(t, string(raw.Body), "[]")
162
+ })
163
+ }
@@ -0,0 +1,127 @@
1
+ package typesafe
2
+
3
+ import (
4
+ "bufio"
5
+ "go/ast"
6
+ "go/parser"
7
+ "go/token"
8
+ "os"
9
+ "path/filepath"
10
+ "sort"
11
+ "strconv"
12
+ "strings"
13
+ "testing"
14
+ )
15
+
16
+ // TestEveryUpstreamCaseHasATwinOrANamedSkip proves the 1:1 mapping of the TypeScript SDK
17
+ // suite: every case in port/twins/typesafe-sdk-js.txt is claimed by exactly one Go test
18
+ // through twin(...) (ported, possibly adapted) or skipTwin(...) (with a reason), and no Go
19
+ // test claims a case that does not exist.
20
+ func TestEveryUpstreamCaseHasATwinOrANamedSkip(t *testing.T) {
21
+ cases := readCaseList(t, filepath.Join("..", "..", "port", "twins", "typesafe-sdk-js.txt"))
22
+ ported, skipped := scanTwinClaims(t, ".")
23
+ claimed := map[string]int{}
24
+ for id := range ported {
25
+ claimed[id]++
26
+ }
27
+ for id := range skipped {
28
+ claimed[id]++
29
+ }
30
+ var missing, dup, unknown []string
31
+ for _, id := range cases {
32
+ switch claimed[id] {
33
+ case 0:
34
+ missing = append(missing, id)
35
+ case 1:
36
+ default:
37
+ dup = append(dup, id)
38
+ }
39
+ }
40
+ known := map[string]bool{}
41
+ for _, id := range cases {
42
+ known[id] = true
43
+ }
44
+ for id := range claimed {
45
+ if !known[id] {
46
+ unknown = append(unknown, id)
47
+ }
48
+ }
49
+ sort.Strings(unknown)
50
+ if len(missing)+len(dup)+len(unknown) > 0 {
51
+ t.Fatalf("%d upstream cases have no twin:\n%s\nclaimed twice:\n%s\nunknown ids:\n%s",
52
+ len(missing), strings.Join(missing, "\n"), strings.Join(dup, "\n"), strings.Join(unknown, "\n"))
53
+ }
54
+ t.Logf("typesafe-sdk-js: %d cases, %d ported twins, %d named skips", len(cases), len(ported), len(skipped))
55
+ }
56
+
57
+ func readCaseList(t *testing.T, path string) []string {
58
+ t.Helper()
59
+ f, err := os.Open(path)
60
+ noErr(t, err)
61
+ defer f.Close()
62
+ var out []string
63
+ sc := bufio.NewScanner(f)
64
+ for sc.Scan() {
65
+ line := strings.TrimRight(sc.Text(), "\r\n")
66
+ if line == "" || strings.HasPrefix(line, "#") {
67
+ continue
68
+ }
69
+ out = append(out, line)
70
+ }
71
+ noErr(t, sc.Err())
72
+ return out
73
+ }
74
+
75
+ // scanTwinClaims returns the ids claimed by twin(t, ids...) and skipTwin(t, reason, ids...)
76
+ // calls in the package's test files, with skip reasons required to be non-empty.
77
+ func scanTwinClaims(t *testing.T, dir string) (ported, skipped map[string]string) {
78
+ t.Helper()
79
+ ported, skipped = map[string]string{}, map[string]string{}
80
+ files, err := filepath.Glob(filepath.Join(dir, "*_test.go"))
81
+ noErr(t, err)
82
+ fset := token.NewFileSet()
83
+ for _, file := range files {
84
+ f, err := parser.ParseFile(fset, file, nil, 0)
85
+ noErr(t, err)
86
+ ast.Inspect(f, func(n ast.Node) bool {
87
+ call, ok := n.(*ast.CallExpr)
88
+ if !ok {
89
+ return true
90
+ }
91
+ id, ok := call.Fun.(*ast.Ident)
92
+ if !ok || (id.Name != "twin" && id.Name != "skipTwin") {
93
+ return true
94
+ }
95
+ args := call.Args[1:]
96
+ reason := ""
97
+ if id.Name == "skipTwin" {
98
+ reason = strLit(t, args[0])
99
+ if strings.TrimSpace(reason) == "" {
100
+ t.Fatalf("%s: skipTwin needs a reason", fset.Position(call.Pos()))
101
+ }
102
+ args = args[1:]
103
+ }
104
+ for _, a := range args {
105
+ key := strLit(t, a)
106
+ dst := ported
107
+ if id.Name == "skipTwin" {
108
+ dst = skipped
109
+ }
110
+ dst[key] = reason
111
+ }
112
+ return true
113
+ })
114
+ }
115
+ return ported, skipped
116
+ }
117
+
118
+ func strLit(t *testing.T, e ast.Expr) string {
119
+ t.Helper()
120
+ lit, ok := e.(*ast.BasicLit)
121
+ if !ok || lit.Kind != token.STRING {
122
+ t.Fatalf("twin ids and reasons must be string literals, got %T", e)
123
+ }
124
+ s, err := strconv.Unquote(lit.Value)
125
+ noErr(t, err)
126
+ return s
127
+ }
@@ -0,0 +1,165 @@
1
+ package typesafe
2
+
3
+ import (
4
+ "encoding/json"
5
+ "net/http"
6
+ "testing"
7
+ )
8
+
9
+ const allAnswersJSON = `{"model":"m","answers":{
10
+ "a":{"type":"noul","noul":0.25},
11
+ "b":{"type":"choice","choice":"yes","confidence":0.5,"probabilities":{"yes":0.75,"no":0.25}},
12
+ "c":{"type":"score","score":0.5,"confidence":0.4,"legend":{"0":"bad","1":"ok"},"probabilities":{"0":0.5,"1":0.5}}},
13
+ "usage":{"input_tokens":3,"output_tokens":4}}`
14
+
15
+ func decodeResult(t *testing.T, s string) *SystemOneResult {
16
+ t.Helper()
17
+ var r SystemOneResult
18
+ noErr(t, json.Unmarshal([]byte(s), &r))
19
+ return &r
20
+ }
21
+
22
+ func TestTypes_MapsEachQuestionToItsResponseTypeWithLiteralCriteriaKeys(t *testing.T) {
23
+ // Adapted: TypeScript infers the answer type from the question literals; Go returns
24
+ // concrete answer types from typed accessors, and a wrong name or type is an error.
25
+ twin(t, "types.test-d.ts | test/types.test-d.ts systemOne result inference maps each question to its response type with literal criteria keys")
26
+ r := decodeResult(t, allAnswersJSON)
27
+ var noul NoulAnswer
28
+ var choice ChoiceAnswer
29
+ var score ScoreAnswer
30
+ var err error
31
+ noul, err = r.Noul("a")
32
+ noErr(t, err)
33
+ choice, err = r.Choice("b")
34
+ noErr(t, err)
35
+ score, err = r.Score("c")
36
+ noErr(t, err)
37
+ var _ float64 = noul.Noul
38
+ var _ string = choice.Choice
39
+ var _ map[string]float64 = choice.Probabilities
40
+ var _ float64 = score.Score
41
+ eq(t, choice.Probabilities, map[string]float64{"yes": 0.75, "no": 0.25})
42
+ if _, err = r.Choice("d"); err == nil {
43
+ t.Fatal("an unknown answer name must be an error")
44
+ }
45
+ if _, err = r.Noul("b"); err == nil {
46
+ t.Fatal("a type mismatch must be an error")
47
+ }
48
+ contains(t, err.Error(), `"b"`)
49
+ }
50
+
51
+ func TestTypes_TypesScoreListsByIndexWithTheLegendKeyedByScore(t *testing.T) {
52
+ twin(t, "types.test-d.ts | test/types.test-d.ts systemOne result inference types score lists by index, with the legend keyed by score")
53
+ r := decodeResult(t, `{"model":"m","answers":{"c":{"type":"score","score":1,"confidence":1,"legend":{"0":"bad","1":"ok","2":{"rich":"great"}},"probabilities":{"0":0,"1":0,"2":1}}},"usage":{"input_tokens":0,"output_tokens":0}}`)
54
+ s, err := r.Score("c")
55
+ noErr(t, err)
56
+ eq(t, s.Probabilities, map[int]float64{0: 0, 1: 0, 2: 1})
57
+ eq(t, s.Legend[2], any(map[string]any{"rich": "great"}))
58
+ eq(t, s.Legend[0], any("bad"))
59
+ if _, has := s.Legend[3]; has {
60
+ t.Fatal("score 3 was not defined")
61
+ }
62
+ }
63
+
64
+ func TestTypes_TypeLevelInference(t *testing.T) {
65
+ skipTwin(t, "TypeScript conditional-type inference and readonly modifiers have no Go counterpart: non-literal score lists, readonly results and client settings (the accessors return copies, TestRetryPolicy_IsolatesThe...), and the APIPromise type",
66
+ "types.test-d.ts | test/types.test-d.ts systemOne result inference degrades to numeric indexing for non-literal score lists",
67
+ "types.test-d.ts | test/types.test-d.ts systemOne result inference results are readonly",
68
+ "types.test-d.ts | test/types.test-d.ts systemOne result inference client settings are readonly",
69
+ "types.test-d.ts | test/types.test-d.ts APIPromise client methods return APIPromise")
70
+ }
71
+
72
+ func TestTypes_AcceptsAPartialRetryPolicyOnTheClientAndPerCall(t *testing.T) {
73
+ twin(t, "types.test-d.ts | test/types.test-d.ts systemOne result inference accepts a partial retry policy on the client and per call")
74
+ partial := RetryOverrides{MaxRetries: Ptr(1), HTTPStatuses: []int{503}}
75
+ c := newClient(t, modelsOK(), func(cfg *Config) { cfg.Retry = partial })
76
+ _, err := c.Models().List(ctxBG(), &RequestOptions{Retry: RetryOverrides{HTTPStatuses: []int{503}, BackoffJitter: Ptr(0.0)}})
77
+ noErr(t, err)
78
+ }
79
+
80
+ func TestTypes_CriteriaShapes(t *testing.T) {
81
+ skipTwin(t, "compile-time rejections (a label list for choice, a numeric-keyed map or a short list for score) are properties of the TypeScript types; in Go Choice takes []Option and Score takes a list, and the runtime rules are Questions.Validate and ParseQuestions",
82
+ "types.test-d.ts | test/types.test-d.ts criteria shapes rejects label-list shorthand for choice",
83
+ "types.test-d.ts | test/types.test-d.ts criteria shapes rejects numeric-keyed maps for score")
84
+ }
85
+
86
+ func TestTypes_ConstrainsDescriptionsWhileAllowingNullForEveryQuestionType(t *testing.T) {
87
+ // Adapted: numbers are not valid descriptions; Go reports them when the request is built.
88
+ twin(t, "types.test-d.ts | test/types.test-d.ts question helpers constrains descriptions while allowing null for every question type")
89
+ for _, q := range []Question{
90
+ Score("q", "ok", nil), Noul(nil).Yes(nil).No(nil), Noul(nil).NullCriteria(), Noul(nil), Choice(nil, Opt("yes", nil)), Score(nil, nil, nil),
91
+ Choice("q", Opt("yes", nil), Opt("no", "ok"), Opt("maybe", map[string]any{"detail": "rich"})),
92
+ Score("q", "bad", "ok", map[string]any{"detail": "rich"}), Noul("q").Yes(map[string]any{"detail": "rich"}), Noul("q").No("only one side"),
93
+ } {
94
+ if _, err := json.Marshal(q); err != nil {
95
+ t.Fatalf("%T: %v", q, err)
96
+ }
97
+ }
98
+ for _, q := range []Question{Choice("q", Opt("yes", 1)), Score("q", 1, 2), Noul("q").Yes(true)} {
99
+ _, err := json.Marshal(q)
100
+ if err == nil {
101
+ t.Fatalf("%T: a number or boolean description must not marshal", q)
102
+ }
103
+ mustAs[*TypeSafeError](t, unwrapMarshal(err))
104
+ }
105
+ }
106
+
107
+ // unwrapMarshal digs the *TypeSafeError out of a json.MarshalerError.
108
+ func unwrapMarshal(err error) error {
109
+ var me *json.MarshalerError
110
+ if asErr(err, &me) {
111
+ return me.Err
112
+ }
113
+ return err
114
+ }
115
+
116
+ func TestTypes_PreservesResultInferenceWithNullsArraysAndOmittedInstructions(t *testing.T) {
117
+ twin(t, "types.test-d.ts | test/types.test-d.ts question helpers preserves result inference with nulls, arrays, and omitted instructions")
118
+ r := decodeResult(t, `{"model":"m","answers":{
119
+ "noul":{"type":"noul","noul":1},
120
+ "choice":{"type":"choice","choice":"yes","confidence":1,"probabilities":{"yes":1,"no":0}},
121
+ "list":{"type":"score","score":1,"confidence":1,"legend":{"0":null,"1":"high"},"probabilities":{"0":0,"1":1}}},"usage":{"input_tokens":0,"output_tokens":0}}`)
122
+ l, err := r.Score("list")
123
+ noErr(t, err)
124
+ eq(t, l.Legend, map[int]any{0: nil, 1: "high"})
125
+ eq(t, l.Probabilities, map[int]float64{0: 0, 1: 1})
126
+ c, err := r.Choice("choice")
127
+ noErr(t, err)
128
+ eq(t, c.Choice, "yes")
129
+ _, err = json.Marshal(Questions{
130
+ Ask("q", Noul([]any{nil, "instructions"}).Yes([]any{nil, "criterion"})),
131
+ Ask("c", Choice([]any{"instructions"}, Opt("yes", []any{nil, "criterion"}))),
132
+ Ask("s", Score([]any{"instructions"}, []any{nil, "criterion"}, nil)),
133
+ })
134
+ noErr(t, err)
135
+ }
136
+
137
+ func TestTypes_AreAllAssignableToQuestion(t *testing.T) {
138
+ twin(t, "types.test-d.ts | test/types.test-d.ts question helpers are all assignable to Question")
139
+ var _ Question = Noul("x")
140
+ var _ Question = Choice("x", Opt("a", nil))
141
+ var _ Question = Choice("x", Opt("a", nil), Opt("b", nil))
142
+ var _ Question = Score("x", "a", "b")
143
+ eq(t, Noul("x").QuestionType(), TypeNoul)
144
+ eq(t, Choice("x").QuestionType(), TypeChoice)
145
+ eq(t, Score("x").QuestionType(), TypeScore)
146
+ }
147
+
148
+ func TestTypes_AwaitingYieldsThePlainResult(t *testing.T) {
149
+ twin(t, "types.test-d.ts | test/types.test-d.ts APIPromise awaiting yields the plain result")
150
+ m := always(func() *http.Response {
151
+ return jsonResp(200, decode(t, allAnswersJSON), "x-typesafe-request-id", "req_1")
152
+ })
153
+ c := newClient(t, m)
154
+ res, err := c.SystemOne(ctxBG(), SystemOneRequest{State: Text("s"), Questions: Questions{Ask("a", Noul("x"))}}, nil)
155
+ noErr(t, err)
156
+ var _ *SystemOneResult = res
157
+ a, err := res.Noul("a")
158
+ noErr(t, err)
159
+ eq(t, a.Noul, 0.25)
160
+ wr, err := c.SystemOneWithResponse(ctxBG(), SystemOneRequest{State: Text("s"), Questions: Questions{Ask("a", Noul("x"))}}, nil)
161
+ noErr(t, err)
162
+ var _ string = wr.RequestID
163
+ eq(t, wr.RequestID, "req_1")
164
+ eq(t, wr.Data.Usage, Usage{InputTokens: 3, OutputTokens: 4})
165
+ }
@@ -0,0 +1,10 @@
1
+ package typesafe
2
+
3
+ // Version is the version of the TypeScript SDK this package ports (@typesafe-ai/sdk).
4
+ const Version = "0.6.0"
5
+
6
+ // Default values used when neither the configuration nor the environment set one.
7
+ const (
8
+ DefaultBaseURL = "https://api.typesafe.ai"
9
+ DefaultModel = "jev-latest"
10
+ )
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@pi-in-go/pigpen-typesafe",
3
+ "version": "0.1.0",
4
+ "description": "Shared Go client for the TypeSafe AI API (a port of the official TypeScript SDK) with an own-model backend that answers the same typed questions with the model PiG is configured with. Libraries only, no extension of its own.",
5
+ "keywords": [
6
+ "pig-package",
7
+ "typesafe",
8
+ "llm",
9
+ "evaluation",
10
+ "library",
11
+ "pigpen"
12
+ ],
13
+ "license": "MIT",
14
+ "author": "Michael Kinsy",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/MichaelKinsy/pigpen.git",
18
+ "directory": "components/typesafe"
19
+ },
20
+ "homepage": "https://github.com/MichaelKinsy/pigpen/tree/main/components/typesafe#readme",
21
+ "bugs": {
22
+ "url": "https://github.com/MichaelKinsy/pigpen/issues"
23
+ },
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "files": [
28
+ "libraries",
29
+ "port",
30
+ "go.mod",
31
+ "CONTRACT.md",
32
+ "README.md",
33
+ "LICENSE",
34
+ "CREDITS.md",
35
+ "provenance.json"
36
+ ]
37
+ }
@@ -0,0 +1,49 @@
1
+ {
2
+ "origin": "ported",
3
+ "authors": [
4
+ "Michael Kinsy"
5
+ ],
6
+ "license": "MIT",
7
+ "licenseFile": "LICENSE",
8
+ "upstreams": [
9
+ {
10
+ "name": "typesafe-sdk-js",
11
+ "authors": [
12
+ "TypeSafe",
13
+ "evinism"
14
+ ],
15
+ "url": "https://github.com/typesafe-ai/typesafe-sdk-js",
16
+ "revision": "66880ccded6cb642dc1809620c2b108c33730214",
17
+ "path": "port/oracle/typesafe-sdk-js",
18
+ "license": "MIT",
19
+ "licenseFile": "port/oracle/typesafe-sdk-js/LICENSE",
20
+ "attributionFile": "CREDITS.md"
21
+ },
22
+ {
23
+ "name": "system-one-adapter-python",
24
+ "authors": [
25
+ "TypeSafe AI",
26
+ "Erik Gafni",
27
+ "Daniel Gafni"
28
+ ],
29
+ "url": "https://github.com/typesafe-ai/system-one-adapter-python",
30
+ "revision": "e1d4cc938204b22fc5a3c3aca7044072fe3f712d",
31
+ "path": "port/oracle/system-one-adapter-python",
32
+ "license": "MIT",
33
+ "licenseFile": "port/oracle/system-one-adapter-python/LICENSE",
34
+ "attributionFile": "CREDITS.md"
35
+ },
36
+ {
37
+ "name": "WorkflowEvals",
38
+ "authors": [
39
+ "Sam"
40
+ ],
41
+ "url": "https://github.com/typesafe-ai/WorkflowEvals",
42
+ "revision": "0ac3b8ad845429f0d8e064ecfb2430a47c5a25cb",
43
+ "path": "port/oracle/WorkflowEvals",
44
+ "license": "Apache-2.0",
45
+ "licenseFile": "port/oracle/WorkflowEvals/LICENSE",
46
+ "attributionFile": "CREDITS.md"
47
+ }
48
+ ]
49
+ }
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@pi-in-go/pigpen-pi-typesafe",
3
+ "version": "0.1.0",
4
+ "description": "Go port of pi-typesafe's extension: the batched typesafe_evaluate tool, the /typesafe command (login, consent, usage, sample test, playground) and result renderers. Off until the operator enables it. Content goes to api.typesafe.ai unless the own-model backend (the model PiG is configured with) is selected.",
5
+ "keywords": [
6
+ "pig-package",
7
+ "pig-extension",
8
+ "typesafe",
9
+ "jev",
10
+ "structured-decisions",
11
+ "pigpen",
12
+ "extension"
13
+ ],
14
+ "license": "MIT",
15
+ "author": "Michael Kinsy",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/MichaelKinsy/pigpen.git",
19
+ "directory": "components/pi-typesafe"
20
+ },
21
+ "homepage": "https://github.com/MichaelKinsy/pigpen/tree/main/components/pi-typesafe#readme",
22
+ "bugs": {
23
+ "url": "https://github.com/MichaelKinsy/pigpen/issues"
24
+ },
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "files": [
29
+ "extensions",
30
+ "port",
31
+ "README.md",
32
+ "LICENSE",
33
+ "CREDITS.md",
34
+ "provenance.json",
35
+ "libs"
36
+ ],
37
+ "pi": {
38
+ "extensions": [
39
+ "extensions/pi-typesafe"
40
+ ]
41
+ }
42
+ }