Skip to content

ontocast.tool.ontology_validation.unit_findings

Deterministic per-unit findings on a unit's ontology delta.

Everything here runs against the unit's net insert/delete delta (:meth:~ontocast.onto.unit_states.UnitOntologyState.build_delta), never the whole working graph: the working graph is snapshot + delta, so validating it would test the shared catalog context against itself and attribute every pre-existing third-party defect to this unit. Two facts-side rules are deliberately absent for the same reason:

  • UNKNOWN_TERM is semantically inverted here — minting new terms in a writable namespace is the ontology renderer's entire job;
  • connectivity is not checked — a per-unit delta is by construction a few terms connecting to the snapshot rather than to each other, and the document-level STRUCTURAL_CHECK node already owns that concern where the context exists.

collect_ontology_unit_findings(*, inserts, deletes, snapshot_graph, merged_graph=None, fact_namespaces=(), policy=None)

Assemble all deterministic findings for one unit's ontology delta.

Parameters:

Name Type Description Default
inserts RDFGraph

Net new triples this unit adds (build_delta().inserts).

required
deletes RDFGraph

Snapshot triples this unit removes.

required
snapshot_graph RDFGraph | None

The prompt ontology context; None/empty means the fresh-create path, where every catalog-relative check is skipped.

required
merged_graph RDFGraph | None

snapshot + delta when the caller already has it (working_graph after updates apply) — passing it avoids a snapshot copy. Built here when omitted.

None
fact_namespaces Sequence[str]

Facts/document namespaces; ontology terms minted there are mandatory findings.

()
policy ValidationPolicy | None

Deployment namespace exemptions; None (tests) uses the built-in standard namespaces only.

None

Returns:

Type Description
list[OntologyUnitFinding]

Findings, mandatory first is not guaranteed — order follows the

list[OntologyUnitFinding]

check sequence; callers filter on mandatory.

Source code in ontocast/tool/ontology_validation/unit_findings.py
def collect_ontology_unit_findings(
    *,
    inserts: RDFGraph,
    deletes: RDFGraph,
    snapshot_graph: RDFGraph | None,
    merged_graph: RDFGraph | None = None,
    fact_namespaces: Sequence[str] = (),
    policy: ValidationPolicy | None = None,
) -> list[OntologyUnitFinding]:
    """Assemble all deterministic findings for one unit's ontology delta.

    Args:
        inserts: Net new triples this unit adds (``build_delta().inserts``).
        deletes: Snapshot triples this unit removes.
        snapshot_graph: The prompt ontology context; ``None``/empty means the
            fresh-create path, where every catalog-relative check is skipped.
        merged_graph: ``snapshot + delta`` when the caller already has it
            (``working_graph`` after updates apply) — passing it avoids a
            snapshot copy. Built here when omitted.
        fact_namespaces: Facts/document namespaces; ontology terms minted
            there are mandatory findings.
        policy: Deployment namespace exemptions; ``None`` (tests) uses the
            built-in standard namespaces only.

    Returns:
        Findings, mandatory first is *not* guaranteed — order follows the
        check sequence; callers filter on ``mandatory``.
    """
    active_policy = policy or ValidationPolicy()
    if merged_graph is None:
        merged_graph = RDFGraph()
        if snapshot_graph is not None:
            for triple in snapshot_graph:
                merged_graph.add(triple)
        for triple in inserts:
            merged_graph.add(triple)

    declared = collect_declared_namespaces(snapshot_graph)
    findings: list[OntologyUnitFinding] = [
        *_namespace_findings(
            inserts, declared, fact_namespaces, active_policy.standard_namespaces()
        ),
        *_degenerate_restriction_findings(inserts),
        *_missing_label_findings(inserts, snapshot_graph),
        *_subclass_cycle_findings(inserts, merged_graph),
        *_role_confusion_findings(inserts, snapshot_graph),
        *_cardinality_contradiction_findings(inserts, merged_graph),
        *_foreign_delete_findings(deletes, snapshot_graph, inserts),
        *_label_collision_findings(inserts, snapshot_graph),
    ]
    return findings

count_fixes_targeting_snapshot(fixes, snapshot_graph, insert_subjects)

Critic fixes aimed at catalog content this unit's delta never touched.

The ontology critic is shown snapshot + delta and can reject a unit for pre-existing catalog defects the renderer cannot own. This counts the proposed fixes whose incorrect_value names a snapshot-declared subject absent from the unit's inserts — by full-IRI or prefixed-name substring match, so it is a lower bound, recorded as telemetry rather than used for control flow.

Source code in ontocast/tool/ontology_validation/unit_findings.py
def count_fixes_targeting_snapshot(
    fixes: Sequence[TripleFix],
    snapshot_graph: RDFGraph | None,
    insert_subjects: set[str],
) -> int:
    """Critic fixes aimed at catalog content this unit's delta never touched.

    The ontology critic is shown ``snapshot + delta`` and can reject a unit
    for pre-existing catalog defects the renderer cannot own. This counts the
    proposed fixes whose ``incorrect_value`` names a snapshot-declared subject
    absent from the unit's inserts — by full-IRI or prefixed-name substring
    match, so it is a lower bound, recorded as telemetry rather than used for
    control flow.
    """
    if snapshot_graph is None or not fixes:
        return 0
    prefix_map = [
        (prefix, str(namespace))
        for prefix, namespace in snapshot_graph.namespaces()
        if prefix
    ]
    snapshot_only: set[str] = set()
    qnames: set[str] = set()
    for subject in snapshot_graph.subjects():
        if not isinstance(subject, URIRef):
            continue
        text = str(subject)
        if text in insert_subjects or text in snapshot_only:
            continue
        snapshot_only.add(text)
        for prefix, namespace in prefix_map:
            if text.startswith(namespace) and len(text) > len(namespace):
                qnames.add(f"{prefix}:{text[len(namespace) :]}")
    count = 0
    for fix in fixes:
        haystack = fix.incorrect_value or ""
        if not haystack:
            continue
        if any(iri in haystack for iri in snapshot_only) or any(
            qname in haystack for qname in qnames
        ):
            count += 1
    return count