Skip to content

ontocast.config.section_labels

Load versioned section-label schemas from YAML in this package.

SectionLabelDef

Bases: BaseModel

One canonical section label, its heading patterns and recall keywords.

heading_patterns are high-precision anchored regexes. keywords are the recall tier: whole-word phrases that identify the label inside a compound or decorated heading ("Results and Discussion", "Experimental Section") which the anchored patterns cannot match.

Source code in ontocast/config/section_labels/__init__.py
class SectionLabelDef(BaseModel):
    """One canonical section label, its heading patterns and recall keywords.

    ``heading_patterns`` are high-precision anchored regexes. ``keywords`` are
    the recall tier: whole-word phrases that identify the label inside a
    compound or decorated heading ("Results and Discussion", "Experimental
    Section") which the anchored patterns cannot match.
    """

    id: str
    heading_patterns: list[str] = Field(default_factory=list)
    keywords: list[str] = Field(default_factory=list)
    order: int | None = Field(
        default=None,
        description=(
            "Canonical position of this section in a well-formed document of "
            "this type. Used only to refuse label fills that would run "
            "backwards; absent means the label is not order-constrained."
        ),
    )

    @property
    def compiled_keywords(self) -> tuple[re.Pattern[str], ...]:
        return tuple(
            re.compile(rf"(?<!\w){re.escape(keyword)}(?!\w)", re.I)
            for keyword in self.keywords
        )

SectionLabelSchema

Bases: BaseModel

Domain-specific section label vocabulary for one document type.

The catalog of schemas is a partition: every document belongs to exactly one cell, with general as the residual. document_profile is what makes that partition checkable -- if two profiles could describe the same document, the cells overlap and one of them is wrong. It is also the text the content-based detector embeds, so it describes the document type, unlike description, which describes this schema's headings.

Source code in ontocast/config/section_labels/__init__.py
class SectionLabelSchema(BaseModel):
    """Domain-specific section label vocabulary for one document type.

    The catalog of schemas is a **partition**: every document belongs to exactly
    one cell, with ``general`` as the residual. ``document_profile`` is what
    makes that partition checkable -- if two profiles could describe the same
    document, the cells overlap and one of them is wrong. It is also the text
    the content-based detector embeds, so it describes the *document type*,
    unlike ``description``, which describes this schema's headings.
    """

    schema_version: str
    id: str
    description: str = ""
    document_profile: str = Field(
        default="",
        description=(
            "One sentence describing the kind of document this schema covers, "
            "written to be true of no other schema in the catalog. Empty means "
            "the schema is not a detection candidate."
        ),
    )
    parent: str | None = Field(
        default=None,
        description=(
            "Reserved for a future document-type hierarchy (e.g. a thesis as a "
            "sub-type of academic). Unused today: the catalog is flat, and "
            "sub-types would blur the cells a flat detector must separate."
        ),
    )
    labels: list[SectionLabelDef]
    ordered: bool = Field(
        default=False,
        description=(
            "Whether this document type has a canonical section order, making "
            "the per-label 'order' values meaningful for fill guarding."
        ),
    )
    default_exclude: list[str] = Field(
        default_factory=list,
        description=(
            "Label ids dropped by default before extraction (boilerplate "
            "sections); overridden by an explicit exclude_sections request "
            "option ([] disables exclusion entirely)."
        ),
    )

    @property
    def compiled_patterns(self) -> tuple[tuple[str, re.Pattern[str]], ...]:
        compiled: list[tuple[str, re.Pattern[str]]] = []
        for label_def in self.labels:
            for pattern in label_def.heading_patterns:
                compiled.append((label_def.id, re.compile(pattern, re.I)))
        return tuple(compiled)

clear_section_label_caches()

Clear loader caches (for tests).

Source code in ontocast/config/section_labels/__init__.py
def clear_section_label_caches() -> None:
    """Clear loader caches (for tests)."""
    load_manifest.cache_clear()
    load_section_label_schema.cache_clear()
    all_known_label_ids.cache_clear()
    _hint_matchers.cache_clear()

