@cavelang/solver 0.27.14 → 0.29.1

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.
@@ -0,0 +1,578 @@
1
+ import type { Limits, Options, Result, SolverAdapter } from './adapter.ts'
2
+ import * as Canonical from './canonical.ts'
3
+ import * as Exact from './exact.ts'
4
+ import * as Explain from './explain.ts'
5
+ import type {
6
+ Assignment,
7
+ Expression,
8
+ HardConstraint,
9
+ Model,
10
+ Objective,
11
+ Value,
12
+ Variable
13
+ } from './model.ts'
14
+ import * as Solve from './solve.ts'
15
+ import * as Validate from './validate.ts'
16
+
17
+ export const schema = 'cave.solver/workflow@1' as const
18
+
19
+ const syntheticPrefix = 'cave.workflow/'
20
+ const defaultMaxSensitivityRuns = 64
21
+ const compareText = (left: string, right: string): number => left < right ? -1 : left > right ? 1 : 0
22
+
23
+ export class WorkflowValidationError extends Error {
24
+ constructor(message: string) {
25
+ super(`invalid solver workflow: ${message}`)
26
+ this.name = 'WorkflowValidationError'
27
+ }
28
+ }
29
+
30
+ export type Domain =
31
+ | { readonly variableId: string, readonly kind: 'finite', readonly sort: 'bool', readonly values: readonly boolean[] }
32
+ | { readonly variableId: string, readonly kind: 'finite', readonly sort: 'enum', readonly domain: string, readonly values: readonly string[] }
33
+ | { readonly variableId: string, readonly kind: 'interval', readonly sort: 'int', readonly min: string, readonly max: string }
34
+ | {
35
+ readonly variableId: string
36
+ readonly kind: 'interval'
37
+ readonly sort: 'real'
38
+ readonly min: { readonly numerator: string, readonly denominator: string }
39
+ readonly max: { readonly numerator: string, readonly denominator: string }
40
+ }
41
+
42
+ export type Scope = {
43
+ readonly domains: readonly Domain[]
44
+ readonly assumptions: readonly string[]
45
+ readonly theories: readonly ('booleans' | 'bounded-integers' | 'exact-rationals' | 'finite-enums')[]
46
+ }
47
+
48
+ export type TieBreak = {
49
+ readonly variableId: string
50
+ readonly preference: 'false-first' | 'smallest-first' | 'lexical-first'
51
+ }
52
+
53
+ export type Operation =
54
+ | { readonly kind: 'feasibility' }
55
+ | { readonly kind: 'optimization' }
56
+ | { readonly kind: 'counterexample', readonly invariantId: string, readonly verdict: 'counterexample' | 'holds-within-scope' | 'unknown' }
57
+
58
+ export type Report = {
59
+ readonly schema: typeof schema
60
+ readonly operation: Operation
61
+ readonly modelDigest: string
62
+ readonly scope: Scope
63
+ readonly tieBreak: readonly TieBreak[]
64
+ readonly explanation: Explain.Report
65
+ }
66
+
67
+ export type Binding = {
68
+ readonly variableId: string
69
+ readonly value: Value
70
+ }
71
+
72
+ export type SensitivityRequest = {
73
+ readonly variableId: string
74
+ readonly samples: readonly Value[]
75
+ /** Assignments to keep constant while the selected variable changes. */
76
+ readonly fixed?: readonly Binding[]
77
+ /** Variables whose assignments define a preferred-result transition. */
78
+ readonly observe?: readonly string[]
79
+ readonly operation?: 'feasibility' | 'optimization'
80
+ readonly maxRuns?: number
81
+ }
82
+
83
+ export type SensitivityPoint = {
84
+ readonly index: number
85
+ readonly value: Value
86
+ readonly report: Report
87
+ }
88
+
89
+ export type Transition = {
90
+ readonly afterIndex: number
91
+ readonly from: Value
92
+ readonly to: Value
93
+ readonly fromSignature: string
94
+ readonly toSignature: string
95
+ }
96
+
97
+ export type UnknownRegion = {
98
+ readonly startIndex: number
99
+ readonly endIndex: number
100
+ readonly from: Value
101
+ readonly to: Value
102
+ }
103
+
104
+ export type SensitivityReport = {
105
+ readonly schema: typeof schema
106
+ readonly operation: 'sensitivity'
107
+ readonly modelDigest: string
108
+ readonly variableId: string
109
+ readonly fixed: readonly Binding[]
110
+ readonly observe: readonly string[]
111
+ readonly points: readonly SensitivityPoint[]
112
+ readonly transitions: readonly Transition[]
113
+ readonly unknownRegions: readonly UnknownRegion[]
114
+ }
115
+
116
+ export const maxSensitivityRuns = defaultMaxSensitivityRuns
117
+
118
+ const boundedScope = (model: Model, limits: Partial<Limits> = {}): Scope => {
119
+ Validate.model(model, limits)
120
+ const domainsById = new Map((model.enums ?? []).map(domain => [domain.id, domain.values]))
121
+ const domains: Domain[] = model.variables
122
+ .map((variable): Domain => {
123
+ switch (variable.sort) {
124
+ case 'bool':
125
+ return { variableId: variable.id, kind: 'finite', sort: variable.sort, values: [false, true] }
126
+ case 'enum': {
127
+ const values = domainsById.get(variable.domain)
128
+ if (values === undefined) throw new WorkflowValidationError(`variable ${JSON.stringify(variable.id)} has no enum domain`)
129
+ return {
130
+ variableId: variable.id,
131
+ kind: 'finite',
132
+ sort: variable.sort,
133
+ domain: variable.domain,
134
+ values: [...values].sort(compareText)
135
+ }
136
+ }
137
+ case 'int':
138
+ return {
139
+ variableId: variable.id,
140
+ kind: 'interval',
141
+ sort: variable.sort,
142
+ min: String(Exact.integer(variable.min)),
143
+ max: String(Exact.integer(variable.max))
144
+ }
145
+ case 'real':
146
+ if (variable.min === undefined || variable.max === undefined) {
147
+ throw new WorkflowValidationError(
148
+ `real variable ${JSON.stringify(variable.id)} needs explicit min and max bounds`
149
+ )
150
+ }
151
+ return {
152
+ variableId: variable.id,
153
+ kind: 'interval',
154
+ sort: variable.sort,
155
+ min: Exact.rational(variable.min),
156
+ max: Exact.rational(variable.max)
157
+ }
158
+ }
159
+ })
160
+ .sort((left, right) => compareText(left.variableId, right.variableId))
161
+ const sorts = new Set(model.variables.map(variable => variable.sort))
162
+ const theories: Scope['theories'][number][] = ['booleans']
163
+ if (sorts.has('int')) theories.push('bounded-integers')
164
+ if (sorts.has('real')) theories.push('exact-rationals')
165
+ if (sorts.has('enum')) theories.push('finite-enums')
166
+ return {
167
+ domains,
168
+ assumptions: model.constraints.map(constraint => constraint.id),
169
+ theories
170
+ }
171
+ }
172
+
173
+ const variableMap = (model: Model): ReadonlyMap<string, Variable> =>
174
+ new Map(model.variables.map(variable => [variable.id, variable]))
175
+
176
+ const normalizedValue = (variable: Variable, value: Value, domains: ReadonlyMap<string, readonly string[]>): Value => {
177
+ if (variable.sort !== value.sort) {
178
+ throw new WorkflowValidationError(
179
+ `variable ${JSON.stringify(variable.id)} is ${variable.sort}, received ${value.sort}`
180
+ )
181
+ }
182
+ switch (value.sort) {
183
+ case 'bool': return value
184
+ case 'int': {
185
+ const normalized = String(Exact.integer(value.value))
186
+ if (Exact.compare(normalized, String(Exact.integer((variable as Variable & { sort: 'int' }).min))) < 0 ||
187
+ Exact.compare(normalized, String(Exact.integer((variable as Variable & { sort: 'int' }).max))) > 0) {
188
+ throw new WorkflowValidationError(`value ${normalized} is outside ${JSON.stringify(variable.id)} bounds`)
189
+ }
190
+ return { sort: value.sort, value: normalized }
191
+ }
192
+ case 'real': {
193
+ const normalized = Exact.rational(value)
194
+ const real = variable as Variable & { sort: 'real' }
195
+ if ((real.min !== undefined && Exact.compare(normalized, real.min) < 0) ||
196
+ (real.max !== undefined && Exact.compare(normalized, real.max) > 0)) {
197
+ throw new WorkflowValidationError(`value ${normalized.numerator}/${normalized.denominator} is outside ${JSON.stringify(variable.id)} bounds`)
198
+ }
199
+ return { sort: value.sort, ...normalized }
200
+ }
201
+ case 'enum': {
202
+ const enumeration = variable as Variable & { sort: 'enum' }
203
+ if (value.domain !== enumeration.domain) {
204
+ throw new WorkflowValidationError(
205
+ `variable ${JSON.stringify(variable.id)} uses enum ${JSON.stringify(enumeration.domain)}, received ${JSON.stringify(value.domain)}`
206
+ )
207
+ }
208
+ if (!(domains.get(value.domain) ?? []).includes(value.value)) {
209
+ throw new WorkflowValidationError(`value ${JSON.stringify(value.value)} is outside enum ${JSON.stringify(value.domain)}`)
210
+ }
211
+ return value
212
+ }
213
+ }
214
+ }
215
+
216
+ const literal = (value: Value): Expression => {
217
+ switch (value.sort) {
218
+ case 'bool': return { kind: 'literal', sort: value.sort, value: value.value }
219
+ case 'int': return { kind: 'literal', sort: value.sort, value: value.value }
220
+ case 'real': return { kind: 'literal', sort: value.sort, value: { numerator: value.numerator, denominator: value.denominator } }
221
+ case 'enum': return { kind: 'literal', sort: value.sort, domain: value.domain, value: value.value }
222
+ }
223
+ }
224
+
225
+ const freshId = (used: Set<string>, name: string): string => {
226
+ let id = `${syntheticPrefix}${name}`
227
+ let suffix = 2
228
+ while (used.has(id)) id = `${syntheticPrefix}${name}-${suffix++}`
229
+ used.add(id)
230
+ return id
231
+ }
232
+
233
+ const enumRank = (variable: Variable & { readonly sort: 'enum' }, values: readonly string[]): Expression => {
234
+ const sorted = [...values].sort(compareText)
235
+ return sorted.slice(0, -1).reduceRight<Expression>(
236
+ (otherwise, value, index) => ({
237
+ kind: 'if',
238
+ condition: {
239
+ kind: 'eq',
240
+ left: { kind: 'variable', id: variable.id },
241
+ right: { kind: 'literal', sort: 'enum', domain: variable.domain, value }
242
+ },
243
+ then: { kind: 'literal', sort: 'int', value: index },
244
+ else: otherwise
245
+ }),
246
+ { kind: 'literal', sort: 'int', value: Math.max(0, sorted.length - 1) }
247
+ )
248
+ }
249
+
250
+ const tieBreak = (model: Model, used: Set<string>): { objectives: readonly Objective[], descriptions: readonly TieBreak[] } => {
251
+ const domains = new Map((model.enums ?? []).map(domain => [domain.id, domain.values]))
252
+ const ordered = [...model.variables].sort((left, right) => compareText(left.id, right.id))
253
+ return {
254
+ objectives: ordered.map(variable => ({
255
+ id: freshId(used, `tie/${variable.id}`),
256
+ direction: 'minimize',
257
+ expression: (() => {
258
+ switch (variable.sort) {
259
+ case 'bool': return {
260
+ kind: 'if',
261
+ condition: { kind: 'variable', id: variable.id },
262
+ then: { kind: 'literal', sort: 'int', value: 1 },
263
+ else: { kind: 'literal', sort: 'int', value: 0 }
264
+ }
265
+ case 'int':
266
+ case 'real': return { kind: 'variable', id: variable.id }
267
+ case 'enum': return enumRank(variable, domains.get(variable.domain) ?? [])
268
+ }
269
+ })()
270
+ })),
271
+ descriptions: ordered.map(variable => ({
272
+ variableId: variable.id,
273
+ preference: variable.sort === 'bool' ? 'false-first' : variable.sort === 'enum' ? 'lexical-first' : 'smallest-first'
274
+ }))
275
+ }
276
+ }
277
+
278
+ const softObjective = (model: Model, used: Set<string>): Objective | undefined => {
279
+ const expressions = (model.softConstraints ?? []).map((constraint): Expression => ({
280
+ kind: 'if',
281
+ condition: constraint.expression,
282
+ then: { kind: 'literal', sort: 'real', value: constraint.weight },
283
+ else: { kind: 'literal', sort: 'real', value: '0' }
284
+ }))
285
+ if (expressions.length === 0) return undefined
286
+ return {
287
+ id: freshId(used, 'soft-score'),
288
+ direction: 'maximize',
289
+ expression: expressions.length === 1 ? expressions[0]! : { kind: 'add', operands: expressions }
290
+ }
291
+ }
292
+
293
+ const preparedModel = (model: Model, kind: 'feasibility' | 'optimization'): {
294
+ readonly model: Model
295
+ readonly tieBreak: readonly TieBreak[]
296
+ readonly authoredObjectiveIds: ReadonlySet<string>
297
+ } => {
298
+ const used = new Set((model.objectives ?? []).map(objective => objective.id))
299
+ const tie = tieBreak(model, used)
300
+ const authoredObjectiveIds = new Set((model.objectives ?? []).map(objective => objective.id))
301
+ if (kind === 'feasibility') {
302
+ return {
303
+ model: { ...model, softConstraints: [], objectives: tie.objectives },
304
+ tieBreak: tie.descriptions,
305
+ authoredObjectiveIds
306
+ }
307
+ }
308
+ if ((model.objectives?.length ?? 0) === 0 && (model.softConstraints?.length ?? 0) === 0) {
309
+ throw new WorkflowValidationError('optimization needs at least one objective or soft constraint')
310
+ }
311
+ const soft = softObjective(model, used)
312
+ return {
313
+ model: {
314
+ ...model,
315
+ softConstraints: [],
316
+ objectives: [...(model.objectives ?? []), ...(soft === undefined ? [] : [soft]), ...tie.objectives]
317
+ },
318
+ tieBreak: tie.descriptions,
319
+ authoredObjectiveIds
320
+ }
321
+ }
322
+
323
+ const feasibleResult = (result: Result): Result => result.status !== 'optimal' ? result : {
324
+ status: 'satisfied',
325
+ assignment: result.assignment,
326
+ backend: result.backend,
327
+ diagnostics: result.diagnostics,
328
+ elapsedMs: result.elapsedMs
329
+ }
330
+
331
+ const optimizedResult = (result: Result, authoredObjectiveIds: ReadonlySet<string>): Result => {
332
+ if (result.status === 'satisfied') {
333
+ return {
334
+ status: 'unknown',
335
+ reason: { kind: 'indeterminate', message: 'backend returned a feasible assignment without proving optimality' },
336
+ backend: result.backend,
337
+ diagnostics: result.diagnostics,
338
+ elapsedMs: result.elapsedMs
339
+ }
340
+ }
341
+ if (result.status !== 'optimal') return result
342
+ return {
343
+ ...result,
344
+ objectives: result.objectives.filter(objective => authoredObjectiveIds.has(objective.objectiveId))
345
+ }
346
+ }
347
+
348
+ const run = async (
349
+ adapter: SolverAdapter,
350
+ sourceModel: Model,
351
+ solveModel: Model,
352
+ kind: 'feasibility' | 'optimization',
353
+ options: Options,
354
+ context: Explain.Context
355
+ ): Promise<{ readonly explanation: Explain.Report, readonly tieBreak: readonly TieBreak[] }> => {
356
+ const limits = Validate.mergeLimits(options.limits)
357
+ const prepared = preparedModel(solveModel, kind)
358
+ const result = await Solve.run(adapter, prepared.model, {
359
+ limits,
360
+ unsatCore: options.unsatCore ?? (kind === 'feasibility')
361
+ })
362
+ const normalized = kind === 'feasibility'
363
+ ? feasibleResult(result)
364
+ : optimizedResult(result, prepared.authoredObjectiveIds)
365
+ return {
366
+ explanation: Explain.report(sourceModel, normalized, limits, context),
367
+ tieBreak: prepared.tieBreak
368
+ }
369
+ }
370
+
371
+ const report = (
372
+ model: Model,
373
+ operation: Operation,
374
+ scope: Scope,
375
+ tie: readonly TieBreak[],
376
+ explanation: Explain.Report
377
+ ): Report => ({
378
+ schema,
379
+ operation,
380
+ modelDigest: Canonical.digest(model),
381
+ scope,
382
+ tieBreak: tie,
383
+ explanation
384
+ })
385
+
386
+ export const feasibility = async (
387
+ adapter: SolverAdapter,
388
+ model: Model,
389
+ options: Options = {},
390
+ context: Explain.Context = {}
391
+ ): Promise<Report> => {
392
+ const scope = boundedScope(model, options.limits)
393
+ const result = await run(adapter, model, model, 'feasibility', options, context)
394
+ return report(model, { kind: 'feasibility' }, scope, result.tieBreak, result.explanation)
395
+ }
396
+
397
+ export const optimization = async (
398
+ adapter: SolverAdapter,
399
+ model: Model,
400
+ options: Options = {},
401
+ context: Explain.Context = {}
402
+ ): Promise<Report> => {
403
+ const scope = boundedScope(model, options.limits)
404
+ const result = await run(adapter, model, model, 'optimization', options, context)
405
+ return report(model, { kind: 'optimization' }, scope, result.tieBreak, result.explanation)
406
+ }
407
+
408
+ export const counterexample = async (
409
+ adapter: SolverAdapter,
410
+ model: Model,
411
+ invariantId: string,
412
+ options: Options = {},
413
+ context: Explain.Context = {}
414
+ ): Promise<Report> => {
415
+ const baseScope = boundedScope(model, options.limits)
416
+ const invariant = model.constraints.find(constraint => constraint.id === invariantId)
417
+ if (invariant === undefined) {
418
+ throw new WorkflowValidationError(`unknown invariant constraint ${JSON.stringify(invariantId)}`)
419
+ }
420
+ const negated: HardConstraint = { ...invariant, expression: { kind: 'not', value: invariant.expression } }
421
+ const solveModel: Model = {
422
+ ...model,
423
+ constraints: model.constraints.map(constraint => constraint.id === invariantId ? negated : constraint)
424
+ }
425
+ const result = await run(adapter, model, solveModel, 'feasibility', { ...options, unsatCore: options.unsatCore ?? true }, context)
426
+ const verdict = result.explanation.outcome.status === 'satisfied'
427
+ ? 'counterexample'
428
+ : result.explanation.outcome.status === 'unsatisfied'
429
+ ? 'holds-within-scope'
430
+ : 'unknown'
431
+ const scope: Scope = {
432
+ ...baseScope,
433
+ assumptions: baseScope.assumptions.filter(id => id !== invariantId)
434
+ }
435
+ return report(model, { kind: 'counterexample', invariantId, verdict }, scope, result.tieBreak, result.explanation)
436
+ }
437
+
438
+ const bindingConstraint = (binding: Binding, id: string): HardConstraint => ({
439
+ id,
440
+ expression: {
441
+ kind: 'eq',
442
+ left: { kind: 'variable', id: binding.variableId },
443
+ right: literal(binding.value)
444
+ },
445
+ scenarioInputIds: [binding.variableId]
446
+ })
447
+
448
+ const normalizeBindings = (model: Model, bindings: readonly Binding[]): readonly Binding[] => {
449
+ const variables = variableMap(model)
450
+ const domains = new Map((model.enums ?? []).map(domain => [domain.id, domain.values]))
451
+ const seen = new Set<string>()
452
+ return bindings.map(binding => {
453
+ if (seen.has(binding.variableId)) {
454
+ throw new WorkflowValidationError(`variable ${JSON.stringify(binding.variableId)} is fixed more than once`)
455
+ }
456
+ seen.add(binding.variableId)
457
+ const variable = variables.get(binding.variableId)
458
+ if (variable === undefined) throw new WorkflowValidationError(`unknown variable ${JSON.stringify(binding.variableId)}`)
459
+ return { variableId: binding.variableId, value: normalizedValue(variable, binding.value, domains) }
460
+ })
461
+ }
462
+
463
+ const assignmentSignature = (report: Report, observe: readonly string[]): string => {
464
+ const outcome = report.explanation.outcome
465
+ if (outcome.status !== 'satisfied' && outcome.status !== 'optimal') return outcome.status
466
+ const assignment: Assignment = Object.fromEntries(outcome.assignments.map(item => [item.id, item.value]))
467
+ return JSON.stringify({
468
+ status: outcome.status,
469
+ assignment: Object.fromEntries(observe.map(id => [id, assignment[id] ?? null]))
470
+ })
471
+ }
472
+
473
+ const transitions = (points: readonly SensitivityPoint[], observe: readonly string[]): readonly Transition[] => {
474
+ const result: Transition[] = []
475
+ for (let index = 1; index < points.length; index += 1) {
476
+ const previous = points[index - 1]!
477
+ const current = points[index]!
478
+ const fromSignature = assignmentSignature(previous.report, observe)
479
+ const toSignature = assignmentSignature(current.report, observe)
480
+ if (fromSignature !== toSignature) {
481
+ result.push({
482
+ afterIndex: index - 1,
483
+ from: previous.value,
484
+ to: current.value,
485
+ fromSignature,
486
+ toSignature
487
+ })
488
+ }
489
+ }
490
+ return result
491
+ }
492
+
493
+ const unknownRegions = (points: readonly SensitivityPoint[]): readonly UnknownRegion[] => {
494
+ const result: UnknownRegion[] = []
495
+ let start: number | undefined
496
+ for (let index = 0; index <= points.length; index += 1) {
497
+ const unknown = index < points.length && points[index]!.report.explanation.outcome.status === 'unknown'
498
+ if (unknown && start === undefined) start = index
499
+ if (!unknown && start !== undefined) {
500
+ const end = index - 1
501
+ result.push({
502
+ startIndex: start,
503
+ endIndex: end,
504
+ from: points[start]!.value,
505
+ to: points[end]!.value
506
+ })
507
+ start = undefined
508
+ }
509
+ }
510
+ return result
511
+ }
512
+
513
+ export const sensitivity = async (
514
+ adapter: SolverAdapter,
515
+ model: Model,
516
+ request: SensitivityRequest,
517
+ options: Options = {},
518
+ context: Explain.Context = {}
519
+ ): Promise<SensitivityReport> => {
520
+ const scope = boundedScope(model, options.limits)
521
+ const maxRuns = request.maxRuns ?? defaultMaxSensitivityRuns
522
+ if (!Number.isSafeInteger(maxRuns) || maxRuns <= 0) {
523
+ throw new WorkflowValidationError(`maxRuns must be a positive safe integer, received ${String(maxRuns)}`)
524
+ }
525
+ if (request.samples.length === 0) throw new WorkflowValidationError('sensitivity needs at least one sample')
526
+ if (request.samples.length > maxRuns) {
527
+ throw new WorkflowValidationError(`sensitivity samples exceed maxRuns: ${request.samples.length} > ${maxRuns}`)
528
+ }
529
+ const variables = variableMap(model)
530
+ const selected = variables.get(request.variableId)
531
+ if (selected === undefined) throw new WorkflowValidationError(`unknown sensitivity variable ${JSON.stringify(request.variableId)}`)
532
+ const domains = new Map((model.enums ?? []).map(domain => [domain.id, domain.values]))
533
+ const samples = request.samples.map(value => normalizedValue(selected, value, domains))
534
+ if (new Set(samples.map(value => JSON.stringify(value))).size !== samples.length) {
535
+ throw new WorkflowValidationError('sensitivity samples contain duplicate values')
536
+ }
537
+ const fixed = normalizeBindings(model, request.fixed ?? [])
538
+ if (fixed.some(binding => binding.variableId === request.variableId)) {
539
+ throw new WorkflowValidationError(`sensitivity variable ${JSON.stringify(request.variableId)} is also fixed`)
540
+ }
541
+ const observe = [...new Set(request.observe ?? [])]
542
+ for (const id of observe) {
543
+ if (!variables.has(id)) throw new WorkflowValidationError(`unknown observed variable ${JSON.stringify(id)}`)
544
+ }
545
+ const kind = request.operation ?? 'optimization'
546
+ const used = new Set(model.constraints.map(constraint => constraint.id))
547
+ const fixedConstraints = fixed.map((binding, index) =>
548
+ bindingConstraint(binding, freshId(used, `fixed/${index}`)))
549
+ const points: SensitivityPoint[] = []
550
+ for (const [index, value] of samples.entries()) {
551
+ const sample: Binding = { variableId: request.variableId, value }
552
+ const solveModel: Model = {
553
+ ...model,
554
+ constraints: [
555
+ ...model.constraints,
556
+ ...fixedConstraints,
557
+ bindingConstraint(sample, freshId(used, `sample/${index}`))
558
+ ]
559
+ }
560
+ const solved = await run(adapter, model, solveModel, kind, options, context)
561
+ points.push({
562
+ index,
563
+ value,
564
+ report: report(model, { kind }, scope, solved.tieBreak, solved.explanation)
565
+ })
566
+ }
567
+ return {
568
+ schema,
569
+ operation: 'sensitivity',
570
+ modelDigest: Canonical.digest(model),
571
+ variableId: request.variableId,
572
+ fixed,
573
+ observe,
574
+ points,
575
+ transitions: transitions(points, observe),
576
+ unknownRegions: unknownRegions(points)
577
+ }
578
+ }