core/generate/constraint_comprehension/model.py
Shay e71531c0c9 feat(constraint): R2 linear-constraint IR — typed problem model (R2 C1)
generate/constraint_comprehension/{expr,model}.py: frozen, slots'd dataclasses, no behavior. expr = LinearExpr (sum(coeff*symbol)+constant) + LinearConstraint (lhs eq rhs, optional source_span). model = Unknown (symbol/entity/unit/finite-integer domain), AttributeFact (per-category coefficient provenance), ConstraintQuery (symbol+unit), ConstraintProblem (unknowns/facts/constraints/query).

Terms pinned as (symbol, coefficient) to match the gold serialization. Query is a minimal dedicated type, not R1's BoundUnknown (no degenerate fit). Off-serving package; no generate.derivation / reliability_gate import. 9 IR tests (shape + frozen + defaults).
2026-06-07 07:10:20 -07:00

91 lines
3.2 KiB
Python

"""Problem model for the R2 finite-integer constraint organ.
The structural layer above ``generate.constraint_comprehension.expr``: the unknowns, the
raw per-category attribute coefficients (provenance), the assembled linear system, and the
query. This is the R2 twin of the binding-graph model — a typed :class:`ConstraintProblem`
the setup oracle grades and the solver consumes.
Pure data — no behavior. Deterministic.
Deviation from the design sketch: the query is a minimal dedicated :class:`ConstraintQuery`
(symbol + unit), NOT the binding-graph ``BoundUnknown`` — R2 has no state-index /
question-form axis, and forcing R1's unknown type onto it would be a degenerate fit.
Multiple-choice options and the provided answer key are NOT part of the problem IR; they are
answer-choice concerns graded separately (C4).
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Literal
from generate.constraint_comprehension.expr import LinearConstraint
#: A finite-integer domain for an unknown. Count categories are nonnegative integers; the
#: broader signed-integer domain is reserved for future signed-quantity problems (so the
#: distinction is explicit, not a silent assumption that every unknown is a count).
Domain = Literal["nonnegative_integer", "integer"]
@dataclass(frozen=True, slots=True)
class Unknown:
"""One unknown category — ``large_bus``, ``chicken``, ``adult_ticket``.
``symbol`` is the canonical identifier used in ``LinearExpr.terms``; ``entity`` is the
surface category noun (provenance); ``unit`` is the category's own count unit (``bus``,
``animal``); ``domain`` constrains the solution set (a count -> ``nonnegative_integer``).
"""
symbol: str
entity: str
unit: str
domain: Domain
@dataclass(frozen=True, slots=True)
class AttributeFact:
"""A per-category attribute coefficient read from the prose: ``large bus holds 50
students`` -> ``AttributeFact("large_bus", "student", 50)``.
This is the RAW reading (provenance); the weighted-total constraint
``50*large_bus + 30*small_bus = 260`` is assembled FROM these. ``value`` is the integer
coefficient — positivity and cross-category distinctness are the reader's/oracle's gate
(C3/C6), not enforced in this pure-data layer.
"""
category: str
measured_unit: str
value: int
@dataclass(frozen=True, slots=True)
class ConstraintQuery:
"""The asked unknown: which category's count is the answer, and in what unit."""
symbol: str
unit: str
@dataclass(frozen=True, slots=True)
class ConstraintProblem:
"""A complete finite-integer constraint setup: the unknowns, the raw attribute
coefficients, the assembled linear system, and the query.
The setup oracle (C2) grades unknowns / units / domains / constraints / query
canonically; the solver (C3) consumes ``unknowns`` + ``constraints``. ``facts`` is
provenance — the coefficients the constraints were built from.
"""
unknowns: tuple[Unknown, ...]
facts: tuple[AttributeFact, ...]
constraints: tuple[LinearConstraint, ...]
query: ConstraintQuery
__all__ = [
"AttributeFact",
"ConstraintProblem",
"ConstraintQuery",
"Domain",
"Unknown",
]