@pi-in-go/pigpen-jev 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 (128) hide show
  1. package/CREDITS.md +22 -0
  2. package/LICENSE +22 -0
  3. package/README.md +237 -0
  4. package/extensions/jev/ask.go +166 -0
  5. package/extensions/jev/ask_test.go +218 -0
  6. package/extensions/jev/backend.go +128 -0
  7. package/extensions/jev/bench_test.go +64 -0
  8. package/extensions/jev/boundaries_test.go +159 -0
  9. package/extensions/jev/command.go +224 -0
  10. package/extensions/jev/commands_test.go +214 -0
  11. package/extensions/jev/config.go +450 -0
  12. package/extensions/jev/errors_test.go +191 -0
  13. package/extensions/jev/extension.go +391 -0
  14. package/extensions/jev/fakehost_test.go +548 -0
  15. package/extensions/jev/gate.go +125 -0
  16. package/extensions/jev/gate_test.go +610 -0
  17. package/extensions/jev/gatekey_test.go +24 -0
  18. package/extensions/jev/go.mod +9 -0
  19. package/extensions/jev/go.sum +2 -0
  20. package/extensions/jev/go.work +10 -0
  21. package/extensions/jev/helpers_test.go +404 -0
  22. package/extensions/jev/memo.go +88 -0
  23. package/extensions/jev/output.go +89 -0
  24. package/extensions/jev/output_test.go +187 -0
  25. package/extensions/jev/ownmodel_test.go +118 -0
  26. package/extensions/jev/render.go +136 -0
  27. package/extensions/jev/review_test.go +310 -0
  28. package/extensions/jev/source_test.go +57 -0
  29. package/extensions/jev/text.go +174 -0
  30. package/extensions/jev/trust_test.go +335 -0
  31. package/extensions/jev/types.go +227 -0
  32. package/libs/typesafe/CONTRACT.md +125 -0
  33. package/libs/typesafe/CREDITS.md +37 -0
  34. package/libs/typesafe/LICENSE +23 -0
  35. package/libs/typesafe/README.md +19 -0
  36. package/libs/typesafe/go.mod +3 -0
  37. package/libs/typesafe/libraries/ownmodel/backend_test.go +496 -0
  38. package/libs/typesafe/libraries/ownmodel/canon.go +190 -0
  39. package/libs/typesafe/libraries/ownmodel/convert.go +199 -0
  40. package/libs/typesafe/libraries/ownmodel/doc.go +15 -0
  41. package/libs/typesafe/libraries/ownmodel/equivalence_test.go +199 -0
  42. package/libs/typesafe/libraries/ownmodel/helpers_test.go +155 -0
  43. package/libs/typesafe/libraries/ownmodel/mutation_test.go +31 -0
  44. package/libs/typesafe/libraries/ownmodel/ownmodel.go +225 -0
  45. package/libs/typesafe/libraries/ownmodel/plan.go +442 -0
  46. package/libs/typesafe/libraries/ownmodel/run.go +288 -0
  47. package/libs/typesafe/libraries/ownmodel/schema_test.go +254 -0
  48. package/libs/typesafe/libraries/ownmodel/twins_test.go +169 -0
  49. package/libs/typesafe/libraries/ownmodel/utils_test.go +125 -0
  50. package/libs/typesafe/libraries/pigmodel/pigmodel.go +264 -0
  51. package/libs/typesafe/libraries/pigmodel/pigmodel_test.go +410 -0
  52. package/libs/typesafe/libraries/typesafe/answers.go +268 -0
  53. package/libs/typesafe/libraries/typesafe/api_response_test.go +113 -0
  54. package/libs/typesafe/libraries/typesafe/batch.go +80 -0
  55. package/libs/typesafe/libraries/typesafe/batch_test.go +133 -0
  56. package/libs/typesafe/libraries/typesafe/bench_test.go +71 -0
  57. package/libs/typesafe/libraries/typesafe/client.go +561 -0
  58. package/libs/typesafe/libraries/typesafe/client_test.go +495 -0
  59. package/libs/typesafe/libraries/typesafe/crosscheck_test.go +464 -0
  60. package/libs/typesafe/libraries/typesafe/crosscheck_workflowevals_test.go +219 -0
  61. package/libs/typesafe/libraries/typesafe/doc.go +27 -0
  62. package/libs/typesafe/libraries/typesafe/entry.go +142 -0
  63. package/libs/typesafe/libraries/typesafe/env.go +11 -0
  64. package/libs/typesafe/libraries/typesafe/errors.go +310 -0
  65. package/libs/typesafe/libraries/typesafe/errors_test.go +175 -0
  66. package/libs/typesafe/libraries/typesafe/helpers_test.go +294 -0
  67. package/libs/typesafe/libraries/typesafe/live_test.go +96 -0
  68. package/libs/typesafe/libraries/typesafe/logging.go +160 -0
  69. package/libs/typesafe/libraries/typesafe/logging_test.go +259 -0
  70. package/libs/typesafe/libraries/typesafe/marshal_test.go +112 -0
  71. package/libs/typesafe/libraries/typesafe/mutation_test.go +39 -0
  72. package/libs/typesafe/libraries/typesafe/questions.go +490 -0
  73. package/libs/typesafe/libraries/typesafe/questions_test.go +166 -0
  74. package/libs/typesafe/libraries/typesafe/regressions_test.go +159 -0
  75. package/libs/typesafe/libraries/typesafe/reliability_test.go +649 -0
  76. package/libs/typesafe/libraries/typesafe/retry.go +350 -0
  77. package/libs/typesafe/libraries/typesafe/retry_test.go +297 -0
  78. package/libs/typesafe/libraries/typesafe/runtime_test.go +26 -0
  79. package/libs/typesafe/libraries/typesafe/transport_test.go +163 -0
  80. package/libs/typesafe/libraries/typesafe/twins_test.go +127 -0
  81. package/libs/typesafe/libraries/typesafe/types_test.go +165 -0
  82. package/libs/typesafe/libraries/typesafe/version.go +10 -0
  83. package/libs/typesafe/package.json +37 -0
  84. package/libs/typesafe/provenance.json +49 -0
  85. package/package.json +42 -0
  86. package/port/PORT.md +107 -0
  87. package/port/e2e/gate-and-output.py +35 -0
  88. package/port/e2e/jev-ask.py +36 -0
  89. package/port/e2e/model-switch.py +44 -0
  90. package/port/e2e/off-by-default.py +34 -0
  91. package/port/gen-scenarios.py +103 -0
  92. package/port/golden/cache-identical-calls.jsonl +30 -0
  93. package/port/golden/clear.jsonl +22 -0
  94. package/port/golden/commands.jsonl +43 -0
  95. package/port/golden/enforce-accept.jsonl +23 -0
  96. package/port/golden/enforce-decline.jsonl +22 -0
  97. package/port/golden/jev-ask.jsonl +20 -0
  98. package/port/golden/output-advice.jsonl +23 -0
  99. package/port/golden/output-leak.jsonl +24 -0
  100. package/port/golden/output-low-confidence.jsonl +22 -0
  101. package/port/golden/shadow-flagged.jsonl +23 -0
  102. package/port/golden/unjudged-tools.jsonl +19 -0
  103. package/port/golden/write-elision.jsonl +21 -0
  104. package/port/mutate-unit.py +63 -0
  105. package/port/mutations.json +578 -0
  106. package/port/oracle/LICENSE +21 -0
  107. package/port/oracle/README.md +181 -0
  108. package/port/oracle/SHA256SUMS +8 -0
  109. package/port/oracle/package.json +43 -0
  110. package/port/oracle/src/client.ts +409 -0
  111. package/port/oracle/src/config.ts +363 -0
  112. package/port/oracle/src/gate.ts +229 -0
  113. package/port/oracle/src/index.ts +649 -0
  114. package/port/oracle/src/output.ts +163 -0
  115. package/port/red-run.log +309 -0
  116. package/port/scenarios/cache-identical-calls.json +71 -0
  117. package/port/scenarios/clear.json +61 -0
  118. package/port/scenarios/commands.json +119 -0
  119. package/port/scenarios/enforce-accept.json +66 -0
  120. package/port/scenarios/enforce-decline.json +57 -0
  121. package/port/scenarios/jev-ask.json +83 -0
  122. package/port/scenarios/output-advice.json +61 -0
  123. package/port/scenarios/output-leak.json +61 -0
  124. package/port/scenarios/output-low-confidence.json +61 -0
  125. package/port/scenarios/shadow-flagged.json +61 -0
  126. package/port/scenarios/unjudged-tools.json +55 -0
  127. package/port/scenarios/write-elision.json +53 -0
  128. package/provenance.json +18 -0
