From 651de239b962e796439dde6190ba3f21f6536d6b Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 23 Aug 2026 19:18:17 +0000 Subject: [PATCH 1/4] fix(inference): remove ghost bridge migration authority MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit engine/inference_bridge.py was a tombstone that raised ImportError while directing callers to engine.inference_bridge_v2.DerivationGraph and docs/migration/inference_bridge_v2.md. Neither exists in this repository, so the module advertised a successor protocol that was never implemented. Reverse-import verification at this base found no live consumer: the only references were the module's own text and a stale comment in engine/startup_wiring.py. The tombstone is deleted rather than replaced — no successor bridge is introduced for compatibility alone. Task: CEG-001 Claude-Session: https://claude.ai/code/session_01Fc1ayR9FNiXRQ22HxMSRsh --- engine/inference_bridge.py | 28 ---------------------------- 1 file changed, 28 deletions(-) delete mode 100644 engine/inference_bridge.py diff --git a/engine/inference_bridge.py b/engine/inference_bridge.py deleted file mode 100644 index c5b94188..00000000 --- a/engine/inference_bridge.py +++ /dev/null @@ -1,28 +0,0 @@ -""" ---- L9_META --- -l9_schema: 1 -origin: engine-specific -engine: graph -layer: [config] -tags: [inference, bridge] -owner: engine-team -status: active ---- /L9_META --- - -GAP-9 FIX: Replace engine/inference_bridge.py with this file. - -Any direct import of the v1 bridge now raises ImportError immediately, -forcing all callers to migrate to inference_bridge_v2.py (DAG engine). - -This eliminates silent bypass of the DerivationGraph topological sort -and unlock-value targeting that the v1 bridge was causing. -""" - -raise ImportError( - "engine.inference_bridge (v1) is DISABLED.\n\n" - "It bypasses the DerivationGraph DAG engine, causing inference to fire " - "outside topological sort order with no unlock-value targeting.\n\n" - "Migrate all callers to:\n" - " from engine.inference_bridge_v2 import DerivationGraph\n\n" - "See docs/migration/inference_bridge_v2.md for the migration guide." -) From 5815927d59ad273bce85a4a935802d8828649d5c Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 23 Aug 2026 19:18:17 +0000 Subject: [PATCH 2/4] fix(inference): remove unowned raw KB rule loading MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit engine/startup_wiring.py attempted `load_domain_rules(spec.kb)` for every domain, but DomainSpec does not declare a `kb` field and the production GraphLifecycle boot path never invokes apply_all_gap_fixes(). The path was dead and, had it run, would have raised AttributeError. Removes the dead recipe block, the raw-dictionary rule loader (load_domain_rules / _register_condition_rule), and the stale bridge comment. The typed DomainSpec / DomainPackLoader boundary stays the single owner of domain configuration; no second rule-configuration surface and no `kb` field are added. All built-in @register_inference_rule functions and the execute_rule() registry boundary are preserved unchanged. InferenceContext.domain_kb is retained as optional caller-supplied tuning context — infer_material_grade_from_mfi still reads it — but it is no longer documented as populated by domain-spec injection. Task: CEG-002 Claude-Session: https://claude.ai/code/session_01Fc1ayR9FNiXRQ22HxMSRsh --- engine/inference_rule_registry.py | 94 +++---------------------------- engine/startup_wiring.py | 14 +---- 2 files changed, 9 insertions(+), 99 deletions(-) diff --git a/engine/inference_rule_registry.py b/engine/inference_rule_registry.py index c9b9e0b6..fbc9ca44 100644 --- a/engine/inference_rule_registry.py +++ b/engine/inference_rule_registry.py @@ -13,8 +13,8 @@ Previously _RULE_REGISTRY was empty at startup, causing every `derived_from` inference rule that referenced a named rule to block with NO_RULE. -This module registers all production inference functions and wires them -into the DerivationGraph execution engine. +This module registers the supported in-code inference functions and exposes +them through the explicit execute_rule() registry boundary. Registration pattern: @register_inference_rule("rule_name") @@ -47,7 +47,8 @@ class InferenceContext: domain_id: str pass_number: int known_fields: dict[str, Any] = field(default_factory=dict) - domain_kb: dict[str, Any] = field(default_factory=dict) # from domain spec KB injection + # Optional caller-supplied tuning context. DomainSpec/DomainPackLoader do not populate it. + domain_kb: dict[str, Any] = field(default_factory=dict) confidence_floor: float = 0.55 @@ -132,8 +133,9 @@ def execute_rule( # =========================================================================== # PRODUCTION INFERENCE RULES -# Register all domain-agnostic rules here. Domain-specific rules are -# loaded from domain KB via load_domain_rules() below. +# Register supported in-code inference rules here. Domain configuration remains +# owned by the typed DomainSpec/DomainPackLoader boundary; raw rule dictionaries +# are not loaded into this registry. # =========================================================================== @@ -416,87 +418,5 @@ def infer_buyer_persona(entity: dict, ctx: InferenceContext) -> InferenceResult ) -# --------------------------------------------------------------------------- -# Dynamic domain rule loader (Gap-3: KB injection pathway) -# --------------------------------------------------------------------------- - - -def load_domain_rules(domain_kb: dict[str, Any]) -> int: - """ - Load domain-specific inference rules from domain KB. - Returns count of rules registered. - - The KB may contain a 'inference_rules' list of: - {name: str, field: str, conditions: [...], value: Any, confidence: float} - - Simple condition-based rules are auto-registered as closures. - Complex rules should be registered via @register_inference_rule in domain pack files. - """ - rules_spec = domain_kb.get("inference_rules", []) - registered = 0 - for spec in rules_spec: - rule_name = spec.get("name") - if not rule_name: - continue - if rule_name in _RULE_REGISTRY: - continue # already registered — don't overwrite - _register_condition_rule(rule_name, spec) - registered += 1 - logger.info("Loaded %d domain-specific inference rules from KB", registered) - return registered - - -def _register_condition_rule(rule_name: str, spec: dict[str, Any]) -> None: - """Auto-generate and register a simple condition-based inference rule.""" - target_field = spec["field"] - conditions = spec.get("conditions", []) - default_value = spec.get("value") - confidence = float(spec.get("confidence", 0.65)) - - def _rule(entity: dict, ctx: InferenceContext) -> InferenceResult | None: - for cond in conditions: - src_field = cond.get("source_field") - operator = cond.get("operator", "eq") - cond_value = cond.get("value") - entity_val = entity.get(src_field) - if entity_val is None: - return None - match operator: - case "eq": - if entity_val != cond_value: - return None - case "gt": - try: - if float(entity_val) <= float(cond_value): - return None - except (TypeError, ValueError): - return None - case "lt": - try: - if float(entity_val) >= float(cond_value): - return None - except (TypeError, ValueError): - return None - case "contains": - if str(cond_value).lower() not in str(entity_val).lower(): - return None - case _: - return None - output_value = spec.get("output_value", default_value) - if output_value is None: - return None - return InferenceResult( - field_name=target_field, - value=output_value, - confidence=confidence, - rule_name=rule_name, - provenance="domain_kb", - rationale="condition rule from KB", - ) - - _RULE_REGISTRY[rule_name] = _rule - logger.debug("Auto-registered condition rule: %s → %s", rule_name, target_field) - - def list_registered_rules() -> list[str]: return sorted(_RULE_REGISTRY.keys()) diff --git a/engine/startup_wiring.py b/engine/startup_wiring.py index 6229badb..38695f2c 100644 --- a/engine/startup_wiring.py +++ b/engine/startup_wiring.py @@ -39,15 +39,6 @@ async def apply_all_gap_fixes(pg_dsn: str, neo4j_driver, domain_pack_loader) -> await configure_audit_pool(pg_pool) logger.info("startup: Gap-5 audit pool wired") - # ── Gap 3: Load domain KB rules into inference registry ────────────────── - from engine.inference_rule_registry import load_domain_rules - - for domain_id in domain_pack_loader.list_domains(): - spec = domain_pack_loader.load_domain(domain_id) - if spec and spec.kb: - load_domain_rules(spec.kb) - logger.info("startup: Gap-3 inference rules loaded") - # ── Gap 2: Initialise GRAPH→ENRICH return channel ──────────────────────── from engine.graph_return_channel import GraphToEnrichReturnChannel @@ -68,8 +59,7 @@ async def apply_all_gap_fixes(pg_dsn: str, neo4j_driver, domain_pack_loader) -> except ImportError: logger.warning("startup: GDSScheduler not found — register Gap-6 hook manually") - # ── Gap 9: v1 bridge blocked by file replacement (no action needed here) ── - # engine/inference_bridge.py has been replaced with inference_bridge_v1_guard.py - # Any stray import will raise ImportError at import time, not at startup. + # Gap 9: the removed v1 inference bridge has no startup wiring. + # Do not add a successor bridge unless a real producer/consumer contract exists. logger.info("startup: all gap fixes applied successfully") From 32f736aead3f510a5b34cc3517aa5ad50e339d24 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 23 Aug 2026 19:18:17 +0000 Subject: [PATCH 3/4] test(inference): lock removed bridge and KB contracts Adds tests/gap_fixes/test_gap9_inference_authority.py alongside the existing gap-fix test owners. It asserts that the bridge module stays deleted, that the startup recipe does not reintroduce spec.kb / load_domain_rules, that the registry exposes no raw-KB or n-ary symbols, that no engine/ source references the nonexistent v2 successor or its migration guide, and that execute_rule() still returns the canonical InferenceResult for a registered built-in rule. Task: CEG-003 (regression portion) Claude-Session: https://claude.ai/code/session_01Fc1ayR9FNiXRQ22HxMSRsh --- .../test_gap9_inference_authority.py | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 tests/gap_fixes/test_gap9_inference_authority.py diff --git a/tests/gap_fixes/test_gap9_inference_authority.py b/tests/gap_fixes/test_gap9_inference_authority.py new file mode 100644 index 00000000..89aff02a --- /dev/null +++ b/tests/gap_fixes/test_gap9_inference_authority.py @@ -0,0 +1,51 @@ +"""Regression coverage for removed inference-bridge and raw-KB authority.""" + +from __future__ import annotations + +from pathlib import Path + +import engine.inference_rule_registry as registry +from engine.inference_rule_registry import InferenceContext, InferenceResult, execute_rule + +ROOT = Path(__file__).resolve().parents[2] + + +def test_removed_inference_bridge_has_no_compatibility_module() -> None: + assert not (ROOT / "engine" / "inference_bridge.py").exists() + + +def test_startup_recipe_does_not_reintroduce_undeclared_kb_loading() -> None: + source = (ROOT / "engine" / "startup_wiring.py").read_text(encoding="utf-8") + assert "spec.kb" not in source + assert "load_domain_rules" not in source + + +def test_registry_exposes_only_supported_in_code_rule_surface() -> None: + assert not hasattr(registry, "load_domain_rules") + assert not hasattr(registry, "NaryFact") + assert not hasattr(registry, "to_rule_engine_format") + + +def test_supported_registry_still_returns_canonical_result_type() -> None: + context = InferenceContext(tenant_id="test", domain_id="plasticos", pass_number=1) + result = execute_rule( + "infer_material_grade_from_mfi", + {"melt_flow_index": 5.0, "material_type": "HDPE"}, + context, + ) + assert isinstance(result, InferenceResult) + assert result.field_name == "material_grade" + assert result.value == "HD_injection" + + +def test_runtime_surface_contains_no_ghost_successor_reference() -> None: + ghost_module = "inference_bridge" + "_v2" + ghost_doc = "docs/migration/" + ghost_module + ".md" + offenders: list[str] = [] + + for path in sorted((ROOT / "engine").rglob("*.py")): + source = path.read_text(encoding="utf-8") + if ghost_module in source or ghost_doc in source: + offenders.append(str(path.relative_to(ROOT))) + + assert offenders == [] From 030f2a925da59ee0a46fe08c6fd24d66297347f6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 23 Aug 2026 19:26:47 +0000 Subject: [PATCH 4/4] style(test): use a single import style for the inference registry github-code-quality flagged tests/gap_fixes/test_gap9_inference_authority.py for importing engine.inference_rule_registry with both `import ... as` and `from ... import ...`. Keeps the module alias only, and reaches InferenceContext, InferenceResult and execute_rule through it. The module-surface hasattr() assertions stay exactly as they were; no test behaviour changes. Claude-Session: https://claude.ai/code/session_01Fc1ayR9FNiXRQ22HxMSRsh --- tests/gap_fixes/test_gap9_inference_authority.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/gap_fixes/test_gap9_inference_authority.py b/tests/gap_fixes/test_gap9_inference_authority.py index 89aff02a..95c9ace0 100644 --- a/tests/gap_fixes/test_gap9_inference_authority.py +++ b/tests/gap_fixes/test_gap9_inference_authority.py @@ -4,8 +4,7 @@ from pathlib import Path -import engine.inference_rule_registry as registry -from engine.inference_rule_registry import InferenceContext, InferenceResult, execute_rule +from engine import inference_rule_registry as registry ROOT = Path(__file__).resolve().parents[2] @@ -27,13 +26,13 @@ def test_registry_exposes_only_supported_in_code_rule_surface() -> None: def test_supported_registry_still_returns_canonical_result_type() -> None: - context = InferenceContext(tenant_id="test", domain_id="plasticos", pass_number=1) - result = execute_rule( + context = registry.InferenceContext(tenant_id="test", domain_id="plasticos", pass_number=1) + result = registry.execute_rule( "infer_material_grade_from_mfi", {"melt_flow_index": 5.0, "material_type": "HDPE"}, context, ) - assert isinstance(result, InferenceResult) + assert isinstance(result, registry.InferenceResult) assert result.field_name == "material_grade" assert result.value == "HD_injection"