PR-β of the CompositionClaim wave (CC-2 + CC-3 bundled per the brief
pack — CC-3's heuristic depends on CC-2's new change_kind Literal value).
Mirrors the F1 / ADR-0168 FrameClaim template 1:1 with composition-specific
substitutions.
CC-2 — handler implementation
- teaching/math_composition_proposal.py — MathCompositionClaimProposal
adapter per ADR-0169.1 §"Data shape". Frozen dataclass, deterministic
proposal_id / claim_signature, source="math_audit" pin at the
proposal layer (rejects "corpus" laundering).
- teaching/math_composition_ratification.py — apply_composition_claim()
handler. SAFE_COMPOSITION_CATEGORIES = {multiplicative,
additive, subtractive}_composition per ADR-0169 §"Initial safe
category scope". New WrongCompositionCategory exception per
ADR-0169.1 §"Trip-wires" #8. Writes only to
language_packs/data/en_core_math_v1/compositions/{category}.jsonl;
no solver / parser / decomposer / runtime mutation.
- workbench/readers.py — _HANDLER_DISPATCH now routes
composition_reclassification → CompositionClaim; suggested_cli
branch added for both read_math_proposal and ratify_math_proposal.
- teaching/math_contemplation_proposal.py — ChangeKind Literal +
_VALID_CHANGE_KINDS frozenset extended with
composition_reclassification.
- language_packs/data/en_core_math_v1/compositions/.gitkeep —
reviewed-pack scaffold.
- tests/test_math_composition_ratification.py — 22 tests including
case 0050 hazard pin, cross-process replay equivalence, queue-order
independence, partition, no-corpus-laundering, dispatch wire,
Literal acceptance, JSONL round-trip.
- tests/test_adr_0172_w1_shape_proposal.py — parametrize round-trip
over all 5 change_kinds.
- core/cli.py — teaching suite tuple includes new test file.
CC-3 — decomposer heuristic tightening
- teaching/math_contemplation.py::_CHANGE_KIND_BY_PAIR:
+ (incomplete_operation, quantity_extraction) → composition_reclassification
+ (incomplete_operation, multi_quantity_composition) → composition_reclassification
- (unexpected_category, multi_subject_sentence) demoted to injector_sub_shape
(was frame_reclassification; FrameClaim SAFE_FRAME_CATEGORIES doesn't
cover this — needs ReferenceClaim/CompositionClaim)
- (unexpected_category, descriptive_frame_question) demoted to injector_sub_shape
(was frame_reclassification; needs SlotClaim, not FrameClaim)
Updated hypothesis-step justification text to reflect new dispatch
table.
- tests/test_adr_0172_w2_decomposer.py — distribution assertion
tightened from "≥3 matcher, ≥2 frame" to exact counts:
3 matcher / 2 composition / 3 injector / 0 frame. New
per-pair tests for the four CC-3 dispatch changes.
Verification on real audit_brief_11.json (20 of 47 highest-leverage
refusals now routable):
2 composition_reclassification (12 quantity_extraction + 8 multi_quantity_composition)
3 injector_sub_shape (2 multi_subject + 2 descriptive_frame + 4 unattached_quantity)
3 matcher_extension (9 pre_frame_filler + 4 fraction_percentage + 4 pronoun)
0 frame_reclassification (the two prior misroutes are gone)
Workbench POST /math-proposals/{id}/ratify on either composition
proposal now returns 200/routed with a real apply_composition_claim()
command instead of 501.
Suites green:
- core test --suite teaching -q → 71 passed
- core test --suite runtime -q → 20 passed
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
476 lines
18 KiB
Python
476 lines
18 KiB
Python
"""ADR-0172 Tier 1 / W1 — MathReaderRefusalShapeProposal schema.
|
|
|
|
Mirror of :class:`teaching.proposals.TeachingChainProposal` for the
|
|
math-domain contemplation corridor. Each proposal carries:
|
|
|
|
- a :class:`~evals.refusal_taxonomy.shape_categories.ShapeCategory` drawn
|
|
from the refusal taxonomy;
|
|
- ≥2 :class:`~teaching.math_evidence.MathReaderRefusalEvidence` pointers as
|
|
evidence floor;
|
|
- a ``proposed_change_kind`` discriminating the structural change class;
|
|
- a ``wrong_zero_assertion`` (≥40 chars) pinning the non-regression claim;
|
|
- a ``reasoning_trace`` (W0 :class:`ReasoningTrace`) carrying the
|
|
contemplation chain.
|
|
|
|
Trust boundary: schema-only module. No filesystem I/O, no teaching-store
|
|
writes, no runtime pipeline hooks. All validation is in :func:`build_proposal`.
|
|
|
|
W0 dependency: ``ReasoningTrace`` is imported under ``TYPE_CHECKING`` until
|
|
``teaching/math_reasoning_trace.py`` (A1 branch) merges. At runtime the
|
|
field is accepted as an opaque object; ``build_proposal`` validates it is
|
|
not ``None`` and carries a non-empty ``trace_id`` attribute.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
from dataclasses import dataclass
|
|
from typing import Any, Literal
|
|
|
|
from evals.refusal_taxonomy.shape_categories import ShapeCategory
|
|
from generate.comprehension.audit import AuditRow
|
|
from teaching.math_evidence import MathReaderRefusalEvidence
|
|
from teaching.math_reasoning_trace import ReasoningStep, ReasoningTrace, build_trace
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Constants
|
|
# ---------------------------------------------------------------------------
|
|
|
|
ChangeKind = Literal[
|
|
"matcher_extension",
|
|
"injector_sub_shape",
|
|
"vocabulary_addition",
|
|
"frame_reclassification",
|
|
"composition_reclassification",
|
|
]
|
|
|
|
_VALID_CHANGE_KINDS: frozenset[str] = frozenset({
|
|
"matcher_extension",
|
|
"injector_sub_shape",
|
|
"vocabulary_addition",
|
|
"frame_reclassification",
|
|
"composition_reclassification",
|
|
})
|
|
|
|
_WRONG_ZERO_MIN_LEN: int = 40
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Schema
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class MathReaderRefusalShapeProposal:
|
|
"""One proposed structural change for a math-reader refusal shape.
|
|
|
|
Construct via :func:`build_proposal` — do not instantiate directly
|
|
(proposal_id is content-derived and requires hash computation).
|
|
|
|
Fields
|
|
------
|
|
proposal_id:
|
|
``sha256(canonical_bytes(...)).hexdigest()`` over all other fields.
|
|
domain:
|
|
Always ``"math"`` — Literal pin enforced by :func:`build_proposal`.
|
|
shape_category:
|
|
A ratified :class:`ShapeCategory` enum member.
|
|
structural_commonality:
|
|
Human-readable description of the shared structural pattern among
|
|
the evidence rows.
|
|
evidence_pointers:
|
|
≥2 :class:`MathReaderRefusalEvidence` records; evidence floor mirrors
|
|
ADR-0057's corpus-pointer requirement.
|
|
proposed_change_kind:
|
|
One of the four Tier-1 change classes.
|
|
proposed_change_payload:
|
|
JSON-serializable payload discriminated by ``proposed_change_kind``.
|
|
wrong_zero_assertion:
|
|
≥40-char natural-language statement pinning the wrong=0 invariant.
|
|
replay_equivalence_hash:
|
|
``sha256`` digest of the replay-equivalence gate output (mirrors
|
|
:class:`teaching.proposals.ReplayEvidence` contract).
|
|
reasoning_trace:
|
|
W0 :class:`ReasoningTrace` carrying the contemplation derivation.
|
|
Mandatory — ``None`` is rejected by :func:`build_proposal`.
|
|
"""
|
|
|
|
proposal_id: str
|
|
domain: Literal["math"]
|
|
shape_category: ShapeCategory
|
|
structural_commonality: str
|
|
evidence_pointers: tuple[MathReaderRefusalEvidence, ...]
|
|
proposed_change_kind: ChangeKind
|
|
proposed_change_payload: object
|
|
wrong_zero_assertion: str
|
|
replay_equivalence_hash: str
|
|
reasoning_trace: Any # ReasoningTrace; symbolic until A1 merges
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Canonical-bytes serialization
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _serialise_evidence_pointer(ev: MathReaderRefusalEvidence) -> str:
|
|
"""Reduce an evidence record to its stable content hash."""
|
|
return ev.evidence_hash
|
|
|
|
|
|
def _serialise_shape_category(sc: ShapeCategory) -> str:
|
|
return sc.value
|
|
|
|
|
|
def _serialise_reasoning_trace(trace: Any) -> str:
|
|
"""Extract trace_id from any object that carries it."""
|
|
trace_id = getattr(trace, "trace_id", None)
|
|
if not isinstance(trace_id, str) or not trace_id:
|
|
raise ValueError(
|
|
"reasoning_trace must have a non-empty str trace_id attribute"
|
|
)
|
|
return trace_id
|
|
|
|
|
|
def canonical_bytes(proposal: MathReaderRefusalShapeProposal) -> bytes:
|
|
"""Return deterministic canonical bytes over all fields except proposal_id.
|
|
|
|
Stable JSON (sorted keys, no whitespace, UTF-8). evidence_pointers are
|
|
reduced to their evidence_hash digests; reasoning_trace is reduced to its
|
|
trace_id. proposed_change_payload must be JSON-serializable (validated
|
|
by :func:`build_proposal` before this function is called).
|
|
"""
|
|
payload: dict[str, Any] = {
|
|
"domain": proposal.domain,
|
|
"evidence_pointers": sorted(
|
|
_serialise_evidence_pointer(ev) for ev in proposal.evidence_pointers
|
|
),
|
|
"proposed_change_kind": proposal.proposed_change_kind,
|
|
"proposed_change_payload": proposal.proposed_change_payload,
|
|
"reasoning_trace_id": _serialise_reasoning_trace(proposal.reasoning_trace),
|
|
"replay_equivalence_hash": proposal.replay_equivalence_hash,
|
|
"shape_category": _serialise_shape_category(proposal.shape_category),
|
|
"structural_commonality": proposal.structural_commonality,
|
|
"wrong_zero_assertion": proposal.wrong_zero_assertion,
|
|
}
|
|
return json.dumps(
|
|
payload,
|
|
ensure_ascii=False,
|
|
sort_keys=True,
|
|
separators=(",", ":"),
|
|
).encode("utf-8")
|
|
|
|
|
|
def compute_proposal_id(
|
|
*,
|
|
domain: Literal["math"],
|
|
shape_category: ShapeCategory,
|
|
structural_commonality: str,
|
|
evidence_pointers: tuple[MathReaderRefusalEvidence, ...],
|
|
proposed_change_kind: ChangeKind,
|
|
proposed_change_payload: object,
|
|
wrong_zero_assertion: str,
|
|
replay_equivalence_hash: str,
|
|
reasoning_trace: Any,
|
|
) -> str:
|
|
"""Hash all content fields to produce a stable proposal_id.
|
|
|
|
Uses a temporary placeholder proposal_id so that :func:`canonical_bytes`
|
|
can operate on a fully-formed dataclass without the chicken-and-egg
|
|
problem of hashing the id field itself.
|
|
"""
|
|
placeholder = MathReaderRefusalShapeProposal(
|
|
proposal_id="",
|
|
domain=domain,
|
|
shape_category=shape_category,
|
|
structural_commonality=structural_commonality,
|
|
evidence_pointers=evidence_pointers,
|
|
proposed_change_kind=proposed_change_kind,
|
|
proposed_change_payload=proposed_change_payload,
|
|
wrong_zero_assertion=wrong_zero_assertion,
|
|
replay_equivalence_hash=replay_equivalence_hash,
|
|
reasoning_trace=reasoning_trace,
|
|
)
|
|
raw = canonical_bytes(placeholder)
|
|
return hashlib.sha256(raw).hexdigest()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Factory
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def build_proposal(
|
|
*,
|
|
domain: Literal["math"] = "math",
|
|
shape_category: ShapeCategory,
|
|
structural_commonality: str,
|
|
evidence_pointers: tuple[MathReaderRefusalEvidence, ...],
|
|
proposed_change_kind: str,
|
|
proposed_change_payload: object,
|
|
wrong_zero_assertion: str,
|
|
replay_equivalence_hash: str,
|
|
reasoning_trace: Any,
|
|
) -> MathReaderRefusalShapeProposal:
|
|
"""Build a :class:`MathReaderRefusalShapeProposal` with all invariants enforced.
|
|
|
|
Raises ``ValueError`` on any violation; the caller must fix the inputs.
|
|
|
|
Invariants
|
|
----------
|
|
1. ``domain`` must be ``"math"``.
|
|
2. ``shape_category`` must be a :class:`ShapeCategory` enum member.
|
|
3. ``len(evidence_pointers) >= 2`` (evidence floor).
|
|
4. ``proposed_change_kind`` in :data:`_VALID_CHANGE_KINDS`.
|
|
5. ``proposed_change_payload`` must be JSON-serializable.
|
|
6. ``len(wrong_zero_assertion.strip()) >= _WRONG_ZERO_MIN_LEN``.
|
|
7. ``reasoning_trace`` is not ``None`` and carries a non-empty ``trace_id``.
|
|
"""
|
|
if domain != "math":
|
|
raise ValueError(f"domain must be 'math'; got {domain!r}")
|
|
|
|
if not isinstance(shape_category, ShapeCategory):
|
|
raise ValueError(
|
|
f"shape_category must be a ShapeCategory enum member; got {shape_category!r}"
|
|
)
|
|
|
|
if len(evidence_pointers) < 2:
|
|
raise ValueError(
|
|
f"evidence_pointers requires ≥2 entries; got {len(evidence_pointers)}"
|
|
)
|
|
|
|
if proposed_change_kind not in _VALID_CHANGE_KINDS:
|
|
raise ValueError(
|
|
f"proposed_change_kind {proposed_change_kind!r} is not a valid ChangeKind; "
|
|
f"allowed: {sorted(_VALID_CHANGE_KINDS)}"
|
|
)
|
|
|
|
try:
|
|
json.dumps(proposed_change_payload, ensure_ascii=False, separators=(",", ":"))
|
|
except (TypeError, ValueError) as exc:
|
|
raise ValueError(
|
|
f"proposed_change_payload is not JSON-serializable: {exc}"
|
|
) from exc
|
|
|
|
if not wrong_zero_assertion or len(wrong_zero_assertion.strip()) < _WRONG_ZERO_MIN_LEN:
|
|
raise ValueError(
|
|
f"wrong_zero_assertion must be ≥{_WRONG_ZERO_MIN_LEN} chars (non-empty); "
|
|
f"got {len(wrong_zero_assertion)!r}"
|
|
)
|
|
|
|
if reasoning_trace is None:
|
|
raise ValueError("reasoning_trace is required and must not be None")
|
|
|
|
# Validate trace_id accessibility (also validates the serialisation path).
|
|
trace_id = getattr(reasoning_trace, "trace_id", None)
|
|
if not isinstance(trace_id, str) or not trace_id:
|
|
raise ValueError(
|
|
"reasoning_trace must carry a non-empty str trace_id attribute"
|
|
)
|
|
|
|
resolved_kind: ChangeKind = proposed_change_kind # type: ignore[assignment]
|
|
|
|
pid = compute_proposal_id(
|
|
domain=domain,
|
|
shape_category=shape_category,
|
|
structural_commonality=structural_commonality,
|
|
evidence_pointers=evidence_pointers,
|
|
proposed_change_kind=resolved_kind,
|
|
proposed_change_payload=proposed_change_payload,
|
|
wrong_zero_assertion=wrong_zero_assertion,
|
|
replay_equivalence_hash=replay_equivalence_hash,
|
|
reasoning_trace=reasoning_trace,
|
|
)
|
|
|
|
return MathReaderRefusalShapeProposal(
|
|
proposal_id=pid,
|
|
domain=domain,
|
|
shape_category=shape_category,
|
|
structural_commonality=structural_commonality,
|
|
evidence_pointers=tuple(evidence_pointers),
|
|
proposed_change_kind=resolved_kind,
|
|
proposed_change_payload=proposed_change_payload,
|
|
wrong_zero_assertion=wrong_zero_assertion,
|
|
replay_equivalence_hash=replay_equivalence_hash,
|
|
reasoning_trace=reasoning_trace,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Self-contained JSONL persistence serializer (ADR-0172 tightening follow-up #1)
|
|
# ---------------------------------------------------------------------------
|
|
#
|
|
# canonical_bytes() above is the content-hash function: it reduces
|
|
# evidence_pointers to evidence_hashes and reasoning_trace to its trace_id.
|
|
# That's the right shape for the proposal_id derivation but NOT for
|
|
# round-tripping through disk — a reader cannot reconstruct the full
|
|
# proposal from canonical bytes without re-running the decomposer.
|
|
#
|
|
# to_jsonl_record() / from_jsonl_record() are a SEPARATE persistence
|
|
# serializer that emits a self-contained record (proposal_id, full
|
|
# evidence_pointers, full reasoning_trace.steps) so the workbench can
|
|
# read proposals.jsonl without re-running decompose_audit().
|
|
#
|
|
# Determinism contract preserved: sort_keys=True, compact separators,
|
|
# no floats (rejected by reasoning_trace validators).
|
|
|
|
|
|
def _audit_row_to_dict(row: AuditRow) -> dict[str, Any]:
|
|
return {
|
|
"case_id": row.case_id,
|
|
"sentence_index": row.sentence_index,
|
|
"token_index": row.token_index,
|
|
"token_text": row.token_text,
|
|
"recognized_terms": list(row.recognized_terms),
|
|
"skipped_frame": row.skipped_frame,
|
|
"missing_operator": row.missing_operator,
|
|
"refusal_reason": row.refusal_reason,
|
|
"refusal_detail": row.refusal_detail,
|
|
}
|
|
|
|
|
|
def _audit_row_from_dict(data: dict[str, Any]) -> AuditRow:
|
|
return AuditRow(
|
|
case_id=str(data["case_id"]),
|
|
sentence_index=int(data["sentence_index"]),
|
|
token_index=int(data["token_index"]),
|
|
token_text=str(data["token_text"]),
|
|
recognized_terms=tuple(data.get("recognized_terms") or ()),
|
|
skipped_frame=data.get("skipped_frame"),
|
|
missing_operator=data.get("missing_operator"),
|
|
refusal_reason=str(data.get("refusal_reason", "")),
|
|
refusal_detail=str(data.get("refusal_detail", "")),
|
|
)
|
|
|
|
|
|
def _evidence_to_dict(ev: MathReaderRefusalEvidence) -> dict[str, Any]:
|
|
return {
|
|
"case_id": ev.case_id,
|
|
"sentence_index": ev.sentence_index,
|
|
"token_index": ev.token_index,
|
|
"refusal_reason": ev.refusal_reason,
|
|
"missing_operator": ev.missing_operator,
|
|
"claim_signature": ev.claim_signature,
|
|
"evidence_hash": ev.evidence_hash,
|
|
"sub_type": ev.sub_type,
|
|
"audit_row": _audit_row_to_dict(ev.audit_row),
|
|
}
|
|
|
|
|
|
def _evidence_from_dict(data: dict[str, Any]) -> MathReaderRefusalEvidence:
|
|
return MathReaderRefusalEvidence(
|
|
case_id=str(data["case_id"]),
|
|
sentence_index=int(data["sentence_index"]),
|
|
token_index=int(data["token_index"]),
|
|
refusal_reason=str(data["refusal_reason"]),
|
|
missing_operator=data.get("missing_operator"),
|
|
claim_signature=str(data.get("claim_signature", "")),
|
|
evidence_hash=str(data["evidence_hash"]),
|
|
audit_row=_audit_row_from_dict(data["audit_row"]),
|
|
sub_type=data["sub_type"],
|
|
)
|
|
|
|
|
|
def _step_to_dict(step: ReasoningStep) -> dict[str, Any]:
|
|
return {
|
|
"step_index": step.step_index,
|
|
"step_kind": step.step_kind,
|
|
"input_pointers": list(step.input_pointers),
|
|
"claim": step.claim,
|
|
"justification": step.justification,
|
|
"output_payload": step.output_payload,
|
|
}
|
|
|
|
|
|
def _step_from_dict(data: dict[str, Any]) -> ReasoningStep:
|
|
return ReasoningStep(
|
|
step_index=int(data["step_index"]),
|
|
step_kind=data["step_kind"],
|
|
input_pointers=tuple(str(p) for p in data.get("input_pointers", ())),
|
|
claim=str(data.get("claim", "")),
|
|
justification=str(data.get("justification", "")),
|
|
output_payload=data.get("output_payload"),
|
|
)
|
|
|
|
|
|
def to_jsonl_record(proposal: MathReaderRefusalShapeProposal) -> dict[str, Any]:
|
|
"""Return a self-contained dict representation suitable for JSONL persistence.
|
|
|
|
Unlike :func:`canonical_bytes`, this record includes:
|
|
- ``proposal_id`` (so consumers don't need to recompute it)
|
|
- full ``evidence_pointers`` (nested dicts — not just hashes)
|
|
- full ``reasoning_trace.steps`` (inline — not just trace_id)
|
|
|
|
The output is JSON-serializable. Encoding to bytes via
|
|
``json.dumps(record, sort_keys=True, separators=(",", ":"),
|
|
ensure_ascii=False)`` produces deterministic byte-identical output
|
|
across reruns.
|
|
"""
|
|
trace = proposal.reasoning_trace
|
|
return {
|
|
"proposal_id": proposal.proposal_id,
|
|
"domain": proposal.domain,
|
|
"shape_category": proposal.shape_category.value,
|
|
"structural_commonality": proposal.structural_commonality,
|
|
"evidence_pointers": [
|
|
_evidence_to_dict(ev) for ev in proposal.evidence_pointers
|
|
],
|
|
"proposed_change_kind": proposal.proposed_change_kind,
|
|
"proposed_change_payload": proposal.proposed_change_payload,
|
|
"wrong_zero_assertion": proposal.wrong_zero_assertion,
|
|
"replay_equivalence_hash": proposal.replay_equivalence_hash,
|
|
"reasoning_trace": {
|
|
"trace_id": trace.trace_id,
|
|
"steps": [_step_to_dict(s) for s in trace.steps],
|
|
},
|
|
}
|
|
|
|
|
|
def from_jsonl_record(record: dict[str, Any]) -> MathReaderRefusalShapeProposal:
|
|
"""Reconstruct a proposal from a :func:`to_jsonl_record` dict.
|
|
|
|
Goes through :func:`build_proposal` so all invariants are re-validated
|
|
(evidence floor, change_kind allowlist, wrong_zero min-length, trace_id
|
|
presence, JSON-serializable payload). The reconstructed ``proposal_id``
|
|
must match the persisted one — mismatch indicates tampering or schema
|
|
drift and raises :class:`ValueError`.
|
|
"""
|
|
evidence_records = tuple(
|
|
_evidence_from_dict(d) for d in record.get("evidence_pointers", ())
|
|
)
|
|
steps = tuple(_step_from_dict(d) for d in record["reasoning_trace"]["steps"])
|
|
trace = build_trace(steps)
|
|
|
|
shape_category = ShapeCategory(record["shape_category"])
|
|
|
|
proposal = build_proposal(
|
|
domain=record.get("domain", "math"),
|
|
shape_category=shape_category,
|
|
structural_commonality=str(record["structural_commonality"]),
|
|
evidence_pointers=evidence_records,
|
|
proposed_change_kind=str(record["proposed_change_kind"]),
|
|
proposed_change_payload=record.get("proposed_change_payload"),
|
|
wrong_zero_assertion=str(record["wrong_zero_assertion"]),
|
|
replay_equivalence_hash=str(record["replay_equivalence_hash"]),
|
|
reasoning_trace=trace,
|
|
)
|
|
|
|
persisted_id = str(record.get("proposal_id", ""))
|
|
if persisted_id and persisted_id != proposal.proposal_id:
|
|
raise ValueError(
|
|
"proposal_id mismatch on JSONL round-trip: persisted "
|
|
f"{persisted_id!r} != recomputed {proposal.proposal_id!r}"
|
|
)
|
|
return proposal
|
|
|
|
|
|
__all__ = [
|
|
"ChangeKind",
|
|
"MathReaderRefusalShapeProposal",
|
|
"build_proposal",
|
|
"canonical_bytes",
|
|
"compute_proposal_id",
|
|
"from_jsonl_record",
|
|
"to_jsonl_record",
|
|
]
|