@@ -0,0 +1,442 @@
1
+ package ownmodel
2
+
3
+ import (
4
+ "encoding/json"
5
+ "fmt"
6
+ "io"
7
+ "math"
8
+ "regexp"
9
+ "strconv"
10
+ "strings"
11
+
12
+ "github.com/MichaelKinsy/pigpen/components/typesafe/libraries/typesafe"
13
+ )
14
+
15
+ // The per-request plan of the Python oracle (_schema.py): the answer schema and the prompt
16
+ // descriptions derived from the questions, and the strict validation of the output. Where
17
+ // the oracle builds Pydantic models per request, this builds the same JSON Schema by hand
18
+ // and validates the parsed output against the same rules.
19
+
20
+ const noDescription = "No additional instructions."
21
+
22
+ // qplan is one question prepared for prompting, schema generation and decoding.
23
+ type qplan struct {
24
+ name string
25
+ index int
26
+ kind typesafe.QuestionType
27
+ // question is the prompt text of the instructions.
28
+ instructions string
29
+ // noul
30
+ hasNoulCriteria bool
31
+ trueDesc, falseDesc string
32
+ // choice and score: one label and one prompt description per option; for a score the
33
+ // labels are "0", "1", ... and legend holds the JSON of each criterion.
34
+ labels []string
35
+ descs []string
36
+ legend []any
37
+ }
38
+
39
+ type plan struct {
40
+ mode AnswerMode
41
+ questions []qplan
42
+ }
43
+
44
+ // serialize renders an instruction or description value for a prompt: plain text as is,
45
+ // null or omitted as a fixed phrase, JSON compactly (the oracle's
46
+ // _serialize_instruction_value_for_prompt).
47
+ func serialize(e typesafe.Entry) (string, error) {
48
+ if e.IsOmitted() || e.IsNull() {
49
+ return noDescription, nil
50
+ }
51
+ raw, err := e.MarshalJSON()
52
+ if err != nil {
53
+ return "", err
54
+ }
55
+ if raw[0] == '"' {
56
+ var s string
57
+ if err := json.Unmarshal(raw, &s); err != nil {
58
+ return "", err
59
+ }
60
+ return validUTF8(s), nil
61
+ }
62
+ return canonicalJSON(raw)
63
+ }
64
+
65
+ func (p *plan) find(name string) *qplan {
66
+ for i := range p.questions {
67
+ if p.questions[i].name == name {
68
+ return &p.questions[i]
69
+ }
70
+ }
71
+ return nil
72
+ }
73
+
74
+ // newPlan validates the questions for own-model evaluation (non-empty, unique names,
75
+ // at least two criteria for score and choice questions, descriptions that encode).
76
+ func newPlan(qs typesafe.Questions, mode AnswerMode) (*plan, error) {
77
+ if err := qs.Validate(); err != nil {
78
+ return nil, err
79
+ }
80
+ p := &plan{mode: mode}
81
+ for i, nq := range qs {
82
+ q := qplan{name: nq.Name, index: i, kind: nq.Question.QuestionType()}
83
+ var err error
84
+ fail := func(e error) error {
85
+ return &typesafe.TypeSafeError{Message: fmt.Sprintf("Question %q: %v", nq.Name, e), Cause: e}
86
+ }
87
+ switch v := nq.Question.(type) {
88
+ case typesafe.NoulQuestion:
89
+ if q.instructions, err = serialize(v.Instructions); err != nil {
90
+ return nil, fail(err)
91
+ }
92
+ if c := v.Criteria; c != nil && !c.Null {
93
+ q.hasNoulCriteria = true
94
+ if q.trueDesc, err = serialize(c.True); err != nil {
95
+ return nil, fail(err)
96
+ }
97
+ if q.falseDesc, err = serialize(c.False); err != nil {
98
+ return nil, fail(err)
99
+ }
100
+ }
101
+ case typesafe.ChoiceQuestion:
102
+ if len(v.Criteria) < 2 {
103
+ return nil, &typesafe.TypeSafeError{Message: fmt.Sprintf("Question %q: score and choice questions require at least two criteria.", nq.Name)}
104
+ }
105
+ if q.instructions, err = serialize(v.Instructions); err != nil {
106
+ return nil, fail(err)
107
+ }
108
+ for _, o := range v.Criteria {
109
+ d, err := serialize(o.Description)
110
+ if err != nil {
111
+ return nil, fail(err)
112
+ }
113
+ q.labels = append(q.labels, o.Label)
114
+ q.descs = append(q.descs, d)
115
+ }
116
+ case typesafe.ScoreQuestion:
117
+ if q.instructions, err = serialize(v.Instructions); err != nil {
118
+ return nil, fail(err)
119
+ }
120
+ for j, c := range v.Criteria {
121
+ d, err := serialize(c)
122
+ if err != nil {
123
+ return nil, fail(err)
124
+ }
125
+ q.labels = append(q.labels, strconv.Itoa(j))
126
+ q.descs = append(q.descs, d)
127
+ q.legend = append(q.legend, c.Data())
128
+ }
129
+ default:
130
+ return nil, &typesafe.TypeSafeError{Message: fmt.Sprintf("Question %q has an unsupported type %T.", nq.Name, nq.Question)}
131
+ }
132
+ p.questions = append(p.questions, q)
133
+ }
134
+ return p, nil
135
+ }
136
+
137
+ // questionDescription is _build_llm_output_question_description.
138
+ func (p *plan) questionDescription(q *qplan) string {
139
+ if p.mode == Probabilities {
140
+ switch q.kind {
141
+ case typesafe.TypeNoul:
142
+ return "Probability that the answer is yes or the assertion is true. 0 means no or false, 0.5 means uncertain, and 1 means yes or true.\nQuestion: " + q.instructions
143
+ case typesafe.TypeScore:
144
+ return "Each property maps a rubric level to the probability that the document matches it.\nQuestion: " + q.instructions
145
+ case typesafe.TypeChoice:
146
+ return "Each property maps an option to the probability that it is the best answer.\nQuestion: " + q.instructions
147
+ }
148
+ }
149
+ return q.instructions
150
+ }
151
+
152
+ func (p *plan) optionLines(q *qplan) string {
153
+ lines := make([]string, len(q.labels))
154
+ for i := range q.labels {
155
+ lines[i] = q.labels[i] + " = " + q.descs[i]
156
+ }
157
+ return strings.Join(lines, "\n")
158
+ }
159
+
160
+ // fieldDescription is _build_llm_output_field_description.
161
+ func (p *plan) fieldDescription(q *qplan) string {
162
+ d := p.questionDescription(q)
163
+ switch q.kind {
164
+ case typesafe.TypeScore:
165
+ if p.mode == Discrete {
166
+ return d + "\nScore levels, answer with the integer:\n" + p.optionLines(q)
167
+ }
168
+ return d + "\nRequired probability keys:\n" + p.optionLines(q)
169
+ case typesafe.TypeChoice:
170
+ if p.mode == Discrete {
171
+ return d + "\nChoice labels, answer with one label:\n" + p.optionLines(q)
172
+ }
173
+ return d + "\nRequired probability keys:\n" + p.optionLines(q)
174
+ }
175
+ if !q.hasNoulCriteria {
176
+ return d
177
+ }
178
+ return d + "\nTrue criteria: " + q.trueDesc + "\nFalse criteria: " + q.falseDesc
179
+ }
180
+
181
+ // build returns the schema as an ordered object, keys sorted like pydantic's output
182
+ // (properties keep the question order).
183
+ func (p *plan) build() obj {
184
+ defs := map[string]obj{}
185
+ var props obj
186
+ var required []string
187
+ for i := range p.questions {
188
+ q := &p.questions[i]
189
+ required = append(required, q.name)
190
+ switch {
191
+ case q.kind == typesafe.TypeNoul && p.mode == Discrete:
192
+ props = append(props, kv{q.name, obj{{"description", p.fieldDescription(q)}, {"type", "boolean"}}})
193
+ case q.kind == typesafe.TypeNoul:
194
+ props = append(props, kv{q.name, obj{{"description", p.fieldDescription(q)}, {"type", "number"}}})
195
+ case q.kind == typesafe.TypeScore && p.mode == Discrete:
196
+ props = append(props, kv{q.name, obj{{"description", p.fieldDescription(q)}, {"type", "integer"}}})
197
+ case q.kind == typesafe.TypeChoice && p.mode == Discrete:
198
+ props = append(props, kv{q.name, obj{{"description", p.fieldDescription(q)}, {"enum", append([]string(nil), q.labels...)}, {"type", "string"}}})
199
+ default: // a probability map per label
200
+ name := "ProbabilityMap" + strconv.Itoa(q.index)
201
+ var pm obj
202
+ for j, label := range q.labels {
203
+ pm = append(pm, kv{label, obj{{"description", q.descs[j]}, {"type", "number"}}})
204
+ }
205
+ defs[name] = obj{
206
+ {"additionalProperties", false},
207
+ {"description", p.questionDescription(q)},
208
+ {"properties", pm},
209
+ {"required", append([]string(nil), q.labels...)},
210
+ {"type", "object"},
211
+ }
212
+ props = append(props, kv{q.name, obj{{"$ref", "#/$defs/" + name}}})
213
+ }
214
+ }
215
+ defs["TypeSafeAnswers"] = obj{
216
+ {"additionalProperties", false},
217
+ {"description", "Exactly one answer per property below. Use these property names verbatim and do not add, rename, or nest them under any other key."},
218
+ {"properties", props},
219
+ {"required", required},
220
+ {"type", "object"},
221
+ }
222
+ var defList obj
223
+ for _, name := range sortedKeys(defs) {
224
+ defList = append(defList, kv{name, defs[name]})
225
+ }
226
+ return obj{
227
+ {"$defs", defList},
228
+ {"additionalProperties", false},
229
+ {"properties", obj{{"answers", obj{{"$ref", "#/$defs/TypeSafeAnswers"}}}}},
230
+ {"required", []string{"answers"}},
231
+ {"type", "object"},
232
+ }
233
+ }
234
+
235
+ // schema returns the JSON Schema of the model's answer as a generic map.
236
+ func (p *plan) schema() map[string]any { return toMap(p.build()).(map[string]any) }
237
+
238
+ // schemaJSON returns the schema as compact JSON in the key order of the Python oracle.
239
+ func (p *plan) schemaJSON() string {
240
+ var b strings.Builder
241
+ writeAny(&b, p.build())
242
+ return b.String()
243
+ }
244
+
245
+ // value is one validated answer.
246
+ type value struct {
247
+ prob float64 // noul, probabilities
248
+ flag bool // noul, discrete
249
+ probs map[string]float64 // choice and score, probabilities
250
+ selected string // choice, discrete: the label; score, discrete: the level as text
251
+ }
252
+
253
+ // decoded is a validated model output.
254
+ type decoded struct {
255
+ answers map[string]value
256
+ }
257
+
258
+ type fieldError struct{ loc, msg string }
259
+
260
+ // validationError lists why an output does not match the schema.
261
+ type validationError struct{ errs []fieldError }
262
+
263
+ func (e *validationError) Error() string {
264
+ var b strings.Builder
265
+ n := len(e.errs)
266
+ fmt.Fprintf(&b, "%d validation error", n)
267
+ if n != 1 {
268
+ b.WriteByte('s')
269
+ }
270
+ b.WriteString(" for TypeSafeEvaluation")
271
+ for _, f := range e.errs {
272
+ b.WriteString("\n" + f.loc + "\n " + f.msg)
273
+ }
274
+ return b.String()
275
+ }
276
+
277
+ // extractJSON strips the Markdown fence a prompted model may wrap around the JSON object.
278
+ func extractJSON(text string) string {
279
+ text = strings.TrimSpace(text)
280
+ if strings.HasPrefix(text, "```") {
281
+ text = text[3:]
282
+ if len(text) >= 4 && strings.EqualFold(text[:4], "json") {
283
+ text = text[4:]
284
+ }
285
+ text = strings.TrimSpace(text)
286
+ if strings.HasSuffix(text, "```") {
287
+ text = strings.TrimSpace(text[:len(text)-3])
288
+ }
289
+ }
290
+ return text
291
+ }
292
+
293
+ var intLiteral = regexp.MustCompile(`^-?\d+$`)
294
+
295
+ // decode strips Markdown fences from text and validates it strictly against the schema.
296
+ func (p *plan) decode(text string) (*decoded, error) {
297
+ dec := json.NewDecoder(strings.NewReader(extractJSON(text)))
298
+ dec.UseNumber()
299
+ var root any
300
+ if err := dec.Decode(&root); err != nil {
301
+ return nil, fmt.Errorf("Invalid JSON: %v", err)
302
+ }
303
+ if _, err := dec.Token(); err != io.EOF {
304
+ return nil, fmt.Errorf("Invalid JSON: trailing characters after the JSON object")
305
+ }
306
+ var errs []fieldError
307
+ bad := func(loc, msg string) { errs = append(errs, fieldError{loc, msg}) }
308
+ top, ok := root.(map[string]any)
309
+ if !ok {
310
+ return nil, &validationError{[]fieldError{{"", "Input should be a valid dictionary or object"}}}
311
+ }
312
+ for _, k := range sortedKeys(top) {
313
+ if k != "answers" {
314
+ bad(k, "Extra inputs are not permitted")
315
+ }
316
+ }
317
+ rawAnswers, present := top["answers"]
318
+ if !present {
319
+ bad("answers", "Field required")
320
+ }
321
+ out := &decoded{answers: map[string]value{}}
322
+ if present {
323
+ answers, ok := rawAnswers.(map[string]any)
324
+ if !ok {
325
+ bad("answers", "Input should be a valid dictionary or object")
326
+ } else {
327
+ known := map[string]bool{}
328
+ for i := range p.questions {
329
+ q := &p.questions[i]
330
+ known[q.name] = true
331
+ raw, has := answers[q.name]
332
+ loc := "answers." + q.name
333
+ if !has {
334
+ bad(loc, "Field required")
335
+ continue
336
+ }
337
+ v, ferrs := p.validateAnswer(q, raw, loc)
338
+ errs = append(errs, ferrs...)
339
+ out.answers[q.name] = v
340
+ }
341
+ for _, k := range sortedKeys(answers) {
342
+ if !known[k] {
343
+ bad("answers."+k, "Extra inputs are not permitted")
344
+ }
345
+ }
346
+ }
347
+ }
348
+ if len(errs) > 0 {
349
+ return nil, &validationError{errs}
350
+ }
351
+ return out, nil
352
+ }
353
+
354
+ func (p *plan) validateAnswer(q *qplan, raw any, loc string) (value, []fieldError) {
355
+ var errs []fieldError
356
+ bad := func(loc, msg string) { errs = append(errs, fieldError{loc, msg}) }
357
+ var v value
358
+ switch {
359
+ case q.kind == typesafe.TypeNoul && p.mode == Discrete:
360
+ b, ok := raw.(bool)
361
+ if !ok {
362
+ bad(loc, "Input should be a valid boolean")
363
+ }
364
+ v.flag = b
365
+ case q.kind == typesafe.TypeNoul:
366
+ v.prob, errs = probability(raw, loc)
367
+ case q.kind == typesafe.TypeScore && p.mode == Discrete:
368
+ n, ok := raw.(json.Number)
369
+ if !ok || !intLiteral.MatchString(n.String()) {
370
+ bad(loc, "Input should be a valid integer")
371
+ break
372
+ }
373
+ level, err := strconv.Atoi(n.String())
374
+ switch {
375
+ case err != nil || level < 0:
376
+ bad(loc, "Input should be greater than or equal to 0")
377
+ case level >= len(q.labels):
378
+ bad(loc, fmt.Sprintf("Input should be less than %d", len(q.labels)))
379
+ }
380
+ v.selected = strconv.Itoa(level)
381
+ case q.kind == typesafe.TypeChoice && p.mode == Discrete:
382
+ s, ok := raw.(string)
383
+ if !ok {
384
+ bad(loc, "Input should be a valid string")
385
+ break
386
+ }
387
+ found := false
388
+ for _, l := range q.labels {
389
+ if l == s {
390
+ found = true
391
+ }
392
+ }
393
+ if !found {
394
+ bad(loc, "Input should be one of the allowed labels: "+strings.Join(q.labels, ", "))
395
+ }
396
+ v.selected = s
397
+ default: // a probability per label
398
+ m, ok := raw.(map[string]any)
399
+ if !ok {
400
+ bad(loc, "Input should be a valid dictionary or object")
401
+ break
402
+ }
403
+ v.probs = map[string]float64{}
404
+ known := map[string]bool{}
405
+ for _, l := range q.labels {
406
+ known[l] = true
407
+ item, has := m[l]
408
+ if !has {
409
+ bad(loc+"."+l, "Field required")
410
+ continue
411
+ }
412
+ f, ferrs := probability(item, loc+"."+l)
413
+ errs = append(errs, ferrs...)
414
+ v.probs[l] = f
415
+ }
416
+ for _, k := range sortedKeys(m) {
417
+ if !known[k] {
418
+ bad(loc+"."+k, "Extra inputs are not permitted")
419
+ }
420
+ }
421
+ }
422
+ return v, errs
423
+ }
424
+
425
+ // probability validates a strict float in [0, 1]: a JSON number, not a boolean or a string.
426
+ func probability(raw any, loc string) (float64, []fieldError) {
427
+ n, ok := raw.(json.Number)
428
+ if !ok {
429
+ return 0, []fieldError{{loc, "Input should be a valid number"}}
430
+ }
431
+ f, err := n.Float64()
432
+ if err != nil || math.IsNaN(f) || math.IsInf(f, 0) {
433
+ return 0, []fieldError{{loc, "Input should be a finite number"}}
434
+ }
435
+ switch {
436
+ case f < 0:
437
+ return f, []fieldError{{loc, "Input should be greater than or equal to 0"}}
438
+ case f > 1:
439
+ return f, []fieldError{{loc, "Input should be less than or equal to 1"}}
440
+ }
441
+ return f, nil
442
+ }