orchestrata_ultimate.factory.detectors.clause

Reference › orchestrata_ultimate › factory › detectors › clause

clause is the clause segmenter behind the C3 detector set (SPEC-033 row B22, ADR-0029 rule 1). The comparative and negation detectors must skip conditional clauses, which needs the start and end of each conditional clause and not only the presence of a word like "if". sentences splits text into (line number, sentence) pairs; a newline always ends a sentence, and periods end one except after listed abbreviations and inside decimal numbers (_is_abbreviation_boundary, _is_decimal_point, _is_sentence_boundary). segment splits one sentence into Segment records of kind conditional, rationale or main: first on sentence-internal punctuation (_piece_offsets), then at conditional and rationale markers from lexicons found inside a piece (_marker_boundaries), classifying each part by its first token (_classify). The docstring states the limits: no real grammar, no attachment or coordination resolution, with a grammar-based upgrade named as the future path. rules is the only importer; tests are in tests/factory/test_detector_clause.py.

At a glance

Source src/orchestrata_ultimate/factory/detectors/clause.py
Lines of code 205
Classes 1
Functions 9
Imports (internal) orchestrata_ultimate.factory.detectors.lexicons
Imported by orchestrata_ultimate.factory.detectors.rules
Tests that import it tests/factory/test_detector_clause.py

Class diagram

Class diagram orchestrata_ultimate.factory.detectors.clause

Dependency diagram

Dependency diagram orchestrata_ultimate.factory.detectors.clause

API

Classes

Segment (dataclass)

Decorators: dataclass(frozen=True) · Line 72

One clause segment of a sentence, with offsets into that sentence.

Attribute Type Default
text str
start int
end int
kind SegmentKind

Functions

Function Signature Summary
_is_abbreviation_boundary _is_abbreviation_boundary(line: str, terminator_end: int) -> bool True if the ./!/? ending at terminator_end is part of a known abbreviation (e.g. the second "." of "e.g.") and must not split.
_is_sentence_boundary _is_sentence_boundary(line: str, terminator_end: int) -> bool True if the char after (skipping spaces) starts a new sentence.
_is_decimal_point _is_decimal_point(line: str, terminator_end: int) -> bool True if this "." sits between two digits (e.g. "3.14") -- never a boundary.
_split_line_into_sentences _split_line_into_sentences(line: str) -> list[str] Split one physical line into sentence fragments.
sentences sentences(text: str) -> list[tuple[int, str]] Split text into (1-indexed line number, sentence text) pairs.
_piece_offsets _piece_offsets(sentence: str) -> list[tuple[int, int]] Offsets of the sentence-internal-punctuation-delimited pieces.
_marker_boundaries _marker_boundaries(piece_text: str) -> list[int] Offsets (relative to piece_text) where a clause marker starts a fresh sub-segment -- always includes 0.
_classify _classify(piece_text: str) -> SegmentKind Classify a sub-segment by what its first token matches.
segment segment(sentence: str) -> list[Segment] Split sentence into clause :class:Segment\ s.

Source

Permalink: https://gitlab.com/MSamy/orchestrata-ultimate-architecture/-/blob/a4cbc2bdf/src/orchestrata_ultimate/factory/detectors/clause.py

"""Clause segmenter for the SPEC-033 C3 detector set (B22, ADR-0029 rule 1).

A parser, not a word list: exempting conditional clauses from the
comparative/negation detectors ("~48% of comparative false positives were
comparisons against absolute numbers inside a condition") requires knowing
where a conditional clause *starts and ends*, not just whether the word "if"
appears somewhere in the sentence.

Two functions:

- :func:`sentences` -- line/offset-aware sentence splitting. Deliberately
  simple (periods + newlines, with a small abbreviation exception list) --
  not a full sentence boundary detector. A newline always ends a sentence,
  matching this codebase's one-requirement-per-line convention.
- :func:`segment` -- splits one sentence into ``conditional``/``rationale``/
  ``main`` clauses on sentence-internal punctuation (``,``, ``;``, em-dash)
  and on :data:`~.lexicons.CONDITIONAL_MARKERS` /
  :data:`~.lexicons.RATIONALE_MARKERS` occurring *inside* a punctuation-
  delimited piece (a marker there starts a fresh segment running to the next
  boundary, so "X, and if Y then Z" splits into three segments, not two).

Known, documented limits: no real grammar (no attachment resolution, no
coordination scope). A DCG-based upgrade able to resolve nested/coordinated
clauses precisely is the stated future path (ADR-0029 accepts this cost
explicitly: "a real implementation cost, bought with a measured ~48%
reduction in comparative false positives").
"""

from __future__ import annotations

import re
from dataclasses import dataclass
from typing import Literal

from orchestrata_ultimate.factory.detectors.lexicons import (
    CONDITIONAL_MARKERS,
    RATIONALE_MARKERS,
    compile_lexicon,
)

SegmentKind = Literal["conditional", "rationale", "main"]

# Abbreviations whose trailing "." must never be treated as a sentence
# terminator. Matched case-insensitively against the token immediately
# preceding the candidate terminator.
_ABBREVIATIONS = frozenset(
    {
        "e.g.",
        "i.e.",
        "etc.",
        "vs.",
        "cf.",
        "et al.",
        "no.",
        "fig.",
        "approx.",
        "dr.",
        "mr.",
        "mrs.",
        "ms.",
    }
)

