Source code for cadrumo.core.prose_elision

"""Canonical elision primitive for length-capped operator-facing prose.

A diagnostic, finding, or issue whose message is assembled from taxpayer data
has a length nobody authored: fixed prose plus interpolated terms sized by the
household rather than by the writer. When such a message crosses its field's
cap the model raises, and a NON-BLOCKING advisory becomes a blocking failure --
the filing stops at exactly the moment the advisory had something to say. Cap
enforcement on system-assembled prose therefore cuts rather than refuses.

Making that a property of the TYPE is the point. A clamp applied at the call
site is only as complete as the author's memory: the next builder added to the
module raises, and nothing catches it until a real household is large enough.
A type that elides is total by construction, so no present or future author
carries a character budget in their head.

Three homes grew this same clamp independently, with three different markers
and three different behaviours -- a word-boundary-aware type validator using
``" [...]"``, a mid-word call-site clamp using ``"…"``, and a third
hard-cut call-site clamp using ``"..."``. Nothing made them agree, and two of
the three were partial (a construction site that forgot the wrapper still
raised). This module is their single home, so an operator meets one elision
convention rather than three and a new capped prose field inherits the
behaviour instead of reimplementing it.

The elision is deliberately VISIBLE. A silently shortened advisory is its own
defect: these messages carry remedies, and an operator cannot tell a terse
message from a cut one. The marker costs six characters to make the difference
legible.

Eliding is a floor, not a licence. A message that reaches the validator has
already lost words an operator was meant to read, so authoring-time headroom
assertions still belong on messages whose length scales with taxpayer data.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import TYPE_CHECKING, Annotated, Final

from pydantic import BeforeValidator, StringConstraints

__all__ = ["PROSE_ELISION_MARKER", "ElidedProse", "elide_to_cap", "elided_prose"]

#: Appended to prose the cap truncated, so the elision is visible to a reader.
PROSE_ELISION_MARKER: Final[str] = " [...]"


[docs] def elide_to_cap(value: str, *, cap: int) -> str: """Return *value* shortened to at most *cap* characters, marking any cut. Total: every input produces a string of at most *cap* characters, so a caller can rely on the result satisfying the bound whatever the data. Cuts on a word boundary where one is reasonably placed rather than mid-word, because a message ending in half an identifier reads as corruption rather than as truncation. "Reasonably placed" means past the halfway mark of the available room; a boundary earlier than that would discard more than it saves, so the cut falls mid-word instead. A *cap* too small to hold the marker leaves no room to signal anything, so the value is hard-cut to the bound. That case does not arise for prose fields in this tree -- the tightest declared cap is an order of magnitude larger -- but the function stays total rather than producing a string longer than the cap it was asked to satisfy. Args: value: The prose to bound. cap: The maximum length of the result, in characters. Returns: *value* unchanged when it already fits, otherwise a shortened form ending in :data:`PROSE_ELISION_MARKER`. """ if len(value) <= cap: return value if cap <= len(PROSE_ELISION_MARKER): return value[:cap] keep = cap - len(PROSE_ELISION_MARKER) head = value[:keep].rstrip() boundary = head.rfind(" ") if boundary > keep // 2: head = head[:boundary].rstrip() return head + PROSE_ELISION_MARKER
[docs] def elided_prose( cap: int, *, min_length: int = 1, strip_whitespace: bool = False, ) -> object: """Return a ``str`` annotation that elides to *cap* instead of refusing. The returned annotation carries the cap as a real constraint, so the bound stays readable on the model and discoverable by anything that inspects field metadata; the eliding validator runs first, so the constraint sees an already-bounded value and never fires on length. Use this for prose the SYSTEM assembles. Operator-supplied text -- a note typed at the CLI, an imported feedback package -- should keep a raising cap: the operator can shorten what they wrote, and silently swallowing half of it would be the worse failure. Args: cap: The field's maximum length, in characters. min_length: Minimum length after elision; the default rejects empty prose, which is a real defect rather than a short message. strip_whitespace: Whether to strip surrounding whitespace, for fields that already declared that constraint. Returns: An :data:`~typing.Annotated` ``str`` suitable as a field annotation. """ def _elide(value: object) -> object: if not isinstance(value, str): return value return elide_to_cap(value, cap=cap) return Annotated[ str, StringConstraints( strip_whitespace=strip_whitespace, min_length=min_length, max_length=cap, ), BeforeValidator(_elide), ]
if TYPE_CHECKING: # pragma: no cover - import cycle only matters to type checkers from pydantic import GetCoreSchemaHandler from pydantic_core import CoreSchema
[docs] @dataclass(frozen=True, slots=True) class ElidedProse: """Annotated metadata declaring an eliding prose cap. Carries the same constraint set as :func:`elided_prose` but as *metadata* rather than as a returned annotation, so a field can be written ``Annotated[str, ElidedProse(512)]``. That form is a literal type expression, which a static type checker can follow; assigning the result of a call to a name and using the name as an annotation is not, and leaves the field's type unknown along with every value read from it. Args: cap: The field's maximum length, in characters. min_length: Minimum length after elision. strip_whitespace: Whether to strip surrounding whitespace. """ cap: int min_length: int = 1 strip_whitespace: bool = False @property def max_length(self) -> int: """Return the cap under the name field-metadata inspectors look for. :func:`elided_prose` placed a :class:`~pydantic.StringConstraints` in the field metadata, and callers read ``max_length`` off it to size their own buffers. Exposing the same name here keeps that contract intact: the bound stays discoverable by anything inspecting metadata, which is the property the eliding annotation documents. """ return self.cap def __get_pydantic_core_schema__(self, source_type: object, handler: GetCoreSchemaHandler) -> CoreSchema: """Return the schema of the equivalent :func:`elided_prose` annotation.""" return handler.generate_schema( elided_prose( self.cap, min_length=self.min_length, strip_whitespace=self.strip_whitespace, ) )
#: The traceable-exclusion ``detail`` annotation: elides rather than refusing. #: #: Homed here rather than beside its first consumer because BOTH the ledger #: preflight surface and the aggregation income ledgers annotate a field with #: it, and the ledger-side home made that a cycle: preflight imports the #: aggregation package, whose modules imported this name back out of a #: half-initialised preflight. #: #: These issues explain why a ledger row was excluded, so refusing one over its #: length would drop the explanation for the exclusion AND fail the aggregation #: that produced it -- a silent under-declaration dressed as a validation error. #: Shortening the sentence is strictly the lesser loss. IssueDetail = Annotated[str, ElidedProse(512)]