520 lines
19 KiB
Python
520 lines
19 KiB
Python
"""Read-only CORE-Logos Workbench projections.
|
|
|
|
The engine owns pack truth. This module projects committed language-pack
|
|
artifacts into stable Workbench read models; it never mutates packs, compiles
|
|
new state, or repairs broken links.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
import re
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from language_packs.schema import AlignmentEdge, LexicalEntry, MorphologyEntry
|
|
from workbench.schemas import (
|
|
LogosAlignmentRow,
|
|
LogosAlignmentTargetIssue,
|
|
LogosGlossRow,
|
|
LogosLexiconRow,
|
|
LogosMorphologyLinkIssue,
|
|
LogosMorphologyRow,
|
|
LogosPackContents,
|
|
LogosPackOverview,
|
|
LogosPackSummary,
|
|
LogosSafetyReport,
|
|
SafetyVerdict,
|
|
)
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
LANGUAGE_PACK_ROOT = REPO_ROOT / "language_packs" / "data"
|
|
READ_CHUNK_BYTES = 64 * 1024
|
|
SAFE_LOGOS_PACK_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$")
|
|
|
|
_LOGOS_DEPTH_ROLES = frozenset({"depth_root", "depth_relation"})
|
|
_LOGOS_DOMAIN_ID = "hebrew_greek_textual_reasoning"
|
|
_OOV_POLICIES = frozenset(
|
|
{"fail_closed", "tagged_fallback", "propose_vocab_expansion"}
|
|
)
|
|
|
|
|
|
def list_logos_packs(
|
|
*, data_root: Path | None = None
|
|
) -> list[LogosPackSummary]:
|
|
"""Return the deterministic CORE-Logos pack universe."""
|
|
|
|
root = _data_root(data_root)
|
|
summaries: list[LogosPackSummary] = []
|
|
for manifest_path in sorted(root.glob("*/manifest.json")):
|
|
manifest = _read_json_object(manifest_path)
|
|
pack_dir = manifest_path.parent
|
|
if not _is_logos_pack(manifest, pack_dir):
|
|
continue
|
|
overview = logos_pack_overview(pack_dir.name, data_root=root)
|
|
summaries.append(
|
|
LogosPackSummary(
|
|
pack_id=overview.pack_id,
|
|
language=overview.language,
|
|
role=overview.role,
|
|
script=overview.script,
|
|
version=overview.version,
|
|
determinism_class=overview.determinism_class,
|
|
gate_engaged=overview.gate_engaged,
|
|
oov_policy=overview.oov_policy,
|
|
lexicon_count=overview.lexicon_count,
|
|
gloss_count=overview.gloss_count,
|
|
morphology_count=overview.morphology_count,
|
|
frame_count=overview.frame_count,
|
|
composition_count=overview.composition_count,
|
|
alignment_edge_count=overview.alignment_edge_count,
|
|
holonomy_case_count=overview.holonomy_case_count,
|
|
safety_status=overview.safety_status,
|
|
manifest_digest=overview.manifest_digest,
|
|
manifest_path=overview.manifest_path,
|
|
)
|
|
)
|
|
return sorted(summaries, key=lambda item: item.pack_id)
|
|
|
|
|
|
def logos_pack_overview(
|
|
pack_id: str, *, data_root: Path | None = None
|
|
) -> LogosPackOverview:
|
|
root = _data_root(data_root)
|
|
pack_dir = _require_logos_pack_dir(pack_id, root)
|
|
manifest_path = pack_dir / "manifest.json"
|
|
manifest = _read_json_object(manifest_path)
|
|
lexicon = _load_lexicon(pack_dir)
|
|
morphology = _load_morphology(pack_id, root)
|
|
alignment = _load_alignment(pack_id, root)
|
|
safety = logos_pack_safety(pack_id, data_root=root)
|
|
return LogosPackOverview(
|
|
pack_id=str(manifest.get("pack_id") or pack_id),
|
|
language=_optional_str(manifest.get("language")),
|
|
role=_optional_str(manifest.get("role")),
|
|
script=_optional_str(manifest.get("script")),
|
|
version=_optional_str(manifest.get("version")),
|
|
determinism_class=_optional_str(manifest.get("determinism_class")),
|
|
gate_engaged=bool(manifest.get("gate_engaged", False)),
|
|
oov_policy=_optional_str(manifest.get("oov_policy")),
|
|
lexicon_count=len(lexicon),
|
|
gloss_count=len(_read_jsonl_objects(pack_dir / "glosses.jsonl")),
|
|
morphology_count=len(morphology),
|
|
frame_count=len(_read_jsonl_objects(pack_dir / "frames.jsonl")),
|
|
composition_count=len(_read_jsonl_objects(pack_dir / "compositions.jsonl")),
|
|
alignment_edge_count=len(alignment),
|
|
holonomy_case_count=0,
|
|
safety_status=safety.verdict,
|
|
manifest_digest=_sha256_file(manifest_path),
|
|
manifest_path=_display_path(manifest_path),
|
|
normalization_policy=_optional_str(manifest.get("normalization_policy")),
|
|
source_manifest=_optional_str(manifest.get("source_manifest")),
|
|
known_gaps=_string_list(manifest.get("known_gaps")),
|
|
)
|
|
|
|
|
|
def logos_pack_contents(
|
|
pack_id: str, *, data_root: Path | None = None
|
|
) -> LogosPackContents:
|
|
root = _data_root(data_root)
|
|
pack_dir = _require_logos_pack_dir(pack_id, root)
|
|
manifest = _read_json_object(pack_dir / "manifest.json")
|
|
lexicon = _load_lexicon(pack_dir)
|
|
lexicon_rows = [_lexicon_row(entry) for entry in lexicon]
|
|
entry_ids_by_lemma = _entry_ids_by_lemma(lexicon_rows)
|
|
return LogosPackContents(
|
|
schema_version="logos_pack_contents_v1",
|
|
pack_id=str(manifest.get("pack_id") or pack_id),
|
|
manifest=manifest,
|
|
lexicon=lexicon_rows,
|
|
glosses=_gloss_rows(pack_dir, entry_ids_by_lemma),
|
|
morphology=[
|
|
_morphology_row(entry)
|
|
for entry in _load_morphology(pack_id, root)
|
|
],
|
|
frames=_read_jsonl_objects(pack_dir / "frames.jsonl"),
|
|
compositions=_read_jsonl_objects(pack_dir / "compositions.jsonl"),
|
|
alignment_edges=logos_pack_alignment(pack_id, data_root=root),
|
|
holonomy_cases=[],
|
|
)
|
|
|
|
|
|
def logos_pack_alignment(
|
|
pack_id: str, *, data_root: Path | None = None
|
|
) -> list[LogosAlignmentRow]:
|
|
root = _data_root(data_root)
|
|
_require_logos_pack_dir(pack_id, root) # validate id / logos membership
|
|
target_index = _entry_id_index(root)
|
|
return [
|
|
_alignment_row(edge, target_index)
|
|
for edge in _load_alignment(pack_id, root)
|
|
]
|
|
|
|
|
|
def logos_pack_safety(
|
|
pack_id: str, *, data_root: Path | None = None
|
|
) -> LogosSafetyReport:
|
|
root = _data_root(data_root)
|
|
pack_dir = _require_logos_pack_dir(pack_id, root)
|
|
manifest = _read_json_object(pack_dir / "manifest.json")
|
|
lexicon = _load_lexicon(pack_dir)
|
|
morphology = _load_morphology(pack_id, root)
|
|
alignment_rows = logos_pack_alignment(pack_id, data_root=root)
|
|
|
|
checksum_status, checksum_errors = _checksum_status(pack_id, pack_dir, root)
|
|
domain_contract = _domain_contract_status(pack_id, root)
|
|
domain_status = (
|
|
SafetyVerdict.CLEAR
|
|
if bool(domain_contract.get("valid", False))
|
|
else SafetyVerdict.FAILED
|
|
)
|
|
morphology_ids = {entry.morphology_id for entry in morphology}
|
|
dangling = [
|
|
LogosMorphologyLinkIssue(
|
|
entry_id=entry.entry_id,
|
|
morphology_id=entry.morphology_id or "",
|
|
)
|
|
for entry in lexicon
|
|
if entry.morphology_id and entry.morphology_id not in morphology_ids
|
|
]
|
|
invalid_targets = [
|
|
LogosAlignmentTargetIssue(
|
|
edge_id=row.edge_id,
|
|
source_id=row.source_id,
|
|
target_id=row.target_id,
|
|
relation=row.relation,
|
|
target_pack_id=row.target_pack_id,
|
|
)
|
|
for row in alignment_rows
|
|
if row.invalid_target
|
|
]
|
|
epistemic_counts: dict[str, int] = {}
|
|
speculative: list[str] = []
|
|
contested: list[str] = []
|
|
falsified: list[str] = []
|
|
for entry in lexicon:
|
|
status = entry.epistemic_status or "speculative"
|
|
epistemic_counts[status] = epistemic_counts.get(status, 0) + 1
|
|
if status == "speculative":
|
|
speculative.append(entry.entry_id)
|
|
elif status == "contested":
|
|
contested.append(entry.entry_id)
|
|
elif status == "falsified":
|
|
falsified.append(entry.entry_id)
|
|
|
|
oov_policy = _optional_str(manifest.get("oov_policy"))
|
|
role = _optional_str(manifest.get("role"))
|
|
oov_policy_ok = oov_policy in _OOV_POLICIES
|
|
gate_policy_ok = not (
|
|
role in _LOGOS_DEPTH_ROLES
|
|
and bool(manifest.get("gate_engaged", False))
|
|
and oov_policy != "fail_closed"
|
|
)
|
|
warning_present = bool(dangling or invalid_targets or contested or falsified)
|
|
known_gaps = _string_list(manifest.get("known_gaps"))
|
|
if checksum_status is SafetyVerdict.FAILED or domain_status is SafetyVerdict.FAILED:
|
|
verdict = SafetyVerdict.FAILED
|
|
elif not (oov_policy_ok and gate_policy_ok):
|
|
verdict = SafetyVerdict.FAILED
|
|
elif warning_present or known_gaps:
|
|
verdict = SafetyVerdict.WARNING
|
|
else:
|
|
verdict = SafetyVerdict.UNKNOWN
|
|
|
|
return LogosSafetyReport(
|
|
schema_version="logos_safety_report_v1",
|
|
pack_id=str(manifest.get("pack_id") or pack_id),
|
|
checksum_status=checksum_status,
|
|
checksum_errors=checksum_errors,
|
|
domain_contract=domain_contract,
|
|
domain_contract_status=domain_status,
|
|
oov_policy_ok=oov_policy_ok,
|
|
gate_policy_ok=gate_policy_ok,
|
|
path_safety_ok=True,
|
|
dangling_morphology_links=dangling,
|
|
invalid_alignment_targets=invalid_targets,
|
|
missing_holonomy_refs=SafetyVerdict.UNKNOWN,
|
|
epistemic_status_counts=dict(sorted(epistemic_counts.items())),
|
|
speculative_entries=sorted(speculative),
|
|
contested_entries=sorted(contested),
|
|
falsified_entries=sorted(falsified),
|
|
known_gaps=known_gaps,
|
|
verdict=verdict,
|
|
)
|
|
|
|
|
|
def _data_root(data_root: Path | None) -> Path:
|
|
return (data_root or LANGUAGE_PACK_ROOT).resolve()
|
|
|
|
|
|
def _validate_pack_id(pack_id: str) -> str:
|
|
if not SAFE_LOGOS_PACK_ID_RE.fullmatch(pack_id) or ".." in pack_id:
|
|
raise ValueError("pack id contains unsafe characters")
|
|
return pack_id
|
|
|
|
|
|
def _require_logos_pack_dir(pack_id: str, root: Path) -> Path:
|
|
safe_id = _validate_pack_id(pack_id)
|
|
candidate = (root / safe_id).resolve()
|
|
if candidate != root and root not in candidate.parents:
|
|
raise ValueError("pack id resolves outside language pack root")
|
|
manifest_path = candidate / "manifest.json"
|
|
if not manifest_path.is_file():
|
|
raise FileNotFoundError(pack_id)
|
|
manifest = _read_json_object(manifest_path)
|
|
if not _is_logos_pack(manifest, candidate):
|
|
raise FileNotFoundError(pack_id)
|
|
return candidate
|
|
|
|
|
|
def _is_logos_pack(manifest: dict[str, Any], pack_dir: Path) -> bool:
|
|
if (pack_dir / "alignment.jsonl").is_file():
|
|
return True
|
|
role = _optional_str(manifest.get("role"))
|
|
if role not in _LOGOS_DEPTH_ROLES:
|
|
return False
|
|
pack_id = str(manifest.get("pack_id") or pack_dir.name)
|
|
# The committed tree also contains a generic English relation seed with
|
|
# role=depth_relation. Role-only admission is constrained to the Logos
|
|
# substrate so the Studio universe stays the four-pack wave contract.
|
|
return (
|
|
manifest.get("domain_id") == _LOGOS_DOMAIN_ID
|
|
or "logos" in pack_id
|
|
or pack_id.startswith(("he_", "grc_"))
|
|
)
|
|
|
|
|
|
def _optional_str(value: Any) -> str | None:
|
|
return None if value is None else str(value)
|
|
|
|
|
|
def _string_list(value: Any) -> list[str]:
|
|
if not isinstance(value, list):
|
|
return []
|
|
return [item for item in (str(v) for v in value) if item]
|
|
|
|
|
|
def _sha256_file(path: Path) -> str:
|
|
hasher = hashlib.sha256()
|
|
with path.open("rb") as fh:
|
|
for chunk in iter(lambda: fh.read(READ_CHUNK_BYTES), b""):
|
|
hasher.update(chunk)
|
|
return "sha256:" + hasher.hexdigest()
|
|
|
|
|
|
def _sha256_hex(path: Path) -> str:
|
|
hasher = hashlib.sha256()
|
|
with path.open("rb") as fh:
|
|
for chunk in iter(lambda: fh.read(READ_CHUNK_BYTES), b""):
|
|
hasher.update(chunk)
|
|
return hasher.hexdigest()
|
|
|
|
|
|
def _display_path(path: Path) -> str:
|
|
try:
|
|
return path.resolve().relative_to(REPO_ROOT.resolve()).as_posix()
|
|
except ValueError:
|
|
return path.resolve().as_posix()
|
|
|
|
|
|
def _read_json_object(path: Path) -> dict[str, Any]:
|
|
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
if not isinstance(payload, dict):
|
|
raise ValueError(f"expected JSON object in {_display_path(path)}")
|
|
return payload
|
|
|
|
|
|
def _read_jsonl_records(path: Path) -> list[tuple[int, dict[str, Any]]]:
|
|
if not path.exists():
|
|
return []
|
|
records: list[tuple[int, dict[str, Any]]] = []
|
|
for line_no, raw_line in enumerate(
|
|
path.read_text(encoding="utf-8").splitlines(), start=1
|
|
):
|
|
line = raw_line.strip()
|
|
if not line:
|
|
continue
|
|
payload = json.loads(line)
|
|
if not isinstance(payload, dict):
|
|
raise ValueError(f"expected JSON object at {_display_path(path)}:{line_no}")
|
|
records.append((line_no, payload))
|
|
return records
|
|
|
|
|
|
def _read_jsonl_objects(path: Path) -> list[dict[str, Any]]:
|
|
return [payload for _, payload in _read_jsonl_records(path)]
|
|
|
|
|
|
def _load_lexicon(pack_dir: Path) -> list[LexicalEntry]:
|
|
from language_packs.compiler import _parse_entry
|
|
|
|
return [
|
|
_parse_entry(payload)
|
|
for _, payload in _read_jsonl_records(pack_dir / "lexicon.jsonl")
|
|
]
|
|
|
|
|
|
def _load_morphology(pack_id: str, root: Path) -> tuple[MorphologyEntry, ...]:
|
|
from morphology.registry import load_morphology
|
|
|
|
return tuple(load_morphology(pack_id, data_root=root).entries)
|
|
|
|
|
|
def _load_alignment(pack_id: str, root: Path) -> tuple[AlignmentEdge, ...]:
|
|
from alignment.graph import load_alignment
|
|
|
|
return tuple(load_alignment(pack_id, data_root=root).edges)
|
|
|
|
|
|
def _lexicon_row(entry: LexicalEntry) -> LogosLexiconRow:
|
|
return LogosLexiconRow(
|
|
entry_id=entry.entry_id,
|
|
surface=entry.surface,
|
|
lemma=entry.lemma,
|
|
language=entry.language,
|
|
part_of_speech=entry.part_of_speech,
|
|
pos=entry.pos,
|
|
morphology_id=entry.morphology_id,
|
|
morphology_tags=list(entry.morphology_tags),
|
|
semantic_domains=list(entry.semantic_domains),
|
|
provenance_ids=list(entry.provenance_ids),
|
|
epistemic_status=entry.epistemic_status,
|
|
)
|
|
|
|
|
|
def _entry_ids_by_lemma(rows: list[LogosLexiconRow]) -> dict[str, list[str]]:
|
|
out: dict[str, list[str]] = {}
|
|
for row in rows:
|
|
out.setdefault(row.lemma, []).append(row.entry_id)
|
|
return {lemma: sorted(ids) for lemma, ids in sorted(out.items())}
|
|
|
|
|
|
def _gloss_rows(
|
|
pack_dir: Path, entry_ids_by_lemma: dict[str, list[str]]
|
|
) -> list[LogosGlossRow]:
|
|
rows: list[LogosGlossRow] = []
|
|
for line_no, payload in _read_jsonl_records(pack_dir / "glosses.jsonl"):
|
|
lemma = str(payload.get("lemma") or "")
|
|
gloss = str(payload.get("gloss") or "")
|
|
pos = _optional_str(payload.get("pos"))
|
|
gloss_id = hashlib.sha256(
|
|
f"{pack_dir.name}|{line_no}|{lemma}|{pos or ''}|{gloss}".encode("utf-8")
|
|
).hexdigest()[:16]
|
|
rows.append(
|
|
LogosGlossRow(
|
|
gloss_id=gloss_id,
|
|
lemma=lemma,
|
|
gloss=gloss,
|
|
pos=pos,
|
|
entry_ids=entry_ids_by_lemma.get(lemma, []),
|
|
provenance_ids=_string_list(payload.get("provenance_ids")),
|
|
epistemic_status=_optional_str(
|
|
payload.get("epistemic_status", payload.get("status"))
|
|
),
|
|
raw=dict(payload),
|
|
)
|
|
)
|
|
return rows
|
|
|
|
|
|
def _morphology_row(entry: MorphologyEntry) -> LogosMorphologyRow:
|
|
return LogosMorphologyRow(
|
|
morphology_id=entry.morphology_id,
|
|
surface=entry.surface,
|
|
lemma=entry.lemma,
|
|
language=entry.language,
|
|
root=entry.root,
|
|
prefix_chain=list(entry.prefix_chain),
|
|
stem=entry.stem,
|
|
inflection={str(k): str(v) for k, v in entry.inflection.items()},
|
|
suffix_chain=list(entry.suffix_chain),
|
|
)
|
|
|
|
|
|
def _edge_id(edge: AlignmentEdge) -> str:
|
|
return hashlib.sha256(
|
|
f"{edge.source_id}|{edge.target_id}|{edge.relation}".encode("utf-8")
|
|
).hexdigest()[:16]
|
|
|
|
|
|
def _entry_id_index(root: Path) -> dict[str, str]:
|
|
index: dict[str, str] = {}
|
|
for lexicon_path in sorted(root.glob("*/lexicon.jsonl")):
|
|
for _, payload in _read_jsonl_records(lexicon_path):
|
|
entry_id = payload.get("entry_id")
|
|
if isinstance(entry_id, str) and entry_id:
|
|
index[entry_id] = lexicon_path.parent.name
|
|
return index
|
|
|
|
|
|
def _alignment_row(
|
|
edge: AlignmentEdge, target_index: dict[str, str]
|
|
) -> LogosAlignmentRow:
|
|
target_pack_id = target_index.get(edge.target_id)
|
|
target_resolved = target_pack_id is not None
|
|
# A target is valid iff it resolves to a real declared lexicon entry in some
|
|
# pack (collapse anchors like ``en-collapse-love`` are declared entries in
|
|
# ``en_collapse_anchors_v1``). An ``en-collapse-*`` target that is declared
|
|
# nowhere is a genuine dangling reference and is reported as such — no
|
|
# relation/prefix carve-out, so the safety reader cannot silently pass an
|
|
# undeclared anchor.
|
|
return LogosAlignmentRow(
|
|
edge_id=_edge_id(edge),
|
|
source_id=edge.source_id,
|
|
target_id=edge.target_id,
|
|
relation=edge.relation,
|
|
weight=edge.weight,
|
|
evidence_ids=list(edge.evidence_ids),
|
|
target_pack_id=target_pack_id,
|
|
target_resolved=target_resolved,
|
|
invalid_target=not target_resolved,
|
|
)
|
|
|
|
|
|
def _checksum_status(
|
|
pack_id: str, pack_dir: Path, root: Path
|
|
) -> tuple[SafetyVerdict, list[str]]:
|
|
manifest = _read_json_object(pack_dir / "manifest.json")
|
|
errors: list[str] = []
|
|
lexicon_path = pack_dir / "lexicon.jsonl"
|
|
declared = manifest.get("checksum")
|
|
if not isinstance(declared, str) or not declared:
|
|
errors.append("manifest.checksum:missing")
|
|
elif not lexicon_path.is_file():
|
|
errors.append("lexicon.jsonl:missing")
|
|
else:
|
|
actual = _sha256_hex(lexicon_path)
|
|
if actual != declared:
|
|
errors.append(f"lexicon_checksum:mismatch:{actual}!={declared}")
|
|
|
|
for filename, key in (
|
|
("glosses.jsonl", "glosses_checksum"),
|
|
("frames.jsonl", "frame_checksum"),
|
|
("compositions.jsonl", "composition_checksum"),
|
|
):
|
|
expected = manifest.get(key)
|
|
if expected is None:
|
|
continue
|
|
path = pack_dir / filename
|
|
if not path.is_file():
|
|
errors.append(f"{filename}:missing_for:{key}")
|
|
continue
|
|
actual = _sha256_hex(path)
|
|
if actual != expected:
|
|
errors.append(f"{key}:mismatch:{actual}!={expected}")
|
|
|
|
if not errors and root == LANGUAGE_PACK_ROOT.resolve():
|
|
try:
|
|
from language_packs.compiler import load_pack
|
|
|
|
load_pack(pack_id)
|
|
except ValueError as exc:
|
|
errors.append(f"compiler:{exc}")
|
|
return (SafetyVerdict.FAILED, errors) if errors else (SafetyVerdict.CLEAR, [])
|
|
|
|
|
|
def _domain_contract_status(pack_id: str, root: Path) -> dict[str, Any]:
|
|
from language_packs.domain_contract import validate_domain_contract_pack
|
|
|
|
return validate_domain_contract_pack(pack_id, data_root=root).as_dict()
|