label_order(label, schema)

Canonical position of a label in this schema, when order-constrained.

Source code in ontocast/config/section_labels/__init__.py
def label_order(label: str, schema: SectionLabelSchema) -> int | None:
    """Canonical position of a label in this schema, when order-constrained."""
    if not schema.ordered:
        return None
    for label_def in schema.labels:
        if label_def.id == label:
            return label_def.order
    return None

match_heading_keywords(line, schema)

Match a heading by keyword, for compound and non-canonical headings.

The winner is the label whose keyword appears earliest in the heading, so a compound heading resolves to its leading component ("Results and Discussion" is results, "Conclusions and Outlook" is conclusion). Ties on position are broken by the longer keyword, then by schema order.

Parameters:

Name Type Description Default
line str

Raw heading line.

required
schema SectionLabelSchema

Active section label schema.

required

Returns:

Type Description
tuple[str, float] | None

(label, confidence) or None when no keyword matches.

Source code in ontocast/config/section_labels/__init__.py
def match_heading_keywords(
    line: str, schema: SectionLabelSchema
) -> tuple[str, float] | None:
    """Match a heading by keyword, for compound and non-canonical headings.

    The winner is the label whose keyword appears earliest in the heading, so a
    compound heading resolves to its leading component ("Results and
    Discussion" is results, "Conclusions and Outlook" is conclusion). Ties on
    position are broken by the longer keyword, then by schema order.

    Args:
        line: Raw heading line.
        schema: Active section label schema.

    Returns:
        ``(label, confidence)`` or ``None`` when no keyword matches.
    """
    normalised = normalise_heading_line(line)
    if not normalised or len(normalised) > _MAX_HEADING_LINE_LEN:
        return None

    best: tuple[int, int, int, str] | None = None
    for index, label_def in enumerate(schema.labels):
        for pattern in label_def.compiled_keywords:
            found = pattern.search(normalised)
            if found is None:
                continue
            candidate = (
                found.start(),
                -(found.end() - found.start()),
                index,
                label_def.id,
            )
            if best is None or candidate < best:
                best = candidate
    if best is None:
        return None
    return best[3], 0.7

match_heading_line(line, schema)

Match a heading against the schema's anchored patterns (high precision).

Deliberately exact: this function also gates segment coalescing, where a fuzzy match on a body first line would join distinct sections. Recall lives in :func:match_heading_keywords.

Source code in ontocast/config/section_labels/__init__.py
def match_heading_line(line: str, schema: SectionLabelSchema) -> str | None:
    """Match a heading against the schema's anchored patterns (high precision).

    Deliberately exact: this function also gates segment coalescing, where a
    fuzzy match on a body first line would join distinct sections. Recall lives
    in :func:`match_heading_keywords`.
    """
    normalised = normalise_heading_line(line)
    if not normalised or len(normalised) > _MAX_HEADING_LINE_LEN:
        return None
    for label, pattern in schema.compiled_patterns:
        if pattern.match(normalised):
            return label
    return None

normalise_heading_line(line)

Reduce a raw heading line to its bare section name.

Strips markdown syntax, publisher decoration glyphs, extraction artefacts and section numbering, so that "## ■ REFERENCES" and "2.1 Synthesis of films" reach the matchers as "REFERENCES" and "Synthesis of films".

Source code in ontocast/config/section_labels/__init__.py
def normalise_heading_line(line: str) -> str:
    """Reduce a raw heading line to its bare section name.

    Strips markdown syntax, publisher decoration glyphs, extraction artefacts
    and section numbering, so that "## ■ REFERENCES" and "2.1 Synthesis of
    films" reach the matchers as "REFERENCES" and "Synthesis of films".
    """
    stripped = unicodedata.normalize("NFKC", line).strip()
    if stripped.startswith("#"):
        stripped = stripped.lstrip("#").strip()
    stripped = _MARKDOWN_EMPHASIS.sub("", stripped).strip()
    stripped = _LEADING_DECORATION.sub("", stripped)
    stripped = _TRAILING_DECORATION.sub("", stripped)
    stripped = _ARTEFACT_TOKEN.sub("", stripped)
    stripped = _STRUCTURAL_PREFIX.sub("", stripped).strip()
    stripped = _DIGIT_NUMBERING.sub("", stripped)
    stripped = _ALPHA_NUMBERING.sub("", stripped)
    return stripped.strip()

