@cavelang/solver 0.27.14

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 (48) hide show
  1. package/License.md +8 -0
  2. package/README.md +95 -0
  3. package/dist/src/adapter.d.ts +77 -0
  4. package/dist/src/adapter.d.ts.map +1 -0
  5. package/dist/src/adapter.js +24 -0
  6. package/dist/src/adapter.js.map +1 -0
  7. package/dist/src/canonical.d.ts +5 -0
  8. package/dist/src/canonical.d.ts.map +1 -0
  9. package/dist/src/canonical.js +88 -0
  10. package/dist/src/canonical.js.map +1 -0
  11. package/dist/src/capability.d.ts +10 -0
  12. package/dist/src/capability.d.ts.map +1 -0
  13. package/dist/src/capability.js +92 -0
  14. package/dist/src/capability.js.map +1 -0
  15. package/dist/src/exact.d.ts +10 -0
  16. package/dist/src/exact.d.ts.map +1 -0
  17. package/dist/src/exact.js +75 -0
  18. package/dist/src/exact.js.map +1 -0
  19. package/dist/src/index.d.ts +15 -0
  20. package/dist/src/index.d.ts.map +1 -0
  21. package/dist/src/index.js +15 -0
  22. package/dist/src/index.js.map +1 -0
  23. package/dist/src/linear.d.ts +8 -0
  24. package/dist/src/linear.d.ts.map +1 -0
  25. package/dist/src/linear.js +52 -0
  26. package/dist/src/linear.js.map +1 -0
  27. package/dist/src/model.d.ts +130 -0
  28. package/dist/src/model.d.ts.map +1 -0
  29. package/dist/src/model.js +3 -0
  30. package/dist/src/model.js.map +1 -0
  31. package/dist/src/solve.d.ts +5 -0
  32. package/dist/src/solve.d.ts.map +1 -0
  33. package/dist/src/solve.js +12 -0
  34. package/dist/src/solve.js.map +1 -0
  35. package/dist/src/validate.d.ts +24 -0
  36. package/dist/src/validate.d.ts.map +1 -0
  37. package/dist/src/validate.js +283 -0
  38. package/dist/src/validate.js.map +1 -0
  39. package/package.json +39 -0
  40. package/src/adapter.ts +110 -0
  41. package/src/canonical.ts +94 -0
  42. package/src/capability.ts +82 -0
  43. package/src/exact.ts +85 -0
  44. package/src/index.ts +15 -0
  45. package/src/linear.ts +58 -0
  46. package/src/model.ts +80 -0
  47. package/src/solve.ts +13 -0
  48. package/src/validate.ts +302 -0
