@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,490 @@
1
+ package typesafe
2
+
3
+ import (
4
+ "bytes"
5
+ "encoding/json"
6
+ "errors"
7
+ "fmt"
8
+ "io"
9
+ )
10
+
11
+ // QuestionType names a kind of question on the wire (the "type" field).
12
+ type QuestionType string
13
+
14
+ // The three question kinds.
15
+ const (
16
+ TypeNoul QuestionType = "noul"
17
+ TypeChoice QuestionType = "choice"
18
+ TypeScore QuestionType = "score"
19
+ )
20
+
21
+ // Question is a yes/no ([NoulQuestion]), named-alternatives ([ChoiceQuestion]) or
22
+ // ordered-rubric ([ScoreQuestion]) question. The interface is sealed to this package.
23
+ type Question interface {
24
+ // QuestionType returns the wire type of the question.
25
+ QuestionType() QuestionType
26
+ json.Marshaler
27
+ isQuestion()
28
+ }
29
+
30
+ // NoulCriteria describes the yes ("true") and no ("false") outcomes. Each side is
31
+ // omitted, null or a description independently. Null makes the whole criteria value
32
+ // null on the wire (the TypeScript `noul(x, null)`).
33
+ type NoulCriteria struct {
34
+ True Entry
35
+ False Entry
36
+ Null bool
37
+ }
38
+
39
+ // MarshalJSON writes null when Null is set, else an object with the sides that are not omitted.
40
+ func (c NoulCriteria) MarshalJSON() ([]byte, error) {
41
+ if c.Null {
42
+ return []byte("null"), nil
43
+ }
44
+ o := newObject()
45
+ o.entry("true", c.True)
46
+ o.entry("false", c.False)
47
+ return o.done()
48
+ }
49
+
50
+ // NoulQuestion is a yes/no question. Instructions nil-valued (zero) is omitted from
51
+ // the wire; the [Noul] builder sets null, like the TypeScript builder.
52
+ type NoulQuestion struct {
53
+ Instructions Entry
54
+ // Criteria is nil when omitted from the request.
55
+ Criteria *NoulCriteria
56
+ }
57
+
58
+ // Noul builds a yes/no question; instructions is text, JSON or nil (null).
59
+ func Noul(instructions any) NoulQuestion { return NoulQuestion{Instructions: EntryOf(instructions)} }
60
+
61
+ func (q NoulQuestion) withCriteria(mutate func(*NoulCriteria)) NoulQuestion {
62
+ c := NoulCriteria{}
63
+ if q.Criteria != nil {
64
+ c = *q.Criteria
65
+ }
66
+ c.Null = false
67
+ mutate(&c)
68
+ q.Criteria = &c
69
+ return q
70
+ }
71
+
72
+ // Yes returns a copy that describes the yes outcome; nil describes it as null.
73
+ func (q NoulQuestion) Yes(description any) NoulQuestion {
74
+ return q.withCriteria(func(c *NoulCriteria) { c.True = EntryOf(description) })
75
+ }
76
+
77
+ // No returns a copy that describes the no outcome; nil describes it as null.
78
+ func (q NoulQuestion) No(description any) NoulQuestion {
79
+ return q.withCriteria(func(c *NoulCriteria) { c.False = EntryOf(description) })
80
+ }
81
+
82
+ // NullCriteria returns a copy whose criteria are sent as null.
83
+ func (q NoulQuestion) NullCriteria() NoulQuestion {
84
+ q.Criteria = &NoulCriteria{Null: true}
85
+ return q
86
+ }
87
+
88
+ // QuestionType implements [Question].
89
+ func (NoulQuestion) QuestionType() QuestionType { return TypeNoul }
90
+ func (NoulQuestion) isQuestion() {}
91
+
92
+ // jsonObject writes a JSON object with its keys in insertion order.
93
+ type jsonObject struct {
94
+ buf bytes.Buffer
95
+ n int
96
+ err error
97
+ }
98
+
99
+ func newObject() *jsonObject {
100
+ o := &jsonObject{}
101
+ o.buf.WriteByte('{')
102
+ return o
103
+ }
104
+
105
+ func (o *jsonObject) key(name string) {
106
+ if o.n > 0 {
107
+ o.buf.WriteByte(',')
108
+ }
109
+ o.n++
110
+ if plainKey(name) {
111
+ o.buf.WriteByte('"')
112
+ o.buf.WriteString(name)
113
+ o.buf.WriteByte('"')
114
+ } else {
115
+ k, _ := marshalPlain(name)
116
+ o.buf.Write(k)
117
+ }
118
+ o.buf.WriteByte(':')
119
+ }
120
+
121
+ // plainKey reports whether name is printable ASCII that JSON writes as itself between quotes: no quote,
122
+ // backslash, control character, DEL, or HTML-sensitive character (marshalPlain would leave those alone, but the
123
+ // slow path is kept for anything not obviously safe).
124
+ func plainKey(name string) bool {
125
+ for i := 0; i < len(name); i++ {
126
+ if c := name[i]; c < 0x20 || c >= 0x7f || c == '"' || c == '\\' || c == '<' || c == '>' || c == '&' {
127
+ return false
128
+ }
129
+ }
130
+ return true
131
+ }
132
+
133
+ func (o *jsonObject) raw(name string, raw []byte) {
134
+ o.key(name)
135
+ o.buf.Write(raw)
136
+ }
137
+
138
+ func (o *jsonObject) value(name string, v any) {
139
+ raw, err := marshalPlain(v)
140
+ if err != nil {
141
+ if o.err == nil {
142
+ o.err = unwrapMarshalError(err)
143
+ }
144
+ raw = []byte("null")
145
+ }
146
+ o.raw(name, raw)
147
+ }
148
+
149
+ func (o *jsonObject) entry(name string, e Entry) {
150
+ if e.IsOmitted() {
151
+ return
152
+ }
153
+ o.value(name, e)
154
+ }
155
+
156
+ func (o *jsonObject) done() ([]byte, error) {
157
+ if o.err != nil {
158
+ return nil, o.err
159
+ }
160
+ o.buf.WriteByte('}')
161
+ return o.buf.Bytes(), nil
162
+ }
163
+
164
+ // MarshalJSON implements [Question].
165
+ func (q NoulQuestion) MarshalJSON() ([]byte, error) {
166
+ o := newObject()
167
+ o.value("type", TypeNoul)
168
+ o.entry("instructions", q.Instructions)
169
+ if q.Criteria != nil {
170
+ raw, err := q.Criteria.MarshalJSON()
171
+ if err != nil {
172
+ return nil, err
173
+ }
174
+ o.raw("criteria", raw)
175
+ }
176
+ return o.done()
177
+ }
178
+
179
+ // Option is one choice label with its description (omitted or nil is sent as null).
180
+ type Option struct {
181
+ Label string
182
+ Description Entry
183
+ }
184
+
185
+ // Opt builds an Option; description is text, JSON or nil (null).
186
+ func Opt(label string, description any) Option {
187
+ return Option{Label: label, Description: EntryOf(description)}
188
+ }
189
+
190
+ // ChoiceQuestion selects between named alternatives. Options keep their order on the wire.
191
+ type ChoiceQuestion struct {
192
+ Instructions Entry
193
+ Criteria []Option
194
+ }
195
+
196
+ // Choice builds a question that selects between named alternatives.
197
+ func Choice(instructions any, options ...Option) ChoiceQuestion {
198
+ return ChoiceQuestion{Instructions: EntryOf(instructions), Criteria: append([]Option(nil), options...)}
199
+ }
200
+
201
+ // QuestionType implements [Question].
202
+ func (ChoiceQuestion) QuestionType() QuestionType { return TypeChoice }
203
+ func (ChoiceQuestion) isQuestion() {}
204
+
205
+ // MarshalJSON implements [Question]; Criteria is written as a JSON object in order.
206
+ func (q ChoiceQuestion) MarshalJSON() ([]byte, error) {
207
+ o := newObject()
208
+ o.value("type", TypeChoice)
209
+ o.entry("instructions", q.Instructions)
210
+ sub := newObject()
211
+ for _, opt := range q.Criteria {
212
+ sub.value(opt.Label, opt.Description)
213
+ }
214
+ raw, err := sub.done()
215
+ if err != nil {
216
+ return nil, err
217
+ }
218
+ o.raw("criteria", raw)
219
+ return o.done()
220
+ }
221
+
222
+ // ScoreQuestion assigns a score from an ordered rubric: Criteria[i] describes score i.
223
+ // At least two entries are required ([Questions.Validate]).
224
+ type ScoreQuestion struct {
225
+ Instructions Entry
226
+ Criteria []Entry
227
+ }
228
+
229
+ // Score builds a score question; each criterion is text, JSON or nil (null).
230
+ func Score(instructions any, criteria ...any) ScoreQuestion {
231
+ q := ScoreQuestion{Instructions: EntryOf(instructions), Criteria: make([]Entry, len(criteria))}
232
+ for i, c := range criteria {
233
+ q.Criteria[i] = EntryOf(c)
234
+ }
235
+ return q
236
+ }
237
+
238
+ // QuestionType implements [Question].
239
+ func (ScoreQuestion) QuestionType() QuestionType { return TypeScore }
240
+ func (ScoreQuestion) isQuestion() {}
241
+
242
+ // MarshalJSON implements [Question].
243
+ func (q ScoreQuestion) MarshalJSON() ([]byte, error) {
244
+ o := newObject()
245
+ o.value("type", TypeScore)
246
+ o.entry("instructions", q.Instructions)
247
+ criteria := q.Criteria
248
+ if criteria == nil {
249
+ criteria = []Entry{}
250
+ }
251
+ o.value("criteria", criteria)
252
+ return o.done()
253
+ }
254
+
255
+ // NamedQuestion is a question with the name that identifies its answer.
256
+ type NamedQuestion struct {
257
+ Name string
258
+ Question Question
259
+ }
260
+
261
+ // Ask names a question.
262
+ func Ask(name string, q Question) NamedQuestion { return NamedQuestion{Name: name, Question: q} }
263
+
264
+ // Questions is an ordered set of named questions; the order is kept on the wire and in
265
+ // prompts. Names must be unique.
266
+ type Questions []NamedQuestion
267
+
268
+ // Validate rejects an empty set, duplicate names, and score questions with fewer than
269
+ // two criteria, before anything is sent (the TypeScript validateQuestions).
270
+ func (qs Questions) Validate() error {
271
+ if len(qs) == 0 {
272
+ return errorf("At least one question is required.")
273
+ }
274
+ seen := make(map[string]bool, len(qs))
275
+ for _, nq := range qs {
276
+ if seen[nq.Name] {
277
+ return errorf("Duplicate question name %q.", nq.Name)
278
+ }
279
+ seen[nq.Name] = true
280
+ if nq.Question == nil {
281
+ return errorf("Question %q is nil.", nq.Name)
282
+ }
283
+ if s, ok := nq.Question.(ScoreQuestion); ok && len(s.Criteria) < 2 {
284
+ return errorf("Score question %q has %d criteria; at least two scores are required.", nq.Name, len(s.Criteria))
285
+ }
286
+ if c, ok := nq.Question.(ChoiceQuestion); ok {
287
+ labels := make(map[string]bool, len(c.Criteria))
288
+ for _, opt := range c.Criteria {
289
+ if labels[opt.Label] {
290
+ return errorf("Choice question %q has the label %q twice.", nq.Name, opt.Label)
291
+ }
292
+ labels[opt.Label] = true
293
+ }
294
+ }
295
+ }
296
+ return nil
297
+ }
298
+
299
+ // MarshalJSON writes the set as a JSON object keyed by name, in order.
300
+ func (qs Questions) MarshalJSON() ([]byte, error) {
301
+ o := newObject()
302
+ for _, nq := range qs {
303
+ if nq.Question == nil {
304
+ return nil, errorf("Question %q is nil.", nq.Name)
305
+ }
306
+ o.value(nq.Name, nq.Question)
307
+ }
308
+ return o.done()
309
+ }
310
+
311
+ // ParseQuestions decodes the JSON object form (as written by MarshalJSON, or by hand
312
+ // or by a tool caller) into an ordered set, keeping key order and the omitted/null
313
+ // distinctions. Score criteria must be a list and choice criteria a map, as in the
314
+ // TypeScript builders.
315
+ func ParseQuestions(data []byte) (Questions, error) {
316
+ dec := json.NewDecoder(bytes.NewReader(data))
317
+ tok, err := dec.Token()
318
+ if err != nil {
319
+ return nil, &TypeSafeError{Message: "Questions must be a JSON object: " + err.Error(), Cause: err}
320
+ }
321
+ if d, ok := tok.(json.Delim); !ok || d != '{' {
322
+ return nil, errorf("Questions must be a JSON object keyed by question name.")
323
+ }
324
+ var out Questions
325
+ seen := map[string]bool{}
326
+ for dec.More() {
327
+ keyTok, err := dec.Token()
328
+ if err != nil {
329
+ return nil, &TypeSafeError{Message: "Questions must be a JSON object: " + err.Error(), Cause: err}
330
+ }
331
+ name := keyTok.(string)
332
+ var raw json.RawMessage
333
+ if err := dec.Decode(&raw); err != nil {
334
+ return nil, &TypeSafeError{Message: fmt.Sprintf("Question %q: %v", name, err), Cause: err}
335
+ }
336
+ if seen[name] {
337
+ return nil, errorf("Duplicate question name %q.", name)
338
+ }
339
+ seen[name] = true
340
+ q, err := parseQuestion(name, raw)
341
+ if err != nil {
342
+ return nil, err
343
+ }
344
+ out = append(out, NamedQuestion{Name: name, Question: q})
345
+ }
346
+ if _, err := dec.Token(); err != nil {
347
+ return nil, &TypeSafeError{Message: "Questions must be a JSON object: " + err.Error(), Cause: err}
348
+ }
349
+ if rest, err := dec.Token(); err != io.EOF {
350
+ return nil, errorf("Unexpected data after the questions object: %v", rest)
351
+ }
352
+ return out, nil
353
+ }
354
+
355
+ func parseQuestion(name string, raw json.RawMessage) (Question, error) {
356
+ var fields map[string]json.RawMessage
357
+ if err := json.Unmarshal(raw, &fields); err != nil || fields == nil {
358
+ return nil, errorf("Question %q must be a JSON object with a \"type\".", name)
359
+ }
360
+ var qtype string
361
+ if t, ok := fields["type"]; ok {
362
+ if err := json.Unmarshal(t, &qtype); err != nil {
363
+ return nil, errorf("Question %q: \"type\" must be text.", name)
364
+ }
365
+ }
366
+ entry := func(field string) (Entry, error) {
367
+ v, ok := fields[field]
368
+ if !ok {
369
+ return Entry{}, nil
370
+ }
371
+ var e Entry
372
+ if err := json.Unmarshal(v, &e); err != nil {
373
+ return Entry{}, errorf("Question %q: %s must be text, a JSON object, a JSON array or null.", name, field)
374
+ }
375
+ return e, nil
376
+ }
377
+ instructions, err := entry("instructions")
378
+ if err != nil {
379
+ return nil, err
380
+ }
381
+ switch QuestionType(qtype) {
382
+ case TypeNoul:
383
+ q := NoulQuestion{Instructions: instructions}
384
+ if c, ok := fields["criteria"]; ok {
385
+ crit, err := parseNoulCriteria(name, c)
386
+ if err != nil {
387
+ return nil, err
388
+ }
389
+ q.Criteria = crit
390
+ }
391
+ return q, nil
392
+ case TypeChoice:
393
+ opts, err := parseChoiceCriteria(name, fields["criteria"])
394
+ if err != nil {
395
+ return nil, err
396
+ }
397
+ return ChoiceQuestion{Instructions: instructions, Criteria: opts}, nil
398
+ case TypeScore:
399
+ var list []json.RawMessage
400
+ c := bytes.TrimSpace(fields["criteria"])
401
+ if len(c) == 0 || c[0] != '[' {
402
+ if len(c) > 0 && c[0] == '{' {
403
+ return nil, errorf("Question %q: Score criteria must be a list of descriptions indexed by score from zero, not a map.", name)
404
+ }
405
+ return nil, errorf("Question %q: score criteria must be a list of descriptions indexed by score from zero.", name)
406
+ }
407
+ if err := json.Unmarshal(c, &list); err != nil {
408
+ return nil, errorf("Question %q: %v", name, err)
409
+ }
410
+ q := ScoreQuestion{Instructions: instructions, Criteria: make([]Entry, len(list))}
411
+ for i, item := range list {
412
+ if err := json.Unmarshal(item, &q.Criteria[i]); err != nil {
413
+ return nil, errorf("Question %q: criteria[%d] must be text, a JSON object, a JSON array or null.", name, i)
414
+ }
415
+ }
416
+ return q, nil
417
+ case "":
418
+ return nil, errorf("Question %q needs a \"type\": noul, choice or score.", name)
419
+ }
420
+ return nil, errorf("Question %q has unknown type %q.", name, qtype)
421
+ }
422
+
423
+ func parseNoulCriteria(name string, raw json.RawMessage) (*NoulCriteria, error) {
424
+ raw = bytes.TrimSpace(raw)
425
+ if string(raw) == "null" {
426
+ return &NoulCriteria{Null: true}, nil
427
+ }
428
+ var sides map[string]json.RawMessage
429
+ if len(raw) == 0 || raw[0] != '{' || json.Unmarshal(raw, &sides) != nil {
430
+ return nil, errorf("Question %q: noul criteria must be an object with \"true\" and \"false\", or null.", name)
431
+ }
432
+ c := &NoulCriteria{}
433
+ for side, dst := range map[string]*Entry{"true": &c.True, "false": &c.False} {
434
+ if v, ok := sides[side]; ok {
435
+ if err := json.Unmarshal(v, dst); err != nil {
436
+ return nil, errorf("Question %q: criteria.%s must be text, a JSON object, a JSON array or null.", name, side)
437
+ }
438
+ }
439
+ }
440
+ return c, nil
441
+ }
442
+
443
+ func parseChoiceCriteria(name string, raw json.RawMessage) ([]Option, error) {
444
+ raw = bytes.TrimSpace(raw)
445
+ if len(raw) > 0 && raw[0] == '[' {
446
+ return nil, errorf("Question %q: Choice criteria must be a map of labels to descriptions, not a list.", name)
447
+ }
448
+ if len(raw) == 0 || raw[0] != '{' {
449
+ return nil, errorf("Question %q: choice criteria must be a map of labels to descriptions.", name)
450
+ }
451
+ dec := json.NewDecoder(bytes.NewReader(raw))
452
+ if _, err := dec.Token(); err != nil {
453
+ return nil, errorf("Question %q: %v", name, err)
454
+ }
455
+ var opts []Option
456
+ seen := map[string]bool{}
457
+ for dec.More() {
458
+ keyTok, err := dec.Token()
459
+ if err != nil {
460
+ return nil, errorf("Question %q: %v", name, err)
461
+ }
462
+ label := keyTok.(string)
463
+ var item json.RawMessage
464
+ if err := dec.Decode(&item); err != nil {
465
+ return nil, errorf("Question %q: %v", name, err)
466
+ }
467
+ if seen[label] {
468
+ return nil, errorf("Question %q has the label %q twice.", name, label)
469
+ }
470
+ seen[label] = true
471
+ var e Entry
472
+ if err := json.Unmarshal(item, &e); err != nil {
473
+ return nil, errorf("Question %q: the description of %q must be text, a JSON object, a JSON array or null.", name, label)
474
+ }
475
+ opts = append(opts, Option{Label: label, Description: e})
476
+ }
477
+ return opts, nil
478
+ }
479
+
480
+ // unwrapMarshalError returns the error a MarshalJSON method returned, without the
481
+ // json.MarshalerError layers that encoding/json adds around it.
482
+ func unwrapMarshalError(err error) error {
483
+ for {
484
+ var me *json.MarshalerError
485
+ if !errors.As(err, &me) {
486
+ return err
487
+ }
488
+ err = me.Err
489
+ }
490
+ }
@@ -0,0 +1,166 @@
1
+ package typesafe
2
+
3
+ import (
4
+ "encoding/json"
5
+ "testing"
6
+ )
7
+
8
+ // Go-specific cases beyond the upstream suite: entries, ordered questions, JSON parsing.
9
+
10
+ func TestEntry_ZeroIsOmittedAndNullIsExplicit(t *testing.T) {
11
+ var zero Entry
12
+ if !zero.IsOmitted() || zero.IsNull() {
13
+ t.Fatal("the zero Entry is omitted, not null")
14
+ }
15
+ if Null.IsOmitted() || !Null.IsNull() {
16
+ t.Fatal("Null is explicit")
17
+ }
18
+ if !EntryOf(nil).IsNull() || Text("x").IsOmitted() || Value(nil).IsOmitted() || !Value(nil).IsNull() {
19
+ t.Fatal("nil converts to null")
20
+ }
21
+ eq(t, EntryOf(Text("t")), Text("t"))
22
+ eq(t, Text("x").Data(), any("x"))
23
+ eq(t, Value(map[string]any{"a": 1}).Data(), any(map[string]any{"a": float64(1)}))
24
+ eq(t, Null.Data(), nil)
25
+ }
26
+
27
+ func TestEntry_MarshalsJSONValuesAndRejectsNumbersAndBooleans(t *testing.T) {
28
+ for _, tc := range []struct {
29
+ e Entry
30
+ want string
31
+ }{{Text("a\"b"), `"a\"b"`}, {Null, `null`}, {Entry{}, `null`}, {Value([]any{nil, 1}), `[null,1]`}, {Value(map[string]any{"k": true}), `{"k":true}`}, {Value(struct {
32
+ A int `json:"a"`
33
+ }{2}), `{"a":2}`}} {
34
+ raw, err := json.Marshal(tc.e)
35
+ noErr(t, err)
36
+ eq(t, string(raw), tc.want)
37
+ }
38
+ for _, e := range []Entry{Value(1), Value(2.5), Value(true), Value(json.RawMessage(`3`)), Value(make(chan int))} {
39
+ _, err := json.Marshal(e)
40
+ if err == nil {
41
+ t.Fatalf("%v: must not marshal", e)
42
+ }
43
+ }
44
+ }
45
+
46
+ func TestEntry_ValueKeepsRawMessageKeyOrder(t *testing.T) {
47
+ raw, err := json.Marshal(Noul(json.RawMessage(`{"z":1,"a":{"y":2,"b":3}}`)))
48
+ noErr(t, err)
49
+ eq(t, string(raw), `{"type":"noul","instructions":{"z":1,"a":{"y":2,"b":3}}}`)
50
+ }
51
+
52
+ func TestEntry_UnmarshalJSON(t *testing.T) {
53
+ var e Entry
54
+ noErr(t, json.Unmarshal([]byte(`null`), &e))
55
+ if !e.IsNull() {
56
+ t.Fatal("null decodes to Null")
57
+ }
58
+ noErr(t, json.Unmarshal([]byte(` "s" `), &e))
59
+ eq(t, e.Data(), any("s"))
60
+ noErr(t, json.Unmarshal([]byte(`{"a":[1]}`), &e))
61
+ eq(t, e.Data(), any(map[string]any{"a": []any{float64(1)}}))
62
+ if err := json.Unmarshal([]byte(`5`), &e); err == nil {
63
+ t.Fatal("a number is not an Entry")
64
+ }
65
+ }
66
+
67
+ func TestQuestions_MarshalKeepsOrderAndNames(t *testing.T) {
68
+ raw, err := json.Marshal(Questions{Ask("z", Noul("1")), Ask("a", Noul("2")), Ask("m", Noul("3"))})
69
+ noErr(t, err)
70
+ eq(t, string(raw), `{"z":{"type":"noul","instructions":"1"},"a":{"type":"noul","instructions":"2"},"m":{"type":"noul","instructions":"3"}}`)
71
+ raw, err = json.Marshal(Choice("q", Opt("b", nil), Opt("a", "x")))
72
+ noErr(t, err)
73
+ eq(t, string(raw), `{"type":"choice","instructions":"q","criteria":{"b":null,"a":"x"}}`)
74
+ }
75
+
76
+ func TestQuestions_ValidateRejectsDuplicatesEmptyAndShortScores(t *testing.T) {
77
+ noErr(t, Questions{Ask("a", Noul("x")), Ask("", Noul("y"))}.Validate())
78
+ err := Questions{Ask("a", Noul("x")), Ask("a", Noul("y"))}.Validate()
79
+ mustAs[*TypeSafeError](t, err)
80
+ contains(t, err.Error(), `Duplicate question name "a"`)
81
+ err = Questions{}.Validate()
82
+ contains(t, err.Error(), "At least one question is required.")
83
+ err = Questions{Ask("s", Score("x", "only"))}.Validate()
84
+ contains(t, err.Error(), `Score question "s" has 1 criteria; at least two scores are required.`)
85
+ err = Questions{Ask("n", nil)}.Validate()
86
+ if err == nil {
87
+ t.Fatal("a nil question must be rejected")
88
+ }
89
+ }
90
+
91
+ func TestParseQuestions_RoundTripsInOrderKeepingOmittedAndNull(t *testing.T) {
92
+ in := `{"zeta":{"type":"noul","criteria":null},"alpha":{"type":"noul","instructions":null,"criteria":{"false":"no"}},` +
93
+ `"c":{"type":"choice","instructions":["a",null],"criteria":{"b":null,"a":{"k":"v"}}},"s":{"type":"score","instructions":"s","criteria":[null,"hi",["x"]]},` +
94
+ `"plain":{"type":"noul"}}`
95
+ qs, err := ParseQuestions([]byte(in))
96
+ noErr(t, err)
97
+ eq(t, len(qs), 5)
98
+ eq(t, qs[0].Name, "zeta")
99
+ eq(t, qs[1].Name, "alpha")
100
+ raw, err := json.Marshal(qs)
101
+ noErr(t, err)
102
+ eq(t, string(raw), in)
103
+ n := qs[4].Question.(NoulQuestion)
104
+ if !n.Instructions.IsOmitted() || n.Criteria != nil {
105
+ t.Fatal("omitted stays omitted")
106
+ }
107
+ z := qs[0].Question.(NoulQuestion)
108
+ if z.Criteria == nil || !z.Criteria.Null {
109
+ t.Fatal("null criteria stays null")
110
+ }
111
+ }
112
+
113
+ func TestParseQuestions_RejectsAChoiceLabelList(t *testing.T) {
114
+ _, err := ParseQuestions([]byte(`{"q":{"type":"choice","instructions":"q","criteria":["a","b"]}}`))
115
+ mustAs[*TypeSafeError](t, err)
116
+ contains(t, err.Error(), "Choice criteria must be a map of labels to descriptions, not a list.")
117
+ }
118
+
119
+ func TestParseQuestions_RejectsAScoreMap(t *testing.T) {
120
+ _, err := ParseQuestions([]byte(`{"q":{"type":"score","criteria":{"0":"bad","1":"good"}}}`))
121
+ mustAs[*TypeSafeError](t, err)
122
+ contains(t, err.Error(), "Score criteria must be a list of descriptions indexed by score from zero, not a map.")
123
+ }
124
+
125
+ func TestParseQuestions_RejectsMalformedInput(t *testing.T) {
126
+ for _, tc := range []struct{ in, want string }{
127
+ {`[]`, "object"},
128
+ {`{"q":{"type":"unknown"}}`, `unknown type "unknown"`},
129
+ {`{"q":{"instructions":"x"}}`, `"q"`},
130
+ {`{"q":5}`, `"q"`},
131
+ {`{"q":{"type":"noul"},"q":{"type":"noul"}}`, `Duplicate question name "q"`},
132
+ {`{"q":{"type":"noul","instructions":5}}`, `"q"`},
133
+ {`{"q":{"type":"choice","criteria":null}}`, `"q"`},
134
+ {`not json`, ""},
135
+ } {
136
+ _, err := ParseQuestions([]byte(tc.in))
137
+ if err == nil {
138
+ t.Fatalf("%s: want an error", tc.in)
139
+ }
140
+ contains(t, err.Error(), tc.want)
141
+ }
142
+ }
143
+
144
+ func TestResult_DecodingIsLenientAndAnswersByType(t *testing.T) {
145
+ r := decodeResult(t, `{}`)
146
+ eq(t, len(r.Answers), 0)
147
+ r = decodeResult(t, `{"model":"m","answers":{"x":{"type":"future","v":1}},"usage":{"input_tokens":1,"output_tokens":2},"extra":true}`)
148
+ raw, ok := r.Answers["x"].(RawAnswer)
149
+ if !ok || raw.Type != "future" || string(raw.Raw) != `{"type":"future","v":1}` {
150
+ t.Fatalf("unknown answer types keep their JSON: %#v", r.Answers["x"])
151
+ }
152
+ if _, err := r.Noul("x"); err == nil {
153
+ t.Fatal("a raw answer is not a Noul")
154
+ }
155
+ var bad SystemOneResult
156
+ if err := json.Unmarshal([]byte(`{"answers":{"a":{"type":"noul","noul":"high"}}}`), &bad); err == nil {
157
+ t.Fatal("a mistyped field must fail")
158
+ }
159
+ }
160
+
161
+ func TestResult_MarshalsTheWireForm(t *testing.T) {
162
+ r := decodeResult(t, allAnswersJSON)
163
+ raw, err := json.Marshal(r)
164
+ noErr(t, err)
165
+ jsonEq(t, json.RawMessage(raw), allAnswersJSON)
166
+ }