@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.
- package/CREDITS.md +22 -0
- package/LICENSE +22 -0
- package/README.md +237 -0
- package/extensions/jev/ask.go +166 -0
- package/extensions/jev/ask_test.go +218 -0
- package/extensions/jev/backend.go +128 -0
- package/extensions/jev/bench_test.go +64 -0
- package/extensions/jev/boundaries_test.go +159 -0
- package/extensions/jev/command.go +224 -0
- package/extensions/jev/commands_test.go +214 -0
- package/extensions/jev/config.go +450 -0
- package/extensions/jev/errors_test.go +191 -0
- package/extensions/jev/extension.go +391 -0
- package/extensions/jev/fakehost_test.go +548 -0
- package/extensions/jev/gate.go +125 -0
- package/extensions/jev/gate_test.go +610 -0
- package/extensions/jev/gatekey_test.go +24 -0
- package/extensions/jev/go.mod +9 -0
- package/extensions/jev/go.sum +2 -0
- package/extensions/jev/go.work +10 -0
- package/extensions/jev/helpers_test.go +404 -0
- package/extensions/jev/memo.go +88 -0
- package/extensions/jev/output.go +89 -0
- package/extensions/jev/output_test.go +187 -0
- package/extensions/jev/ownmodel_test.go +118 -0
- package/extensions/jev/render.go +136 -0
- package/extensions/jev/review_test.go +310 -0
- package/extensions/jev/source_test.go +57 -0
- package/extensions/jev/text.go +174 -0
- package/extensions/jev/trust_test.go +335 -0
- package/extensions/jev/types.go +227 -0
- package/libs/typesafe/CONTRACT.md +125 -0
- package/libs/typesafe/CREDITS.md +37 -0
- package/libs/typesafe/LICENSE +23 -0
- package/libs/typesafe/README.md +19 -0
- package/libs/typesafe/go.mod +3 -0
- package/libs/typesafe/libraries/ownmodel/backend_test.go +496 -0
- package/libs/typesafe/libraries/ownmodel/canon.go +190 -0
- package/libs/typesafe/libraries/ownmodel/convert.go +199 -0
- package/libs/typesafe/libraries/ownmodel/doc.go +15 -0
- package/libs/typesafe/libraries/ownmodel/equivalence_test.go +199 -0
- package/libs/typesafe/libraries/ownmodel/helpers_test.go +155 -0
- package/libs/typesafe/libraries/ownmodel/mutation_test.go +31 -0
- package/libs/typesafe/libraries/ownmodel/ownmodel.go +225 -0
- package/libs/typesafe/libraries/ownmodel/plan.go +442 -0
- package/libs/typesafe/libraries/ownmodel/run.go +288 -0
- package/libs/typesafe/libraries/ownmodel/schema_test.go +254 -0
- package/libs/typesafe/libraries/ownmodel/twins_test.go +169 -0
- package/libs/typesafe/libraries/ownmodel/utils_test.go +125 -0
- package/libs/typesafe/libraries/pigmodel/pigmodel.go +264 -0
- package/libs/typesafe/libraries/pigmodel/pigmodel_test.go +410 -0
- package/libs/typesafe/libraries/typesafe/answers.go +268 -0
- package/libs/typesafe/libraries/typesafe/api_response_test.go +113 -0
- package/libs/typesafe/libraries/typesafe/batch.go +80 -0
- package/libs/typesafe/libraries/typesafe/batch_test.go +133 -0
- package/libs/typesafe/libraries/typesafe/bench_test.go +71 -0
- package/libs/typesafe/libraries/typesafe/client.go +561 -0
- package/libs/typesafe/libraries/typesafe/client_test.go +495 -0
- package/libs/typesafe/libraries/typesafe/crosscheck_test.go +464 -0
- package/libs/typesafe/libraries/typesafe/crosscheck_workflowevals_test.go +219 -0
- package/libs/typesafe/libraries/typesafe/doc.go +27 -0
- package/libs/typesafe/libraries/typesafe/entry.go +142 -0
- package/libs/typesafe/libraries/typesafe/env.go +11 -0
- package/libs/typesafe/libraries/typesafe/errors.go +310 -0
- package/libs/typesafe/libraries/typesafe/errors_test.go +175 -0
- package/libs/typesafe/libraries/typesafe/helpers_test.go +294 -0
- package/libs/typesafe/libraries/typesafe/live_test.go +96 -0
- package/libs/typesafe/libraries/typesafe/logging.go +160 -0
- package/libs/typesafe/libraries/typesafe/logging_test.go +259 -0
- package/libs/typesafe/libraries/typesafe/marshal_test.go +112 -0
- package/libs/typesafe/libraries/typesafe/mutation_test.go +39 -0
- package/libs/typesafe/libraries/typesafe/questions.go +490 -0
- package/libs/typesafe/libraries/typesafe/questions_test.go +166 -0
- package/libs/typesafe/libraries/typesafe/regressions_test.go +159 -0
- package/libs/typesafe/libraries/typesafe/reliability_test.go +649 -0
- package/libs/typesafe/libraries/typesafe/retry.go +350 -0
- package/libs/typesafe/libraries/typesafe/retry_test.go +297 -0
- package/libs/typesafe/libraries/typesafe/runtime_test.go +26 -0
- package/libs/typesafe/libraries/typesafe/transport_test.go +163 -0
- package/libs/typesafe/libraries/typesafe/twins_test.go +127 -0
- package/libs/typesafe/libraries/typesafe/types_test.go +165 -0
- package/libs/typesafe/libraries/typesafe/version.go +10 -0
- package/libs/typesafe/package.json +37 -0
- package/libs/typesafe/provenance.json +49 -0
- package/package.json +42 -0
- package/port/PORT.md +107 -0
- package/port/e2e/gate-and-output.py +35 -0
- package/port/e2e/jev-ask.py +36 -0
- package/port/e2e/model-switch.py +44 -0
- package/port/e2e/off-by-default.py +34 -0
- package/port/gen-scenarios.py +103 -0
- package/port/golden/cache-identical-calls.jsonl +30 -0
- package/port/golden/clear.jsonl +22 -0
- package/port/golden/commands.jsonl +43 -0
- package/port/golden/enforce-accept.jsonl +23 -0
- package/port/golden/enforce-decline.jsonl +22 -0
- package/port/golden/jev-ask.jsonl +20 -0
- package/port/golden/output-advice.jsonl +23 -0
- package/port/golden/output-leak.jsonl +24 -0
- package/port/golden/output-low-confidence.jsonl +22 -0
- package/port/golden/shadow-flagged.jsonl +23 -0
- package/port/golden/unjudged-tools.jsonl +19 -0
- package/port/golden/write-elision.jsonl +21 -0
- package/port/mutate-unit.py +63 -0
- package/port/mutations.json +578 -0
- package/port/oracle/LICENSE +21 -0
- package/port/oracle/README.md +181 -0
- package/port/oracle/SHA256SUMS +8 -0
- package/port/oracle/package.json +43 -0
- package/port/oracle/src/client.ts +409 -0
- package/port/oracle/src/config.ts +363 -0
- package/port/oracle/src/gate.ts +229 -0
- package/port/oracle/src/index.ts +649 -0
- package/port/oracle/src/output.ts +163 -0
- package/port/red-run.log +309 -0
- package/port/scenarios/cache-identical-calls.json +71 -0
- package/port/scenarios/clear.json +61 -0
- package/port/scenarios/commands.json +119 -0
- package/port/scenarios/enforce-accept.json +66 -0
- package/port/scenarios/enforce-decline.json +57 -0
- package/port/scenarios/jev-ask.json +83 -0
- package/port/scenarios/output-advice.json +61 -0
- package/port/scenarios/output-leak.json +61 -0
- package/port/scenarios/output-low-confidence.json +61 -0
- package/port/scenarios/shadow-flagged.json +61 -0
- package/port/scenarios/unjudged-tools.json +55 -0
- package/port/scenarios/write-elision.json +53 -0
- 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
|
+
}
|