package/src/adapter.ts ADDED
@@ -0,0 +1,110 @@
1
+ import type { Assignment, Model, Rational, Value } from './model.ts'
2
+
3
+ export const capabilities = [
4
+ 'booleans',
5
+ 'integers',
6
+ 'rationals',
7
+ 'finite-enums',
8
+ 'conditionals',
9
+ 'nonlinear-arithmetic',
10
+ 'optimization',
11
+ 'lexicographic-objectives',
12
+ 'soft-constraints',
13
+ 'unsat-cores'
14
+ ] as const
15
+
16
+ export type Capability = typeof capabilities[number]
17
+
18
+ export type Limits = {
19
+ readonly timeoutMs: number
20
+ /** Backend working-memory ceiling. Adapters must reject or isolate when they cannot enforce it. */
21
+ readonly maxMemoryBytes: number
22
+ readonly maxVariables: number
23
+ readonly maxConstraints: number
24
+ readonly maxObjectives: number
25
+ readonly maxEnumValues: number
26
+ readonly maxExpressionNodes: number
27
+ readonly maxExpressionDepth: number
28
+ readonly maxOutputBytes: number
29
+ }
30
+
31
+ export const defaultLimits: Limits = Object.freeze({
32
+ timeoutMs: 10_000,
33
+ maxMemoryBytes: 512 * 1024 * 1024,
34
+ maxVariables: 1_000,
35
+ maxConstraints: 5_000,
36
+ maxObjectives: 16,
37
+ maxEnumValues: 1_000,
38
+ maxExpressionNodes: 100_000,
39
+ maxExpressionDepth: 128,
40
+ maxOutputBytes: 1_000_000
41
+ })
42
+
43
+ export type Backend = {
44
+ readonly name: string
45
+ readonly version: string
46
+ }
47
+
48
+ export type Diagnostic = {
49
+ readonly level: 'info' | 'warning' | 'error'
50
+ readonly code: string
51
+ readonly message: string
52
+ }
53
+
54
+ type CommonResult = {
55
+ readonly backend: Backend
56
+ readonly diagnostics: readonly Diagnostic[]
57
+ readonly elapsedMs: number
58
+ }
59
+
60
+ export type ObjectiveValue = {
61
+ readonly objectiveId: string
62
+ readonly value: Value
63
+ readonly bound?: Value
64
+ }
65
+
66
+ export type UnknownReason = {
67
+ readonly kind: 'timeout' | 'resource-limit' | 'cancelled' | 'backend-error' | 'indeterminate'
68
+ readonly message: string
69
+ readonly limit?: keyof Limits
70
+ }
71
+
72
+ export type Result = CommonResult & (
73
+ | { readonly status: 'satisfied', readonly assignment: Assignment }
74
+ | {
75
+ readonly status: 'optimal'
76
+ readonly assignment: Assignment
77
+ readonly objectives: readonly ObjectiveValue[]
78
+ readonly optimalityProved: true
79
+ }
80
+ | {
81
+ readonly status: 'unsatisfied'
82
+ readonly core?: readonly string[]
83
+ readonly infeasibilityProved: true
84
+ }
85
+ | { readonly status: 'unknown', readonly reason: UnknownReason }
86
+ )
87
+
88
+ export type Options = {
89
+ readonly limits?: Partial<Limits>
90
+ readonly unsatCore?: boolean
91
+ }
92
+
93
+ export type Request = {
94
+ readonly limits: Limits
95
+ readonly unsatCore: boolean
96
+ }
97
+
98
+ export type SolverAdapter = {
99
+ readonly backend: Backend
100
+ readonly capabilities: ReadonlySet<Capability>
101
+ readonly solve: (model: Model, request: Request) => Promise<Result>
102
+ }
103
+
104
+ export type t = SolverAdapter
105
+
106
+ /** Utility type for adapters that report exact objective bounds. */
107
+ export type ExactBound = {
108
+ readonly value: Rational
109
+ readonly bound?: Rational
110
+ }
@@ -0,0 +1,94 @@
1
+ import { createHash } from 'node:crypto'
2
+ import * as Exact from './exact.ts'
3
+ import type { Expression, Model, Rational } from './model.ts'
4
+ import { model as validate } from './validate.ts'
5
+
6
+ type Json = null | boolean | number | string | readonly Json[] | { readonly [key: string]: Json }
7
+
8
+ const compareText = (left: string, right: string): number => left < right ? -1 : left > right ? 1 : 0
9
+
10
+ const exact = (value: Rational): Json => Exact.rational(value)
11
+
12
+ const expression = (input: Expression): Json => {
13
+ switch (input.kind) {
14
+ case 'literal':
15
+ if (input.sort === 'int') return { kind: input.kind, sort: input.sort, value: String(Exact.integer(input.value)) }
16
+ if (input.sort === 'real') return { kind: input.kind, sort: input.sort, value: exact(input.value) }
17
+ if (input.sort === 'enum') return { kind: input.kind, sort: input.sort, domain: input.domain, value: input.value }
18
+ return { kind: input.kind, sort: input.sort, value: input.value }
19
+ case 'variable': return { kind: input.kind, id: input.id }
20
+ case 'not':
21
+ case 'negate': return { kind: input.kind, value: expression(input.value) }
22
+ case 'and':
23
+ case 'or':
24
+ case 'add':
25
+ case 'multiply': {
26
+ const operands = input.operands.map(expression)
27
+ .sort((left, right) => compareText(stableStringify(left), stableStringify(right)))
28
+ return { kind: input.kind, operands }
29
+ }
30
+ case 'implies':
31
+ case 'subtract':
32
+ case 'divide':
33
+ case 'lt':
34
+ case 'lte':
35
+ case 'gt':
36
+ case 'gte':
37
+ return { kind: input.kind, left: expression(input.left), right: expression(input.right) }
38
+ case 'eq':
39
+ case 'neq': {
40
+ const operands = [expression(input.left), expression(input.right)]
41
+ .sort((left, right) => compareText(stableStringify(left), stableStringify(right)))
42
+ return { kind: input.kind, left: operands[0]!, right: operands[1]! }
43
+ }
44
+ case 'if':
45
+ return { kind: input.kind, condition: expression(input.condition), then: expression(input.then), else: expression(input.else) }
46
+ }
47
+ }
48
+
49
+ const stableStringify = (value: Json): string => {
50
+ if (value === null || typeof value !== 'object') return JSON.stringify(value)
51
+ if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]`
52
+ const record = value as Readonly<Record<string, Json>>
53
+ return `{${Object.keys(record).sort().map(key => `${JSON.stringify(key)}:${stableStringify(record[key]!)}`).join(',')}}`
54
+ }
55
+
56
+ const byId = <T extends { readonly id: string }>(values: readonly T[]): readonly T[] =>
57
+ [...values].sort((left, right) => compareText(left.id, right.id))
58
+
59
+ /** Canonical semantic JSON. Descriptions and evidence links are deliberately not identity. */
60
+ export const serialize = (input: Model): string => {
61
+ validate(input)
62
+ const canonical: Json = {
63
+ schema: input.schema,
64
+ enums: byId(input.enums ?? []).map(domain => ({ id: domain.id, values: [...domain.values].sort() })),
65
+ variables: byId(input.variables).map((variable): Json => {
66
+ switch (variable.sort) {
67
+ case 'bool': return { id: variable.id, sort: variable.sort }
68
+ case 'int': return { id: variable.id, sort: variable.sort, min: String(Exact.integer(variable.min)), max: String(Exact.integer(variable.max)) }
69
+ case 'real': return {
70
+ id: variable.id,
71
+ sort: variable.sort,
72
+ ...(variable.min === undefined ? {} : { min: exact(variable.min) }),
73
+ ...(variable.max === undefined ? {} : { max: exact(variable.max) })
74
+ }
75
+ case 'enum': return { id: variable.id, sort: variable.sort, domain: variable.domain }
76
+ }
77
+ }),
78
+ constraints: byId(input.constraints).map(constraint => ({ id: constraint.id, expression: expression(constraint.expression) })),
79
+ softConstraints: byId(input.softConstraints ?? []).map(constraint => ({
80
+ id: constraint.id,
81
+ expression: expression(constraint.expression),
82
+ weight: exact(constraint.weight)
83
+ })),
84
+ objectives: (input.objectives ?? []).map(objective => ({
85
+ id: objective.id,
86
+ direction: objective.direction,
87
+ expression: expression(objective.expression)
88
+ }))
89
+ }
90
+ return stableStringify(canonical)
91
+ }
92
+
93
+ export const digest = (input: Model): string =>
94
+ `sha256:${createHash('sha256').update(serialize(input)).digest('hex')}`
@@ -0,0 +1,82 @@
1
+ import type { Capability, SolverAdapter } from './adapter.ts'
2
+ import type { Expression, Model } from './model.ts'
3
+
4
+ export class UnsupportedModelError extends Error {
5
+ readonly backend: string
6
+ readonly missing: readonly Capability[]
7
+
8
+ constructor(backend: string, missing: readonly Capability[]) {
9
+ super(`solver backend ${JSON.stringify(backend)} lacks capabilities: ${missing.join(', ')}`)
10
+ this.name = 'UnsupportedModelError'
11
+ this.backend = backend
12
+ this.missing = missing
13
+ }
14
+ }
15
+
16
+ const walk = (expression: Expression, visit: (expression: Expression) => void): void => {
17
+ visit(expression)
18
+ switch (expression.kind) {
19
+ case 'literal':
20
+ case 'variable': return
21
+ case 'not':
22
+ case 'negate': walk(expression.value, visit); return
23
+ case 'and':
24
+ case 'or':
25
+ case 'add':
26
+ case 'multiply': expression.operands.forEach(operand => walk(operand, visit)); return
27
+ case 'if':
28
+ walk(expression.condition, visit)
29
+ walk(expression.then, visit)
30
+ walk(expression.else, visit)
31
+ return
32
+ default:
33
+ walk(expression.left, visit)
34
+ walk(expression.right, visit)
35
+ }
36
+ }
37
+
38
+ const hasNonlinearArithmetic = (expression: Expression): boolean => {
39
+ let nonlinear = false
40
+ const containsVariable = (candidate: Expression): boolean => {
41
+ let found = false
42
+ walk(candidate, node => { if (node.kind === 'variable') found = true })
43
+ return found
44
+ }
45
+ walk(expression, node => {
46
+ if (node.kind === 'multiply' && node.operands.filter(containsVariable).length > 1) nonlinear = true
47
+ if (node.kind === 'divide' && containsVariable(node.right)) nonlinear = true
48
+ })
49
+ return nonlinear
50
+ }
51
+
52
+ export const required = (model: Model, unsatCore = false): ReadonlySet<Capability> => {
53
+ const result = new Set<Capability>(['booleans'])
54
+ if (model.variables.some(variable => variable.sort === 'int')) result.add('integers')
55
+ if (model.variables.some(variable => variable.sort === 'real')) result.add('rationals')
56
+ if ((model.enums?.length ?? 0) > 0 || model.variables.some(variable => variable.sort === 'enum')) result.add('finite-enums')
57
+ if ((model.softConstraints?.length ?? 0) > 0) {
58
+ result.add('soft-constraints')
59
+ result.add('rationals')
60
+ }
61
+ if ((model.objectives?.length ?? 0) > 0) result.add('optimization')
62
+ if ((model.objectives?.length ?? 0) > 1) result.add('lexicographic-objectives')
63
+ if (unsatCore) result.add('unsat-cores')
64
+ const expressions = [
65
+ ...model.constraints.map(constraint => constraint.expression),
66
+ ...(model.softConstraints ?? []).map(constraint => constraint.expression),
67
+ ...(model.objectives ?? []).map(objective => objective.expression)
68
+ ]
69
+ for (const expression of expressions) {
70
+ walk(expression, node => {
71
+ if (node.kind === 'literal' && node.sort === 'int') result.add('integers')
72
+ if (node.kind === 'literal' && node.sort === 'real') result.add('rationals')
73
+ if (node.kind === 'literal' && node.sort === 'enum') result.add('finite-enums')
74
+ if (node.kind === 'if') result.add('conditionals')
75
+ })
76
+ if (hasNonlinearArithmetic(expression)) result.add('nonlinear-arithmetic')
77
+ }
78
+ return result
79
+ }
80
+
81
+ export const missing = (adapter: SolverAdapter, model: Model, unsatCore = false): readonly Capability[] =>
82
+ [...required(model, unsatCore)].filter(capability => !adapter.capabilities.has(capability)).sort()
package/src/exact.ts ADDED
@@ -0,0 +1,85 @@
1
+ import type { Integer, Rational } from './model.ts'
2
+
3
+ export type Normalized = {
4
+ readonly numerator: string
5
+ readonly denominator: string
6
+ }
7
+
8
+ const integerPattern = /^[+-]?\d+$/
9
+ const decimalPattern = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/
10
+
11
+ const gcd = (left: bigint, right: bigint): bigint => {
12
+ let a = left < 0n ? -left : left
13
+ let b = right < 0n ? -right : right
14
+ while (b !== 0n) {
15
+ const remainder = a % b
16
+ a = b
17
+ b = remainder
18
+ }
19
+ return a
20
+ }
21
+
22
+ export const integer = (input: Integer): bigint => {
23
+ if (typeof input === 'number') {
24
+ if (!Number.isSafeInteger(input)) {
25
+ throw new TypeError(`expected a safe integer, received ${String(input)}`)
26
+ }
27
+ return BigInt(input)
28
+ }
29
+ if (!integerPattern.test(input)) {
30
+ throw new TypeError(`expected an integer string, received ${JSON.stringify(input)}`)
31
+ }
32
+ return BigInt(input)
33
+ }
34
+
35
+ const decimal = (input: string): readonly [bigint, bigint] => {
36
+ if (!decimalPattern.test(input)) {
37
+ throw new TypeError(`expected an exact decimal string, received ${JSON.stringify(input)}`)
38
+ }
39
+ const [coefficient = '', exponentText] = input.toLowerCase().split('e')
40
+ const exponent = exponentText === undefined ? 0 : Number(exponentText)
41
+ if (!Number.isSafeInteger(exponent)) {
42
+ throw new TypeError(`decimal exponent is outside the supported range: ${JSON.stringify(input)}`)
43
+ }
44
+ const negative = coefficient.startsWith('-')
45
+ const unsigned = coefficient.replace(/^[+-]/, '')
46
+ const [whole = '', fraction = ''] = unsigned.split('.')
47
+ const digits = `${whole === '' ? '0' : whole}${fraction}`
48
+ const scale = fraction.length - exponent
49
+ const signed = BigInt(digits === '' ? '0' : digits) * (negative ? -1n : 1n)
50
+ return scale <= 0
51
+ ? [signed * (10n ** BigInt(-scale)), 1n]
52
+ : [signed, 10n ** BigInt(scale)]
53
+ }
54
+
55
+ export const rational = (input: Rational): Normalized => {
56
+ let numerator: bigint
57
+ let denominator: bigint
58
+ if (typeof input === 'string') {
59
+ ;[numerator, denominator] = decimal(input)
60
+ } else {
61
+ numerator = integer(input.numerator)
62
+ denominator = integer(input.denominator)
63
+ }
64
+ if (denominator === 0n) {
65
+ throw new TypeError('rational denominator must not be zero')
66
+ }
67
+ if (denominator < 0n) {
68
+ numerator = -numerator
69
+ denominator = -denominator
70
+ }
71
+ const divisor = gcd(numerator, denominator)
72
+ return {
73
+ numerator: String(numerator / divisor),
74
+ denominator: String(denominator / divisor)
75
+ }
76
+ }
77
+
78
+ export const compare = (left: Rational, right: Rational): -1 | 0 | 1 => {
79
+ const a = rational(left)
80
+ const b = rational(right)
81
+ const difference = BigInt(a.numerator) * BigInt(b.denominator) - BigInt(b.numerator) * BigInt(a.denominator)
82
+ return difference < 0n ? -1 : difference > 0n ? 1 : 0
83
+ }
84
+
85
+ export const isZero = (input: Rational): boolean => rational(input).numerator === '0'
package/src/index.ts ADDED
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Solver-neutral formal reasoning contracts for CAVE.
3
+ *
4
+ * This package contains no solver implementation and never imports Z3 or
5
+ * another backend. Adapters receive only validated portable model data.
6
+ */
7
+
8
+ export * as Adapter from './adapter.ts'
9
+ export * as Canonical from './canonical.ts'
10
+ export * as Capability from './capability.ts'
11
+ export * as Exact from './exact.ts'
12
+ export * as Linear from './linear.ts'
13
+ export * as Model from './model.ts'
14
+ export * as Solve from './solve.ts'
15
+ export * as Validate from './validate.ts'
package/src/linear.ts ADDED
@@ -0,0 +1,58 @@
1
+ import type { Expression, Model } from './model.ts'
2
+ import { model as validate } from './validate.ts'
3
+
4
+ export type Analysis = {
5
+ readonly linear: boolean
6
+ readonly problems: readonly string[]
7
+ }
8
+
9
+ type Affine = { readonly linear: boolean, readonly constant: boolean }
10
+
11
+ const affine = (expression: Expression): Affine => {
12
+ switch (expression.kind) {
13
+ case 'literal': return { linear: expression.sort === 'int' || expression.sort === 'real', constant: true }
14
+ case 'variable': return { linear: true, constant: false }
15
+ case 'negate': return affine(expression.value)
16
+ case 'add': {
17
+ const parts = expression.operands.map(affine)
18
+ return { linear: parts.every(part => part.linear), constant: parts.every(part => part.constant) }
19
+ }
20
+ case 'subtract': {
21
+ const left = affine(expression.left)
22
+ const right = affine(expression.right)
23
+ return { linear: left.linear && right.linear, constant: left.constant && right.constant }
24
+ }
25
+ case 'multiply': {
26
+ const parts = expression.operands.map(affine)
27
+ return { linear: parts.every(part => part.linear) && parts.filter(part => !part.constant).length <= 1, constant: parts.every(part => part.constant) }
28
+ }
29
+ case 'divide': {
30
+ const left = affine(expression.left)
31
+ const right = affine(expression.right)
32
+ return { linear: left.linear && right.linear && right.constant, constant: left.constant && right.constant }
33
+ }
34
+ default: return { linear: false, constant: false }
35
+ }
36
+ }
37
+
38
+ const constraintIsLinear = (expression: Expression): boolean => {
39
+ if (expression.kind !== 'eq' && expression.kind !== 'lte' && expression.kind !== 'gte') return false
40
+ return affine(expression.left).linear && affine(expression.right).linear
41
+ }
42
+
43
+ /** Recognizes the portable LP/MIP subset without consulting a backend AST. */
44
+ export const model = (input: Model): Analysis => {
45
+ validate(input)
46
+ const problems: string[] = []
47
+ if (input.variables.some(variable => variable.sort === 'bool' || variable.sort === 'enum')) {
48
+ problems.push('Boolean and enum variables are outside the linear subset')
49
+ }
50
+ if ((input.softConstraints?.length ?? 0) > 0) problems.push('soft constraints are outside the linear subset')
51
+ input.constraints.forEach(constraint => {
52
+ if (!constraintIsLinear(constraint.expression)) problems.push(`constraint ${constraint.id} is not a non-strict linear comparison`)
53
+ })
54
+ ;(input.objectives ?? []).forEach(objective => {
55
+ if (!affine(objective.expression).linear) problems.push(`objective ${objective.id} is not linear`)
56
+ })
57
+ return { linear: problems.length === 0, problems }
58
+ }
package/src/model.ts ADDED
@@ -0,0 +1,80 @@
1
+ /** Portable model schema understood by solver adapters. */
2
+ export const schema = 'cave.solver/model@1' as const
3
+
4
+ export type Integer = string | number
5
+
6
+ /** Exact rational input. Decimal strings are parsed without using `number`. */
7
+ export type Rational = string | {
8
+ readonly numerator: Integer
9
+ readonly denominator: Integer
10
+ }
11
+
12
+ export type EnumDomain = {
13
+ readonly id: string
14
+ readonly values: readonly string[]
15
+ readonly description?: string
16
+ }
17
+
18
+ export type Variable =
19
+ | { readonly id: string, readonly sort: 'bool', readonly description?: string }
20
+ | { readonly id: string, readonly sort: 'int', readonly min: Integer, readonly max: Integer, readonly description?: string }
21
+ | { readonly id: string, readonly sort: 'real', readonly min?: Rational, readonly max?: Rational, readonly description?: string }
22
+ | { readonly id: string, readonly sort: 'enum', readonly domain: string, readonly description?: string }
23
+
24
+ export type Literal =
25
+ | { readonly kind: 'literal', readonly sort: 'bool', readonly value: boolean }
26
+ | { readonly kind: 'literal', readonly sort: 'int', readonly value: Integer }
27
+ | { readonly kind: 'literal', readonly sort: 'real', readonly value: Rational }
28
+ | { readonly kind: 'literal', readonly sort: 'enum', readonly domain: string, readonly value: string }
29
+
30
+ export type Expression =
31
+ | Literal
32
+ | { readonly kind: 'variable', readonly id: string }
33
+ | { readonly kind: 'not', readonly value: Expression }
34
+ | { readonly kind: 'and' | 'or', readonly operands: readonly Expression[] }
35
+ | { readonly kind: 'implies', readonly left: Expression, readonly right: Expression }
36
+ | { readonly kind: 'eq' | 'neq' | 'lt' | 'lte' | 'gt' | 'gte', readonly left: Expression, readonly right: Expression }
37
+ | { readonly kind: 'add' | 'multiply', readonly operands: readonly Expression[] }
38
+ | { readonly kind: 'subtract' | 'divide', readonly left: Expression, readonly right: Expression }
39
+ | { readonly kind: 'negate', readonly value: Expression }
40
+ | { readonly kind: 'if', readonly condition: Expression, readonly then: Expression, readonly else: Expression }
41
+
42
+ export type HardConstraint = {
43
+ readonly id: string
44
+ readonly expression: Expression
45
+ readonly description?: string
46
+ readonly evidenceRowIds?: readonly string[]
47
+ }
48
+
49
+ export type SoftConstraint = HardConstraint & {
50
+ /** Deliberate preference weight; never inferred from CAVE confidence. */
51
+ readonly weight: Rational
52
+ }
53
+
54
+ export type Objective = {
55
+ readonly id: string
56
+ readonly direction: 'minimize' | 'maximize'
57
+ readonly expression: Expression
58
+ readonly description?: string
59
+ readonly evidenceRowIds?: readonly string[]
60
+ }
61
+
62
+ export type Model = {
63
+ readonly schema: typeof schema
64
+ readonly enums?: readonly EnumDomain[]
65
+ readonly variables: readonly Variable[]
66
+ readonly constraints: readonly HardConstraint[]
67
+ readonly softConstraints?: readonly SoftConstraint[]
68
+ /** Objective order is semantic and means lexicographic priority. */
69
+ readonly objectives?: readonly Objective[]
70
+ }
71
+
72
+ export type t = Model
73
+
74
+ export type Value =
75
+ | { readonly sort: 'bool', readonly value: boolean }
76
+ | { readonly sort: 'int', readonly value: string }
77
+ | { readonly sort: 'real', readonly numerator: string, readonly denominator: string }
78
+ | { readonly sort: 'enum', readonly domain: string, readonly value: string }
79
+
80
+ export type Assignment = Readonly<Record<string, Value>>
package/src/solve.ts ADDED
@@ -0,0 +1,13 @@
1
+ import type { Options, Result, SolverAdapter } from './adapter.ts'
2
+ import * as Capability from './capability.ts'
3
+ import type { Model } from './model.ts'
4
+ import * as Validate from './validate.ts'
5
+
6
+ /** Validates and negotiates a model before crossing the adapter boundary. */
7
+ export const run = async (adapter: SolverAdapter, model: Model, options: Options = {}): Promise<Result> => {
8
+ const limits = Validate.mergeLimits(options.limits)
9
+ Validate.model(model, limits)
10
+ const missing = Capability.missing(adapter, model, options.unsatCore ?? false)
11
+ if (missing.length > 0) throw new Capability.UnsupportedModelError(adapter.backend.name, missing)
12
+ return adapter.solve(model, { limits, unsatCore: options.unsatCore ?? false })
13
+ }