normalise_user_section_label(raw, *, schema_id=None)

Map user-supplied section name to a canonical label.

Source code in ontocast/config/section_labels/__init__.py
def normalise_user_section_label(
    raw: str,
    *,
    schema_id: str | None = None,
) -> str | None:
    """Map user-supplied section name to a canonical label."""
    if raw.strip() == "*":
        return "*"

    cleaned = raw.strip().lower().replace(" ", "_").replace("-", "_")
    if cleaned in all_known_label_ids():
        return cleaned

    resolved_id = resolve_section_schema_id(
        section_schema_id=schema_id,
        document_type_hint=None,
    )
    schema = load_section_label_schema(resolved_id)
    if cleaned in canonical_labels(schema):
        return cleaned

    matched = match_heading_line(raw, schema)
    if matched is not None:
        return matched

    for entry in load_manifest().schemas:
        other = load_section_label_schema(entry.id)
        matched = match_heading_line(raw, other)
        if matched is not None:
            return matched

    return None

resolve_heading_label(line, schema)

Resolve a heading to a label via patterns, then keywords.

Returns:

Type Description
tuple[str, float, str] | None

(label, confidence, source) where source is "heading_pattern"

tuple[str, float, str] | None

or "heading_keyword"; None when the heading is unrecognised.

Source code in ontocast/config/section_labels/__init__.py
def resolve_heading_label(
    line: str, schema: SectionLabelSchema
) -> tuple[str, float, str] | None:
    """Resolve a heading to a label via patterns, then keywords.

    Returns:
        ``(label, confidence, source)`` where source is ``"heading_pattern"``
        or ``"heading_keyword"``; ``None`` when the heading is unrecognised.
    """
    exact = match_heading_line(line, schema)
    if exact is not None:
        return exact, 0.95, "heading_pattern"
    keyword = match_heading_keywords(line, schema)
    if keyword is not None:
        return keyword[0], keyword[1], "heading_keyword"
    return None

resolve_section_schema_id(*, section_schema_id=None, document_type_hint=None)

Pick schema: explicit id, then hint substring match, then manifest default.

Source code in ontocast/config/section_labels/__init__.py
def resolve_section_schema_id(
    *,
    section_schema_id: str | None = None,
    document_type_hint: str | None = None,
) -> str:
    """Pick schema: explicit id, then hint substring match, then manifest default."""
    manifest = load_manifest()
    if section_schema_id and section_schema_id.strip():
        schema_id = section_schema_id.strip().lower()
        load_section_label_schema(schema_id)
        return schema_id

    from_hint = schema_id_from_hint(document_type_hint)
    if from_hint is not None:
        return from_hint

    return manifest.default_schema

schema_id_from_hint(document_type_hint)

Schema a free-text document-type hint maps to, or None if it maps to none.

Distinct from :func:resolve_section_schema_id, which cannot express "the caller told us nothing": it returns the manifest default both for an unmatched hint and for no hint at all. Automatic detection must run in exactly those cases, so it needs this finer answer.

Source code in ontocast/config/section_labels/__init__.py
def schema_id_from_hint(document_type_hint: str | None) -> str | None:
    """Schema a free-text document-type hint maps to, or ``None`` if it maps to none.

    Distinct from :func:`resolve_section_schema_id`, which cannot express "the
    caller told us nothing": it returns the manifest default both for an
    unmatched hint and for no hint at all. Automatic detection must run in
    exactly those cases, so it needs this finer answer.
    """
    if not document_type_hint or not document_type_hint.strip():
        return None
    hint_lower = document_type_hint.strip().lower()
    for pattern, schema_id in _hint_matchers():
        if pattern.search(hint_lower):
            return schema_id
    return None