core/generate/kernel_facts.py

306 lines
10 KiB
Python

"""Kernel fact and provenance primitives for the substrate layer.
Tranche 1 — broad base-layer foundations.
Immutable, typed records that form the canonical substrate fact model.
All records are frozen dataclasses per the codebase immutability convention.
Provenance rules enforced at construction time:
- ``problem_text`` facts require exact source spans
- ``derived`` facts require input_fact_ids
- Pack/world facts must not masquerade as problem text
- ``speculative`` facts cannot be consumed by serving
These primitives are domain-agnostic — they are not tied to GSM8K or
any specific benchmark. They provide the shared vocabulary that future
organs and the ProblemFrame IR will consume.
"""
from __future__ import annotations
from dataclasses import dataclass
from fractions import Fraction
from typing import Literal, Union
# ---------------------------------------------------------------------------
# Provenance kinds — closed set per Tranche 1 brief.
# ---------------------------------------------------------------------------
PROVENANCE_KINDS: frozenset[str] = frozenset({
"problem_text",
"derived",
"kernel_unit",
"kernel_calendar",
"kernel_math",
"kernel_world_fact",
"reviewed_pack",
"speculative",
})
# Provenance kinds that require exact source spans.
_SPAN_REQUIRED_KINDS: frozenset[str] = frozenset({"problem_text"})
# Provenance kinds that require input_fact_ids.
_INPUT_REQUIRED_KINDS: frozenset[str] = frozenset({"derived"})
# Provenance kinds that are NOT allowed to carry source spans
# (they must not masquerade as problem text).
_SPAN_FORBIDDEN_KINDS: frozenset[str] = frozenset({
"kernel_unit",
"kernel_calendar",
"kernel_math",
"kernel_world_fact",
"reviewed_pack",
})
# Fact types for SubstrateFact.
FACT_TYPES: frozenset[str] = frozenset({
"grounded_scalar",
"grounded_unit",
"candidate_relation",
})
# ---------------------------------------------------------------------------
# Source span — exact character range in problem text.
# ---------------------------------------------------------------------------
@dataclass(frozen=True, slots=True)
class SourceSpan:
"""Exact character range in the original problem text.
``start`` and ``end`` are zero-indexed character offsets (inclusive start,
exclusive end — matching Python slice semantics). ``sentence_index`` is
an optional zero-indexed sentence ordinal within the problem.
"""
text: str
start: int
end: int
sentence_index: int | None = None
def __post_init__(self) -> None:
if self.start < 0:
raise ValueError(f"SourceSpan.start must be >= 0, got {self.start}")
if self.end < self.start:
raise ValueError(
f"SourceSpan.end ({self.end}) must be >= start ({self.start})"
)
# ---------------------------------------------------------------------------
# Kernel provenance — tracks origin of every substrate fact.
# ---------------------------------------------------------------------------
@dataclass(frozen=True, slots=True)
class KernelProvenance:
"""Provenance record for a substrate fact.
Construction enforces:
- ``kind`` must be in :data:`PROVENANCE_KINDS`
- ``problem_text`` requires non-empty ``source_spans``
- ``derived`` requires non-empty ``input_fact_ids``
- Pack/world kinds must not carry source spans
"""
kind: str
source_spans: tuple[SourceSpan, ...] = ()
input_fact_ids: tuple[str, ...] = ()
pack_id: str | None = None
def __post_init__(self) -> None:
if self.kind not in PROVENANCE_KINDS:
raise ValueError(
f"KernelProvenance.kind must be one of {sorted(PROVENANCE_KINDS)}, "
f"got {self.kind!r}"
)
if self.kind in _SPAN_REQUIRED_KINDS and not self.source_spans:
raise ValueError(
f"Provenance kind {self.kind!r} requires non-empty source_spans"
)
if self.kind in _INPUT_REQUIRED_KINDS and not self.input_fact_ids:
raise ValueError(
f"Provenance kind {self.kind!r} requires non-empty input_fact_ids"
)
if self.kind in _SPAN_FORBIDDEN_KINDS and self.source_spans:
raise ValueError(
f"Provenance kind {self.kind!r} must not carry source_spans "
f"(pack/world facts must not masquerade as problem text)"
)
# ---------------------------------------------------------------------------
# Kernel hazard — annotates risk on a fact or surface.
# ---------------------------------------------------------------------------
@dataclass(frozen=True, slots=True)
class KernelHazard:
"""An ambiguity or risk annotation on a substrate fact or surface.
The hazard registry (:mod:`packs.ambiguity_hazards`) owns the
canonical set of hazard categories; this record carries a reference to
one of them.
"""
hazard_id: str
category: str
surface: str
description: str
context_required: tuple[str, ...] = ()
# ---------------------------------------------------------------------------
# Grounded scalar — exact rational value from problem text or pack.
# ---------------------------------------------------------------------------
@dataclass(frozen=True, slots=True)
class GroundedScalar:
"""A grounded scalar quantity as an exact rational number.
``value`` is always a :class:`~fractions.Fraction` — never a float.
The ``provenance`` tracks where this scalar came from.
"""
fact_id: str
surface: str
value: Fraction
provenance: KernelProvenance
hazards: tuple[KernelHazard, ...] = ()
def __post_init__(self) -> None:
if not isinstance(self.value, Fraction):
raise TypeError(
f"GroundedScalar.value must be Fraction, got {type(self.value).__name__}"
)
# ---------------------------------------------------------------------------
# Grounded unit — unit fact with dimension classification.
# ---------------------------------------------------------------------------
@dataclass(frozen=True, slots=True)
class GroundedUnit:
"""A unit fact with its dimension classification.
``dimension`` is the dimension class name (e.g., ``'length'``, ``'money'``).
"""
fact_id: str
surface: str
dimension: str
singular: str
provenance: KernelProvenance
# ---------------------------------------------------------------------------
# Relation role — role in a candidate relation.
# ---------------------------------------------------------------------------
@dataclass(frozen=True, slots=True)
class RelationRole:
"""A typed role within a :class:`CandidateRelation`."""
name: str
required: bool
description: str
# ---------------------------------------------------------------------------
# Candidate relation — typed relation between quantities/entities.
# ---------------------------------------------------------------------------
@dataclass(frozen=True, slots=True)
class CandidateRelation:
"""A typed candidate relation between quantities, entities, or both.
This is a *candidate* — it annotates structure, not a solved answer.
"""
relation_id: str
relation_type: str
roles: tuple[RelationRole, ...] = ()
provenance: KernelProvenance | None = None
hazards: tuple[KernelHazard, ...] = ()
# ---------------------------------------------------------------------------
# Span-grounded mention and binding primitives.
# ---------------------------------------------------------------------------
MentionKind = Literal["entity", "actor", "object", "quantity", "unit"]
BindingKind = Literal["quantity_entity", "quantity_unit"]
@dataclass(frozen=True, slots=True)
class GroundedMention:
"""A deterministic, source-grounded mention; never a derived answer."""
mention_id: str
kind: MentionKind
surface: str
span: SourceSpan
fact_id: str | None = None
@dataclass(frozen=True, slots=True)
class MentionBinding:
"""A typed edge between two grounded mentions."""
binding_id: str
binding_type: BindingKind
source_mention_id: str
target_mention_id: str
evidence_spans: tuple[SourceSpan, ...]
@dataclass(frozen=True, slots=True)
class BoundRole:
"""A declared relation role bound to a mention or substrate fact."""
role: str
target_id: str
target_kind: str
evidence_spans: tuple[SourceSpan, ...]
@dataclass(frozen=True, slots=True)
class BoundRelation:
"""A candidate relation whose roles have explicit grounded referents."""
relation_id: str
relation_type: str
roles: tuple[BoundRole, ...]
evidence_spans: tuple[SourceSpan, ...]
# ---------------------------------------------------------------------------
# Substrate fact — the canonical union wrapper.
# ---------------------------------------------------------------------------
# Content type union for SubstrateFact.
SubstrateContent = Union[GroundedScalar, GroundedUnit, CandidateRelation]
@dataclass(frozen=True, slots=True)
class SubstrateFact:
"""Union wrapper — the canonical substrate fact record.
``fact_type`` must be one of :data:`FACT_TYPES` and must match the
runtime type of ``content``.
``speculative`` provenance facts are blocked from serving consumption —
the provenance check is on the ``provenance`` field of this wrapper.
"""
fact_id: str
fact_type: str
content: SubstrateContent
provenance: KernelProvenance
hazards: tuple[KernelHazard, ...] = ()
def __post_init__(self) -> None:
if self.fact_type not in FACT_TYPES:
raise ValueError(
f"SubstrateFact.fact_type must be one of {sorted(FACT_TYPES)}, "
f"got {self.fact_type!r}"
)
# Verify content type matches fact_type.
_CONTENT_TYPE_MAP = {
"grounded_scalar": GroundedScalar,
"grounded_unit": GroundedUnit,
"candidate_relation": CandidateRelation,
}
expected = _CONTENT_TYPE_MAP[self.fact_type]
if not isinstance(self.content, expected):
raise TypeError(
f"SubstrateFact with fact_type={self.fact_type!r} expects "
f"{expected.__name__} content, got {type(self.content).__name__}"
)
@property
def is_speculative(self) -> bool:
"""True if this fact has speculative provenance and must not be
consumed by serving."""
return self.provenance.kind == "speculative"