From 5fde0829c59f7f45196dcb3bc3bd39b1f2b53f13 Mon Sep 17 00:00:00 2001 From: Jonah Gray Date: Wed, 8 Jul 2026 00:07:29 -0700 Subject: [PATCH 1/2] fix(OMN-14141): fail-closed guard + CI gate for handler_routing flat-schema silent-zero-parse MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The flat handler_class:/handler_module: schema silently parsed to zero handlers, then auto-wiring reported WIRED with zero dispatchers while still subscribing to and committing Kafka offsets on the topic — silent phantom-wiring (the WI-14 root cause, OMN-14139/OMN-14135). Enforcement (CLAUDE.md Rule #5 — gate ships WITH the fixes): - Guard A (parse-time): discovery._parse_handler_routing raises on a flat/ malformed handlers[] entry instead of silently skipping it. Legacy top-level handler: fallback preserved (empty handlers list never raises). - Guard B (wire-time): _commit_contract_wiring reports FAILED (not WIRED) and does NOT subscribe when a contract declares subscribe topics but registers zero dispatchers. Never fires on legacy fallbacks (>=1 dispatcher) or resolver-ownership/quarantine skips. - CI gate + pre-commit hook: handler_routing_schema validator rejects flat entries across all node contracts (SYNC'd ci.yml step + pre-commit hook). Contract fixes (7 infra contracts the gate would reject; ledger done in #2234): node_savings_estimation_compute, node_build_loop_write_effect, node_session_state_effect, node_pattern_lifecycle_effect, node_validation_executor, node_decision_store_query_compute, node_baseline_comparison_compute. Verified: 111/111 contracts discover with zero errors; Guard B fires on zero real contracts; 354 auto-wiring/kernel tests + 11 new guard tests green. Follow-up (separate PR): 3 omnimarket flat-schema contracts + omnimarket-side gate. --- .github/workflows/ci.yml | 11 + .pre-commit-config.yaml | 14 + .../contract.yaml | 5 +- .../contract.yaml | 5 +- .../contract.yaml | 5 +- .../contract.yaml | 9 +- .../contract.yaml | 5 +- .../node_session_state_effect/contract.yaml | 25 +- .../node_validation_executor/contract.yaml | 5 +- .../runtime/auto_wiring/discovery.py | 38 ++- .../runtime/auto_wiring/handler_wiring.py | 34 +++ .../validators/handler_routing_schema.py | 166 ++++++++++++ .../test_handler_routing_flat_schema_guard.py | 254 ++++++++++++++++++ 13 files changed, 546 insertions(+), 30 deletions(-) create mode 100644 src/omnibase_infra/validators/handler_routing_schema.py create mode 100644 tests/unit/runtime/auto_wiring/test_handler_routing_flat_schema_guard.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index be45c6d164..35cc5794ad 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -144,6 +144,17 @@ jobs: - name: operation_match operation-field gate (OMN-13530) run: uv run python -m omnibase_core.validators.operation_match_requires_operation src/omnibase_infra + # OMN-14141: every handler_routing.handlers[] entry must carry a nested + # 'handler: {name, module}' mapping. The flat 'handler_class'/ + # 'handler_module' schema silently parses to ZERO dispatchers and then + # phantom-wires the subscribed topic (WIRED with no live handler, offsets + # still committed) — the WI-14 root cause (OMN-14139/OMN-14135). Fail-closed + # gate paired with the runtime parse guard in + # auto_wiring/discovery._parse_handler_routing. SYNC with pre-commit hook + # 'handler-routing-schema-gate'. + - name: handler_routing flat-schema gate (OMN-14141) + run: PYTHONPATH=src uv run python -m omnibase_infra.validators.handler_routing_schema src/omnibase_infra + - name: Check handler Any signature gate (OMN-10820) run: PYTHONPATH=src uv run python -m omnibase_infra.validators.handler_any_signature --max-violations 0 src/omnibase_infra diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cd7a7fcebe..7d53c2b15e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -252,6 +252,20 @@ repos: files: (^|/)contract\.yaml$ pass_filenames: true stages: [pre-commit] + # OMN-14141: reject flat handler_class/handler_module handler_routing + # entries. They silently parse to ZERO dispatchers and phantom-wire the + # subscribed topic (WIRED, Kafka offsets committed, no live handler) — the + # WI-14 root cause (OMN-14139/OMN-14135). Every routed handler must use the + # nested handler:{name,module} shape. Fail-closed companion to the runtime + # parse guard in auto_wiring/discovery._parse_handler_routing. SYNC with + # ci.yml step "handler_routing flat-schema gate". + - id: handler-routing-schema-gate + name: handler_routing flat-schema gate (OMN-14141) + entry: env PYTHONPATH=src uv run --frozen python -m omnibase_infra.validators.handler_routing_schema + language: system + files: (^|/)contract\.yaml$ + pass_filenames: true + stages: [pre-commit] # Migration freeze enforcement - block new migrations during DB-per-repo refactor - id: onex-validate-migration-freeze name: ONEX Migration Freeze Enforcement diff --git a/src/omnibase_infra/nodes/node_baseline_comparison_compute/contract.yaml b/src/omnibase_infra/nodes/node_baseline_comparison_compute/contract.yaml index 97fe91805c..b66b022b73 100644 --- a/src/omnibase_infra/nodes/node_baseline_comparison_compute/contract.yaml +++ b/src/omnibase_infra/nodes/node_baseline_comparison_compute/contract.yaml @@ -47,8 +47,9 @@ handler_routing: routing_strategy: "operation_match" handlers: - operation: "baseline.compare" - handler_class: "HandlerBaselineComparison" - handler_module: "omnibase_infra.nodes.node_baseline_comparison_compute.handlers.handler_baseline_comparison" + handler: + name: "HandlerBaselineComparison" + module: "omnibase_infra.nodes.node_baseline_comparison_compute.handlers.handler_baseline_comparison" description: "Compute cost/outcome deltas and ROI from paired run results" input_model: name: "ModelBaselineComparisonInput" diff --git a/src/omnibase_infra/nodes/node_build_loop_write_effect/contract.yaml b/src/omnibase_infra/nodes/node_build_loop_write_effect/contract.yaml index 3db194cd09..1446f01f26 100644 --- a/src/omnibase_infra/nodes/node_build_loop_write_effect/contract.yaml +++ b/src/omnibase_infra/nodes/node_build_loop_write_effect/contract.yaml @@ -43,8 +43,9 @@ handler_routing: routing_strategy: "operation_match" handlers: - operation: "build_loop.append" - handler_class: "HandlerBuildLoopAppend" - handler_module: "omnibase_infra.nodes.node_build_loop_write_effect.handlers.handler_build_loop_append" + handler: + name: "HandlerBuildLoopAppend" + module: "omnibase_infra.nodes.node_build_loop_write_effect.handlers.handler_build_loop_append" description: "Append-only INSERT into build_loop_runs" capabilities: - name: "build_loop.write" diff --git a/src/omnibase_infra/nodes/node_decision_store_query_compute/contract.yaml b/src/omnibase_infra/nodes/node_decision_store_query_compute/contract.yaml index 1cfd96cea8..96df0ff286 100644 --- a/src/omnibase_infra/nodes/node_decision_store_query_compute/contract.yaml +++ b/src/omnibase_infra/nodes/node_decision_store_query_compute/contract.yaml @@ -57,8 +57,9 @@ handler_routing: default_handler: "omnibase_infra.nodes.node_decision_store_query_compute.handlers.handler_query_decisions:HandlerQueryDecisions" handlers: - operation: "decision_store.query_decisions" - handler_class: "HandlerQueryDecisions" - handler_module: "omnibase_infra.nodes.node_decision_store_query_compute.handlers.handler_query_decisions" + handler: + name: "HandlerQueryDecisions" + module: "omnibase_infra.nodes.node_decision_store_query_compute.handlers.handler_query_decisions" description: > Cursor-paginated SELECT from decision_store with dynamic WHERE clause, scope_services filter (ANY/ALL/EXACT), and COUNT(*) for total_active. diff --git a/src/omnibase_infra/nodes/node_pattern_lifecycle_effect/contract.yaml b/src/omnibase_infra/nodes/node_pattern_lifecycle_effect/contract.yaml index 5ec0967a10..7b12f0c30d 100644 --- a/src/omnibase_infra/nodes/node_pattern_lifecycle_effect/contract.yaml +++ b/src/omnibase_infra/nodes/node_pattern_lifecycle_effect/contract.yaml @@ -59,13 +59,10 @@ handler_routing: routing_strategy: "operation_match" handlers: - operation: "lifecycle.update" - handler_class: "HandlerLifecycleUpdate" - handler_module: "omnibase_infra.nodes.node_pattern_lifecycle_effect.handlers.handler_lifecycle_update" + handler: + name: "HandlerLifecycleUpdate" + module: "omnibase_infra.nodes.node_pattern_lifecycle_effect.handlers.handler_lifecycle_update" description: "Apply verdict and compute tier transition" - input_model: - name: "ModelLifecycleState" - module: "omnibase_infra.nodes.node_pattern_lifecycle_effect.models" - description: "Current lifecycle state plus verdict to apply." # Error handling error_handling: retry_policy: diff --git a/src/omnibase_infra/nodes/node_savings_estimation_compute/contract.yaml b/src/omnibase_infra/nodes/node_savings_estimation_compute/contract.yaml index 08f66a377b..dc4bfc0afe 100644 --- a/src/omnibase_infra/nodes/node_savings_estimation_compute/contract.yaml +++ b/src/omnibase_infra/nodes/node_savings_estimation_compute/contract.yaml @@ -42,8 +42,9 @@ handler_routing: routing_strategy: "operation_match" handlers: - operation: "savings.estimate" - handler_class: "HandlerSavingsEstimation" - handler_module: "omnibase_infra.nodes.node_savings_estimation_compute.handlers.handler_savings_estimation" + handler: + name: "HandlerSavingsEstimation" + module: "omnibase_infra.nodes.node_savings_estimation_compute.handlers.handler_savings_estimation" description: "Compute savings from injection effectiveness metrics" input_model: name: "ModelSavingsEstimationInput" diff --git a/src/omnibase_infra/nodes/node_session_state_effect/contract.yaml b/src/omnibase_infra/nodes/node_session_state_effect/contract.yaml index 3f0998d52a..90881bfe39 100644 --- a/src/omnibase_infra/nodes/node_session_state_effect/contract.yaml +++ b/src/omnibase_infra/nodes/node_session_state_effect/contract.yaml @@ -87,36 +87,41 @@ handler_routing: routing_strategy: "operation_match" handlers: - operation: "session.index.read" - handler_class: "HandlerSessionIndexRead" - handler_module: "omnibase_infra.nodes.node_session_state_effect.handlers.handler_session_index_read" + handler: + name: "HandlerSessionIndexRead" + module: "omnibase_infra.nodes.node_session_state_effect.handlers.handler_session_index_read" description: "Read session.json index" input_model: name: "None" description: "No input required (reads current session.json from disk)." - operation: "session.index.write" - handler_class: "HandlerSessionIndexWrite" - handler_module: "omnibase_infra.nodes.node_session_state_effect.handlers.handler_session_index_write" + handler: + name: "HandlerSessionIndexWrite" + module: "omnibase_infra.nodes.node_session_state_effect.handlers.handler_session_index_write" description: "Atomic write session.json with flock" input_model: name: "ModelSessionIndex" module: "omnibase_infra.nodes.node_session_state_effect.models" - operation: "session.run.read" - handler_class: "HandlerRunContextRead" - handler_module: "omnibase_infra.nodes.node_session_state_effect.handlers.handler_run_context_read" + handler: + name: "HandlerRunContextRead" + module: "omnibase_infra.nodes.node_session_state_effect.handlers.handler_run_context_read" description: "Read run context document" input_model: name: "str" description: "run_id string identifying the run context to read." - operation: "session.run.write" - handler_class: "HandlerRunContextWrite" - handler_module: "omnibase_infra.nodes.node_session_state_effect.handlers.handler_run_context_write" + handler: + name: "HandlerRunContextWrite" + module: "omnibase_infra.nodes.node_session_state_effect.handlers.handler_run_context_write" description: "Atomic write run context document" input_model: name: "ModelRunContext" module: "omnibase_infra.nodes.node_session_state_effect.models" - operation: "session.gc" - handler_class: "HandlerStaleRunGC" - handler_module: "omnibase_infra.nodes.node_session_state_effect.handlers.handler_stale_run_gc" + handler: + name: "HandlerStaleRunGC" + module: "omnibase_infra.nodes.node_session_state_effect.handlers.handler_stale_run_gc" description: "Garbage-collect stale run documents" input_model: name: "None" diff --git a/src/omnibase_infra/nodes/node_validation_executor/contract.yaml b/src/omnibase_infra/nodes/node_validation_executor/contract.yaml index 1b9d505406..4d1ef7c0bb 100644 --- a/src/omnibase_infra/nodes/node_validation_executor/contract.yaml +++ b/src/omnibase_infra/nodes/node_validation_executor/contract.yaml @@ -59,8 +59,9 @@ handler_routing: routing_strategy: "operation_match" handlers: - operation: "validation.run_checks" - handler_class: "HandlerRunChecks" - handler_module: "omnibase_infra.nodes.node_validation_executor.handlers.handler_run_checks" + handler: + name: "HandlerRunChecks" + module: "omnibase_infra.nodes.node_validation_executor.handlers.handler_run_checks" description: "Execute all checks in a validation plan" input_model: name: "ModelValidationPlan" diff --git a/src/omnibase_infra/runtime/auto_wiring/discovery.py b/src/omnibase_infra/runtime/auto_wiring/discovery.py index c54d2c6a4d..065be3b93d 100644 --- a/src/omnibase_infra/runtime/auto_wiring/discovery.py +++ b/src/omnibase_infra/runtime/auto_wiring/discovery.py @@ -483,14 +483,44 @@ def _extract_runtime_profiles(raw: dict) -> tuple[str, ...]: def _parse_handler_routing(hr_raw: dict) -> ModelHandlerRouting: - """Parse the handler_routing section from a contract YAML dict.""" + """Parse the handler_routing section from a contract YAML dict. + + Fail-closed (OMN-14141): a ``handlers[]`` entry that cannot be parsed into a + dispatcher raises ``ValueError`` instead of being silently skipped. The + historical FLAT ``handler_class:`` / ``handler_module:`` string schema — which + ``ModelHandlerRoutingEntry`` does not understand — previously fell through the + ``continue`` here and produced ``handlers=()`` with NO error. Auto-wiring + then reported ``EnumWiringOutcome.WIRED`` with zero dispatchers while still + subscribing to and committing Kafka offsets on the topic: silent + phantom-wiring (the WI-14 root cause, OMN-14139/OMN-14135). Every routed + handler MUST carry a nested ``handler: {name, module}`` mapping. + + The legacy top-level ``handler: {module, class}`` fallback in + ``_parse_contract`` is unaffected: those contracts declare an EMPTY / ABSENT + ``handlers`` list, so this loop never runs and never raises — the zero-length + parse still triggers the fallback. + """ entries: list[ModelHandlerRoutingEntry] = [] - for h in hr_raw.get("handlers", []): + for index, h in enumerate(hr_raw.get("handlers", [])): if not isinstance(h, dict): - continue + raise ValueError( + f"handler_routing.handlers[{index}] must be a mapping, got " + f"{type(h).__name__} — cannot be parsed into a dispatcher " + "(OMN-14141)." + ) handler_ref_raw = h.get("handler") if not isinstance(handler_ref_raw, dict): - continue + raise ValueError( + f"handler_routing.handlers[{index}] is missing a nested " + "'handler: {name, module}' mapping (found keys: " + f"{sorted(h.keys())}). The flat 'handler_class'/'handler_module' " + "schema is not parseable and silently produces zero dispatchers, " + "which then phantom-wires the subscribed topic with no live " + "handler (OMN-14141). Use the nested shape:\n" + " handler:\n" + " name: \n" + " module: " + ) handler_ref = ModelHandlerRef( name=handler_ref_raw.get("name", ""), module=handler_ref_raw.get("module", ""), diff --git a/src/omnibase_infra/runtime/auto_wiring/handler_wiring.py b/src/omnibase_infra/runtime/auto_wiring/handler_wiring.py index f9972bb8a4..8ccbfb53dc 100644 --- a/src/omnibase_infra/runtime/auto_wiring/handler_wiring.py +++ b/src/omnibase_infra/runtime/auto_wiring/handler_wiring.py @@ -3126,6 +3126,40 @@ async def _commit_contract_wiring( ) ) + # Fail-closed phantom-wiring guard (OMN-14141). A contract that declares + # subscribe topics but registered ZERO dispatchers — and quarantined / + # resolver-skipped NOTHING — is a silent phantom-wire: the topic would be + # consumed and its Kafka offsets committed with no handler ever running. + # This is the wiring-side backstop for the flat-schema silent-zero-parse + # defect (the parse guard in discovery._parse_handler_routing is the first + # line). When all four hold, pcw.prepared_wirings was empty — handler_routing + # produced no parseable handlers. Legacy top-level ``handler:`` fallbacks and + # resolver-ownership skips always leave a dispatcher, a skipped_handler, or a + # quarantine, so this never fires on them. Returned as FAILED (not WIRED) so + # total_failed is accurate and ONEX_WIRING_STRICT_MODE crashes boot loudly; + # the topic is NOT subscribed either way. + if ( + pcw.subscription_topics + and not dispatchers_registered + and not skipped_handlers + and not quarantined + ): + return ModelContractWiringResult( + contract_name=contract.name, + package_name=contract.package_name, + outcome=EnumWiringOutcome.FAILED, + reason=( + "phantom wiring: contract declares " + f"{len(pcw.subscription_topics)} subscribe topic(s) but " + "registered zero dispatchers (handler_routing produced no " + "parseable handlers). Convert flat handler_class/handler_module " + "entries to nested handler:{name,module} (OMN-14141)." + ), + wirings=tuple(wirings), + skipped_handlers=tuple(skipped_handlers), + quarantined_handlers=tuple(quarantined), + ) + if subscribe_immediately and event_bus is not None and pcw.subscription_topics: topics_subscribed.extend( await _subscribe_contract_topics( diff --git a/src/omnibase_infra/validators/handler_routing_schema.py b/src/omnibase_infra/validators/handler_routing_schema.py new file mode 100644 index 0000000000..355373ea8b --- /dev/null +++ b/src/omnibase_infra/validators/handler_routing_schema.py @@ -0,0 +1,166 @@ +# SPDX-FileCopyrightText: 2025 OmniNode.ai Inc. +# SPDX-License-Identifier: MIT +"""Fail-closed gate for the handler_routing flat-schema defect class (OMN-14141). + +A ``handler_routing.handlers[]`` entry MUST carry a nested +``handler: {name, module}`` mapping. The historical FLAT +``handler_class:`` / ``handler_module:`` string schema is not understood by the +auto-wiring discovery parser (``ModelHandlerRoutingEntry``): it silently parsed +to ``handlers=()`` — zero handlers, no error — and auto-wiring then reported +``EnumWiringOutcome.WIRED`` with zero dispatchers while STILL subscribing to and +committing Kafka offsets on the topic. That is silent phantom-wiring: the topic +looks consumed but no handler ever runs (the WI-14 root cause, OMN-14139 / +OMN-14135). + +This gate is the mechanical enforcement companion (CLAUDE.md Rule #5) to the +runtime parse guard in +``omnibase_infra.runtime.auto_wiring.discovery._parse_handler_routing``. It scans +node ``contract.yaml`` files and fails when any ``handler_routing.handlers[]`` +entry cannot be parsed into a dispatcher (missing nested ``handler`` mapping). + +Legacy contracts that use the top-level ``handler: {module, class}`` fallback are +NOT flagged — that fallback declares an EMPTY / ABSENT ``handlers`` list, so it +has no offending ``handlers[]`` entry. + +Usage (pre-commit, per-file): + uv run python -m omnibase_infra.validators.handler_routing_schema \\ + src/omnibase_infra/nodes//contract.yaml + +Usage (CI, whole tree): + uv run python -m omnibase_infra.validators.handler_routing_schema src/omnibase_infra +""" + +from __future__ import annotations + +import argparse +import sys +from collections.abc import Iterator, Sequence +from dataclasses import dataclass +from pathlib import Path + +import yaml + +DEFAULT_SCAN_ROOT = Path("src/omnibase_infra") + + +@dataclass(frozen=True, slots=True) # internal-dataclass-ok: validator-internal finding +class RoutingSchemaFinding: + path: Path + handler_index: int + operation: str + found_keys: tuple[str, ...] + + def format(self) -> str: + op = f" operation={self.operation!r}" if self.operation else "" + return ( + f"{self.path}: handler_routing.handlers[{self.handler_index}]{op} " + f"has no nested 'handler: {{name, module}}' mapping " + f"(found keys: {list(self.found_keys)}). Flat " + f"'handler_class'/'handler_module' entries silently parse to ZERO " + f"dispatchers and phantom-wire the subscribed topic (OMN-14141)." + ) + + +def validate_file(path: Path) -> list[RoutingSchemaFinding]: + try: + raw = yaml.safe_load(path.read_text(encoding="utf-8")) + except (OSError, UnicodeDecodeError, yaml.YAMLError): + # Malformed / unreadable YAML is another gate's concern; do not crash here. + return [] + + if not isinstance(raw, dict): + return [] + + hr_raw = raw.get("handler_routing") + if not isinstance(hr_raw, dict): + return [] + + handlers = hr_raw.get("handlers") + if not isinstance(handlers, list): + return [] + + findings: list[RoutingSchemaFinding] = [] + for index, entry in enumerate(handlers): + if not isinstance(entry, dict): + findings.append( + RoutingSchemaFinding( + path=path, + handler_index=index, + operation="", + found_keys=(type(entry).__name__,), + ) + ) + continue + if isinstance(entry.get("handler"), dict): + continue # valid nested shape — the only accepted form + findings.append( + RoutingSchemaFinding( + path=path, + handler_index=index, + operation=str(entry.get("operation", "")), + found_keys=tuple(sorted(entry.keys())), + ) + ) + return findings + + +def validate_paths(paths: Sequence[Path]) -> list[RoutingSchemaFinding]: + findings: list[RoutingSchemaFinding] = [] + for path in _iter_contract_files(paths): + findings.extend(validate_file(path)) + return findings + + +def _iter_contract_files(paths: Sequence[Path]) -> Iterator[Path]: + scan_paths = paths or (DEFAULT_SCAN_ROOT,) + for path in scan_paths: + if path.is_file(): + if path.name == "contract.yaml" or path.name.endswith(".contract.yaml"): + yield path + elif path.is_dir(): + yield from sorted( + p for p in path.rglob("contract.yaml") if "__pycache__" not in p.parts + ) + + +def _parse_args(argv: Sequence[str]) -> argparse.Namespace: + parser = argparse.ArgumentParser( + description=( + "Fail-closed gate: reject handler_routing.handlers[] entries using " + "the flat handler_class/handler_module schema (OMN-14141)." + ) + ) + parser.add_argument( + "paths", + nargs="*", + type=Path, + default=[DEFAULT_SCAN_ROOT], + ) + return parser.parse_args(argv) + + +def main(argv: Sequence[str] | None = None) -> int: + args = _parse_args(sys.argv[1:] if argv is None else list(argv)) + findings = validate_paths(args.paths) + + if not findings: + return 0 + + for finding in findings: + sys.stderr.write(f" {finding.format()}\n") + + sys.stderr.write( + f"\n[handler-routing-schema-gate] {len(findings)} flat-schema " + "handler_routing entr(y/ies) found.\n" + "Every routed handler must use the nested shape:\n" + " handler:\n" + " name: \n" + " module: \n" + "The flat 'handler_class'/'handler_module' shape silently produces zero " + "dispatchers and phantom-wires the subscribed topic (OMN-14141).\n" + ) + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/unit/runtime/auto_wiring/test_handler_routing_flat_schema_guard.py b/tests/unit/runtime/auto_wiring/test_handler_routing_flat_schema_guard.py new file mode 100644 index 0000000000..e0e9a3bb53 --- /dev/null +++ b/tests/unit/runtime/auto_wiring/test_handler_routing_flat_schema_guard.py @@ -0,0 +1,254 @@ +# SPDX-FileCopyrightText: 2025 OmniNode.ai Inc. +# SPDX-License-Identifier: MIT +"""Fail-closed guards for the handler_routing flat-schema defect class (OMN-14141). + +The flat ``handler_class:``/``handler_module:`` schema silently parsed to zero +handlers and then phantom-wired the subscribed topic (WIRED with no dispatcher, +Kafka offsets still committed) — the WI-14 root cause (OMN-14139/OMN-14135). +These tests pin the three enforcement surfaces: + +1. Parse guard — ``discovery._parse_handler_routing`` raises on a flat/malformed + entry, does NOT raise on the nested shape or an empty handlers list, and does + NOT break the legacy top-level ``handler:`` fallback. +2. Wire guard — ``wire_from_manifest`` reports FAILED (not WIRED) when a contract + subscribes to topics but registers zero dispatchers, and does NOT fire on a + valid nested contract. +3. CI/pre-commit gate — the ``handler_routing_schema`` validator flags flat + entries and passes nested + legacy-fallback contracts. +""" + +from __future__ import annotations + +from pathlib import Path +from textwrap import dedent + +import pytest + +from omnibase_infra.runtime.auto_wiring.discovery import ( + _parse_contract, + _parse_handler_routing, +) +from omnibase_infra.runtime.auto_wiring.handler_wiring import wire_from_manifest +from omnibase_infra.runtime.auto_wiring.models import ( + ModelAutoWiringManifest, + ModelContractVersion, + ModelDiscoveredContract, + ModelEventBusWiring, + ModelHandlerRouting, +) +from omnibase_infra.runtime.auto_wiring.report import EnumWiringOutcome +from omnibase_infra.runtime.message_dispatch_engine import MessageDispatchEngine +from omnibase_infra.validators.handler_routing_schema import main, validate_file + + +class TestParseGuardFlatSchema: + """Guard A: discovery._parse_handler_routing fails closed on flat entries.""" + + @pytest.mark.unit + def test_flat_handler_class_module_entry_raises(self) -> None: + hr_raw = { + "routing_strategy": "operation_match", + "handlers": [ + { + "operation": "x.do", + "handler_class": "HandlerX", + "handler_module": "pkg.handlers.handler_x", + } + ], + } + with pytest.raises(ValueError, match="OMN-14141"): + _parse_handler_routing(hr_raw) + + @pytest.mark.unit + def test_non_dict_entry_raises(self) -> None: + hr_raw = {"routing_strategy": "operation_match", "handlers": ["not-a-dict"]} + with pytest.raises(ValueError, match="must be a mapping"): + _parse_handler_routing(hr_raw) + + @pytest.mark.unit + def test_nested_handler_entry_parses(self) -> None: + hr_raw = { + "routing_strategy": "operation_match", + "handlers": [ + { + "operation": "x.do", + "handler": {"name": "HandlerX", "module": "pkg.handlers.handler_x"}, + } + ], + } + routing = _parse_handler_routing(hr_raw) + assert len(routing.handlers) == 1 + assert routing.handlers[0].handler.name == "HandlerX" + + @pytest.mark.unit + def test_empty_handlers_list_does_not_raise(self) -> None: + # Empty/absent handlers is the precondition for the legacy fallback — it + # must never raise (the loop simply does not run). + routing = _parse_handler_routing( + {"routing_strategy": "operation_match", "handlers": []} + ) + assert routing.handlers == () + + @pytest.mark.unit + def test_legacy_top_level_handler_fallback_preserved(self, tmp_path: Path) -> None: + # A contract that declares an empty handler_routing (default_handler only) + # plus a top-level legacy handler: block must still resolve to one handler + # and must NOT trip the flat-schema guard. + path = tmp_path / "contract.yaml" + path.write_text( + dedent("""\ + name: legacy_node + node_type: orchestrator + handler: + module: omnimarket.handlers.ledger + class: HandlerLedger + input_model: omnimarket.models.ModelTick + handler_routing: + default_handler: omnimarket.handlers.ledger:HandlerLedger + event_bus: + subscribe_topics: + - onex.cmd.omnimarket.ledger-tick.v1 + """) + ) + contract = _parse_contract( + contract_path=path, + entry_point_name="legacy_node", + package_name="omnimarket", + package_version="0.1.0", + ) + assert contract.handler_routing is not None + assert len(contract.handler_routing.handlers) == 1 + assert contract.handler_routing.handlers[0].handler.name == "HandlerLedger" + + +def _zero_handler_contract( + *, subscribe_topics: tuple[str, ...] +) -> ModelDiscoveredContract: + """A contract that declares routing + subscribe topics but zero handlers. + + This is the shape the flat-schema defect produced before the parse guard. + Constructed directly (bypassing discovery) to exercise the wire-time + backstop in isolation. + """ + return ModelDiscoveredContract( + name="node_phantom", + node_type="EFFECT_GENERIC", + contract_version=ModelContractVersion(major=1, minor=0, patch=0), + contract_path=Path("/fake/contract.yaml"), + entry_point_name="node_phantom", + package_name="local", + event_bus=ModelEventBusWiring(subscribe_topics=subscribe_topics), + handler_routing=ModelHandlerRouting( + routing_strategy="operation_match", handlers=() + ), + ) + + +class TestPhantomWiringGuard: + """Guard B: wire_from_manifest fails closed on subscribe + zero dispatchers.""" + + @pytest.mark.asyncio + async def test_subscribe_with_zero_dispatchers_reports_failed(self) -> None: + contract = _zero_handler_contract( + subscribe_topics=("onex.evt.platform.test-input.v1",) + ) + manifest = ModelAutoWiringManifest(contracts=(contract,)) + engine = MessageDispatchEngine() + + report = await wire_from_manifest( + manifest, engine, event_bus=None, subscribe_immediately=False + ) + + assert report.total_wired == 0 + result = report.results[0] + assert result.outcome is EnumWiringOutcome.FAILED + assert "phantom wiring" in (result.reason or "") + assert result.dispatchers_registered == () + + @pytest.mark.asyncio + async def test_zero_handlers_without_subscribe_is_skipped_not_failed(self) -> None: + # No subscribe topics → nothing is phantom-wired; the contract is a plain + # SKIP (the guard must not fire on it). + contract = _zero_handler_contract(subscribe_topics=()) + manifest = ModelAutoWiringManifest(contracts=(contract,)) + engine = MessageDispatchEngine() + + report = await wire_from_manifest( + manifest, engine, event_bus=None, subscribe_immediately=False + ) + + assert report.results[0].outcome is EnumWiringOutcome.SKIPPED + + +class TestHandlerRoutingSchemaValidator: + """The CI/pre-commit gate flags flat entries and passes valid shapes.""" + + @pytest.mark.unit + def test_validator_flags_flat_entry(self, tmp_path: Path) -> None: + path = tmp_path / "contract.yaml" + path.write_text( + dedent("""\ + name: node_flat + node_type: EFFECT_GENERIC + handler_routing: + routing_strategy: "operation_match" + handlers: + - operation: "x.do" + handler_class: "HandlerX" + handler_module: "pkg.handlers.handler_x" + """) + ) + findings = validate_file(path) + assert len(findings) == 1 + assert findings[0].operation == "x.do" + assert main([str(path)]) == 1 + + @pytest.mark.unit + def test_validator_passes_nested_entry(self, tmp_path: Path) -> None: + path = tmp_path / "contract.yaml" + path.write_text( + dedent("""\ + name: node_nested + node_type: EFFECT_GENERIC + handler_routing: + routing_strategy: "operation_match" + handlers: + - operation: "x.do" + handler: + name: "HandlerX" + module: "pkg.handlers.handler_x" + """) + ) + assert validate_file(path) == [] + assert main([str(path)]) == 0 + + @pytest.mark.unit + def test_validator_passes_legacy_fallback_contract(self, tmp_path: Path) -> None: + # Top-level legacy handler: with an empty handler_routing.handlers list — + # no offending handlers[] entry, so the gate stays silent. + path = tmp_path / "contract.yaml" + path.write_text( + dedent("""\ + name: legacy_node + node_type: orchestrator + handler: + module: omnimarket.handlers.ledger + class: HandlerLedger + handler_routing: + default_handler: omnimarket.handlers.ledger:HandlerLedger + """) + ) + assert validate_file(path) == [] + assert main([str(path)]) == 0 + + @pytest.mark.unit + def test_validator_clean_on_repo_tree(self) -> None: + # Regression fence: the shipped omnibase_infra contract tree must stay + # free of flat-schema entries so the gate is green at HEAD. + root = Path(__file__).resolve() + for parent in root.parents: + candidate = parent / "src" / "omnibase_infra" + if candidate.is_dir(): + assert main([str(candidate)]) == 0 + return + pytest.skip("src/omnibase_infra not found from test location") From 6a10b51368a1ab9c27d965e607580d3b3cdb4a8d Mon Sep 17 00:00:00 2001 From: Jonah Gray Date: Wed, 8 Jul 2026 05:00:30 -0700 Subject: [PATCH 2/2] test(OMN-14141): refresh dispatch parity oracle --- .../baseline-selection-v2.json | 209 ++++++++++++++++-- 1 file changed, 185 insertions(+), 24 deletions(-) diff --git a/tests/fixtures/dispatch_parity/baseline-selection-v2.json b/tests/fixtures/dispatch_parity/baseline-selection-v2.json index 132d26b098..020a738531 100644 --- a/tests/fixtures/dispatch_parity/baseline-selection-v2.json +++ b/tests/fixtures/dispatch_parity/baseline-selection-v2.json @@ -126,6 +126,21 @@ "package": "omnibase_infra", "type_scoped": false }, + "dispatcher.auto.node_build_loop_write_effect.HandlerBuildLoopAppend.build_loop_append_1eb0a5fb": { + "category": "command", + "contract_name": "node_build_loop_write_effect", + "event_model_module": null, + "event_model_name": null, + "handler_id": "dispatcher.auto.node_build_loop_write_effect.HandlerBuildLoopAppend.build_loop_append_1eb0a5fb", + "handler_module": "omnibase_infra.nodes.node_build_loop_write_effect.handlers.handler_build_loop_append", + "handler_name": "HandlerBuildLoopAppend", + "message_types": [ + "omnibase-infra.build-loop-append", + "onex.cmd.omnibase-infra.build-loop-append.v1" + ], + "package": "omnibase_infra", + "type_scoped": false + }, "dispatcher.auto.node_chain_orchestrator.HandlerChainLearnInitiate": { "category": "command", "contract_name": "node_chain_orchestrator", @@ -703,6 +718,18 @@ "package": "omnibase_infra", "type_scoped": true }, + "dispatcher.auto.node_pattern_lifecycle_effect.HandlerLifecycleUpdate.lifecycle_update_0b997dc0": { + "category": "event", + "contract_name": "node_pattern_lifecycle_effect", + "event_model_module": null, + "event_model_name": null, + "handler_id": "dispatcher.auto.node_pattern_lifecycle_effect.HandlerLifecycleUpdate.lifecycle_update_0b997dc0", + "handler_module": "omnibase_infra.nodes.node_pattern_lifecycle_effect.handlers.handler_lifecycle_update", + "handler_name": "HandlerLifecycleUpdate", + "message_types": null, + "package": "omnibase_infra", + "type_scoped": false + }, "dispatcher.auto.node_registration_orchestrator.HandlerCatalogRequest": { "category": "command", "contract_name": "node_registration_orchestrator", @@ -1141,6 +1168,33 @@ "package": "omnibase_infra", "type_scoped": false }, + "dispatcher.auto.node_savings_estimation_compute.HandlerSavingsEstimation.savings_estimate_548c2375": { + "category": "event", + "contract_name": "node_savings_estimation_compute", + "event_model_module": null, + "event_model_name": null, + "handler_id": "dispatcher.auto.node_savings_estimation_compute.HandlerSavingsEstimation.savings_estimate_548c2375", + "handler_module": "omnibase_infra.nodes.node_savings_estimation_compute.handlers.handler_savings_estimation", + "handler_name": "HandlerSavingsEstimation", + "message_types": [ + "omniclaude.hook-context-injected", + "omniclaude.pattern-enforcement", + "omniclaude.session-ended", + "omniclaude.session-outcome", + "omniclaude.validator-catch", + "omniintelligence.dispatch-outcome-evaluated", + "omniintelligence.llm-call-completed", + "onex.evt.omniclaude.hook-context-injected.v1", + "onex.evt.omniclaude.pattern-enforcement.v1", + "onex.evt.omniclaude.session-ended.v1", + "onex.evt.omniclaude.session-outcome.v1", + "onex.evt.omniclaude.validator-catch.v1", + "onex.evt.omniintelligence.dispatch-outcome-evaluated.v1", + "onex.evt.omniintelligence.llm-call-completed.v1" + ], + "package": "omnibase_infra", + "type_scoped": false + }, "dispatcher.auto.node_scope_workflow_orchestrator.HandlerScopeCheckInitiate": { "category": "command", "contract_name": "node_scope_workflow_orchestrator", @@ -1263,6 +1317,18 @@ "package": "omnibase_infra", "type_scoped": false }, + "dispatcher.auto.node_validation_executor.HandlerRunChecks.validation_run_checks_da8e1227": { + "category": "event", + "contract_name": "node_validation_executor", + "event_model_module": null, + "event_model_name": null, + "handler_id": "dispatcher.auto.node_validation_executor.HandlerRunChecks.validation_run_checks_da8e1227", + "handler_module": "omnibase_infra.nodes.node_validation_executor.handlers.handler_run_checks", + "handler_name": "HandlerRunChecks", + "message_types": null, + "package": "omnibase_infra", + "type_scoped": false + }, "dispatcher.auto.node_validation_ledger_projection_compute.HandlerValidationLedgerProjection.validation_ledger_project_7c0294fc": { "category": "event", "contract_name": "node_validation_ledger_projection_compute", @@ -1363,8 +1429,8 @@ "discovery_errors": 0, "distinct_subscribe_topics": 88, "probe_count": 326, - "registered_dispatchers": 78, - "registered_routes": 90, + "registered_dispatchers": 82, + "registered_routes": 98, "type_scoped_dispatchers": 41 }, "duplicate_winners": [], @@ -1405,7 +1471,7 @@ "omnimemory" ], "fixture_version": "v2", - "generated_at_utc": "2026-07-08T08:09:52.074484+00:00", + "generated_at_utc": "2026-07-08T11:59:50.613983+00:00", "installed_package_versions": { "omnibase-core": "0.46.5", "omnibase-infra": "0.38.4", @@ -1515,11 +1581,13 @@ "payload": "_DictPayload" }, "selection": { - "dispatcher_ids": [], - "dlq_topic": "onex.dlq.omnibase-infra.omnibase-infra.v1", + "dispatcher_ids": [ + "dispatcher.auto.node_build_loop_write_effect.HandlerBuildLoopAppend.build_loop_append_1eb0a5fb" + ], + "dlq_topic": null, "message_category": "command", "message_type": "omnibase-infra.build-loop-append", - "status": "no_dispatcher" + "status": "success" }, "topic": "onex.cmd.omnibase-infra.build-loop-append.v1" }, @@ -2315,11 +2383,13 @@ "payload": "_DictPayload" }, "selection": { - "dispatcher_ids": [], - "dlq_topic": "onex.dlq.omnibase-infra.omniclaude.v1", + "dispatcher_ids": [ + "dispatcher.auto.node_savings_estimation_compute.HandlerSavingsEstimation.savings_estimate_548c2375" + ], + "dlq_topic": null, "message_category": "event", "message_type": "omniclaude.hook-context-injected", - "status": "no_dispatcher" + "status": "success" }, "topic": "onex.evt.omniclaude.hook-context-injected.v1" }, @@ -2330,11 +2400,13 @@ "payload": "_DictPayload" }, "selection": { - "dispatcher_ids": [], - "dlq_topic": "onex.dlq.omnibase-infra.omniclaude.v1", + "dispatcher_ids": [ + "dispatcher.auto.node_savings_estimation_compute.HandlerSavingsEstimation.savings_estimate_548c2375" + ], + "dlq_topic": null, "message_category": "event", "message_type": "omniclaude.pattern-enforcement", - "status": "no_dispatcher" + "status": "success" }, "topic": "onex.evt.omniclaude.pattern-enforcement.v1" }, @@ -2345,11 +2417,13 @@ "payload": "_DictPayload" }, "selection": { - "dispatcher_ids": [], - "dlq_topic": "onex.dlq.omnibase-infra.omniclaude.v1", + "dispatcher_ids": [ + "dispatcher.auto.node_savings_estimation_compute.HandlerSavingsEstimation.savings_estimate_548c2375" + ], + "dlq_topic": null, "message_category": "event", "message_type": "omniclaude.session-ended", - "status": "no_dispatcher" + "status": "success" }, "topic": "onex.evt.omniclaude.session-ended.v1" }, @@ -2360,11 +2434,13 @@ "payload": "_DictPayload" }, "selection": { - "dispatcher_ids": [], - "dlq_topic": "onex.dlq.omnibase-infra.omniclaude.v1", + "dispatcher_ids": [ + "dispatcher.auto.node_savings_estimation_compute.HandlerSavingsEstimation.savings_estimate_548c2375" + ], + "dlq_topic": null, "message_category": "event", "message_type": "omniclaude.session-outcome", - "status": "no_dispatcher" + "status": "success" }, "topic": "onex.evt.omniclaude.session-outcome.v1" }, @@ -2375,11 +2451,13 @@ "payload": "_DictPayload" }, "selection": { - "dispatcher_ids": [], - "dlq_topic": "onex.dlq.omnibase-infra.omniclaude.v1", + "dispatcher_ids": [ + "dispatcher.auto.node_savings_estimation_compute.HandlerSavingsEstimation.savings_estimate_548c2375" + ], + "dlq_topic": null, "message_category": "event", "message_type": "omniclaude.validator-catch", - "status": "no_dispatcher" + "status": "success" }, "topic": "onex.evt.omniclaude.validator-catch.v1" }, @@ -2390,11 +2468,13 @@ "payload": "_DictPayload" }, "selection": { - "dispatcher_ids": [], - "dlq_topic": "onex.dlq.omnibase-infra.omniintelligence.v1", + "dispatcher_ids": [ + "dispatcher.auto.node_savings_estimation_compute.HandlerSavingsEstimation.savings_estimate_548c2375" + ], + "dlq_topic": null, "message_category": "event", "message_type": "omniintelligence.dispatch-outcome-evaluated", - "status": "no_dispatcher" + "status": "success" }, "topic": "onex.evt.omniintelligence.dispatch-outcome-evaluated.v1" }, @@ -2406,6 +2486,7 @@ }, "selection": { "dispatcher_ids": [ + "dispatcher.auto.node_savings_estimation_compute.HandlerSavingsEstimation.savings_estimate_548c2375", "dispatcher.auto.node_waste_detection_compute.HandlerWasteDetection.waste_detect_53946f25" ], "dlq_topic": null, @@ -7090,6 +7171,16 @@ "topic_pattern": "*.evt.omnimarket.build-loop-orchestrator-completed.*", "type_scoped": false }, + { + "contract_name": "node_build_loop_write_effect", + "dispatcher_id": "dispatcher.auto.node_build_loop_write_effect.HandlerBuildLoopAppend.build_loop_append_1eb0a5fb", + "handler_id": "dispatcher.auto.node_build_loop_write_effect.HandlerBuildLoopAppend.build_loop_append_1eb0a5fb", + "message_category": "command", + "route_id": "route.auto.node_build_loop_write_effect.HandlerBuildLoopAppend.build_loop_append_1eb0a5fb.onex_cmd_omnibase_infra_build_loop_append_v1", + "topic": "onex.cmd.omnibase-infra.build-loop-append.v1", + "topic_pattern": "*.cmd.omnibase-infra.build-loop-append.*", + "type_scoped": false + }, { "contract_name": "node_chain_orchestrator", "dispatcher_id": "dispatcher.auto.node_chain_orchestrator.HandlerChainLearnInitiate", @@ -7730,6 +7821,76 @@ "topic_pattern": "*.evt.omnibase-infra.runtime-booted.*", "type_scoped": false }, + { + "contract_name": "node_savings_estimation_compute", + "dispatcher_id": "dispatcher.auto.node_savings_estimation_compute.HandlerSavingsEstimation.savings_estimate_548c2375", + "handler_id": "dispatcher.auto.node_savings_estimation_compute.HandlerSavingsEstimation.savings_estimate_548c2375", + "message_category": "event", + "route_id": "route.auto.node_savings_estimation_compute.HandlerSavingsEstimation.savings_estimate_548c2375.onex_evt_omniclaude_hook_context_injected_v1", + "topic": "onex.evt.omniclaude.hook-context-injected.v1", + "topic_pattern": "*.evt.omniclaude.hook-context-injected.*", + "type_scoped": false + }, + { + "contract_name": "node_savings_estimation_compute", + "dispatcher_id": "dispatcher.auto.node_savings_estimation_compute.HandlerSavingsEstimation.savings_estimate_548c2375", + "handler_id": "dispatcher.auto.node_savings_estimation_compute.HandlerSavingsEstimation.savings_estimate_548c2375", + "message_category": "event", + "route_id": "route.auto.node_savings_estimation_compute.HandlerSavingsEstimation.savings_estimate_548c2375.onex_evt_omniclaude_pattern_enforcement_v1", + "topic": "onex.evt.omniclaude.pattern-enforcement.v1", + "topic_pattern": "*.evt.omniclaude.pattern-enforcement.*", + "type_scoped": false + }, + { + "contract_name": "node_savings_estimation_compute", + "dispatcher_id": "dispatcher.auto.node_savings_estimation_compute.HandlerSavingsEstimation.savings_estimate_548c2375", + "handler_id": "dispatcher.auto.node_savings_estimation_compute.HandlerSavingsEstimation.savings_estimate_548c2375", + "message_category": "event", + "route_id": "route.auto.node_savings_estimation_compute.HandlerSavingsEstimation.savings_estimate_548c2375.onex_evt_omniclaude_session_ended_v1", + "topic": "onex.evt.omniclaude.session-ended.v1", + "topic_pattern": "*.evt.omniclaude.session-ended.*", + "type_scoped": false + }, + { + "contract_name": "node_savings_estimation_compute", + "dispatcher_id": "dispatcher.auto.node_savings_estimation_compute.HandlerSavingsEstimation.savings_estimate_548c2375", + "handler_id": "dispatcher.auto.node_savings_estimation_compute.HandlerSavingsEstimation.savings_estimate_548c2375", + "message_category": "event", + "route_id": "route.auto.node_savings_estimation_compute.HandlerSavingsEstimation.savings_estimate_548c2375.onex_evt_omniclaude_session_outcome_v1", + "topic": "onex.evt.omniclaude.session-outcome.v1", + "topic_pattern": "*.evt.omniclaude.session-outcome.*", + "type_scoped": false + }, + { + "contract_name": "node_savings_estimation_compute", + "dispatcher_id": "dispatcher.auto.node_savings_estimation_compute.HandlerSavingsEstimation.savings_estimate_548c2375", + "handler_id": "dispatcher.auto.node_savings_estimation_compute.HandlerSavingsEstimation.savings_estimate_548c2375", + "message_category": "event", + "route_id": "route.auto.node_savings_estimation_compute.HandlerSavingsEstimation.savings_estimate_548c2375.onex_evt_omniclaude_validator_catch_v1", + "topic": "onex.evt.omniclaude.validator-catch.v1", + "topic_pattern": "*.evt.omniclaude.validator-catch.*", + "type_scoped": false + }, + { + "contract_name": "node_savings_estimation_compute", + "dispatcher_id": "dispatcher.auto.node_savings_estimation_compute.HandlerSavingsEstimation.savings_estimate_548c2375", + "handler_id": "dispatcher.auto.node_savings_estimation_compute.HandlerSavingsEstimation.savings_estimate_548c2375", + "message_category": "event", + "route_id": "route.auto.node_savings_estimation_compute.HandlerSavingsEstimation.savings_estimate_548c2375.onex_evt_omniintelligence_dispatch_outcome_evaluated_v1", + "topic": "onex.evt.omniintelligence.dispatch-outcome-evaluated.v1", + "topic_pattern": "*.evt.omniintelligence.dispatch-outcome-evaluated.*", + "type_scoped": false + }, + { + "contract_name": "node_savings_estimation_compute", + "dispatcher_id": "dispatcher.auto.node_savings_estimation_compute.HandlerSavingsEstimation.savings_estimate_548c2375", + "handler_id": "dispatcher.auto.node_savings_estimation_compute.HandlerSavingsEstimation.savings_estimate_548c2375", + "message_category": "event", + "route_id": "route.auto.node_savings_estimation_compute.HandlerSavingsEstimation.savings_estimate_548c2375.onex_evt_omniintelligence_llm_call_completed_v1", + "topic": "onex.evt.omniintelligence.llm-call-completed.v1", + "topic_pattern": "*.evt.omniintelligence.llm-call-completed.*", + "type_scoped": false + }, { "contract_name": "node_scope_workflow_orchestrator", "dispatcher_id": "dispatcher.auto.node_scope_workflow_orchestrator.HandlerScopeCheckInitiate",