ishaq101's picture sofhiaazzhr's picture
/feat knowledge management (#20)
b68816f
Raw History Blame Contribute Delete
4.95 kB
"""The declared half of a domain — the TBox (V4-9, V4-10).
`measures`, `policies` and `units` are DERIVED from entries. What a domain *is*,
and what it deliberately is *not*, cannot be derived from a term list — that is
the ontology literature's expert-authored TBox, and it is what the review loop
exists for. Decided 2026-09-08: **the model proposes, the expert approves.** A
draft to correct beats a blank form.
**`boundary` is derived, not written.** v3's real lesson was not the rename, it
was *changing the question so the answer is checkable*: `purpose` (composed,
unverifiable) became `purpose_verbatim` (located, span-checked). A boundary
sentence is inherently unquotable — it is a statement about absence — so asking
a model to write one produces exactly the unfalsifiable claim v3 removed.
Instead the boundary is the **complement of the covered subdomains** over a
closed enum: 12 members, this corpus covers 6, so the domain demonstrably has no
approved knowledge about the other 6. That is deterministic, checkable, and it
is what makes an honest refusal possible — "this domain governs equipment
availability, not cost" becomes a fact about the corpus rather than an opinion.
What still needs a human (or a model draft an expert corrects) is the free-text
elaboration, the entities and the dimensions. Those are left EMPTY rather than
guessed: an empty field an expert fills is better than a plausible one nobody
checks.
"""
from __future__ import annotations
import hashlib
from typing import Any
from src.knowledge_extraction.models import SubdomainEnum
from src.middlewares.logging import get_logger
logger = get_logger("knowledge_declare")
# `other` is a catch-all. It is evidence of nothing, so it counts neither as
# coverage nor as a gap - including it would make every corpus look broader than
# it is and every boundary narrower.
_UNINFORMATIVE = {"other"}
ALL_SUBDOMAINS = [s.value for s in SubdomainEnum if s.value not in _UNINFORMATIVE]
def declaration_id(scope_id: str) -> str:
"""Identity of one company's domain declaration.
Scope-derived, not document-derived, which is the whole difference between
this and a `DocumentBrief`. Deterministic for the same reason every other id
is: an expert's approval must survive a re-proposal.
"""
digest = hashlib.sha256(scope_id.encode("utf-8")).hexdigest()[:10]
return f"d_{digest}"
def propose_declaration(
scope_id: str,
*,
covered_subdomains: list[str],
document_titles: list[str],
units: list[str] | None = None,
) -> dict[str, Any]:
"""Draft a domain declaration from what the corpus demonstrably contains.
Returns the payload for a `kind='domain'` entry. **No model is called.**
Every field here is derived or left empty; nothing is invented, so nothing
in the draft can be wrong in a way an expert would have to detect rather
than simply complete.
"""
covered = [s for s in covered_subdomains if s not in _UNINFORMATIVE]
uncovered = [s for s in ALL_SUBDOMAINS if s not in covered]
payload: dict[str, Any] = {
"domain_id": declaration_id(scope_id),
"scope_id": scope_id,
# A name a reader recognises, taken from the documents themselves rather
# than composed. One document means its own title; several means the
# corpus needs a name only an expert can give it.
"domain_name": document_titles[0] if len(document_titles) == 1 else None,
"subdomains": covered,
# THE boundary, derived: what this corpus demonstrably says nothing
# about. A planner meeting a question in one of these has a DOMAIN gap,
# which is a different and more honest answer than a data gap.
"uncovered_subdomains": uncovered,
# The FACT, not a sentence: which subdomains this corpus has no approved
# knowledge about. The renderer owns the wording, so the two cannot
# disagree and a consumer can read the list rather than parse prose.
"boundary": ", ".join(uncovered) if uncovered else None,
# Left for the expert. Empty beats plausible-but-unchecked.
"boundary_note": None,
"conventions": {
"time_grain": None,
"calendar": None,
"units": [{"symbol": u} for u in (units or [])],
},
"entities": [],
"dimensions": [],
# Says how this draft was made, so a reviewer knows what they are
# correcting and a later reader knows no model touched it.
"proposed_by": "derived",
"derived_from": {
"n_documents": len(document_titles),
"covered": covered,
"enum_size": len(ALL_SUBDOMAINS),
},
}
logger.info(
"domain_declaration_proposed",
extra={
"scope_id": scope_id,
"covered": len(covered),
"uncovered": len(uncovered),
},
)
return payload