_LAST_TOKEN_RE = re.compile(r"(\S+)$")
_PUNCT_SPLIT_RE = re.compile(r",|;|--|—")

_CONDITIONAL_RE = compile_lexicon(CONDITIONAL_MARKERS)
_RATIONALE_RE = compile_lexicon(RATIONALE_MARKERS)


@dataclass(frozen=True)
class Segment:
    """One clause segment of a sentence, with offsets into that sentence."""

    text: str
    start: int
    end: int
    kind: SegmentKind


def _is_abbreviation_boundary(line: str, terminator_end: int) -> bool:
    """True if the ``.``/``!``/``?`` ending at ``terminator_end`` is part of
    a known abbreviation (e.g. the second "." of "e.g.") and must not split.
    """
    before = line[:terminator_end]
    match = _LAST_TOKEN_RE.search(before)
    token = match.group(1).lower() if match else ""
    return token in _ABBREVIATIONS


def _is_sentence_boundary(line: str, terminator_end: int) -> bool:
    """True if the char after (skipping spaces) starts a new sentence."""
    rest = line[terminator_end:].lstrip(" ")
    return rest == "" or rest[0].isupper() or rest[0].isdigit()


def _is_decimal_point(line: str, terminator_end: int) -> bool:
    """True if this "." sits between two digits (e.g. "3.14") -- never a boundary."""
    before_ok = terminator_end >= 2 and line[terminator_end - 2].isdigit()
    after_ok = terminator_end < len(line) and line[terminator_end].isdigit()
    return before_ok and after_ok


def _split_line_into_sentences(line: str) -> list[str]:
    """Split one physical line into sentence fragments."""
    split_points: list[int] = []
    for match in re.finditer(r"[.!?]", line):
        pos = match.end()
        if match.group(0) == "." and _is_decimal_point(line, pos):
            continue
        if _is_abbreviation_boundary(line, pos):
            continue
        if _is_sentence_boundary(line, pos):
            split_points.append(pos)
    parts: list[str] = []
    start = 0
    for pos in split_points:
        parts.append(line[start:pos])
        start = pos
    parts.append(line[start:])
    return [p.strip() for p in parts if p.strip()]


def sentences(text: str) -> list[tuple[int, str]]:
    """Split ``text`` into ``(1-indexed line number, sentence text)`` pairs.

    A newline always ends a sentence (this codebase's AC/requirement lines
    are one-sentence-per-line by convention); within a line, ``.``/``!``/``?``
    end a sentence only when followed by whitespace-then-uppercase/digit (or
    end of line) AND the preceding token is not a known abbreviation.
    """
    return [
        (line_no, sentence)
        for line_no, line in enumerate(text.splitlines(), start=1)
        for sentence in _split_line_into_sentences(line)
    ]


def _piece_offsets(sentence: str) -> list[tuple[int, int]]:
    """Offsets of the sentence-internal-punctuation-delimited pieces."""
    pieces: list[tuple[int, int]] = []
    start = 0
    for match in _PUNCT_SPLIT_RE.finditer(sentence):
        pieces.append((start, match.start()))
        start = match.end()
    pieces.append((start, len(sentence)))
    return pieces


def _marker_boundaries(piece_text: str) -> list[int]:
    """Offsets (relative to ``piece_text``) where a clause marker starts a
    fresh sub-segment -- always includes 0.
    """
    offsets = {0}
    for m in _CONDITIONAL_RE.finditer(piece_text):
        offsets.add(m.start())
    for m in _RATIONALE_RE.finditer(piece_text):
        offsets.add(m.start())
    return sorted(offsets)


def _classify(piece_text: str) -> SegmentKind:
    """Classify a sub-segment by what its first token matches."""
    if _CONDITIONAL_RE.match(piece_text):
        return "conditional"
    if _RATIONALE_RE.match(piece_text):
        return "rationale"
    return "main"


def segment(sentence: str) -> list[Segment]:
    """Split ``sentence`` into clause :class:`Segment`\\ s.

    Sentence-internal punctuation (``,``, ``;``, em-dash) delimits the first
    cut; within each resulting piece, any occurrence of a
    :data:`~.lexicons.CONDITIONAL_MARKERS`/:data:`~.lexicons.RATIONALE_MARKERS`
    entry that is not already at the piece's start begins a further
    sub-segment running to the next boundary. Each final segment is
    classified by whether it begins with a conditional marker, a rationale
    marker, or neither (``main``). Empty (whitespace-only) segments are
    dropped; offsets are trimmed to the stripped text they describe.
    """
    results: list[Segment] = []
    for piece_start, piece_end in _piece_offsets(sentence):
        piece_text = sentence[piece_start:piece_end]
        boundaries = _marker_boundaries(piece_text)
        for i, sub_start in enumerate(boundaries):
            sub_end = boundaries[i + 1] if i + 1 < len(boundaries) else len(piece_text)
            raw = piece_text[sub_start:sub_end]
            stripped = raw.strip()
            if not stripped:
                continue
            lead_ws = len(raw) - len(raw.lstrip())
            trail_ws = len(raw) - len(raw.rstrip())
            abs_start = piece_start + sub_start + lead_ws
            abs_end = piece_start + sub_end - trail_ws
            results.append(
                Segment(
                    text=stripped,
                    start=abs_start,
                    end=abs_end,
                    kind=_classify(stripped),
                )
            )
    return results