@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,191 @@
1
+ package pi_typesafe
2
+
3
+ import (
4
+ "encoding/json"
5
+ "fmt"
6
+ "math"
7
+ "math/big"
8
+ "sort"
9
+ "strconv"
10
+ "strings"
11
+ "unicode/utf8"
12
+ )
13
+
14
+ // jsonString is JSON.stringify for a string.
15
+ func jsonString(s string) string {
16
+ var b strings.Builder
17
+ enc := json.NewEncoder(&b)
18
+ enc.SetEscapeHTML(false)
19
+ _ = enc.Encode(s)
20
+ return strings.TrimSuffix(b.String(), "\n")
21
+ }
22
+
23
+ func number(v any) float64 {
24
+ f, _ := v.(float64)
25
+ return f
26
+ }
27
+
28
+ // toFixed3 is Number.prototype.toFixed(3): the exact value rounded half up in magnitude (Go's own
29
+ // formatting rounds an exact tie to even).
30
+ func toFixed3(f float64) string {
31
+ if math.IsNaN(f) || math.IsInf(f, 0) {
32
+ return strconv.FormatFloat(f, 'f', -1, 64)
33
+ }
34
+ neg := f < 0
35
+ r := new(big.Rat).SetFloat64(math.Abs(f))
36
+ r.Mul(r, big.NewRat(1000, 1))
37
+ n := new(big.Int).Quo(r.Num(), r.Denom())
38
+ rem := new(big.Rat).Sub(r, new(big.Rat).SetInt(n))
39
+ if rem.Cmp(big.NewRat(1, 2)) >= 0 {
40
+ n.Add(n, big.NewInt(1))
41
+ }
42
+ digits := n.String()
43
+ for len(digits) < 4 {
44
+ digits = "0" + digits
45
+ }
46
+ out := digits[:len(digits)-3] + "." + digits[len(digits)-3:]
47
+ if neg && strings.Trim(out, "0.") != "" {
48
+ out = "-" + out
49
+ }
50
+ return out
51
+ }
52
+
53
+ // orderedIDs lists answer ids in question order when the details carry it, else sorted.
54
+ func orderedIDs(details map[string]any, answers map[string]any) []string {
55
+ var ids []string
56
+ seen := map[string]bool{}
57
+ if order, ok := details["order"].([]any); ok {
58
+ for _, o := range order {
59
+ if id, ok := o.(string); ok {
60
+ if _, present := answers[id]; present && !seen[id] {
61
+ ids = append(ids, id)
62
+ seen[id] = true
63
+ }
64
+ }
65
+ }
66
+ }
67
+ var rest []string
68
+ for id := range answers {
69
+ if !seen[id] {
70
+ rest = append(rest, id)
71
+ }
72
+ }
73
+ sort.Strings(rest)
74
+ return append(ids, rest...)
75
+ }
76
+
77
+ // probabilitiesText is JSON.stringify of a probabilities object: numeric keys ascending (a Score's levels), else sorted.
78
+ func probabilitiesText(v any) string {
79
+ probs, _ := v.(map[string]any)
80
+ keys := make([]string, 0, len(probs))
81
+ for k := range probs {
82
+ keys = append(keys, k)
83
+ }
84
+ sort.Slice(keys, func(i, j int) bool {
85
+ a, errA := strconv.Atoi(keys[i])
86
+ b, errB := strconv.Atoi(keys[j])
87
+ if errA == nil && errB == nil {
88
+ return a < b
89
+ }
90
+ return keys[i] < keys[j]
91
+ })
92
+ parts := make([]string, len(keys))
93
+ for i, k := range keys {
94
+ parts[i] = jsonString(k) + ":" + strconv.FormatFloat(number(probs[k]), 'f', -1, 64)
95
+ }
96
+ return "{" + strings.Join(parts, ",") + "}"
97
+ }
98
+
99
+ // format renders an evaluation (the tool's details) as the original's text block.
100
+ func format(details map[string]any, expanded bool) string {
101
+ model, _ := details["model"].(string)
102
+ lines := []string{fmt.Sprintf("TypeSafe · %s · %s ms", jsonString(model), strconv.FormatFloat(number(details["elapsedMs"]), 'f', -1, 64))}
103
+ answers, _ := details["answers"].(map[string]any)
104
+ for _, id := range orderedIDs(details, answers) {
105
+ answer, _ := answers[id].(map[string]any)
106
+ label := jsonString(id)
107
+ switch answer["type"] {
108
+ case "noul":
109
+ lines = append(lines, fmt.Sprintf("%s: P(yes) = %s", label, toFixed3(number(answer["noul"]))))
110
+ case "choice":
111
+ choice, _ := answer["choice"].(string)
112
+ lines = append(lines, fmt.Sprintf("%s: %s · confidence %s", label, jsonString(choice), toFixed3(number(answer["confidence"]))))
113
+ default:
114
+ lines = append(lines, fmt.Sprintf("%s: %s · confidence %s", label, toFixed3(number(answer["score"])), toFixed3(number(answer["confidence"]))))
115
+ }
116
+ if expanded && answer["type"] != "noul" {
117
+ lines = append(lines, " "+probabilitiesText(answer["probabilities"]))
118
+ }
119
+ }
120
+ usage, _ := details["usage"].(map[string]any)
121
+ lines = append(lines, fmt.Sprintf("%s input / %s output tokens", strconv.FormatFloat(number(usage["input_tokens"]), 'f', -1, 64), strconv.FormatFloat(number(usage["output_tokens"]), 'f', -1, 64)))
122
+ lines = append(lines, "Confidence is distribution concentration, not proof of correctness.")
123
+ return strings.Join(lines, "\n")
124
+ }
125
+
126
+ // cellWidth is the terminal width of a rune: 2 for East Asian wide and emoji ranges, 0 for combining marks.
127
+ func cellWidth(r rune) int {
128
+ switch {
129
+ case r >= 0x300 && r <= 0x36f, r == 0x200d, r >= 0xfe00 && r <= 0xfe0f:
130
+ return 0
131
+ case r >= 0x1100 && r <= 0x115f, r >= 0x2e80 && r <= 0xa4cf, r >= 0xac00 && r <= 0xd7a3, r >= 0xf900 && r <= 0xfaff,
132
+ r >= 0xfe30 && r <= 0xfe6f, r >= 0xff00 && r <= 0xff60, r >= 0xffe0 && r <= 0xffe6, r >= 0x1f300 && r <= 0x1faff, r >= 0x20000 && r <= 0x3fffd:
133
+ return 2
134
+ }
135
+ return 1
136
+ }
137
+
138
+ func visibleWidth(s string) int {
139
+ w := 0
140
+ for _, r := range s {
141
+ w += cellWidth(r)
142
+ }
143
+ return w
144
+ }
145
+
146
+ // wrapText breaks text into lines of at most width cells, at spaces where it can, mid-word where it must.
147
+ func wrapText(text string, width int) []string {
148
+ if width < 1 {
149
+ width = 1
150
+ }
151
+ var out []string
152
+ for _, paragraph := range strings.Split(text, "\n") {
153
+ if paragraph == "" {
154
+ out = append(out, "")
155
+ continue
156
+ }
157
+ line, lineW := "", 0
158
+ flush := func() {
159
+ out = append(out, strings.TrimRight(line, " "))
160
+ line, lineW = "", 0
161
+ }
162
+ for _, word := range strings.SplitAfter(paragraph, " ") {
163
+ w := visibleWidth(word)
164
+ if lineW+w > width && line != "" {
165
+ flush()
166
+ }
167
+ for w > width { // a word longer than a line
168
+ var head strings.Builder
169
+ hw := 0
170
+ for len(word) > 0 {
171
+ r, n := utf8.DecodeRuneInString(word)
172
+ if hw+cellWidth(r) > width {
173
+ break
174
+ }
175
+ head.WriteString(word[:n])
176
+ hw += cellWidth(r)
177
+ word = word[n:]
178
+ }
179
+ line, lineW = head.String(), hw
180
+ flush()
181
+ w = visibleWidth(word)
182
+ }
183
+ line += word
184
+ lineW += w
185
+ }
186
+ if line != "" {
187
+ flush()
188
+ }
189
+ }
190
+ return out
191
+ }
@@ -0,0 +1,75 @@
1
+ package pi_typesafe
2
+
3
+ import (
4
+ "strings"
5
+ "testing"
6
+ )
7
+
8
+ func TestToFixed3IsJavaScriptsToFixed(t *testing.T) {
9
+ // 0.0625 is exactly representable: JavaScript rounds the tie up, Go's formatting rounds it to even.
10
+ cases := map[float64]string{0.9: "0.900", 0.0625: "0.063", 1: "1.000", 0.0005: "0.001", 2.5: "2.500", -0.0625: "-0.063", 0: "0.000", 12.3456: "12.346"}
11
+ for in, want := range cases {
12
+ if got := toFixed3(in); got != want {
13
+ t.Errorf("toFixed3(%v) = %s, want %s", in, got, want)
14
+ }
15
+ }
16
+ }
17
+
18
+ func TestFormatFollowsTheOriginalLayoutAndQuestionOrder(t *testing.T) {
19
+ details := map[string]any{
20
+ "model": "jev-test", "elapsedMs": 12.0, "usage": map[string]any{"input_tokens": 42.0, "output_tokens": 0.0},
21
+ "order": []any{"z", "a", "s"},
22
+ "answers": map[string]any{
23
+ "a": map[string]any{"type": "choice", "choice": "billing", "confidence": 1.0, "probabilities": map[string]any{"billing": 1.0, "other": 0.0}},
24
+ "z": map[string]any{"type": "noul", "noul": 0.9},
25
+ "s": map[string]any{"type": "score", "score": 1.5, "confidence": 0.5, "probabilities": map[string]any{"0": 0.25, "1": 0.5, "10": 0.25}},
26
+ },
27
+ }
28
+ want := strings.Join([]string{
29
+ `TypeSafe · "jev-test" · 12 ms`,
30
+ `"z": P(yes) = 0.900`,
31
+ `"a": "billing" · confidence 1.000`,
32
+ ` {"billing":1,"other":0}`,
33
+ `"s": 1.500 · confidence 0.500`,
34
+ ` {"0":0.25,"1":0.5,"10":0.25}`,
35
+ `42 input / 0 output tokens`,
36
+ `Confidence is distribution concentration, not proof of correctness.`,
37
+ }, "\n")
38
+ if got := format(details, true); got != want {
39
+ t.Fatalf("got:\n%s\nwant:\n%s", got, want)
40
+ }
41
+ if got := format(details, false); strings.Contains(got, `{"billing"`) {
42
+ t.Error("collapsed output must not list probabilities")
43
+ }
44
+ delete(details, "order")
45
+ if got := format(details, false); !strings.HasPrefix(strings.Split(got, "\n")[1], `"a"`) {
46
+ t.Errorf("without order the ids are sorted: %s", got)
47
+ }
48
+ }
49
+
50
+ func TestWrapTextKeepsEveryLineWithinWidth(t *testing.T) {
51
+ text := "TypeSafe · a fairly long line with unbreakable_" + strings.Repeat("x", 60) + " and 🙂 wide cells\n\nnext"
52
+ for _, width := range []int{10, 40, 80} {
53
+ for _, line := range wrapText(text, width) {
54
+ if visibleWidth(line) > width {
55
+ t.Errorf("width %d: %q is %d cells", width, line, visibleWidth(line))
56
+ }
57
+ }
58
+ }
59
+ if got := wrapText("a b", 80); len(got) != 1 || got[0] != "a b" {
60
+ t.Errorf("short text = %v", got)
61
+ }
62
+ }
63
+
64
+ func TestDisclosureNamesTheDestination(t *testing.T) {
65
+ if !strings.Contains(Disclosure("typesafe"), "api.typesafe.ai") {
66
+ t.Error("the TypeSafe disclosure must name api.typesafe.ai")
67
+ }
68
+ own := Disclosure("ownmodel")
69
+ if !strings.Contains(own, "Nothing is sent to api.typesafe.ai") || !strings.Contains(own, "model PiG is configured with") {
70
+ t.Errorf("own-model disclosure = %s", own)
71
+ }
72
+ if !strings.Contains(ToolDescription("ownmodel"), own) || !strings.Contains(ToolDescription("typesafe"), Disclosure("typesafe")) {
73
+ t.Error("the tool description carries the disclosure of its backend")
74
+ }
75
+ }
@@ -0,0 +1,10 @@
1
+ module github.com/MichaelKinsy/pigpen/components/pi-typesafe/extensions/pi-typesafe
2
+
3
+ go 1.26
4
+
5
+ // PiG resolves this requirement to the version-matched staged SDK at build time.
6
+ require github.com/MichaelKinsy/PiG/extensions/sdk v0.4.1
7
+
8
+ // The typed API (components/pi-typesafe-api) and, through it, the shared client (components/typesafe),
9
+ // found through go.work in this directory.
10
+ require github.com/MichaelKinsy/pigpen/components/pi-typesafe-api v0.0.0
@@ -0,0 +1,2 @@
1
+ github.com/MichaelKinsy/PiG/extensions/sdk v0.4.1/go.mod h1:4j0XhCW0EPOBWvqYE7XKjTvIAahI7K1VyyW3KpjTKj8=
2
+ github.com/MichaelKinsy/PiG/extensions/sdk v0.4.1 h1:Ql9sTkc6dvAXaYQLq5nxqmSZgqu8S0L53ZTs8MkOuxw=
@@ -0,0 +1,11 @@
1
+ go 1.26
2
+
3
+ // PiG's extension resolver reads this file (extension/source: goWorkspaceModuleRoots) to
4
+ // find the shared library modules, and adds the require and replace itself when it fuses
5
+ // or builds. The relative paths are the same in the repository and in a staged Piglet
6
+ // (packages/<package>/extensions/<name> -> packages/pi-typesafe-api, packages/typesafe).
7
+ use (
8
+ .
9
+ ../../libs/pi-typesafe-api
10
+ ../../libs/typesafe
11
+ )
@@ -0,0 +1,200 @@
1
+ package pi_typesafe_test
2
+
3
+ import (
4
+ "bytes"
5
+ "encoding/json"
6
+ "io"
7
+ "net/http"
8
+ "os"
9
+ "path/filepath"
10
+ "strings"
11
+ "sync"
12
+ "sync/atomic"
13
+ "testing"
14
+
15
+ pi_typesafe "github.com/MichaelKinsy/pigpen/components/pi-typesafe/extensions/pi-typesafe"
16
+ )
17
+
18
+ // rig is one fake PiG host running the extension, with a fake TypeSafe server behind its HTTP client and
19
+ // scripted answers for the dialogs the extension opens. No test here reaches the network or the real agent
20
+ // directory (rule 17).
21
+ type rig struct {
22
+ t *testing.T
23
+ host *Host
24
+ agentDir string
25
+ mu sync.Mutex
26
+ notices []string
27
+ messages []string
28
+ confirmResult bool
29
+ confirmations int
30
+ confirmTitles []string
31
+ confirmBodies []string
32
+ editorText string
33
+ editorOK bool
34
+ inputText string
35
+ customResult any // nil: cancelled; string: the typed key; "unsupported": the host has no custom components
36
+ network atomic.Int32
37
+ modelList atomic.Int32
38
+ respond func(r *http.Request) *http.Response
39
+ entries []json.RawMessage
40
+ entryTypes []string
41
+ levels []string
42
+ customTypes []string
43
+ // onConfirm runs when a confirm dialog opens, before it is answered (outside the rig's lock).
44
+ onConfirm func()
45
+ // modelInfo answers getModelInfo (the model PiG is configured with); nil answers {} (none selected).
46
+ modelInfo map[string]any
47
+ }
48
+
49
+ func isolate(t *testing.T) string {
50
+ t.Helper()
51
+ dir := t.TempDir()
52
+ agent := filepath.Join(dir, "agent")
53
+ t.Setenv("HOME", dir)
54
+ t.Setenv("PIG_HOME", filepath.Join(dir, "pighome"))
55
+ t.Setenv("PIG_CODING_AGENT_DIR", agent)
56
+ t.Setenv("PI_CODING_AGENT_DIR", filepath.Join(dir, "pi-agent"))
57
+ t.Setenv("PIG_USE_PI_DIRS", "")
58
+ for _, name := range []string{"TYPESAFE_API_KEY", "OPENROUTER_API_KEY", "PI_TYPESAFE_ENABLED", "PI_TYPESAFE_BACKEND",
59
+ "PI_TYPESAFE_MAX_REQUESTS_PER_DAY", "PI_TYPESAFE_MAX_INPUT_TOKENS_PER_DAY", "PI_TYPESAFE_MAX_USD_PER_DAY", "TYPESAFE_BASE_URL", "TYPESAFE_LOG_LEVEL"} {
60
+ t.Setenv(name, "")
61
+ }
62
+ return agent
63
+ }
64
+
65
+ type doerFunc func(*http.Request) (*http.Response, error)
66
+
67
+ func (f doerFunc) Do(r *http.Request) (*http.Response, error) { return f(r) }
68
+
69
+ func jsonResponse(status int, body any) *http.Response {
70
+ data, _ := json.Marshal(body)
71
+ return &http.Response{StatusCode: status, Header: http.Header{"Content-Type": {"application/json"}}, Body: io.NopCloser(bytes.NewReader(data))}
72
+ }
73
+
74
+ func newRig(t *testing.T, opts ...func(*pi_typesafe.Options)) *rig {
75
+ t.Helper()
76
+ r := &rig{t: t, agentDir: isolate(t), confirmResult: true}
77
+ options := pi_typesafe.Options{HTTPClient: doerFunc(func(req *http.Request) (*http.Response, error) {
78
+ if strings.HasSuffix(req.URL.Path, "/v1/models") {
79
+ r.modelList.Add(1)
80
+ return jsonResponse(200, map[string]any{"models": []any{map[string]any{"name": "jev-latest", "description": "", "release_date": "2026-01-01"}}}), nil
81
+ }
82
+ r.network.Add(1)
83
+ if r.respond != nil {
84
+ return r.respond(req), nil
85
+ }
86
+ return jsonResponse(200, map[string]any{"model": "jev-test", "answers": map[string]any{"yes": map[string]any{"type": "noul", "noul": 0.9}}, "usage": map[string]any{"input_tokens": 12, "output_tokens": 0}}), nil
87
+ })}
88
+ for _, o := range opts {
89
+ o(&options)
90
+ }
91
+ r.host = StartHost(t, pi_typesafe.New(options), HostOptions{OnCall: r.onCall})
92
+ return r
93
+ }
94
+
95
+ func (r *rig) onCall(method string, args map[string]any) (map[string]any, string) {
96
+ if method == "ui.confirm" && r.onConfirm != nil {
97
+ r.onConfirm()
98
+ }
99
+ r.mu.Lock()
100
+ defer r.mu.Unlock()
101
+ switch method {
102
+ case "getModelInfo":
103
+ return r.modelInfo, ""
104
+ case "ui.notify":
105
+ r.notices = append(r.notices, args["message"].(string))
106
+ r.levels = append(r.levels, args["level"].(string))
107
+ case "sendMessage":
108
+ if m, ok := args["message"].(map[string]any); ok {
109
+ r.messages = append(r.messages, m["content"].(string))
110
+ r.customTypes = append(r.customTypes, m["customType"].(string))
111
+ }
112
+ case "appendEntry":
113
+ r.entryTypes = append(r.entryTypes, args["customType"].(string))
114
+ data, _ := json.Marshal(args["data"])
115
+ r.entries = append(r.entries, data)
116
+ case "ui.confirm":
117
+ r.confirmations++
118
+ r.confirmTitles = append(r.confirmTitles, args["title"].(string))
119
+ r.confirmBodies = append(r.confirmBodies, args["message"].(string))
120
+ return map[string]any{"confirmed": r.confirmResult}, ""
121
+ case "ui.editor":
122
+ return map[string]any{"text": r.editorText, "ok": r.editorOK}, ""
123
+ case "ui.input":
124
+ if r.customResult != "unsupported" {
125
+ return nil, "plain input must not be used when custom UI exists"
126
+ }
127
+ return map[string]any{"text": r.inputText, "ok": true}, ""
128
+ case "ui.custom":
129
+ switch v := r.customResult.(type) {
130
+ case nil:
131
+ return map[string]any{"ok": false}, ""
132
+ case string:
133
+ if v == "unsupported" {
134
+ return nil, "custom components are unavailable"
135
+ }
136
+ return map[string]any{"ok": true, "result": v}, ""
137
+ }
138
+ }
139
+ return nil, ""
140
+ }
141
+
142
+ func (r *rig) lastNotice() string {
143
+ r.mu.Lock()
144
+ defer r.mu.Unlock()
145
+ if len(r.notices) == 0 {
146
+ return ""
147
+ }
148
+ return r.notices[len(r.notices)-1]
149
+ }
150
+
151
+ func (r *rig) noticeCount() int { r.mu.Lock(); defer r.mu.Unlock(); return len(r.notices) }
152
+
153
+ func (r *rig) noticesSince(n int) []string {
154
+ r.mu.Lock()
155
+ defer r.mu.Unlock()
156
+ return append([]string(nil), r.notices[n:]...)
157
+ }
158
+
159
+ func (r *rig) command(args string) {
160
+ r.t.Helper()
161
+ if failure := r.host.Command("typesafe", args); failure != "" {
162
+ r.t.Fatalf("/typesafe %s failed: %s", args, failure)
163
+ }
164
+ }
165
+
166
+ var defaultParams = map[string]any{"state": "synthetic", "questions": map[string]any{"yes": map[string]any{"type": "noul", "instructions": "Is this synthetic?"}}}
167
+
168
+ // tool runs typesafe_evaluate and returns its details (nil on failure) and the failure text.
169
+ func (r *rig) tool(params map[string]any) (map[string]any, string) {
170
+ r.t.Helper()
171
+ if params == nil {
172
+ params = defaultParams
173
+ }
174
+ raw, failure := r.host.Tool("typesafe_evaluate", params)
175
+ if failure != "" {
176
+ return nil, failure
177
+ }
178
+ var result struct {
179
+ Content string `json:"content"`
180
+ Details map[string]any `json:"details"`
181
+ }
182
+ if err := json.Unmarshal(raw, &result); err != nil {
183
+ r.t.Fatalf("tool result %s: %v", raw, err)
184
+ }
185
+ return result.Details, ""
186
+ }
187
+
188
+ func (r *rig) startSession(reason string) {
189
+ r.t.Helper()
190
+ r.host.Fire("session_start", map[string]any{"reason": reason})
191
+ }
192
+
193
+ func yes(details map[string]any) float64 {
194
+ answers, _ := details["answers"].(map[string]any)
195
+ answer, _ := answers["yes"].(map[string]any)
196
+ f, _ := answer["noul"].(float64)
197
+ return f
198
+ }
199
+
200
+ func fileExists(path string) bool { _, err := os.Stat(path); return err == nil }
@@ -0,0 +1,100 @@
1
+ package pi_typesafe_test
2
+
3
+ import (
4
+ "context"
5
+ "strings"
6
+ "testing"
7
+
8
+ pi_typesafe "github.com/MichaelKinsy/pigpen/components/pi-typesafe/extensions/pi-typesafe"
9
+ "github.com/MichaelKinsy/pigpen/components/typesafe/libraries/typesafe"
10
+ )
11
+
12
+ // fakeOwn is an own-model evaluator: it answers every question and records that it was used.
13
+ type fakeOwn struct{ calls int }
14
+
15
+ func (f *fakeOwn) SystemOne(_ context.Context, req typesafe.SystemOneRequest, _ *typesafe.RequestOptions) (*typesafe.SystemOneResult, error) {
16
+ f.calls++
17
+ answers := map[string]typesafe.Answer{}
18
+ for _, q := range req.Questions {
19
+ answers[q.Name] = typesafe.NoulAnswer{Noul: 0.25}
20
+ }
21
+ return &typesafe.SystemOneResult{Model: "own-test", Answers: answers, Usage: typesafe.Usage{InputTokens: 5}}, nil
22
+ }
23
+
24
+ func TestOwnModelBackendSendsNothingToTypeSafe(t *testing.T) {
25
+ own := &fakeOwn{}
26
+ r := newRig(t, func(o *pi_typesafe.Options) { o.Evaluator = own })
27
+ r.command("backend ownmodel")
28
+ if !strings.Contains(r.lastNotice(), "consent to sending content to the provider of the model PiG is configured with") {
29
+ t.Fatalf("switch notice = %s", r.lastNotice())
30
+ }
31
+ // Consent is per destination: the switch leaves the tool disabled.
32
+ if _, failure := r.tool(nil); !strings.Contains(failure, "disabled") {
33
+ t.Fatalf("failure = %q", failure)
34
+ }
35
+ r.confirmResult = true
36
+ r.command("enable")
37
+ if !strings.Contains(r.confirmBodies[len(r.confirmBodies)-1], "Nothing is sent to api.typesafe.ai") {
38
+ t.Fatalf("the consent dialog must name the own-model destination: %v", r.confirmBodies)
39
+ }
40
+ details, failure := r.tool(nil)
41
+ if failure != "" || details["model"] != "own-test" || own.calls != 1 {
42
+ t.Fatalf("details=%v failure=%q calls=%d", details, failure, own.calls)
43
+ }
44
+ if r.network.Load() != 0 || r.modelList.Load() != 0 {
45
+ t.Fatalf("the own-model backend must not call the TypeSafe API (network=%d)", r.network.Load())
46
+ }
47
+ r.command("status")
48
+ if !strings.Contains(r.lastNotice(), "no key needed") || !strings.Contains(r.lastNotice(), "Nothing is sent to api.typesafe.ai") {
49
+ t.Fatalf("status = %s", r.lastNotice())
50
+ }
51
+ r.command("login")
52
+ if !strings.Contains(r.lastNotice(), "needs no key") {
53
+ t.Fatalf("login = %s", r.lastNotice())
54
+ }
55
+ r.command("backend typesafe")
56
+ if _, failure := r.tool(nil); !strings.Contains(failure, "disabled") {
57
+ t.Fatalf("switching back must disable again: %q", failure)
58
+ }
59
+ r.command("backend nonsense")
60
+ if !strings.Contains(r.lastNotice(), "Unknown backend") {
61
+ t.Fatalf("notice = %s", r.lastNotice())
62
+ }
63
+ }
64
+
65
+ func TestStartupBackendFromEnvironment(t *testing.T) {
66
+ t.Setenv("PI_TYPESAFE_BACKEND", "ownmodel")
67
+ r := newRig(t, func(o *pi_typesafe.Options) { o.Evaluator = &fakeOwn{} })
68
+ t.Setenv("PI_TYPESAFE_BACKEND", "ownmodel")
69
+ r.startSession("startup")
70
+ r.command("backend")
71
+ if !strings.Contains(r.lastNotice(), "Judgment backend: ownmodel") {
72
+ t.Fatalf("notice = %s", r.lastNotice())
73
+ }
74
+ }
75
+
76
+ func TestHeadlessCommandsReportThroughMessages(t *testing.T) {
77
+ agent := isolate(t)
78
+ _ = agent
79
+ no := false
80
+ ext := pi_typesafe.New(pi_typesafe.Options{})
81
+ var messages []string
82
+ host := StartHost(t, ext, HostOptions{HasUI: &no, Mode: "print", OnCall: func(method string, args map[string]any) (map[string]any, string) {
83
+ if method == "sendMessage" {
84
+ messages = append(messages, args["message"].(map[string]any)["content"].(string))
85
+ }
86
+ return nil, ""
87
+ }})
88
+ if failure := host.Command("typesafe", "enable"); failure != "" {
89
+ t.Fatal(failure)
90
+ }
91
+ if len(messages) != 1 || !strings.Contains(messages[0], "needs interactive Pi") {
92
+ t.Fatalf("messages = %v", messages)
93
+ }
94
+ if failure := host.Command("typesafe", "status"); failure != "" || len(messages) != 2 || !strings.Contains(messages[1], "TypeSafe: disabled.") {
95
+ t.Fatalf("status: %q %v", failure, messages)
96
+ }
97
+ if failure := host.Command("typesafe", "bogus"); failure != "" || !strings.Contains(messages[2], "Usage: /typesafe login | logout | setup | status | enable | disable | test | playground") {
98
+ t.Fatalf("usage: %q %v", failure, messages)
99
+ }
100
+ }