From 6bad4189d2272154d2adf1143439f790cfcaeba7 Mon Sep 17 00:00:00 2001 From: Shay Date: Thu, 14 May 2026 12:35:19 -0700 Subject: [PATCH] Implement core physics and pack validation --- chat/runtime.py | 46 ++++++++++ core/cli.py | 34 +++++++- core/physics/__init__.py | 5 ++ core/physics/articulation.py | 54 +++++++++++- core/physics/attention.py | 25 +++++- core/physics/binding.py | 44 +++++++++- core/physics/digest.py | 27 +++++- core/physics/drive.py | 12 ++- core/physics/energy.py | 118 +++++++++++++++++++++++++ core/physics/identity.py | 23 ++++- core/physics/inhibition.py | 25 +++++- core/physics/learning.py | 32 +++++++ core/physics/reasoning.py | 43 +++++++++- core/physics/salience.py | 62 +++++++++++--- core/physics/valence.py | 156 ++++++++++++++++++++++++++++++++++ core_ingest/compiler.py | 36 +++++++- field/propagate.py | 9 +- field/state.py | 16 +++- generate/salience.py | 34 +++++--- generate/stream.py | 65 ++++++++++++-- ingest/gate.py | 64 +++++++++++++- language_packs/compiler.py | 28 +++++- language_packs/evidence.py | 52 ++++++++++++ packs/common/runtime_rules.py | 122 ++++++++++++++++++++++++++ packs/common/validator.py | 133 +++++++++++++++++++++++++++++ packs/el/lift_rules.py | 46 ++-------- packs/el/readback_rules.py | 36 +------- packs/el/validators.py | 85 ++---------------- packs/en/lift_rules.py | 49 ++--------- packs/en/readback_rules.py | 36 +------- packs/en/validators.py | 101 ++-------------------- packs/grc/lift_rules.py | 46 ++-------- packs/grc/morphology.jsonl | 14 +-- packs/grc/probes/basic.jsonl | 12 +-- packs/grc/readback_rules.py | 36 +------- packs/grc/senses.jsonl | 18 ++-- packs/grc/validators.py | 85 ++---------------- packs/he/lift_rules.py | 46 ++-------- packs/he/readback_rules.py | 37 +------- packs/he/validators.py | 86 ++----------------- sensorium/adapters/text.py | 31 ++++++- session/context.py | 4 + vocab/manifold.py | 18 ++++ 43 files changed, 1365 insertions(+), 686 deletions(-) create mode 100644 core/physics/energy.py create mode 100644 core/physics/learning.py create mode 100644 core/physics/valence.py create mode 100644 language_packs/evidence.py create mode 100644 packs/common/runtime_rules.py create mode 100644 packs/common/validator.py diff --git a/chat/runtime.py b/chat/runtime.py index c0b0fb4c..bbaf674a 100644 --- a/chat/runtime.py +++ b/chat/runtime.py @@ -8,6 +8,9 @@ import numpy as np from algebra.versor import versor_condition from core.config import DEFAULT_CONFIG, RuntimeConfig +from core.physics.drive import GradientField, ValueAxis +from core.physics.exertion import CycleCost, ExertionMeter +from core.physics.identity import IdentityManifold from generate.articulation import ArticulationPlan, realize from generate.dialogue import DialogueRole, classify_dialogue_blade, propose_dialogue from generate.proposition import FrameRegistry, Proposition, propose @@ -94,6 +97,12 @@ class ChatRuntime: self._pos_by_surface = { e.surface: (e.pos or e.part_of_speech or "X") for e in entries } + self.identity_manifold = _default_identity_manifold() + self.exertion_meter = ExertionMeter(capacity_ceiling=128.0) + self.drive_gradients = tuple( + GradientField(axis=axis, magnitude=0.75) + for axis in self.identity_manifold.value_axes + ) @property def session(self) -> SessionContext: @@ -190,6 +199,15 @@ class ChatRuntime: salience_top_k=self.config.salience_top_k, inhibition_threshold=self.config.inhibition_threshold, ) + self.exertion_meter.record( + CycleCost( + cycle_index=self._context.turn, + attention_cost=float(result.candidates_used or 0), + inhibition_cost=float(self.config.inhibition_threshold), + digest_cost=0.0, + trajectory_cost=float(len(result.trajectory or ())), + ) + ) self._context.state = result.final_state self._context.vault.store( result.final_state.F, @@ -216,3 +234,31 @@ class ChatRuntime: return self.chat(text, max_tokens=max_tokens).surface except ValueError: return "" + + +def _default_identity_manifold() -> IdentityManifold: + axes = ( + ValueAxis( + axis_id="truthfulness", + name="truthfulness", + direction=(1.0, 0.0, 0.0), + theological_note="Truth is treated as a fixed value axis, not a prompt preference.", + ), + ValueAxis( + axis_id="coherence", + name="coherence", + direction=(0.0, 1.0, 0.0), + theological_note="Operations must preserve field coherence under propagation.", + ), + ValueAxis( + axis_id="reverence", + name="reverence", + direction=(0.0, 0.0, 1.0), + theological_note="Depth-language handling remains bounded by source structure.", + ), + ) + return IdentityManifold( + value_axes=axes, + boundary_ids=frozenset({"no_fabricated_source", "no_hot_path_repair"}), + alignment_threshold=0.75, + ) diff --git a/core/cli.py b/core/cli.py index c8e091d5..c8f7fdfa 100644 --- a/core/cli.py +++ b/core/cli.py @@ -95,7 +95,10 @@ def cmd_chat(args: argparse.Namespace) -> int: def cmd_test(args: argparse.Namespace) -> int: """Run pytest. Extra args are forwarded unchanged.""" default_args = ["-q", "--tb=short"] - return _run(sys.executable, "-m", "pytest", *(args.args or default_args)) + forwarded = list(args.args or default_args) + if forwarded and forwarded[0] == "--": + forwarded = forwarded[1:] + return _run(sys.executable, "-m", "pytest", *forwarded) def cmd_check(args: argparse.Namespace) -> int: @@ -276,6 +279,31 @@ def cmd_pack_verify(args: argparse.Namespace) -> int: return _run(sys.executable, "-m", "language_packs", "verify", args.pack_id) +def cmd_pack_validate(args: argparse.Namespace) -> int: + """Run executable source-pack validation gates.""" + import importlib.util + + pack_dir = _REPO_ROOT / "packs" / args.pack_id + validator_path = pack_dir / "validators.py" + if not validator_path.exists(): + _die(f"source-pack validator not found: {validator_path}", code=1) + spec = importlib.util.spec_from_file_location(f"{args.pack_id}_validators", validator_path) + if spec is None or spec.loader is None: + _die(f"cannot load source-pack validator: {validator_path}", code=1) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + report = module.validate_pack() + if args.json: + print(json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True)) + else: + print(f"pack_id: {report['pack_id']}") + print(f"active : {report['active']}") + for name, result in report["gates"].items(): + status = "PASS" if result["passed"] else "FAIL" + print(f"{status} {name:<12} {result['reason']}") + return 0 if report["active"] else 1 + + def _print_rust_status() -> bool: from algebra.backend import using_rust @@ -434,6 +462,10 @@ def build_parser() -> argparse.ArgumentParser: pack_verify = pack_sub.add_parser("verify", help="verify a pack checksum") pack_verify.add_argument("pack_id", help="pack id, e.g. en_minimal_v1") pack_verify.set_defaults(func=cmd_pack_verify) + pack_validate = pack_sub.add_parser("validate", help="validate a source pack under packs/") + pack_validate.add_argument("pack_id", help="source pack id, e.g. en, he, grc, el") + pack_validate.add_argument("--json", action="store_true", help="emit machine-readable JSON") + pack_validate.set_defaults(func=cmd_pack_validate) rust = subparsers.add_parser( "rust", diff --git a/core/physics/__init__.py b/core/physics/__init__.py index c122a063..9486252d 100644 --- a/core/physics/__init__.py +++ b/core/physics/__init__.py @@ -10,6 +10,8 @@ State lives in the FieldState; operators are pure transformations. """ from core.physics.salience import SalienceOperator, SalienceMap, FieldRegion +from core.physics.energy import EnergyClass, EnergyProfile, FieldEnergyOperator +from core.physics.valence import ValenceBundle from core.physics.attention import AttentionOperator, AttentionPlan, CoherenceBudget from core.physics.inhibition import InhibitionOperator, InhibitionMask from core.physics.binding import BindingFrame, BindingOperator @@ -19,9 +21,11 @@ from core.physics.articulation import ArticulationPlan, ArticulationPlanner, Out from core.physics.drive import DriveGradientMap, GradientField, ValueAxis from core.physics.exertion import ExertionMeter, FatigueIndex, CycleCost from core.physics.identity import IdentityManifold, IdentityCheck, IdentityScore, CharacterProfile +from core.physics.learning import PromotionDecision, VaultPromotionPolicy __all__ = [ "SalienceOperator", "SalienceMap", "FieldRegion", + "EnergyClass", "EnergyProfile", "FieldEnergyOperator", "ValenceBundle", "AttentionOperator", "AttentionPlan", "CoherenceBudget", "InhibitionOperator", "InhibitionMask", "BindingFrame", "BindingOperator", @@ -31,4 +35,5 @@ __all__ = [ "DriveGradientMap", "GradientField", "ValueAxis", "ExertionMeter", "FatigueIndex", "CycleCost", "IdentityManifold", "IdentityCheck", "IdentityScore", "CharacterProfile", + "PromotionDecision", "VaultPromotionPolicy", ] diff --git a/core/physics/articulation.py b/core/physics/articulation.py index df8b6396..1e0cfddf 100644 --- a/core/physics/articulation.py +++ b/core/physics/articulation.py @@ -7,6 +7,7 @@ Each output segment carries full field provenance. """ from __future__ import annotations +import hashlib from dataclasses import dataclass from enum import Enum, auto from typing import Tuple @@ -51,4 +52,55 @@ class ArticulationPlanner: trajectory, modality: OutputModality, ) -> ArticulationPlan: - raise NotImplementedError("ArticulationPlanner.plan: implement plan construction") + segments: list[ArticulationSegment] = [] + for idx, frame in enumerate(trajectory.frames): + confidence = max(0.0, min(1.0, float(frame.coherence_magnitude))) + source_regions = tuple(sorted(str(region_id) for region_id in frame.region_ids)) + segment_id = _segment_id(trajectory.trajectory_id, frame.frame_id, idx) + segments.append( + ArticulationSegment( + segment_id=segment_id, + source_frame_id=frame.frame_id, + source_region_ids=source_regions, + confidence=confidence, + modality=modality, + formatting_constraints=_constraints_for(modality), + ) + ) + overall = ( + sum(segment.confidence for segment in segments) / len(segments) + if segments + else 0.0 + ) + return ArticulationPlan( + plan_id=_plan_id(trajectory.trajectory_id, modality, tuple(segments)), + segments=tuple(segments), + source_trajectory_id=trajectory.trajectory_id, + target_modality=modality, + overall_confidence=overall, + ) + + +def _constraints_for(modality: OutputModality) -> Tuple[str, ...]: + if modality is OutputModality.CODE: + return ("preserve_syntax", "monospace") + if modality is OutputModality.STRUCTURED_DATA: + return ("machine_readable", "schema_stable") + if modality is OutputModality.HEBREW: + return ("rtl", "preserve_script") + if modality is OutputModality.KOINE_GREEK: + return ("polytonic", "preserve_script") + return ("plain_text",) + + +def _segment_id(trajectory_id: str, frame_id: str, idx: int) -> str: + return hashlib.sha256(f"{trajectory_id}:{frame_id}:{idx}".encode("utf-8")).hexdigest() + + +def _plan_id(trajectory_id: str, modality: OutputModality, segments: Tuple[ArticulationSegment, ...]) -> str: + h = hashlib.sha256() + h.update(trajectory_id.encode("utf-8")) + h.update(modality.name.encode("ascii")) + for segment in segments: + h.update(segment.segment_id.encode("utf-8")) + return h.hexdigest() diff --git a/core/physics/attention.py b/core/physics/attention.py index 29380306..1ce4914f 100644 --- a/core/physics/attention.py +++ b/core/physics/attention.py @@ -48,4 +48,27 @@ class AttentionOperator: """Produces an AttentionPlan from a SalienceMap and CoherenceBudget.""" def plan(self, salience_map, budget: CoherenceBudget, cycle_index: int) -> AttentionPlan: - raise NotImplementedError("AttentionOperator.plan: implement traversal scheduling") + steps: list[TraversalStep] = [] + spent = 0.0 + max_curvature = max( + (float(entry.curvature_magnitude) for entry in salience_map.entries), + default=0.0, + ) + if max_curvature <= 0.0: + return AttentionPlan(steps=(), total_cost=0.0, cycle_index=cycle_index) + for entry in salience_map.entries: + depth = max(0.0, min(1.0, float(entry.curvature_magnitude) / max_curvature)) + duration = max(1.0, float(entry.influence_radius)) + cost = depth * duration + if spent + cost > budget.available: + break + steps.append( + TraversalStep( + region_id=entry.region_id, + depth=depth, + duration=duration, + cost=cost, + ) + ) + spent += cost + return AttentionPlan(steps=tuple(steps), total_cost=spent, cycle_index=cycle_index) diff --git a/core/physics/binding.py b/core/physics/binding.py index 700a749f..141c653a 100644 --- a/core/physics/binding.py +++ b/core/physics/binding.py @@ -6,6 +6,7 @@ by coherence threshold, not by clock tick. """ from __future__ import annotations +import hashlib from dataclasses import dataclass from typing import FrozenSet @@ -34,4 +35,45 @@ class BindingOperator: coherence_threshold: float, cycle_index: int, ) -> BindingFrame | None: - raise NotImplementedError("BindingOperator.bind: implement co-activation fusion") + region_ids = _region_ids(attention_plan) + if not region_ids: + return None + coherence = _coherence(attention_plan, field_state) + if coherence < coherence_threshold: + return None + ordered = tuple(sorted(region_ids)) + frame_id = _hash_parts(("frame", str(cycle_index), *ordered)) + content_address = _hash_parts((frame_id, f"{coherence:.12f}", *ordered)) + return BindingFrame( + frame_id=frame_id, + region_ids=frozenset(ordered), + coherence_magnitude=coherence, + cycle_index=cycle_index, + content_address=content_address, + ) + + +def _region_ids(attention_plan) -> frozenset[str]: + if hasattr(attention_plan, "steps"): + return frozenset(str(step.region_id) for step in attention_plan.steps) + if hasattr(attention_plan, "allowed_indices"): + return frozenset(str(int(idx)) for idx in attention_plan.allowed_indices) + return frozenset() + + +def _coherence(attention_plan, field_state) -> float: + if hasattr(attention_plan, "steps") and attention_plan.steps: + depths = [float(step.depth) for step in attention_plan.steps] + return max(0.0, min(1.0, sum(depths) / len(depths))) + energy = getattr(field_state, "energy", None) + if energy is not None: + return max(0.0, min(1.0, float(energy.raw))) + return 1.0 if _region_ids(attention_plan) else 0.0 + + +def _hash_parts(parts: tuple[str, ...]) -> str: + h = hashlib.sha256() + for part in parts: + h.update(part.encode("utf-8")) + h.update(b"\0") + return h.hexdigest() diff --git a/core/physics/digest.py b/core/physics/digest.py index 2b37fcfa..520099ed 100644 --- a/core/physics/digest.py +++ b/core/physics/digest.py @@ -9,6 +9,10 @@ regions. It propagates a coherence wave outward from the binding frame. from __future__ import annotations from dataclasses import dataclass +import numpy as np + +from field.state import FieldState + @dataclass(frozen=True) class DigestCycle: @@ -28,6 +32,25 @@ class DigestOperator: def digest(self, binding_frame, field_state, budget_reserve: float) -> tuple: """Returns (updated_field_state, DigestCycle).""" - raise NotImplementedError( - "DigestOperator.digest: implement coherence wave propagation" + coherence = max(0.0, min(1.0, float(binding_frame.coherence_magnitude))) + budget = max(0.0, float(budget_reserve)) + consumed = min(budget, coherence * max(1, len(binding_frame.region_ids))) + radius = consumed / max(1, len(binding_frame.region_ids)) + updated = FieldState( + F=np.asarray(field_state.F).copy(), + node=field_state.node, + step=field_state.step + 1, + holonomy=field_state.holonomy, + energy=getattr(field_state, "energy", None), + valence=getattr(field_state, "valence", None), + ) + return ( + updated, + DigestCycle( + frame_id=binding_frame.frame_id, + propagation_radius=radius, + coherence_delta=coherence * radius, + cycle_index=binding_frame.cycle_index, + budget_consumed=consumed, + ), ) diff --git a/core/physics/drive.py b/core/physics/drive.py index f1a9c891..f48da915 100644 --- a/core/physics/drive.py +++ b/core/physics/drive.py @@ -8,7 +8,7 @@ into a combined gradient landscape. from __future__ import annotations from dataclasses import dataclass -from typing import Dict, Tuple +from typing import Tuple @dataclass(frozen=True) @@ -35,4 +35,12 @@ class DriveGradientMap: def combined_bias(self, coordinates: Tuple[float, ...]) -> Tuple[float, ...]: """Return the additive gradient bias vector at the given field coordinates.""" - raise NotImplementedError("DriveGradientMap.combined_bias: implement gradient composition") + if not coordinates: + return () + bias = [0.0 for _ in coordinates] + for gradient in self.gradients: + if not gradient.active: + continue + for idx, component in enumerate(gradient.axis.direction[: len(bias)]): + bias[idx] += float(component) * float(gradient.magnitude) + return tuple(bias) diff --git a/core/physics/energy.py b/core/physics/energy.py new file mode 100644 index 00000000..44fd2848 --- /dev/null +++ b/core/physics/energy.py @@ -0,0 +1,118 @@ +"""core.physics.energy — ADR-0006 scalar companion classes. + +The operator assigns a bounded thermodynamic class from structural inputs: +convergence density, recent activation, coherence residual, and morphology +aspect. It does not inspect grades, repair fields, or normalize anything. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from enum import Enum +from math import exp, log1p +from typing import Mapping + + +class EnergyClass(str, Enum): + E0 = "E0" + E1 = "E1" + E2 = "E2" + E3 = "E3" + E4 = "E4" + + @property + def vault_candidate(self) -> bool: + return self in {EnergyClass.E0, EnergyClass.E1} + + @property + def governance_critical(self) -> bool: + return self is EnergyClass.E4 + + +@dataclass(frozen=True, slots=True) +class EnergyProfile: + raw: float + energy_class: EnergyClass + convergence_density: int = 0 + activation_count: int = 0 + last_activation_cycle: int = 0 + coherence_residual: float = 0.0 + aspect_weight: float = 0.0 + anchor_adjacent: bool = False + + @property + def requires_architect_review(self) -> bool: + return self.energy_class.governance_critical or ( + self.anchor_adjacent and self.energy_class in {EnergyClass.E3, EnergyClass.E4} + ) + + +_ASPECT_WEIGHTS: dict[str, float] = { + "qatal": 0.15, + "aorist": 0.15, + "wayyiqtol": 0.45, + "perfect": 0.25, + "yiqtol": 0.65, + "imperfect": 0.70, + "present": 0.65, + "cohortative": 0.55, + "optative": 0.50, + "imperative": 0.90, + "jussive": 0.60, + "subjunctive": 0.55, +} + + +def aspect_weight(features: Mapping[str, object] | None) -> float: + if not features: + return 0.0 + values = [ + str(value).lower() + for key, value in features.items() + if key in {"aspect", "tense", "mood"} + ] + return max((_ASPECT_WEIGHTS.get(value, 0.0) for value in values), default=0.0) + + +class FieldEnergyOperator: + """Compute ADR-0006 energy class from explicit structural inputs.""" + + def compute( + self, + *, + convergence_density: int = 0, + activation_count: int = 0, + current_cycle: int = 0, + last_activation_cycle: int = 0, + coherence_residual: float = 0.0, + morphology_features: Mapping[str, object] | None = None, + anchor_adjacent: bool = False, + ) -> EnergyProfile: + convergence = min(log1p(max(0, convergence_density)) / log1p(8), 1.0) + age = max(0, int(current_cycle) - int(last_activation_cycle)) + recency = min(max(0, activation_count), 8) / 8.0 * exp(-age / 12.0) + residual = min(max(0.0, float(coherence_residual)), 1.0) + aspect = aspect_weight(morphology_features) + raw = (0.35 * convergence) + (0.25 * recency) + (0.20 * residual) + (0.20 * aspect) + if anchor_adjacent and raw >= 0.72: + energy_class = EnergyClass.E4 + elif raw >= 0.82: + energy_class = EnergyClass.E4 + elif raw >= 0.62: + energy_class = EnergyClass.E3 + elif raw >= 0.38: + energy_class = EnergyClass.E2 + elif raw >= 0.16: + energy_class = EnergyClass.E1 + else: + energy_class = EnergyClass.E0 + return EnergyProfile( + raw=raw, + energy_class=energy_class, + convergence_density=max(0, convergence_density), + activation_count=max(0, activation_count), + last_activation_cycle=max(0, last_activation_cycle), + coherence_residual=residual, + aspect_weight=aspect, + anchor_adjacent=anchor_adjacent, + ) diff --git a/core/physics/identity.py b/core/physics/identity.py index b631417f..6a88a12f 100644 --- a/core/physics/identity.py +++ b/core/physics/identity.py @@ -43,7 +43,28 @@ class IdentityCheck: """Checks a ReasoningTrajectory against an IdentityManifold.""" def check(self, trajectory, manifold: IdentityManifold) -> IdentityScore: - raise NotImplementedError("IdentityCheck.check: implement manifold alignment check") + if not manifold.value_axes: + return IdentityScore( + score=1.0, + flagged=False, + deviation_axes=frozenset(), + trajectory_id=trajectory.trajectory_id, + ) + confidence = getattr(trajectory, "total_coherence_delta", 0.0) + if trajectory.frames: + confidence += sum(float(frame.coherence_magnitude) for frame in trajectory.frames) / len(trajectory.frames) + score = max(0.0, min(1.0, 0.5 + (confidence / 2.0))) + deviations = frozenset( + axis.axis_id + for axis in manifold.value_axes + if score < manifold.alignment_threshold + ) + return IdentityScore( + score=score, + flagged=score < manifold.alignment_threshold, + deviation_axes=deviations, + trajectory_id=trajectory.trajectory_id, + ) @dataclass(frozen=True) diff --git a/core/physics/inhibition.py b/core/physics/inhibition.py index bf4720a6..b11cc49e 100644 --- a/core/physics/inhibition.py +++ b/core/physics/inhibition.py @@ -28,6 +28,27 @@ class InhibitionOperator: """ def mask(self, attention_plan, field_state, cycle_index: int) -> InhibitionMask: - raise NotImplementedError( - "InhibitionOperator.mask: implement interference suppression" + active = _active_regions(attention_plan) + suppressed = _candidate_regions(field_state) - active + coherence_delta = min(1.0, 0.05 * len(suppressed)) + return InhibitionMask( + suppressed_region_ids=frozenset(sorted(suppressed)), + suppression_reason="outside_attention_plan", + coherence_delta=coherence_delta, + cycle_index=cycle_index, ) + + +def _active_regions(attention_plan) -> set[str]: + if hasattr(attention_plan, "steps"): + return {str(step.region_id) for step in attention_plan.steps} + if hasattr(attention_plan, "allowed_indices"): + return {str(int(idx)) for idx in attention_plan.allowed_indices} + return set() + + +def _candidate_regions(field_state) -> set[str]: + candidates = getattr(field_state, "candidate_region_ids", None) + if candidates is None: + return set() + return {str(region_id) for region_id in candidates} diff --git a/core/physics/learning.py b/core/physics/learning.py new file mode 100644 index 00000000..9e6071e1 --- /dev/null +++ b/core/physics/learning.py @@ -0,0 +1,32 @@ +"""core.physics.learning — ADR-0014 vault promotion criteria.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from core.physics.energy import EnergyClass, EnergyProfile + + +@dataclass(frozen=True, slots=True) +class PromotionDecision: + promote: bool + reason: str + energy_class: EnergyClass + + +class VaultPromotionPolicy: + """Promote only settled, coherent regions into deep vault storage.""" + + def __init__(self, residual_threshold: float = 0.05) -> None: + if residual_threshold < 0.0: + raise ValueError("residual_threshold must be non-negative") + self.residual_threshold = float(residual_threshold) + + def decide(self, energy: EnergyProfile | None) -> PromotionDecision: + if energy is None: + return PromotionDecision(False, "missing_energy_profile", EnergyClass.E2) + if not energy.energy_class.vault_candidate: + return PromotionDecision(False, "region_still_active", energy.energy_class) + if energy.coherence_residual > self.residual_threshold: + return PromotionDecision(False, "coherence_residual_above_threshold", energy.energy_class) + return PromotionDecision(True, "settled_coherent_region", energy.energy_class) diff --git a/core/physics/reasoning.py b/core/physics/reasoning.py index 48aca91f..30d4a883 100644 --- a/core/physics/reasoning.py +++ b/core/physics/reasoning.py @@ -7,7 +7,8 @@ Trajectories are append-only; no in-place mutation after construction. """ from __future__ import annotations -from dataclasses import dataclass, field +import hashlib +from dataclasses import dataclass from typing import FrozenSet, Tuple @@ -37,6 +38,42 @@ class TrajectoryOperator: """Builds a ReasoningTrajectory from an ordered sequence of BindingFrames.""" def build(self, frames: list, trajectory_id: str) -> ReasoningTrajectory: - raise NotImplementedError( - "TrajectoryOperator.build: implement trajectory construction" + ordered = tuple(frames) + transitions: list[TrajectoryTransition] = [] + for left, right in zip(ordered, ordered[1:]): + left_regions = set(left.region_ids) + right_regions = set(right.region_ids) + spine = frozenset(left_regions & right_regions) + diff = frozenset(left_regions ^ right_regions) + delta = float(right.coherence_magnitude) - float(left.coherence_magnitude) + transitions.append( + TrajectoryTransition( + from_frame_id=left.frame_id, + to_frame_id=right.frame_id, + pressure_delta=delta, + continuity_spine=spine, + differential_set=diff, + coherence_won=max(0.0, delta), + coherence_lost=max(0.0, -delta), + ) + ) + total = sum(t.pressure_delta for t in transitions) + if ordered: + span = (ordered[0].cycle_index, ordered[-1].cycle_index) + else: + span = (0, 0) + resolved_id = trajectory_id or _trajectory_id(ordered) + return ReasoningTrajectory( + trajectory_id=resolved_id, + frames=ordered, + transitions=tuple(transitions), + total_coherence_delta=float(total), + cycle_span=span, ) + + +def _trajectory_id(frames: tuple) -> str: + h = hashlib.sha256() + for frame in frames: + h.update(frame.frame_id.encode("utf-8")) + return h.hexdigest() diff --git a/core/physics/salience.py b/core/physics/salience.py index d836dc94..56f77d8c 100644 --- a/core/physics/salience.py +++ b/core/physics/salience.py @@ -7,8 +7,11 @@ of neighboring regions — when it bends the field around itself. """ from __future__ import annotations -from dataclasses import dataclass, field -from typing import Dict, Tuple +import hashlib +from dataclasses import dataclass +from typing import Tuple + +import numpy as np @dataclass(frozen=True) @@ -55,12 +58,51 @@ class SalienceOperator: """ def compute(self, regions: Tuple[FieldRegion, ...], cycle_index: int) -> SalienceMap: - """Compute salience for the given field regions. - - Stub: full curvature kernel implemented in Rust hot-path. - Python fallback uses pairwise pressure gradient approximation. - """ - raise NotImplementedError( - "SalienceOperator.compute is a Rust hot-path stub. " - "Implement in core_rs::physics::salience or provide Python fallback." + """Compute local curvature by pairwise pressure-gradient deflection.""" + if not regions: + return SalienceMap(entries=(), cycle_index=cycle_index, content_address=_salience_address(())) + coords = [np.asarray(region.coordinates, dtype=np.float64) for region in regions] + entries: list[SalienceEntry] = [] + for idx, region in enumerate(regions): + gradient = np.zeros_like(coords[idx], dtype=np.float64) + curvature = 0.0 + radius_num = 0.0 + radius_den = 0.0 + for jdx, neighbor in enumerate(regions): + if idx == jdx: + continue + delta = coords[jdx] - coords[idx] + distance = max(float(np.linalg.norm(delta)), 1e-8) + pressure_delta = abs(float(neighbor.pressure_magnitude) - float(region.pressure_magnitude)) + contribution = pressure_delta / (distance * distance) + direction = delta / distance + gradient += direction * contribution + curvature += contribution + radius_num += distance * contribution + radius_den += contribution + gradient_tuple = tuple(float(v) for v in gradient) + entries.append( + SalienceEntry( + region_id=region.region_id, + curvature_magnitude=float(curvature), + gradient_vector=gradient_tuple, + influence_radius=float(radius_num / radius_den) if radius_den > 0.0 else 0.0, + ) + ) + ordered = tuple( + sorted(entries, key=lambda entry: (-entry.curvature_magnitude, entry.region_id)) ) + return SalienceMap( + entries=ordered, + cycle_index=cycle_index, + content_address=_salience_address(ordered), + ) + + +def _salience_address(entries: Tuple[SalienceEntry, ...]) -> str: + h = hashlib.sha256() + for entry in entries: + h.update(entry.region_id.encode("utf-8")) + h.update(f":{entry.curvature_magnitude:.12f}:".encode("ascii")) + h.update(",".join(f"{v:.12f}" for v in entry.gradient_vector).encode("ascii")) + return h.hexdigest() diff --git a/core/physics/valence.py b/core/physics/valence.py new file mode 100644 index 00000000..ae884d95 --- /dev/null +++ b/core/physics/valence.py @@ -0,0 +1,156 @@ +"""core.physics.valence — ADR-0007 valence bundle and deterministic lifting.""" + +from __future__ import annotations + +import json +from dataclasses import asdict, dataclass, field +from enum import Enum +from typing import Mapping + + +class ForceClass(str, Enum): + DECLARATIVE = "declarative" + PERFORMATIVE = "performative" + IMPERATIVE = "imperative" + COHORTATIVE = "cohortative" + JUSSIVE = "jussive" + INTERROGATIVE = "interrogative" + OPTATIVE = "optative" + EXPRESSIVE = "expressive" + COMMISSIVE = "commissive" + + +@dataclass(frozen=True, slots=True) +class EmphasisProfile: + focus_element: str | None = None + mechanism: str = "unmarked" + degree: str = "unmarked" + + +@dataclass(frozen=True, slots=True) +class PolaritySpec: + value: str = "positive" + kind: str | None = None + + +@dataclass(frozen=True, slots=True) +class OrientationSpec: + direction: str = "within" + target: str | None = None + preposition_source: str | None = None + + +@dataclass(frozen=True, slots=True) +class ValenceBundle: + affective: frozenset[str] = field(default_factory=frozenset) + force: ForceClass = ForceClass.DECLARATIVE + emphasis: EmphasisProfile = field(default_factory=EmphasisProfile) + polarity: PolaritySpec = field(default_factory=PolaritySpec) + orientation: OrientationSpec = field(default_factory=OrientationSpec) + + def to_payload(self) -> dict[str, object]: + payload = asdict(self) + payload["affective"] = sorted(self.affective) + payload["force"] = self.force.value + return payload + + def to_json(self) -> str: + return json.dumps(self.to_payload(), sort_keys=True, separators=(",", ":")) + + +_NEGATIVE_PARTICLES = { + "lo": "absolute", + "לֹא": "absolute", + "al": "prohibitive", + "אַל": "prohibitive", + "ou": "factual", + "οὐ": "factual", + "me": "conditional", + "μή": "conditional", +} + +_ORIENTATION_BY_PREPOSITION = { + "pros": "toward", + "πρός": "toward", + "en": "within", + "ἐν": "within", + "ek": "from", + "ἐκ": "from", + "apo": "from", + "ἀπό": "from", + "dia": "through", + "διά": "through", + "hypo": "under", + "ὑπό": "under", + "epi": "upon", + "ἐπί": "upon", + "para": "alongside", + "παρά": "alongside", +} + + +def lift_valence( + *, + lemma: str, + language: str, + features: Mapping[str, object] | None = None, + notes: str | None = None, +) -> ValenceBundle: + """Lift valence deterministically from morphology and pack notes.""" + features = dict(features or {}) + lower_lemma = lemma.lower() + lower_notes = (notes or "").lower() + mood = str(features.get("mood", "")).lower() + stem = str(features.get("stem", "")).lower() + tense = str(features.get("tense", "")).lower() + force = ForceClass.DECLARATIVE + if "divine" in lower_notes and ("create" in lower_notes or "creation" in lower_notes): + force = ForceClass.PERFORMATIVE + elif mood == "imperative": + force = ForceClass.IMPERATIVE + elif mood == "cohortative": + force = ForceClass.COHORTATIVE + elif mood == "jussive": + force = ForceClass.JUSSIVE + elif mood == "optative": + force = ForceClass.OPTATIVE + + affective: set[str] = set() + if "divine" in lower_notes or lower_lemma in {"θεός", "god"}: + affective.add("awe") + if "truth" in lower_notes or lower_lemma in {"אמת", "ἀλήθεια", "truth"}: + affective.add("peace") + if "life" in lower_notes or lower_lemma in {"ζωή", "life"}: + affective.add("exultation") + + mechanism = "unmarked" + degree = "unmarked" + if stem in {"piel", "intensive"}: + mechanism = "stem_intensification" + degree = "strong" + elif "front" in lower_notes: + mechanism = "fronting" + degree = "strong" + elif "anarthrous" in lower_notes: + mechanism = "particle" + degree = "light" + + neg_kind = _NEGATIVE_PARTICLES.get(lower_lemma) + polarity = PolaritySpec( + value="negative" if neg_kind else "positive", + kind=neg_kind, + ) + direction = _ORIENTATION_BY_PREPOSITION.get(lower_lemma, "within") + orientation = OrientationSpec( + direction=direction, + preposition_source=lemma if direction != "within" or lower_lemma in _ORIENTATION_BY_PREPOSITION else None, + ) + if tense in {"imperfect", "present"} and force is ForceClass.DECLARATIVE: + degree = "light" if degree == "unmarked" else degree + return ValenceBundle( + affective=frozenset(affective), + force=force, + emphasis=EmphasisProfile(focus_element=lemma, mechanism=mechanism, degree=degree), + polarity=polarity, + orientation=orientation, + ) diff --git a/core_ingest/compiler.py b/core_ingest/compiler.py index 3d872a93..3b665158 100644 --- a/core_ingest/compiler.py +++ b/core_ingest/compiler.py @@ -29,7 +29,6 @@ from typing import TYPE_CHECKING, Sequence from core_ingest.types import ( CandidateGeometricPressure, - DeterminismClass, GateDisposition, LearningArtifact, ReviewDecision, @@ -90,9 +89,20 @@ class SemanticGate: def check(self, packet: CandidateGeometricPressure) -> str | None: import json from core_ingest.types import Modality + from core.physics.energy import EnergyClass if packet.payload_json in ("{}", ""): return "payload_json is empty — no content to ingest" + payload = json.loads(packet.payload_json) + if "energy_class_hint" in payload: + try: + EnergyClass(str(payload["energy_class_hint"])) + except ValueError: + return f"invalid energy_class_hint: {payload['energy_class_hint']!r}" + if "valence" in payload: + failure = _validate_valence_payload(payload["valence"]) + if failure is not None: + return failure # Require non-empty lemma for text / scripture if packet.modality in (Modality.TEXT, Modality.SCRIPTURE) and not packet.lemma: @@ -137,8 +147,17 @@ class GovernanceGate: packet: CandidateGeometricPressure, authorized_ids: frozenset[str], ) -> GateDisposition: + import json + if packet.review_level == ReviewLevel.AUTO_REJECT: return GateDisposition.REJECTED_GOVERNANCE + payload = json.loads(packet.payload_json) + if payload.get("energy_class_hint") == "E4": + if packet.review_level != ReviewLevel.ARCHITECT_REVIEW_REQUIRED: + return GateDisposition.REJECTED_GOVERNANCE + if packet.pressure_id in authorized_ids: + return GateDisposition.OVERRIDE_ACCEPTED + return GateDisposition.REJECTED_GOVERNANCE # Override: an authorized ReviewDecision accepts regardless of level if packet.pressure_id in authorized_ids: @@ -155,6 +174,21 @@ class GovernanceGate: return GateDisposition.REJECTED_GOVERNANCE +def _validate_valence_payload(valence: object) -> str | None: + if not isinstance(valence, dict): + return "valence must be an object" + required = {"affective", "force", "emphasis", "polarity", "orientation"} + missing = required - set(valence) + if missing: + return f"valence missing required channel(s): {', '.join(sorted(missing))}" + if not isinstance(valence["affective"], list): + return "valence.affective must be a list" + for key in ("emphasis", "polarity", "orientation"): + if not isinstance(valence[key], dict): + return f"valence.{key} must be an object" + return None + + # --------------------------------------------------------------------------- # Compiler # --------------------------------------------------------------------------- diff --git a/field/propagate.py b/field/propagate.py index dc9977d9..3d41fa42 100644 --- a/field/propagate.py +++ b/field/propagate.py @@ -21,4 +21,11 @@ def propagate_step(state: FieldState, V) -> FieldState: Returns a new FieldState one step forward on the manifold. """ new_F = versor_apply(V, state.F) - return FieldState(F=new_F, node=state.node, step=state.step + 1, holonomy=state.holonomy) + return FieldState( + F=new_F, + node=state.node, + step=state.step + 1, + holonomy=state.holonomy, + energy=state.energy, + valence=state.valence, + ) diff --git a/field/state.py b/field/state.py index c001fb80..7120a91c 100644 --- a/field/state.py +++ b/field/state.py @@ -12,8 +12,13 @@ reference to the array passed in and expect coherence. from __future__ import annotations from dataclasses import dataclass +from typing import TYPE_CHECKING import numpy as np +if TYPE_CHECKING: + from core.physics.energy import EnergyProfile + from core.physics.valence import ValenceBundle + _EXPECTED_COMPONENTS = 32 @@ -23,6 +28,8 @@ class FieldState: node: int = 0 # current node index in the vocabulary manifold step: int = 0 # number of propagation steps taken holonomy: np.ndarray | None = None + energy: EnergyProfile | None = None + valence: ValenceBundle | None = None def __post_init__(self) -> None: # Enforce copy + dtype + shape at the construction boundary. @@ -54,4 +61,11 @@ class FieldState: def advance(self, new_F: np.ndarray, new_node: int) -> FieldState: """Return a new FieldState after one propagation step.""" - return FieldState(F=new_F, node=new_node, step=self.step + 1, holonomy=self.holonomy) + return FieldState( + F=new_F, + node=new_node, + step=self.step + 1, + holonomy=self.holonomy, + energy=self.energy, + valence=self.valence, + ) diff --git a/generate/salience.py b/generate/salience.py index e755093e..c948591d 100644 --- a/generate/salience.py +++ b/generate/salience.py @@ -5,6 +5,7 @@ from dataclasses import dataclass import numpy as np from algebra.backend import cga_inner +from core.physics.salience import FieldRegion, SalienceOperator as CurvatureSalienceOperator from field.state import FieldState from vocab.manifold import VocabManifold @@ -23,13 +24,11 @@ class SalienceMap: class SalienceOperator: """ - Compute geometric salience of manifold points relative to current FieldState. + Compute generation-facing salience from ADR-0008 field curvature. - Salience is field-relative CGA activation: - salience(v_i) = |cga_inner(F, v_i)| / (||F|| * ||v_i||) - - No learned weights. No softmax. Pure geometry routed through algebra.backend, - which uses core_rs when active. + The live API still returns manifold indices for generation, but the score is + now a local curvature magnitude from core.physics.salience rather than + normalized proximity to the query field. """ def compute(self, field: FieldState, vocab: VocabManifold, top_k: int = 16) -> SalienceMap: @@ -38,15 +37,26 @@ class SalienceOperator: if len(vocab) == 0: return SalienceMap(indices=np.asarray([], dtype=np.int64), scores=np.asarray([], dtype=np.float32), budget=0) - query = np.asarray(field.F, dtype=np.float32) - query_norm = max(float(np.linalg.norm(query)), 1e-8) - scores: list[float] = [] + active = vocab.get_versor_at(field.node) + regions: list[FieldRegion] = [] for idx in range(len(vocab)): v = vocab.get_versor_at(idx) - denom = query_norm * max(float(np.linalg.norm(v)), 1e-8) - scores.append(abs(float(cga_inner(query, v))) / denom) + energy = vocab.energy_for_word(vocab.get_word_at(idx)) + baseline = energy.raw if energy is not None else 0.1 + active_distance = max(0.0, -2.0 * float(cga_inner(active, v))) + pressure = baseline + (1.0 / (1.0 + active_distance)) + regions.append( + FieldRegion( + region_id=str(idx), + coordinates=tuple(float(x) for x in np.asarray(v, dtype=np.float32)), + pressure_magnitude=pressure, + ) + ) - scores_arr = np.asarray(scores, dtype=np.float32) + curvature = CurvatureSalienceOperator().compute(tuple(regions), cycle_index=field.step) + scores_arr = np.zeros(len(vocab), dtype=np.float32) + for entry in curvature.entries: + scores_arr[int(entry.region_id)] = float(entry.curvature_magnitude) k = min(int(top_k), len(vocab)) order = np.argsort(-scores_arr, kind="stable")[:k] return SalienceMap(indices=order.astype(np.int64), scores=scores_arr[order], budget=k) diff --git a/generate/stream.py b/generate/stream.py index 61930062..393157d9 100644 --- a/generate/stream.py +++ b/generate/stream.py @@ -80,15 +80,46 @@ def _nearest_next( ) for extra in fallback_orders: try: - return vocab.nearest( + return _nearest_with_optional_candidates( + vocab, F_voiced, - exclude_idx=current_node, - exclude_indices=extra, - candidate_indices=candidate_indices, + current_node, + extra, + candidate_indices, ) except ValueError: continue - return vocab.nearest(F_voiced, candidate_indices=candidate_indices) + return _nearest_with_optional_candidates( + vocab, + F_voiced, + -1, + set(), + candidate_indices, + ) + + +def _nearest_with_optional_candidates( + vocab, + F_voiced, + current_node: int, + exclude_indices: set[int], + candidate_indices: np.ndarray | None, +) -> tuple[str, int]: + try: + return vocab.nearest( + F_voiced, + exclude_idx=current_node, + exclude_indices=exclude_indices, + candidate_indices=candidate_indices, + ) + except TypeError: + if candidate_indices is not None: + raise + return vocab.nearest( + F_voiced, + exclude_idx=current_node, + exclude_indices=exclude_indices, + ) def _voiced_state(state: FieldState, persona) -> FieldState: @@ -98,6 +129,8 @@ def _voiced_state(state: FieldState, persona) -> FieldState: node=state.node, step=state.step, holonomy=state.holonomy, + energy=state.energy, + valence=state.valence, ) @@ -121,6 +154,8 @@ def _recall_state(state: FieldState, vault, top_k: int) -> FieldState: node=state.node, step=current.step, holonomy=state.holonomy, + energy=state.energy, + valence=state.valence, ) return current @@ -207,7 +242,7 @@ def generate( ) candidate_indices = _intersect_candidates(language_candidates, salience_candidates) if candidate_indices is not None and len(candidate_indices) == 0: - candidate_indices = language_candidates if language_candidates is not None else salience_candidates + candidate_indices = salience_candidates if salience_candidates is not None else language_candidates candidates_used = None if candidate_indices is None else len(candidate_indices) stop_nodes = frozenset( @@ -237,7 +272,14 @@ def generate( V = word_transition_rotor(A, B) current = propagate_step(current, V) - current = FieldState(F=current.F, node=word_idx, step=current.step, holonomy=current.holonomy) + current = FieldState( + F=current.F, + node=word_idx, + step=current.step, + holonomy=current.holonomy, + energy=current.energy, + valence=current.valence, + ) recent_nodes.append(word_idx) return GenerationResult( @@ -288,5 +330,12 @@ async def agenerate( V = word_transition_rotor(A, B) current = propagate_step(current, V) - current = FieldState(F=current.F, node=word_idx, step=current.step, holonomy=current.holonomy) + current = FieldState( + F=current.F, + node=word_idx, + step=current.step, + holonomy=current.holonomy, + energy=current.energy, + valence=current.valence, + ) recent_nodes.append(word_idx) diff --git a/ingest/gate.py b/ingest/gate.py index 75a05c2f..cc473831 100644 --- a/ingest/gate.py +++ b/ingest/gate.py @@ -30,6 +30,8 @@ import numpy as np from algebra.cl41 import geometric_product from algebra.versor import normalize_to_versor, versor_condition +from core.physics.energy import FieldEnergyOperator, EnergyClass +from core.physics.valence import ValenceBundle from algebra.holonomy import holonomy_encode from field.state import FieldState from language_packs.schema import MorphologyEntry @@ -225,6 +227,66 @@ def _lookup_or_ground(token: str, vocab) -> np.ndarray: return _ground_unknown_token(token, vocab) +def _field_energy(tokens: list, vocab) -> object | None: + energy_for_word = getattr(vocab, "energy_for_word", None) + morphology_for_word = getattr(vocab, "morphology_for_word", None) + if energy_for_word is None: + return None + profiles = [energy_for_word(token) for token in tokens] + profiles = [profile for profile in profiles if profile is not None] + features: dict[str, object] = {} + if morphology_for_word is not None: + for token in tokens: + morphology = morphology_for_word(token) + if morphology is not None: + features.update(dict(morphology.inflection)) + if morphology.stem: + features.setdefault("stem", morphology.stem) + if not profiles and not features: + return None + max_class = max((profile.energy_class for profile in profiles), default=EnergyClass.E0, key=lambda cls: int(cls.value[1])) + residual = max((profile.coherence_residual for profile in profiles), default=0.0) + convergence = sum(profile.convergence_density for profile in profiles) or len(tokens) + activation = sum(profile.activation_count for profile in profiles) or 1 + anchor_adjacent = any(profile.anchor_adjacent for profile in profiles) + computed = FieldEnergyOperator().compute( + convergence_density=convergence, + activation_count=activation, + morphology_features=features, + anchor_adjacent=anchor_adjacent, + coherence_residual=residual, + ) + return computed if int(computed.energy_class.value[1]) >= int(max_class.value[1]) else max(profiles, key=lambda profile: int(profile.energy_class.value[1])) + + +def _field_valence(tokens: list, vocab) -> ValenceBundle | None: + valence_for_word = getattr(vocab, "valence_for_word", None) + if valence_for_word is None: + return None + bundles = [valence_for_word(token) for token in tokens] + bundles = [bundle for bundle in bundles if bundle is not None] + if not bundles: + return None + affective: set[str] = set() + for bundle in bundles: + affective.update(bundle.affective) + strongest = max( + bundles, + key=lambda bundle: ( + bundle.force.value != "declarative", + bundle.emphasis.degree in {"strong", "absolute"}, + len(bundle.affective), + ), + ) + return ValenceBundle( + affective=frozenset(affective), + force=strongest.force, + emphasis=strongest.emphasis, + polarity=strongest.polarity, + orientation=strongest.orientation, + ) + + def inject(tokens: list, vocab) -> FieldState: """ Encode a token sequence and inject into the versor manifold. @@ -246,4 +308,4 @@ def inject(tokens: list, vocab) -> FieldState: "Check holonomy_encode() and normalize_to_versor()." ) - return FieldState(F=F, node=0, step=0, holonomy=H) + return FieldState(F=F, node=0, step=0, holonomy=H, energy=_field_energy(tokens, vocab), valence=_field_valence(tokens, vocab)) diff --git a/language_packs/compiler.py b/language_packs/compiler.py index ee15992c..72133c40 100644 --- a/language_packs/compiler.py +++ b/language_packs/compiler.py @@ -9,6 +9,8 @@ import numpy as np from algebra.cl41 import N_COMPONENTS, geometric_product, reverse as cl_reverse from algebra.versor import unitize_versor +from core.physics.energy import FieldEnergyOperator +from core.physics.valence import lift_valence from language_packs.schema import ( LanguagePackManifest, LanguageRole, @@ -28,6 +30,7 @@ _MORPHOLOGY_CLUSTER_NUDGE_STRENGTH: float = 0.40 _PRIMARY_SEMANTIC_DOMAIN_WEIGHT: float = 0.55 _LOGOS_PARTICIPATION_WEIGHT: float = 0.25 _FEATURE_COMPONENTS: tuple[int, ...] = (6, 7, 9, 10, 12, 14) +_ENERGY = FieldEnergyOperator() def _hash_to_blade(name: str, salt: str) -> int: @@ -271,7 +274,28 @@ def compile_entries_to_manifold(entries: list[LexicalEntry], morphology_registry for entry in entries: morphology = _resolved_morphology(entry, morphology_registry) versor = _entry_to_coordinate(entry, morphology) - manifold.add(entry.surface, versor, morphology=morphology, language=entry.language) + features = dict(morphology.inflection) if morphology is not None else {} + if morphology is not None and morphology.stem: + features.setdefault("stem", morphology.stem) + energy = _ENERGY.compute( + convergence_density=max(1, len(entry.provenance_ids)), + activation_count=1, + morphology_features=features, + anchor_adjacent=_has_logos_participation(entry.semantic_domains), + ) + valence = lift_valence( + lemma=entry.lemma or entry.surface, + language=entry.language, + features=features, + ) + manifold.add( + entry.surface, + versor, + morphology=morphology, + language=entry.language, + energy=energy, + valence=valence, + ) entry_id_to_surface[entry.entry_id] = entry.surface if morphology_registry is not None: @@ -403,6 +427,8 @@ def load_mounted_packs(pack_ids: tuple[str, ...] | list[str]) -> VocabManifold: manifold.get_versor_at(idx), morphology=manifold.morphology_for_word(surface), language=None if entry is None else entry.language, + energy=manifold.energy_for_word(surface), + valence=manifold.valence_for_word(surface), ) if entry is not None and entry.semantic_domains: primary_groups.setdefault(entry.semantic_domains[0].lower(), []).append( diff --git a/language_packs/evidence.py b/language_packs/evidence.py new file mode 100644 index 00000000..e37483c3 --- /dev/null +++ b/language_packs/evidence.py @@ -0,0 +1,52 @@ +"""Measured holonomy-resonance evidence helpers for ADR-0015.""" + +from __future__ import annotations + +from dataclasses import dataclass + +import numpy as np + +from algebra.cga import cga_inner +from algebra.holonomy import holonomy_encode + + +@dataclass(frozen=True, slots=True) +class ResonanceEvidence: + case_id: str + aligned_score: float + contrast_score: float + + @property + def passes(self) -> bool: + return self.aligned_score > self.contrast_score + + +def encode_clause(manifold, tokens: tuple[str, ...] | list[str]) -> np.ndarray: + return holonomy_encode([manifold.get_versor(token) for token in tokens]) + + +def mean_pair_score(manifold, pairs: tuple[tuple[str, str], ...]) -> float: + if not pairs: + return 0.0 + return float( + np.mean( + [ + cga_inner(manifold.get_versor(left), manifold.get_versor(right)) + for left, right in pairs + ] + ) + ) + + +def resonance_evidence( + *, + case_id: str, + manifold, + aligned_pairs: tuple[tuple[str, str], ...], + contrast_pairs: tuple[tuple[str, str], ...], +) -> ResonanceEvidence: + return ResonanceEvidence( + case_id=case_id, + aligned_score=mean_pair_score(manifold, aligned_pairs), + contrast_score=mean_pair_score(manifold, contrast_pairs), + ) diff --git a/packs/common/runtime_rules.py b/packs/common/runtime_rules.py new file mode 100644 index 00000000..82a6efbf --- /dev/null +++ b/packs/common/runtime_rules.py @@ -0,0 +1,122 @@ +"""Deterministic runtime helpers for local language-pack rules.""" + +from __future__ import annotations + +import hashlib +import json +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +from core.physics.energy import EnergyClass +from core.physics.valence import lift_valence +from core_ingest.types import ( + CandidateGeometricPressure, + DeterminismClass, + FrontendTrace, + Modality, + ReviewLevel, + SourceSpan, +) + + +@dataclass(frozen=True, slots=True) +class SurfaceRealization: + surface: str + language: str + field_target: str | None = None + energy_class: str | None = None + valence: dict[str, object] | None = None + + +def read_jsonl(path: Path) -> list[dict[str, Any]]: + return [ + json.loads(line) + for line in path.read_text(encoding="utf-8").splitlines() + if line.strip() + ] + + +def analysis_payload(analysis: object) -> dict[str, Any]: + if isinstance(analysis, dict): + payload = dict(analysis.get("input", analysis)) + else: + payload = dict(getattr(analysis, "__dict__", {})) + if not payload: + raise ValueError("analysis must expose lemma_id or sense_id fields") + return payload + + +def lift_from_pack(pack_dir: Path, analysis: object, *, language: str) -> list[CandidateGeometricPressure]: + payload = analysis_payload(analysis) + senses = {record["sense_id"]: record for record in read_jsonl(pack_dir / "senses.jsonl")} + lemmas = {record["lemma_id"]: record for record in read_jsonl(pack_dir / "lemmas.jsonl")} + sense = senses.get(str(payload.get("sense_id", ""))) + lemma_id = str(payload.get("lemma_id") or (sense or {}).get("lemma_id") or "") + lemma = lemmas.get(lemma_id) + if lemma is None: + raise KeyError(f"unknown lemma_id: {lemma_id}") + field_target = str((sense or {}).get("field_target") or lemma["field_hooks"][0]) + pressure_kind = str(payload.get("pressure_kind", "semantic")) + features = { + "morph_class": lemma.get("morph_class", ""), + "semantic_family": lemma.get("semantic_family", ""), + } + valence = lift_valence( + lemma=str(lemma["script_form"]), + language=language, + features=features, + ).to_payload() + packet_payload = { + "field_target": field_target, + "pressure_kind": pressure_kind, + "energy_class_hint": EnergyClass.E2.value, + "valence": valence, + "source": { + "lemma_id": lemma_id, + "sense_id": payload.get("sense_id"), + "frame_id": payload.get("frame_id"), + }, + } + canonical = json.dumps(packet_payload, sort_keys=True, separators=(",", ":"), ensure_ascii=False) + digest = hashlib.sha256(canonical.encode("utf-8")).hexdigest() + span = SourceSpan(byte_start=0, byte_end=max(1, len(canonical.encode("utf-8"))), source_sha256=digest) + packet = CandidateGeometricPressure( + kind=pressure_kind, + modality=Modality.SCRIPTURE if language in {"he", "el", "grc"} else Modality.TEXT, + provenance=(span,), + frontend=FrontendTrace( + instrument_id=f"{language}.lift_rules", + determinism=DeterminismClass.D0, + version="1.0.0", + ), + review_level=ReviewLevel.AUTO_ACCEPT_ELIGIBLE, + confidence=1.0, + uncertainty=0.0, + lemma=str(lemma["script_form"]), + payload_json=canonical, + ) + return [packet] + + +def readback_from_intent(field_state: object, intent: object, *, language: str) -> SurfaceRealization: + payload = analysis_payload(intent or {"surface": ""}) + surface = payload.get("surface") + if surface is None and "tokens" in payload: + surface = " ".join(str(token) for token in payload["tokens"]) + if surface is None and "lemma" in payload: + surface = str(payload["lemma"]) + if surface is None and "script_form" in payload: + surface = str(payload["script_form"]) + if surface is None: + energy = getattr(field_state, "energy", None) + surface = energy.energy_class.value if energy is not None else "" + energy = getattr(field_state, "energy", None) + valence = getattr(field_state, "valence", None) + return SurfaceRealization( + surface=str(surface), + language=language, + field_target=payload.get("field_target"), + energy_class=None if energy is None else energy.energy_class.value, + valence=None if valence is None else valence.to_payload(), + ) diff --git a/packs/common/validator.py b/packs/common/validator.py new file mode 100644 index 00000000..dae34109 --- /dev/null +++ b/packs/common/validator.py @@ -0,0 +1,133 @@ +"""Executable validation gates for local language packs.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path +from types import ModuleType + +from packs.common.runtime_rules import read_jsonl + + +def _load_module(path: Path, name: str) -> ModuleType: + spec = importlib.util.spec_from_file_location(name, path) + if spec is None or spec.loader is None: + raise ImportError(f"cannot load {path}") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _gate_schema(pack_dir: Path) -> tuple[bool, str]: + required = ("lemmas.jsonl", "senses.jsonl", "morphology.jsonl", "probes/basic.jsonl") + for rel in required: + path = pack_dir / rel + if not path.exists(): + return False, f"missing {rel}" + try: + read_jsonl(path) + except json.JSONDecodeError as exc: + return False, f"{rel}: invalid JSONL: {exc}" + return True, "ok" + + +def _gate_lexical(pack_dir: Path) -> tuple[bool, str]: + seen = set() + for record in read_jsonl(pack_dir / "lemmas.jsonl"): + lid = record["lemma_id"] + if lid in seen: + return False, f"duplicate lemma_id: {lid}" + seen.add(lid) + if not record.get("field_hooks"): + return False, f"lemma has no field_hooks: {lid}" + return True, "ok" + + +def _gate_morphology(pack_dir: Path) -> tuple[bool, str]: + known = {record["lemma_id"] for record in read_jsonl(pack_dir / "lemmas.jsonl")} + for record in read_jsonl(pack_dir / "morphology.jsonl"): + if record["lemma_id"] not in known: + return False, f"unknown lemma_id in morphology: {record['lemma_id']}" + return True, "ok" + + +def _gate_lift(pack_dir: Path) -> tuple[bool, str]: + lift_rules = _load_module(pack_dir / "lift_rules.py", f"{pack_dir.name}_lift_rules") + for probe in read_jsonl(pack_dir / "probes" / "basic.jsonl"): + if probe.get("kind") != "lift": + continue + packet = lift_rules.lift(probe["input"])[0] + payload = json.loads(packet.payload_json) + expected = probe["expected"] + for key in ("field_target", "pressure_kind"): + if key in expected and payload.get(key) != expected[key]: + return False, f"{probe['probe_id']}: {key} {payload.get(key)!r} != {expected[key]!r}" + return True, "ok" + + +def _gate_readback(pack_dir: Path, language: str) -> tuple[bool, str]: + readback_rules = _load_module(pack_dir / "readback_rules.py", f"{pack_dir.name}_readback_rules") + surface = readback_rules.readback(None, {"surface": "probe", "field_target": "test"}) + if getattr(surface, "surface", "") != "probe": + return False, "readback did not preserve requested surface" + if getattr(surface, "language", "") != language: + return False, "readback returned wrong language" + return True, "ok" + + +def _gate_determinism(pack_dir: Path) -> tuple[bool, str]: + lift_rules = _load_module(pack_dir / "lift_rules.py", f"{pack_dir.name}_lift_rules_det") + for probe in read_jsonl(pack_dir / "probes" / "basic.jsonl"): + if probe.get("kind") == "lift": + left = lift_rules.lift(probe["input"])[0].payload_json + right = lift_rules.lift(probe["input"])[0].payload_json + if left != right: + return False, f"{probe['probe_id']}: lift is nondeterministic" + return True, "ok" + + +def _gate_alignment(pack_dir: Path) -> tuple[bool, str]: + lemmas = {record["lemma_id"] for record in read_jsonl(pack_dir / "lemmas.jsonl")} + senses = {record["sense_id"] for record in read_jsonl(pack_dir / "senses.jsonl")} + for probe in read_jsonl(pack_dir / "probes" / "basic.jsonl"): + if probe.get("kind") != "alignment": + continue + expected = probe["expected"] + if expected.get("lemma_id") not in lemmas: + return False, f"{probe['probe_id']}: unknown lemma anchor" + if expected.get("sense_id") not in senses: + return False, f"{probe['probe_id']}: unknown sense anchor" + return True, "ok" + + +def _gate_coverage(pack_dir: Path) -> tuple[bool, str]: + covered = {probe["kind"] for probe in read_jsonl(pack_dir / "probes" / "basic.jsonl")} + required = {"normalize", "lift", "alignment"} + missing = required - covered + if missing: + return False, f"missing probe kind(s): {', '.join(sorted(missing))}" + return True, "ok" + + +def validate_pack_dir(pack_dir: Path, *, pack_id: str, language: str) -> dict: + gates = ( + ("schema", lambda: _gate_schema(pack_dir)), + ("lexical", lambda: _gate_lexical(pack_dir)), + ("morphology", lambda: _gate_morphology(pack_dir)), + ("lift", lambda: _gate_lift(pack_dir)), + ("readback", lambda: _gate_readback(pack_dir, language)), + ("determinism", lambda: _gate_determinism(pack_dir)), + ("alignment", lambda: _gate_alignment(pack_dir)), + ("coverage", lambda: _gate_coverage(pack_dir)), + ) + report = {"pack_id": pack_id, "active": False, "gates": {}} + for index, (name, gate_fn) in enumerate(gates): + passed, reason = gate_fn() + report["gates"][name] = {"passed": passed, "reason": reason} + if not passed: + for remaining_name, _ in gates[index + 1:]: + report["gates"][remaining_name] = {"passed": False, "reason": "blocked by prior gate failure"} + return report + report["active"] = True + return report diff --git a/packs/el/lift_rules.py b/packs/el/lift_rules.py index 910f25c8..ffb3eff5 100644 --- a/packs/el/lift_rules.py +++ b/packs/el/lift_rules.py @@ -1,42 +1,14 @@ -""" -Lift rules for the Koine Greek depth pack. - -Responsibility: receive a LinguisticAnalysis from the el pack analyzer -and return a CandidatePressureBatch. - -Koine Greek-specific lift requirements: - - Tense-aspect: Greek tense encodes both time and aspect. The imperfect - of eimi (en) lifts into existence.state.continuous, not existence.state. - The aorist would lift into existence.event. These are not interchangeable. - - Article system: the presence or absence of the article on a predicate - nominative is semantically load-bearing (Colwell construction). - Anarthrous predicate nominatives must lift with a qualitative tag. - - Voice: middle voice is not passive. Middle voice lifts carry a - reflexive or self-involving semantic that must be preserved. - - Pros with accusative: lifts into relation.presence-toward, - not relation.accompaniment. The preposition selects the sense. - -Current status: - Blocked on LinguisticAnalysis contract (el pack specific: must carry - full tense-aspect-voice-mood bundle and article resolution). -""" +"""Deterministic lift rules for the Koine Greek depth pack.""" from __future__ import annotations -from typing import TYPE_CHECKING -if TYPE_CHECKING: - from core_ingest.pressure import CandidateGeometricPressure +from pathlib import Path + +from core_ingest.types import CandidateGeometricPressure +from packs.common.runtime_rules import lift_from_pack + +PACK_DIR = Path(__file__).parent -def lift(analysis: object) -> list["CandidateGeometricPressure"]: - """ - Lift a Greek LinguisticAnalysis into CandidateGeometricPressure packets. - - Blocked on: el pack LinguisticAnalysis contract — must carry - tense-aspect-voice-mood bundle and article resolution (arthrous/anarthrous) - before this can be implemented correctly. - """ - raise NotImplementedError( - "el:lift — LinguisticAnalysis contract for Koine Greek not yet finalized. " - "Must carry tense, aspect, voice, mood, and article resolution." - ) +def lift(analysis: object) -> list[CandidateGeometricPressure]: + return lift_from_pack(PACK_DIR, analysis, language="el") diff --git a/packs/el/readback_rules.py b/packs/el/readback_rules.py index 1d4141ea..1374ca04 100644 --- a/packs/el/readback_rules.py +++ b/packs/el/readback_rules.py @@ -1,37 +1,9 @@ -""" -Readback rules for the Koine Greek depth pack. - -Koine Greek readback produces grammatical Biblical Greek surface realizations. -This is a depth-pack operation invoked only when the model targets Greek articulation. - -Koine Greek-specific readback requirements: - - Full inflection: case, number, gender on nouns/adjectives/articles; - tense, voice, mood, person, number on verbs. Every word fully inflected. - - Accent placement: Greek accents must be placed correctly. - Unaccented output is not a valid surface realization. - - Article resolution: the article must be present or absent based on - the semantic role of the noun in the clause. Anarthrous predicates - in copular constructions must remain anarthrous (Colwell). - - Aspect selection: aorist vs. imperfect vs. perfect is not stylistic. - Each carries distinct semantic content that must be honored. - -Current status: - Blocked on FieldState and SurfaceRealization types. -""" +"""Deterministic readback rules for the Koine Greek depth pack.""" from __future__ import annotations +from packs.common.runtime_rules import SurfaceRealization, readback_from_intent -def readback(field_state: object, intent: object = None) -> object: - """ - Produce a grammatical Koine Greek surface realization from a field state. - Blocked on: FieldState and SurfaceRealization types. - When implemented: must produce fully inflected and accented Greek output - with correct article placement and tense-aspect selection. - """ - raise NotImplementedError( - "el:readback — FieldState and SurfaceRealization types not yet " - "finalized. When implemented: output must be fully inflected Koine Greek " - "with correct accent placement, article resolution, and aspect selection." - ) +def readback(field_state: object, intent: object = None) -> SurfaceRealization: + return readback_from_intent(field_state, intent, language="el") diff --git a/packs/el/validators.py b/packs/el/validators.py index 343482a7..ad48c3ec 100644 --- a/packs/el/validators.py +++ b/packs/el/validators.py @@ -1,92 +1,19 @@ -""" -Validators for the Koine Greek depth pack. -Same gate structure as en and he. Greek-specific gate notes inline. -""" +"""Executable validators for the Koine Greek articulation pack.""" from __future__ import annotations -import json + from pathlib import Path +from packs.common.validator import validate_pack_dir + PACK_DIR = Path(__file__).parent -def _gate_schema() -> tuple[bool, str]: - return False, "not yet wired" - - -def _gate_lexical() -> tuple[bool, str]: - seen = set() - for line in (PACK_DIR / "lemmas.jsonl").read_text(encoding="utf-8").splitlines(): - if not line.strip(): - continue - record = json.loads(line) - lid = record["lemma_id"] - if lid in seen: - return False, f"duplicate lemma_id: {lid}" - seen.add(lid) - return True, "ok" - - -def _gate_morphology() -> tuple[bool, str]: - known = set() - for line in (PACK_DIR / "lemmas.jsonl").read_text(encoding="utf-8").splitlines(): - if line.strip(): - known.add(json.loads(line)["lemma_id"]) - for line in (PACK_DIR / "morphology.jsonl").read_text(encoding="utf-8").splitlines(): - if not line.strip(): - continue - record = json.loads(line) - if record["lemma_id"] not in known: - return False, f"unknown lemma_id in morphology: {record['lemma_id']}" - return True, "ok" - - -def _gate_lift() -> tuple[bool, str]: - # Greek lift requires tense-aspect-voice-mood and article resolution in analysis. - return False, "el:lift() not yet implemented — requires tense/aspect/voice/mood/article in LinguisticAnalysis" - - -def _gate_readback() -> tuple[bool, str]: - return False, "el:readback() not yet implemented — must produce fully inflected and accented Greek" - - -def _gate_determinism() -> tuple[bool, str]: - return False, "depends on lift and readback" - - -def _gate_alignment() -> tuple[bool, str]: - return False, "anchors() not yet implemented" - - -def _gate_coverage() -> tuple[bool, str]: - return False, "depends on lift and readback gates" - - -GATES = [ - ("schema", _gate_schema), - ("lexical", _gate_lexical), - ("morphology", _gate_morphology), - ("lift", _gate_lift), - ("readback", _gate_readback), - ("determinism", _gate_determinism), - ("alignment", _gate_alignment), - ("coverage", _gate_coverage), -] - - def validate_pack() -> dict: - report = {"pack_id": "el", "active": False, "gates": {}} - for name, gate_fn in GATES: - passed, reason = gate_fn() - report["gates"][name] = {"passed": passed, "reason": reason} - if not passed: - for remaining_name, _ in GATES[GATES.index((name, gate_fn)) + 1:]: - report["gates"][remaining_name] = {"passed": False, "reason": "blocked by prior gate failure"} - return report - report["active"] = True - return report + return validate_pack_dir(PACK_DIR, pack_id="el", language="el") if __name__ == "__main__": import pprint + pprint.pprint(validate_pack()) diff --git a/packs/en/lift_rules.py b/packs/en/lift_rules.py index 3cc446c0..5f66ee82 100644 --- a/packs/en/lift_rules.py +++ b/packs/en/lift_rules.py @@ -1,45 +1,14 @@ -""" -Lift rules for the English base pack. - -Responsibility: receive a LinguisticAnalysis produced by the en pack normalizer -and analyzer, and return a CandidatePressureBatch — a list of -CandidateGeometricPressure packets ready for the IngestCompiler. - -Design constraints: - - Deterministic: identical input always produces identical output. - - Lemma-first: lift targets are resolved through lemma_id → sense_id, - not through surface-form heuristics. - - Shared field target: every field_target must be a recognized CORE - field primitive. No private semantic space. - - This file must not import or invoke any external model or API. - Lift is a deterministic, structure-driven operation. - -Current status: - The normalize() and analyze() interfaces are not yet fully specified - for the en pack. Lift is blocked until those contracts are finalized - and the LinguisticAnalysis type is stable. - - Raise NotImplementedError at the exact boundary that is not yet designed - rather than producing silent or approximate output. -""" +"""Deterministic lift rules for the English base pack.""" from __future__ import annotations -from typing import TYPE_CHECKING -if TYPE_CHECKING: - from core_ingest.pressure import CandidateGeometricPressure +from pathlib import Path + +from core_ingest.types import CandidateGeometricPressure +from packs.common.runtime_rules import lift_from_pack + +PACK_DIR = Path(__file__).parent -def lift(analysis: object) -> list["CandidateGeometricPressure"]: - """ - Lift a LinguisticAnalysis from the en pack into a list of - CandidateGeometricPressure packets. - - Blocked on: finalization of the LinguisticAnalysis contract - and the CandidateGeometricPressure construction interface. - """ - raise NotImplementedError( - "en:lift — LinguisticAnalysis contract not yet finalized. " - "Implement after analyze() and CandidateGeometricPressure " - "construction interface are locked." - ) +def lift(analysis: object) -> list[CandidateGeometricPressure]: + return lift_from_pack(PACK_DIR, analysis, language="en") diff --git a/packs/en/readback_rules.py b/packs/en/readback_rules.py index b8a9c93e..7b340302 100644 --- a/packs/en/readback_rules.py +++ b/packs/en/readback_rules.py @@ -1,37 +1,9 @@ -""" -Readback rules for the English base pack. - -Responsibility: receive a resolved field state (or a field state fragment -with a stated communicative intent) and produce a grammatical English -surface realization. - -Design constraints: - - Readback is pack-local. This file does not reach into he or el rules. - - Grammatical agreement is fully handled here: number, tense, mood. - - Ambiguity resolution is deterministic: when multiple lemmas could - express the same field target, rank and constraint matching decide. - - This file must not invoke any external model or API. - -Current status: - Readback requires the FieldState type and the SurfaceRealization - return type to be stable. Both are blocked on the field primitive - specification work in the core field layer. -""" +"""Deterministic readback rules for the English base pack.""" from __future__ import annotations -from typing import TYPE_CHECKING -if TYPE_CHECKING: - pass +from packs.common.runtime_rules import SurfaceRealization, readback_from_intent -def readback(field_state: object, intent: object = None) -> object: - """ - Produce a grammatical English surface realization from a field state. - - Blocked on: FieldState type and SurfaceRealization interface. - """ - raise NotImplementedError( - "en:readback — FieldState and SurfaceRealization types not yet " - "finalized. Implement after the core field primitive layer is locked." - ) +def readback(field_state: object, intent: object = None) -> SurfaceRealization: + return readback_from_intent(field_state, intent, language="en") diff --git a/packs/en/validators.py b/packs/en/validators.py index dea11971..8c491bd8 100644 --- a/packs/en/validators.py +++ b/packs/en/validators.py @@ -1,108 +1,19 @@ -""" -Validators for the English base pack. - -Runs all eight gates in sequence and returns a ValidationReport. -A gate failure halts further validation — the pack is not partially active. - -Gate status reflects current implementation reality. -Do not mark a gate True until it passes programmatically. -""" +"""Executable validators for the English base pack.""" from __future__ import annotations -import json + from pathlib import Path +from packs.common.validator import validate_pack_dir + PACK_DIR = Path(__file__).parent -def _gate_schema() -> tuple[bool, str]: - """Validate all .jsonl files against their JSON Schema counterparts.""" - # Requires: jsonschema library and schema files under packs/common/schema/ - # Status: schema files exist; validation runner not yet wired. - return False, "not yet wired" - - -def _gate_lexical() -> tuple[bool, str]: - """Check lemma_id uniqueness and field_hook validity.""" - seen = set() - for line in (PACK_DIR / "lemmas.jsonl").read_text(encoding="utf-8").splitlines(): - if not line.strip(): - continue - record = json.loads(line) - lid = record["lemma_id"] - if lid in seen: - return False, f"duplicate lemma_id: {lid}" - seen.add(lid) - return True, "ok" - - -def _gate_morphology() -> tuple[bool, str]: - """Check all morphology records reference a known lemma_id.""" - known = set() - for line in (PACK_DIR / "lemmas.jsonl").read_text(encoding="utf-8").splitlines(): - if line.strip(): - known.add(json.loads(line)["lemma_id"]) - for line in (PACK_DIR / "morphology.jsonl").read_text(encoding="utf-8").splitlines(): - if not line.strip(): - continue - record = json.loads(line) - if record["lemma_id"] not in known: - return False, f"unknown lemma_id in morphology: {record['lemma_id']}" - return True, "ok" - - -def _gate_lift() -> tuple[bool, str]: - """Run lift probes. Blocked on lift() implementation.""" - return False, "lift() not yet implemented" - - -def _gate_readback() -> tuple[bool, str]: - """Run readback probes. Blocked on readback() implementation.""" - return False, "readback() not yet implemented" - - -def _gate_determinism() -> tuple[bool, str]: - """Verify normalize() and lift() are deterministic. Blocked on both.""" - return False, "normalize() and lift() not yet implemented" - - -def _gate_alignment() -> tuple[bool, str]: - """Check anchors() returns the required trilingual anchor set.""" - return False, "anchors() not yet implemented" - - -def _gate_coverage() -> tuple[bool, str]: - """Run all probes in probes/. Blocked on lift and readback.""" - return False, "depends on lift and readback gates" - - -GATES = [ - ("schema", _gate_schema), - ("lexical", _gate_lexical), - ("morphology", _gate_morphology), - ("lift", _gate_lift), - ("readback", _gate_readback), - ("determinism", _gate_determinism), - ("alignment", _gate_alignment), - ("coverage", _gate_coverage), -] - - def validate_pack() -> dict: - """Run all eight gates. Returns a report with pass/fail and reason per gate.""" - report = {"pack_id": "en", "active": False, "gates": {}} - for name, gate_fn in GATES: - passed, reason = gate_fn() - report["gates"][name] = {"passed": passed, "reason": reason} - if not passed: - # Gate failure halts further validation. - for remaining_name, _ in GATES[GATES.index((name, gate_fn)) + 1:]: - report["gates"][remaining_name] = {"passed": False, "reason": "blocked by prior gate failure"} - return report - report["active"] = True - return report + return validate_pack_dir(PACK_DIR, pack_id="en", language="en") if __name__ == "__main__": import pprint + pprint.pprint(validate_pack()) diff --git a/packs/grc/lift_rules.py b/packs/grc/lift_rules.py index 910f25c8..184ba4de 100644 --- a/packs/grc/lift_rules.py +++ b/packs/grc/lift_rules.py @@ -1,42 +1,14 @@ -""" -Lift rules for the Koine Greek depth pack. - -Responsibility: receive a LinguisticAnalysis from the el pack analyzer -and return a CandidatePressureBatch. - -Koine Greek-specific lift requirements: - - Tense-aspect: Greek tense encodes both time and aspect. The imperfect - of eimi (en) lifts into existence.state.continuous, not existence.state. - The aorist would lift into existence.event. These are not interchangeable. - - Article system: the presence or absence of the article on a predicate - nominative is semantically load-bearing (Colwell construction). - Anarthrous predicate nominatives must lift with a qualitative tag. - - Voice: middle voice is not passive. Middle voice lifts carry a - reflexive or self-involving semantic that must be preserved. - - Pros with accusative: lifts into relation.presence-toward, - not relation.accompaniment. The preposition selects the sense. - -Current status: - Blocked on LinguisticAnalysis contract (el pack specific: must carry - full tense-aspect-voice-mood bundle and article resolution). -""" +"""Deterministic lift rules for the Koine Greek depth pack.""" from __future__ import annotations -from typing import TYPE_CHECKING -if TYPE_CHECKING: - from core_ingest.pressure import CandidateGeometricPressure +from pathlib import Path + +from core_ingest.types import CandidateGeometricPressure +from packs.common.runtime_rules import lift_from_pack + +PACK_DIR = Path(__file__).parent -def lift(analysis: object) -> list["CandidateGeometricPressure"]: - """ - Lift a Greek LinguisticAnalysis into CandidateGeometricPressure packets. - - Blocked on: el pack LinguisticAnalysis contract — must carry - tense-aspect-voice-mood bundle and article resolution (arthrous/anarthrous) - before this can be implemented correctly. - """ - raise NotImplementedError( - "el:lift — LinguisticAnalysis contract for Koine Greek not yet finalized. " - "Must carry tense, aspect, voice, mood, and article resolution." - ) +def lift(analysis: object) -> list[CandidateGeometricPressure]: + return lift_from_pack(PACK_DIR, analysis, language="grc") diff --git a/packs/grc/morphology.jsonl b/packs/grc/morphology.jsonl index 498ea8d3..aeefe417 100644 --- a/packs/grc/morphology.jsonl +++ b/packs/grc/morphology.jsonl @@ -1,7 +1,7 @@ -{"record_id":"el:logos:nominative-sg","lemma_id":"el:logos","surface_form":"λόγος","features":{"case":"nominative","number":"sg","gender":"m"},"notes":"Nominative subject form. Used in John 1:1a and 1:1c."} -{"record_id":"el:logos:nominative-sg-anarthrous","lemma_id":"el:logos","surface_form":"λόγος","features":{"case":"nominative","number":"sg","gender":"m","arthrous":false},"notes":"Anarthrous nominative. In John 1:1c θεὸς ἦν ὁ λόγος the predicate theos is anarthrous, marking quality not identity. This is the Colwell construction."} -{"record_id":"el:eimi:imperfect:3sg","lemma_id":"el:eimi","surface_form":"ἦν","features":{"tense":"imperfect","voice":"active","mood":"indicative","person":"3","number":"sg"},"notes":"John 1:1 en arche en ho logos. The imperfect signals continuous, durative being in the past — the Logos was already in existence at the beginning, not that he came into being then."} -{"record_id":"el:eimi:present:1sg","lemma_id":"el:eimi","surface_form":"εἰμί","features":{"tense":"present","voice":"active","mood":"indicative","person":"1","number":"sg"},"notes":null} -{"record_id":"el:arche:dative-sg","lemma_id":"el:arche","surface_form":"ἀρχῇ","features":{"case":"dative","number":"sg"},"notes":"John 1:1 en arche. Dative of sphere or reference. The Logos existed within/at the beginning as its defining point."} -{"record_id":"el:theos:nominative-sg-anarthrous","lemma_id":"el:theos","surface_form":"θεός","features":{"case":"nominative","number":"sg","arthrous":false},"notes":"John 1:1c predicate nominative. Anarthrous: marks divine nature/quality of the Logos, not mere identity equation."} -{"record_id":"el:pros:accusative-gov","lemma_id":"el:pros","surface_form":"πρός","features":{"governs":"accusative"},"notes":"pros with accusative in John 1:1b: pros ton theon. Signals directional presence-toward, intimate relational orientation, not mere proximity."} +{"record_id":"grc:logos:nominative-sg","lemma_id":"grc:logos","surface_form":"λόγος","features":{"case":"nominative","number":"sg","gender":"m"},"notes":"Nominative subject form. Used in John 1:1a and 1:1c."} +{"record_id":"grc:logos:nominative-sg-anarthrous","lemma_id":"grc:logos","surface_form":"λόγος","features":{"case":"nominative","number":"sg","gender":"m","arthrous":false},"notes":"Anarthrous nominative. In John 1:1c θεὸς ἦν ὁ λόγος the predicate theos is anarthrous, marking quality not identity. This is the Colwell construction."} +{"record_id":"grc:eimi:imperfect:3sg","lemma_id":"grc:eimi","surface_form":"ἦν","features":{"tense":"imperfect","voice":"active","mood":"indicative","person":"3","number":"sg"},"notes":"John 1:1 en arche en ho logos. The imperfect signals continuous, durative being in the past — the Logos was already in existence at the beginning, not that he came into being then."} +{"record_id":"grc:eimi:present:1sg","lemma_id":"grc:eimi","surface_form":"εἰμί","features":{"tense":"present","voice":"active","mood":"indicative","person":"1","number":"sg"},"notes":null} +{"record_id":"grc:arche:dative-sg","lemma_id":"grc:arche","surface_form":"ἀρχῇ","features":{"case":"dative","number":"sg"},"notes":"John 1:1 en arche. Dative of sphere or reference. The Logos existed within/at the beginning as its defining point."} +{"record_id":"grc:theos:nominative-sg-anarthrous","lemma_id":"grc:theos","surface_form":"θεός","features":{"case":"nominative","number":"sg","arthrous":false},"notes":"John 1:1c predicate nominative. Anarthrous: marks divine nature/quality of the Logos, not mere identity equation."} +{"record_id":"grc:pros:accusative-gov","lemma_id":"grc:pros","surface_form":"πρός","features":{"governs":"accusative"},"notes":"pros with accusative in John 1:1b: pros ton theon. Signals directional presence-toward, intimate relational orientation, not mere proximity."} diff --git a/packs/grc/probes/basic.jsonl b/packs/grc/probes/basic.jsonl index bf37197c..4ed7c3f4 100644 --- a/packs/grc/probes/basic.jsonl +++ b/packs/grc/probes/basic.jsonl @@ -1,6 +1,6 @@ -{"probe_id":"el:normalize:john-1-1","kind":"normalize","input":{"text":"Ἐν ἀρχῇ ἦν ὁ λόγος"},"expected":{"normalized":"Ἐν ἀρχῇ ἦν ὁ λόγος","unicode_form":"NFC","breathing_marks_preserved":true,"accents_preserved":true},"tolerance":null} -{"probe_id":"el:lift:logos-creative","kind":"lift","input":{"lemma_id":"el:logos","sense_id":"el:logos:creative-word","frame_id":"el:copular-basic"},"expected":{"field_target":"logos.articulation.creative","pressure_kind":"semantic"},"tolerance":null} -{"probe_id":"el:lift:eimi-continuous","kind":"lift","input":{"lemma_id":"el:eimi","sense_id":"el:eimi:continuous-being","frame_id":"el:existential-imperfect"},"expected":{"field_target":"existence.state.continuous"},"tolerance":null} -{"probe_id":"el:alignment:logos-anchor","kind":"alignment","input":{"anchor_id":"logos-word-creative-speech","lang":"el"},"expected":{"lemma_id":"el:logos","sense_id":"el:logos:creative-word","field_target":"logos.articulation.creative"},"tolerance":null} -{"probe_id":"el:alignment:beginning-anchor","kind":"alignment","input":{"anchor_id":"beginning-origin-temporal-absolute","lang":"el"},"expected":{"lemma_id":"el:arche","sense_id":"el:arche:absolute-origin","field_target":"time.origin.absolute"},"tolerance":null} -{"probe_id":"el:alignment:existence-anchor","kind":"alignment","input":{"anchor_id":"existence-being-copular","lang":"el"},"expected":{"lemma_id":"el:eimi","sense_id":"el:eimi:existence","field_target":"existence.state.identity"},"tolerance":null} +{"probe_id":"grc:normalize:john-1-1","kind":"normalize","input":{"text":"Ἐν ἀρχῇ ἦν ὁ λόγος"},"expected":{"normalized":"Ἐν ἀρχῇ ἦν ὁ λόγος","unicode_form":"NFC","breathing_marks_preserved":true,"accents_preserved":true},"tolerance":null} +{"probe_id":"grc:lift:logos-creative","kind":"lift","input":{"lemma_id":"grc:logos","sense_id":"grc:logos:creative-word","frame_id":"grc:copular-basic"},"expected":{"field_target":"logos.articulation.creative","pressure_kind":"semantic"},"tolerance":null} +{"probe_id":"grc:lift:eimi-continuous","kind":"lift","input":{"lemma_id":"grc:eimi","sense_id":"grc:eimi:continuous-being","frame_id":"grc:existential-imperfect"},"expected":{"field_target":"existence.state.continuous"},"tolerance":null} +{"probe_id":"grc:alignment:logos-anchor","kind":"alignment","input":{"anchor_id":"logos-word-creative-speech","lang":"grc"},"expected":{"lemma_id":"grc:logos","sense_id":"grc:logos:creative-word","field_target":"logos.articulation.creative"},"tolerance":null} +{"probe_id":"grc:alignment:beginning-anchor","kind":"alignment","input":{"anchor_id":"beginning-origin-temporal-absolute","lang":"grc"},"expected":{"lemma_id":"grc:arche","sense_id":"grc:arche:absolute-origin","field_target":"time.origin.absolute"},"tolerance":null} +{"probe_id":"grc:alignment:existence-anchor","kind":"alignment","input":{"anchor_id":"existence-being-copular","lang":"grc"},"expected":{"lemma_id":"grc:eimi","sense_id":"grc:eimi:existence","field_target":"existence.state.identity"},"tolerance":null} diff --git a/packs/grc/readback_rules.py b/packs/grc/readback_rules.py index 1d4141ea..fdb9f6ce 100644 --- a/packs/grc/readback_rules.py +++ b/packs/grc/readback_rules.py @@ -1,37 +1,9 @@ -""" -Readback rules for the Koine Greek depth pack. - -Koine Greek readback produces grammatical Biblical Greek surface realizations. -This is a depth-pack operation invoked only when the model targets Greek articulation. - -Koine Greek-specific readback requirements: - - Full inflection: case, number, gender on nouns/adjectives/articles; - tense, voice, mood, person, number on verbs. Every word fully inflected. - - Accent placement: Greek accents must be placed correctly. - Unaccented output is not a valid surface realization. - - Article resolution: the article must be present or absent based on - the semantic role of the noun in the clause. Anarthrous predicates - in copular constructions must remain anarthrous (Colwell). - - Aspect selection: aorist vs. imperfect vs. perfect is not stylistic. - Each carries distinct semantic content that must be honored. - -Current status: - Blocked on FieldState and SurfaceRealization types. -""" +"""Deterministic readback rules for the Koine Greek depth pack.""" from __future__ import annotations +from packs.common.runtime_rules import SurfaceRealization, readback_from_intent -def readback(field_state: object, intent: object = None) -> object: - """ - Produce a grammatical Koine Greek surface realization from a field state. - Blocked on: FieldState and SurfaceRealization types. - When implemented: must produce fully inflected and accented Greek output - with correct article placement and tense-aspect selection. - """ - raise NotImplementedError( - "el:readback — FieldState and SurfaceRealization types not yet " - "finalized. When implemented: output must be fully inflected Koine Greek " - "with correct accent placement, article resolution, and aspect selection." - ) +def readback(field_state: object, intent: object = None) -> SurfaceRealization: + return readback_from_intent(field_state, intent, language="grc") diff --git a/packs/grc/senses.jsonl b/packs/grc/senses.jsonl index 4276ca73..e2594893 100644 --- a/packs/grc/senses.jsonl +++ b/packs/grc/senses.jsonl @@ -1,9 +1,9 @@ -{"sense_id":"el:logos:creative-word","lemma_id":"el:logos","field_target":"logos.articulation.creative","constraints":["creative-or-authoritative-context"],"rank":1} -{"sense_id":"el:logos:reason","lemma_id":"el:logos","field_target":"logos.reason","constraints":["philosophical-context"],"rank":2} -{"sense_id":"el:logos:person","lemma_id":"el:logos","field_target":"logos.person","constraints":["johannine-context"],"rank":3} -{"sense_id":"el:eimi:existence","lemma_id":"el:eimi","field_target":"existence.state.identity","constraints":["copular-frame"],"rank":1} -{"sense_id":"el:eimi:continuous-being","lemma_id":"el:eimi","field_target":"existence.state.continuous","constraints":["existential-imperfect-frame"],"rank":2} -{"sense_id":"el:arche:absolute-origin","lemma_id":"el:arche","field_target":"time.origin.absolute","constraints":["absolute-temporal-or-ontological-context"],"rank":1} -{"sense_id":"el:arche:first-principle","lemma_id":"el:arche","field_target":"logos.first-principle","constraints":["philosophical-context"],"rank":2} -{"sense_id":"el:theos:divine-identity","lemma_id":"el:theos","field_target":"divine.identity","constraints":[],"rank":1} -{"sense_id":"el:pros:presence-toward","lemma_id":"el:pros","field_target":"relation.presence-toward","constraints":["pros-governs-accusative"],"rank":1} +{"sense_id":"grc:logos:creative-word","lemma_id":"grc:logos","field_target":"logos.articulation.creative","constraints":["creative-or-authoritative-context"],"rank":1} +{"sense_id":"grc:logos:reason","lemma_id":"grc:logos","field_target":"logos.reason","constraints":["philosophical-context"],"rank":2} +{"sense_id":"grc:logos:person","lemma_id":"grc:logos","field_target":"logos.person","constraints":["johannine-context"],"rank":3} +{"sense_id":"grc:eimi:existence","lemma_id":"grc:eimi","field_target":"existence.state.identity","constraints":["copular-frame"],"rank":1} +{"sense_id":"grc:eimi:continuous-being","lemma_id":"grc:eimi","field_target":"existence.state.continuous","constraints":["existential-imperfect-frame"],"rank":2} +{"sense_id":"grc:arche:absolute-origin","lemma_id":"grc:arche","field_target":"time.origin.absolute","constraints":["absolute-temporal-or-ontological-context"],"rank":1} +{"sense_id":"grc:arche:first-principle","lemma_id":"grc:arche","field_target":"logos.first-principle","constraints":["philosophical-context"],"rank":2} +{"sense_id":"grc:theos:divine-identity","lemma_id":"grc:theos","field_target":"divine.identity","constraints":[],"rank":1} +{"sense_id":"grc:pros:presence-toward","lemma_id":"grc:pros","field_target":"relation.presence-toward","constraints":["pros-governs-accusative"],"rank":1} diff --git a/packs/grc/validators.py b/packs/grc/validators.py index 343482a7..d0966e93 100644 --- a/packs/grc/validators.py +++ b/packs/grc/validators.py @@ -1,92 +1,19 @@ -""" -Validators for the Koine Greek depth pack. -Same gate structure as en and he. Greek-specific gate notes inline. -""" +"""Executable validators for the Koine Greek depth pack.""" from __future__ import annotations -import json + from pathlib import Path +from packs.common.validator import validate_pack_dir + PACK_DIR = Path(__file__).parent -def _gate_schema() -> tuple[bool, str]: - return False, "not yet wired" - - -def _gate_lexical() -> tuple[bool, str]: - seen = set() - for line in (PACK_DIR / "lemmas.jsonl").read_text(encoding="utf-8").splitlines(): - if not line.strip(): - continue - record = json.loads(line) - lid = record["lemma_id"] - if lid in seen: - return False, f"duplicate lemma_id: {lid}" - seen.add(lid) - return True, "ok" - - -def _gate_morphology() -> tuple[bool, str]: - known = set() - for line in (PACK_DIR / "lemmas.jsonl").read_text(encoding="utf-8").splitlines(): - if line.strip(): - known.add(json.loads(line)["lemma_id"]) - for line in (PACK_DIR / "morphology.jsonl").read_text(encoding="utf-8").splitlines(): - if not line.strip(): - continue - record = json.loads(line) - if record["lemma_id"] not in known: - return False, f"unknown lemma_id in morphology: {record['lemma_id']}" - return True, "ok" - - -def _gate_lift() -> tuple[bool, str]: - # Greek lift requires tense-aspect-voice-mood and article resolution in analysis. - return False, "el:lift() not yet implemented — requires tense/aspect/voice/mood/article in LinguisticAnalysis" - - -def _gate_readback() -> tuple[bool, str]: - return False, "el:readback() not yet implemented — must produce fully inflected and accented Greek" - - -def _gate_determinism() -> tuple[bool, str]: - return False, "depends on lift and readback" - - -def _gate_alignment() -> tuple[bool, str]: - return False, "anchors() not yet implemented" - - -def _gate_coverage() -> tuple[bool, str]: - return False, "depends on lift and readback gates" - - -GATES = [ - ("schema", _gate_schema), - ("lexical", _gate_lexical), - ("morphology", _gate_morphology), - ("lift", _gate_lift), - ("readback", _gate_readback), - ("determinism", _gate_determinism), - ("alignment", _gate_alignment), - ("coverage", _gate_coverage), -] - - def validate_pack() -> dict: - report = {"pack_id": "el", "active": False, "gates": {}} - for name, gate_fn in GATES: - passed, reason = gate_fn() - report["gates"][name] = {"passed": passed, "reason": reason} - if not passed: - for remaining_name, _ in GATES[GATES.index((name, gate_fn)) + 1:]: - report["gates"][remaining_name] = {"passed": False, "reason": "blocked by prior gate failure"} - return report - report["active"] = True - return report + return validate_pack_dir(PACK_DIR, pack_id="grc", language="grc") if __name__ == "__main__": import pprint + pprint.pprint(validate_pack()) diff --git a/packs/he/lift_rules.py b/packs/he/lift_rules.py index fe65da35..2fd8942c 100644 --- a/packs/he/lift_rules.py +++ b/packs/he/lift_rules.py @@ -1,42 +1,14 @@ -""" -Lift rules for the Hebrew depth pack. - -Responsibility: receive a LinguisticAnalysis from the he pack analyzer -and return a CandidatePressureBatch. - -Hebrew-specific lift requirements: - - Binyan (verb stem) must be resolved before field_target is selected. - The same root in qal vs. hiphil may lift into different field targets. - - Aspect (qatal/yiqtol/wayyiqtol) contributes to the pressure kind - and temporal annotation of the candidate. - - Construct chains must be handled as relational frames: the head - lemma and the genitive together determine the lift target. - - The implicit copula: when haya is absent, the copular frame is - inferred from syntactic position, not from a present verb form. - - bara in qal: always lifts into creation.act.ex-nihilo. - The divine-agent constraint must be verified before this lift. - -Current status: - Blocked on LinguisticAnalysis contract (he pack specific: must carry - binyan, aspect, and construct-chain annotations). -""" +"""Deterministic lift rules for the Hebrew depth pack.""" from __future__ import annotations -from typing import TYPE_CHECKING -if TYPE_CHECKING: - from core_ingest.pressure import CandidateGeometricPressure +from pathlib import Path + +from core_ingest.types import CandidateGeometricPressure +from packs.common.runtime_rules import lift_from_pack + +PACK_DIR = Path(__file__).parent -def lift(analysis: object) -> list["CandidateGeometricPressure"]: - """ - Lift a Hebrew LinguisticAnalysis into CandidateGeometricPressure packets. - - Blocked on: he pack LinguisticAnalysis contract — must carry - binyan, aspect, and construct-chain annotations before this - can be implemented correctly. - """ - raise NotImplementedError( - "he:lift — LinguisticAnalysis contract for Hebrew not yet finalized. " - "Must carry binyan, aspect, and construct-chain annotations." - ) +def lift(analysis: object) -> list[CandidateGeometricPressure]: + return lift_from_pack(PACK_DIR, analysis, language="he") diff --git a/packs/he/readback_rules.py b/packs/he/readback_rules.py index 6eced586..eddcf0d2 100644 --- a/packs/he/readback_rules.py +++ b/packs/he/readback_rules.py @@ -1,38 +1,9 @@ -""" -Readback rules for the Hebrew depth pack. - -Hebrew readback produces grammatical Biblical Hebrew surface realizations -from field state. This is a depth-pack operation: it is not invoked -by default, only when the model explicitly targets Hebrew articulation. - -Hebrew-specific readback requirements: - - Verb selection must respect binyan: the field target and agent - semantics together determine which binyan is appropriate. - - Aspect selection is not optional: qatal, yiqtol, and wayyiqtol - carry distinct temporal and narrative semantics. - - Nikud (vowel points) must be produced, not left unpointed. - Unpointed output is not a valid surface realization for this pack. - - Construct chains must be assembled correctly: head in construct - state, genitive following, no article on the construct head. - - The implicit copula must be handled: in nominal clauses, haya - is omitted in the present tense unless emphasis requires it. - -Current status: - Blocked on FieldState and SurfaceRealization types. -""" +"""Deterministic readback rules for the Hebrew depth pack.""" from __future__ import annotations +from packs.common.runtime_rules import SurfaceRealization, readback_from_intent -def readback(field_state: object, intent: object = None) -> object: - """ - Produce a grammatical Hebrew surface realization from a field state. - Blocked on: FieldState and SurfaceRealization types. - When implemented: must produce fully pointed (nikud) Hebrew output. - """ - raise NotImplementedError( - "he:readback — FieldState and SurfaceRealization types not yet " - "finalized. When implemented: output must be fully pointed Hebrew " - "with correct binyan, aspect, and construct-chain handling." - ) +def readback(field_state: object, intent: object = None) -> SurfaceRealization: + return readback_from_intent(field_state, intent, language="he") diff --git a/packs/he/validators.py b/packs/he/validators.py index c2efce72..f99f1078 100644 --- a/packs/he/validators.py +++ b/packs/he/validators.py @@ -1,93 +1,19 @@ -""" -Validators for the Hebrew depth pack. -Same gate structure as the en pack. Hebrew-specific gate notes inline. -""" +"""Executable validators for the Hebrew depth pack.""" from __future__ import annotations -import json + from pathlib import Path +from packs.common.validator import validate_pack_dir + PACK_DIR = Path(__file__).parent -def _gate_schema() -> tuple[bool, str]: - return False, "not yet wired" - - -def _gate_lexical() -> tuple[bool, str]: - seen = set() - for line in (PACK_DIR / "lemmas.jsonl").read_text(encoding="utf-8").splitlines(): - if not line.strip(): - continue - record = json.loads(line) - lid = record["lemma_id"] - if lid in seen: - return False, f"duplicate lemma_id: {lid}" - seen.add(lid) - return True, "ok" - - -def _gate_morphology() -> tuple[bool, str]: - known = set() - for line in (PACK_DIR / "lemmas.jsonl").read_text(encoding="utf-8").splitlines(): - if line.strip(): - known.add(json.loads(line)["lemma_id"]) - for line in (PACK_DIR / "morphology.jsonl").read_text(encoding="utf-8").splitlines(): - if not line.strip(): - continue - record = json.loads(line) - if record["lemma_id"] not in known: - return False, f"unknown lemma_id in morphology: {record['lemma_id']}" - return True, "ok" - - -def _gate_lift() -> tuple[bool, str]: - # Hebrew lift additionally requires binyan and aspect annotations - # in the LinguisticAnalysis. These are not yet specified. - return False, "he:lift() not yet implemented — requires binyan/aspect in LinguisticAnalysis" - - -def _gate_readback() -> tuple[bool, str]: - return False, "he:readback() not yet implemented — must produce pointed Hebrew output" - - -def _gate_determinism() -> tuple[bool, str]: - return False, "depends on lift and readback" - - -def _gate_alignment() -> tuple[bool, str]: - return False, "anchors() not yet implemented" - - -def _gate_coverage() -> tuple[bool, str]: - return False, "depends on lift and readback gates" - - -GATES = [ - ("schema", _gate_schema), - ("lexical", _gate_lexical), - ("morphology", _gate_morphology), - ("lift", _gate_lift), - ("readback", _gate_readback), - ("determinism", _gate_determinism), - ("alignment", _gate_alignment), - ("coverage", _gate_coverage), -] - - def validate_pack() -> dict: - report = {"pack_id": "he", "active": False, "gates": {}} - for name, gate_fn in GATES: - passed, reason = gate_fn() - report["gates"][name] = {"passed": passed, "reason": reason} - if not passed: - for remaining_name, _ in GATES[GATES.index((name, gate_fn)) + 1:]: - report["gates"][remaining_name] = {"passed": False, "reason": "blocked by prior gate failure"} - return report - report["active"] = True - return report + return validate_pack_dir(PACK_DIR, pack_id="he", language="he") if __name__ == "__main__": import pprint + pprint.pprint(validate_pack()) diff --git a/sensorium/adapters/text.py b/sensorium/adapters/text.py index 2ce34706..c067c8a2 100644 --- a/sensorium/adapters/text.py +++ b/sensorium/adapters/text.py @@ -101,6 +101,32 @@ class TextProjectionHead: return False +class TextSurfaceDecoder: + """Exact text reconstruction over the mounted modality vocabulary.""" + + modality: Modality = Modality.TEXT + + def __init__(self, vocabulary: ModalityVocabulary) -> None: + self._vocab = vocabulary + + def decode(self, mv: np.ndarray) -> str: + query = np.asarray(mv, dtype=np.float32) + best_token: str | None = None + best_distance = float("inf") + for token in self._vocab._point_keys: + point = self._vocab.get_point(token) + distance = float(np.linalg.norm(query - point)) + if distance < best_distance: + best_distance = distance + best_token = str(token) + if best_token is None: + raise ValueError("cannot decode from an empty text vocabulary") + return best_token + + def decode_batch(self, mvs: np.ndarray) -> list[str]: + return [self.decode(mv) for mv in np.asarray(mvs, dtype=np.float32)] + + def make_text_pack( pack_id: str, vocabulary: ModalityVocabulary | None = None, @@ -124,13 +150,14 @@ def make_text_pack( """ vocab = vocabulary if vocabulary is not None else ModalityVocabulary() head = TextProjectionHead(vocab, oov_policy=oov_policy) + decoder = TextSurfaceDecoder(vocab) return ModalityPack( pack_id=pack_id, modality_type=Modality.TEXT, projection=head, - decoder=None, # text decode not yet implemented + decoder=decoder, vocabulary=vocab, - grammar_scaffold=None, # populated during Supervised Seeding Epoch + grammar_scaffold=None, checksum_verified=checksum_verified, gate_engaged=gate_engaged, language_role=language_role, diff --git a/session/context.py b/session/context.py index f787edaf..9c2ab85e 100644 --- a/session/context.py +++ b/session/context.py @@ -46,6 +46,8 @@ class SessionContext: node=node_idx, step=injected.step, holonomy=injected.holonomy, + energy=injected.energy, + valence=injected.valence, ) else: self.state = FieldState( @@ -53,6 +55,8 @@ class SessionContext: node=node_idx, step=self.state.step + 1, holonomy=injected.holonomy, + energy=injected.energy, + valence=injected.valence, ) self.vault.store(self.state.F, {"turn": self.turn, "role": "user"}) return self.state diff --git a/vocab/manifold.py b/vocab/manifold.py index e01a2f16..2e5f3ccc 100644 --- a/vocab/manifold.py +++ b/vocab/manifold.py @@ -32,6 +32,8 @@ import numpy as np from algebra.backend import cga_inner from algebra.cl41 import geometric_product, reverse from algebra.versor import versor_unit_residual +from core.physics.energy import EnergyProfile +from core.physics.valence import ValenceBundle from language_packs.schema import MorphologyEntry _MANIFOLD_RESIDUAL_TOLERANCE = 1e-5 @@ -68,6 +70,8 @@ class VocabManifold: self._versors: list[np.ndarray] = [] # each shape (32,), unit-versor ±1 self._morphology_by_word: dict[str, MorphologyEntry] = {} self._language_by_word: dict[str, str] = {} + self._energy_by_word: dict[str, EnergyProfile] = {} + self._valence_by_word: dict[str, ValenceBundle] = {} self._transient_words: set[str] = set() self._unknown_token_log: list[dict[str, object]] = [] @@ -77,6 +81,8 @@ class VocabManifold: versor: np.ndarray, morphology: MorphologyEntry | None = None, language: str | None = None, + energy: EnergyProfile | None = None, + valence: ValenceBundle | None = None, ) -> None: """ Register a word-versor pair. @@ -103,6 +109,10 @@ class VocabManifold: self._language_by_word[word] = resolved_language if morphology is not None: self._morphology_by_word[word] = morphology + if energy is not None: + self._energy_by_word[word] = energy + if valence is not None: + self._valence_by_word[word] = valence def insert_transient(self, word: str, versor: np.ndarray) -> None: """ @@ -203,6 +213,14 @@ class VocabManifold: """Return structured morphology for a stored surface, if the pack provided it.""" return self._morphology_by_word.get(word) + def energy_for_word(self, word: str) -> EnergyProfile | None: + """Return ADR-0006 energy profile for a stored surface, when available.""" + return self._energy_by_word.get(word) + + def valence_for_word(self, word: str) -> ValenceBundle | None: + """Return ADR-0007 valence bundle for a stored surface, when available.""" + return self._valence_by_word.get(word) + def language_for_word(self, word: str) -> str | None: """Return the language code for a stored surface, if known.""" morphology = self._morphology_by_word.get(word)