Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,22 @@ jobs:
echo "================================================================"
uv run pytest tests/audit/test_io_violations.py::TestCIGateIOPurity -v --tb=short

# ARCH-004 imperative-orchestrator ratchet (OMN-13472).
# Full report over the repo: surfaces every contract-declared-but-unbound
# orchestrator FSM driven imperatively by a handler (the delegation-shaped
# anti-pattern ARCH-003 misses). Non-blocking INITIALLY (continue-on-error)
# while the baseline is above threshold — the changed-node ratchet
# (pre-commit, blocking) stops regressions. Promote to a required gate once
# the baseline is below the agreed threshold; this rides OMN-12550
# (validator gating) / OMN-13325 (ratchet enforcement), not a fresh hook.
- name: Run imperative-orchestrator ratchet report (ARCH-004)
continue-on-error: true
run: |
echo "================================================================"
echo "Imperative-Orchestrator Ratchet Report (ARCH-004, OMN-13472)"
echo "================================================================"
uv run python scripts/validate.py imperative_orchestrators --verbose

- name: Run markdown link validation
run: |
echo "================================================================"
Expand Down
22 changes: 21 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,26 @@ repos:
types: [python]
pass_filenames: false
stages: [pre-commit]
# ARCH-004 imperative-orchestrator ratchet (OMN-13472).
#
# Changed-node ratchet (BLOCKING): for any node directory touched by this
# commit, fail if it introduces a NEW / worsened / untracked
# imperative-orchestrator hard-fail (contract-declared-but-unbound FSM
# driven imperatively by a handler — the delegation-shaped anti-pattern
# ARCH-003 structurally misses). Accepted debt is frozen in
# architecture-handshakes/imperative-orchestrator-baseline.yaml (it can
# only shrink).
#
# This rides the validator-gating work of OMN-12550 (wire ARCH-001/002/003
# as blocking gates) and the ratchet-enforcement epic OMN-13325 — it is NOT
# a parallel/competing hook. Owner epic for the baselined nodes: OMN-13471.
- id: onex-imperative-orchestrator-ratchet
name: ONEX Imperative-Orchestrator Ratchet (ARCH-004)
entry: uv run --frozen python scripts/validate.py imperative_orchestrators
language: system
files: '(contract\.yaml|handlers/handler_.*\.py)$'
pass_filenames: true
stages: [pre-commit]
# Architecture layer validation - core/infra separation
# Verifies omnibase_core has no infrastructure dependencies (kafka, httpx, etc.)
#
Expand Down Expand Up @@ -709,4 +729,4 @@ ci:
# Note: onex-validate-clean-root is NOT skipped (standalone script, no omnibase_core dependency)
# Also skip migration freeze (checks staged files via git diff --cached, which is empty in CI;
# CI should run: uv run python scripts/validation/validate_migration_freeze.py --check-committed)
skip: [no-env-file, onex-validate-architecture, onex-validate-architecture-layers, onex-validate-contracts, onex-validate-patterns, onex-validate-unions, onex-validate-imports, onex-validate-any-types, onex-validate-migration-freeze, normalization-symmetry]
skip: [no-env-file, onex-validate-architecture, onex-imperative-orchestrator-ratchet, onex-validate-architecture-layers, onex-validate-contracts, onex-validate-patterns, onex-validate-unions, onex-validate-imports, onex-validate-any-types, onex-validate-migration-freeze, normalization-symmetry]
99 changes: 99 additions & 0 deletions architecture-handshakes/imperative-orchestrator-baseline.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# SPDX-FileCopyrightText: 2026 OmniNode.ai Inc.
# SPDX-License-Identifier: MIT
#
# ARCH-004 imperative-orchestrator ratchet baseline (OMN-13472).
# Generated by:
# uv run python -m omnibase_infra.nodes.node_architecture_validator.validators.scanner_imperative_orchestrator_ratchet \
# --check-all --report --write-baseline
# Owner epic: OMN-13471. Wiring: OMN-12550 / OMN-13325.
schema_version:
major: 1
minor: 0
patch: 0
repo: omnibase_infra
rule: ARCH-004
description: 'Accepted imperative-orchestrator hard-fails for the ARCH-004 ratchet (OMN-13472). One entry per current hard-fail. This list can only SHRINK: a new/worsened/untracked finding on a touched node fails the changed-node ratchet. Each entry carries an owner ticket for its decomposition (delegation -> OMN-13471).'
entries:
- repo: omnimarket
node: autopilot_orchestrator
max_handler_path: src/omnimarket/nodes/node_autopilot_orchestrator/handlers/handler_autopilot_orchestrator.py
line_count: 634
risk_score: 6
finding_codes:
- H1
- W3
- W4
owner_ticket: OMN-13471
- repo: omnibase_infra
node: node_chain_orchestrator
max_handler_path: src/omnibase_infra/nodes/node_chain_orchestrator/handlers/handler_chain_replay_complete.py
line_count: 70
risk_score: 2
finding_codes:
- H2
owner_ticket: OMN-13471
- repo: omnimarket
node: node_delegation_orchestrator
max_handler_path: src/omnimarket/nodes/node_delegation_orchestrator/handlers/handler_delegation_workflow.py
line_count: 1542
risk_score: 10
finding_codes:
- H1
- H2
- H3
- W1
- W2
- W3
- W4
owner_ticket: OMN-13471
- repo: omnibase_infra
node: node_merge_sweep_workflow_orchestrator
max_handler_path: src/omnibase_infra/nodes/node_merge_sweep_workflow_orchestrator/handlers/handler_auto_merge_complete.py
line_count: 90
risk_score: 2
finding_codes:
- H2
owner_ticket: OMN-13471
- repo: omnibase_infra
node: node_registration_orchestrator
max_handler_path: src/omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_registration_acked.py
line_count: 435
risk_score: 2
finding_codes:
- H2
owner_ticket: OMN-13471
- repo: omnibase_infra
node: node_routing_orchestrator
max_handler_path: src/omnibase_infra/nodes/node_routing_orchestrator/handlers/handler_health_complete.py
line_count: 70
risk_score: 2
finding_codes:
- H2
owner_ticket: OMN-13471
- repo: omnibase_infra
node: node_rsd_orchestrator
max_handler_path: src/omnibase_infra/nodes/node_rsd_orchestrator/handlers/handler_rsd_data_fetch_complete.py
line_count: 90
risk_score: 2
finding_codes:
- H2
owner_ticket: OMN-13471
- repo: omnibase_infra
node: node_scope_workflow_orchestrator
max_handler_path: src/omnibase_infra/nodes/node_scope_workflow_orchestrator/handlers/handler_scope_file_read_complete.py
line_count: 73
risk_score: 2
finding_codes:
- H2
owner_ticket: OMN-13471
- repo: omnimarket
node: pr_lifecycle_orchestrator
max_handler_path: src/omnimarket/nodes/node_pr_lifecycle_orchestrator/handlers/handler_pr_lifecycle_orchestrator.py
line_count: 1815
risk_score: 7
finding_codes:
- H1
- W1
- W2
- W4
owner_ticket: OMN-13471
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-- OMN-13407: persist delegation context-pack identity on the canonical
-- delegation projection so context ON/OFF ROI is measurable from the
-- correlation-trace surface. Empty string is the OFF/no-context arm.

ALTER TABLE delegation_events
ADD COLUMN IF NOT EXISTS context_pack_hash TEXT NOT NULL DEFAULT '';

CREATE INDEX IF NOT EXISTS idx_delegation_events_context_pack_hash
ON delegation_events (context_pack_hash);
4 changes: 2 additions & 2 deletions docker/runners/runner-image.lock.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"base_image_digest": "sha256:3ba65aa20f86a0fad9df2b2c259c613df006b2e6d0bfcc8a146afb8c525a9751",
"gh_version": "2.67.0",
"identity_digest": "8c3208f1d0b18f6a94b6fe27a270e7cb",
"identity_digest": "0f337da65f9dbcd3018bccd77bdd1420",
"image_version": 5,
"kubectl_version": "1.32.1",
"python_version": "3.12",
"runner_version": "2.334.0",
"shared_env_digest": "a796970abe13b64c009206f2",
"shared_env_digest": "efb9011b0136952b9f7d9886",
"shared_env_install_args": "--frozen --all-extras --all-groups --no-install-project",
"uv_version": "0.6.14"
}
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,8 @@ ignore = [
# Intentional stdout sinks / fallback output
"src/omnibase_infra/event_bus/service_topic_manager.py" = ["T201"]
"src/omnibase_infra/observability/sinks/sink_logging_structured.py" = ["T201"]
# ARCH-004 imperative-orchestrator ratchet CLI (OMN-13472): intentional CLI/report stdout.
"src/omnibase_infra/nodes/node_architecture_validator/validators/scanner_imperative_orchestrator_ratchet.py" = ["T201"]

[tool.ruff.lint.isort]
# Ensure consistent import sorting between local and CI
Expand Down
95 changes: 94 additions & 1 deletion scripts/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,89 @@ def run_declarative_nodes(
return True


def run_imperative_orchestrators(
verbose: bool = False, files: list[str] | None = None
) -> bool:
"""Run the ARCH-004 imperative-orchestrator ratchet (OMN-13472).

Detects contract-declared-but-unbound orchestrator FSMs driven imperatively
by a handler (the delegation-shaped anti-pattern ARCH-003 misses). Wired
through OMN-12550 (validator gating) / OMN-13325 (ratchet enforcement).

Modes:
- With ``files`` (pre-commit): changed-node ratchet — only node
directories containing a changed file are scanned, and the gate fails
on a NEW / worsened / untracked finding (``--check-changed --ratchet``).
- Without ``files`` (CI/manual full report): full report over the repo
(``--check-all --report``), non-blocking on its own.

The baseline lives at
``architecture-handshakes/imperative-orchestrator-baseline.yaml`` and can
only shrink.

Args:
verbose: Enable verbose output.
files: Optional list of changed files (from pre-commit). If provided,
only the node directories containing those files are ratcheted.
"""
try:
from omnibase_infra.nodes.node_architecture_validator.validators.scanner_imperative_orchestrator_ratchet import (
load_baseline,
node_dirs_for_changed_files,
ratchet_violations,
scan_node_dirs,
)
except ImportError as e:
print(f"Skipping imperative-orchestrator ratchet: {e}")
return True

from pathlib import Path as _Path

repo_root = _Path.cwd()
# Derive the repo key from the working tree (worktrees nest the repo name in
# a ticket dir, so the immediate dir name is authoritative) instead of
# hardcoding it; baseline entries are keyed by repo::node.
repo_name = repo_root.name
baseline = load_baseline(
repo_root / "architecture-handshakes" / "imperative-orchestrator-baseline.yaml"
)

if files:
node_dirs = node_dirs_for_changed_files(repo_root, files)
if not node_dirs:
if verbose:
print("Imperative Orchestrators: SKIP (no node dirs in changeset)")
return True
result = scan_node_dirs(repo_name, node_dirs, repo_root=repo_root)
failures = ratchet_violations(result.hard_fails, baseline)
passed = not failures
if verbose or not passed:
print(f"Imperative Orchestrators: {'PASS' if passed else 'FAIL'}")
print(
f" Changed node dirs: {len(node_dirs)}, "
f"hard-fails: {len(result.hard_fails)}"
)
for f in failures:
print(f" - {f}")
return passed

# Full report mode (non-blocking on its own).
from omnibase_infra.nodes.node_architecture_validator.validators.scanner_imperative_orchestrator_ratchet import (
discover_node_dirs,
)

node_dirs = discover_node_dirs(repo_root)
result = scan_node_dirs(repo_name, node_dirs, repo_root=repo_root)
print(
f"Imperative Orchestrators (full report): "
f"{len(result.hard_fails)} hard-fail node(s) across {len(node_dirs)} dirs."
)
if verbose:
for line in result.report_lines:
print(line)
return True


def run_io_audit(verbose: bool = False) -> bool:
"""Run I/O purity audit for REDUCER and COMPUTE nodes.

Expand Down Expand Up @@ -1074,6 +1157,7 @@ def main() -> int:
"any_types",
"localhandler",
"declarative_nodes",
"imperative_orchestrators",
"io_audit",
"imports",
"markdown_links",
Expand All @@ -1084,7 +1168,10 @@ def main() -> int:
parser.add_argument(
"files",
nargs="*",
help="Optional list of files to validate (for declarative_nodes or markdown_links)",
help=(
"Optional list of files to validate (for declarative_nodes, "
"imperative_orchestrators, or markdown_links)"
),
)
parser.add_argument("--verbose", "-v", action="store_true", help="Verbose output")
parser.add_argument(
Expand All @@ -1105,6 +1192,7 @@ def main() -> int:
"any_types": run_any_types,
"localhandler": run_localhandler,
"declarative_nodes": run_declarative_nodes,
"imperative_orchestrators": run_imperative_orchestrators,
"io_audit": run_io_audit,
"imports": run_imports,
"markdown_links": run_markdown_links,
Expand All @@ -1121,6 +1209,11 @@ def main() -> int:
# Pass files to declarative_nodes validator if provided
files = args.files if args.files else None
success = run_declarative_nodes(args.verbose, files=files)
elif args.validator == "imperative_orchestrators":
# Pass changed files for the changed-node ratchet (pre-commit); without
# files this runs the full non-blocking report.
files = args.files if args.files else None
success = run_imperative_orchestrators(args.verbose, files=files)
else:
success = validator_map[args.validator](args.verbose)

Expand Down
20 changes: 20 additions & 0 deletions src/omnibase_infra/nodes/node_architecture_validator/contract.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,26 @@ validation_rules:
suggested_fix: >
Remove state transition logic from orchestrator. State transitions belong exclusively to reducer nodes. Orchestrators should call reducers for FSM decisions.

- rule_id: "ARCH-004"
name: "Contract-Declared Orchestrator Workflow Must Be Bound To An Executor"
description: >
Cross-file, node-directory rule (OMN-13472). An orchestrator-like node that declares an fsm:/workflow-state set MUST bind it to a runtime executor. It must NOT leave the contract transition table decorative (ModelContractOrchestrator has no typed fsm/state_machine field, no traverser consumes orchestrator fsm.transitions) while a handler drives the transitions itself. Catches the delegation-shaped anti-pattern (_transition(...) calls in a non-"*Orchestrator" handler, payload_type_match catchall funneling 3+ payload types into one handler, and one handler that both selects the next state and constructs terminal/compat events) that ARCH-003's single-file, class-name-gated AST approach structurally misses. Reducers are exempt.

# ERROR severity: a declared-but-unbound orchestrator FSM driven imperatively
# by a handler is the OMN-13408-class footgun (terminal/projection corruption).
severity: "ERROR"
detection_strategy:
type: "node_directory_join"
checks:
- decorative_fsm_with_handler_driven_transitions
- payload_type_match_fanin_3plus
- handler_selects_state_and_constructs_events
target_files:
- "contract.yaml"
- "handlers/handler_*.py"
suggested_fix: >
Bind the contract fsm to an executor (migrate to a typed, executor-bound workflow/DAG field per OMN-12835) or decompose the monolithic handler into per-step thin handlers driven by a reducer. See OMN-13471 (delegation decomposition epic). New/worsened cases on touched nodes fail the changed-node ratchet; accepted debt is recorded in architecture-handshakes/imperative-orchestrator-baseline.yaml with an owner ticket.

# Dependencies (protocols this node requires)
dependencies:
- name: "protocol_ast_analyzer"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@
Reducers own state machines; orchestrators are "reaction planners"
that coordinate work based on reducer outputs.

- ARCH-004: Contract-Declared Orchestrator Workflow Must Be Bound To An Executor
Cross-file, node-directory rule (OMN-13472). An orchestrator-like node
that declares an fsm:/workflow-state set must bind it to a runtime
executor; it must not leave the contract table decorative while a
handler drives the transitions itself. Catches the delegation-shaped
anti-pattern (_transition(...) in a non-"*Orchestrator" handler) that
ARCH-003's single-file, class-name-gated AST approach structurally
misses. Reducers are exempt.

Two Interfaces:
**1. Function-based validators** - Direct file validation, returns detailed results.

Expand Down Expand Up @@ -79,6 +88,11 @@

from __future__ import annotations

from omnibase_infra.nodes.node_architecture_validator.validators.validator_contract_declared_orchestrator_workflow import (
RuleContractDeclaredOrchestratorWorkflow,
analyze_node_directory,
validate_contract_declared_orchestrator_workflow,
)
from omnibase_infra.nodes.node_architecture_validator.validators.validator_no_direct_dispatch import (
RuleNoDirectDispatch,
validate_no_direct_dispatch,
Expand All @@ -97,8 +111,12 @@
"validate_no_direct_dispatch",
"validate_no_handler_publishing",
"validate_no_orchestrator_fsm",
# Functions (node-directory cross-file validators)
"validate_contract_declared_orchestrator_workflow",
"analyze_node_directory",
# Classes (protocol-compliant rules)
"RuleNoDirectDispatch",
"RuleNoHandlerPublishing",
"RuleNoOrchestratorFSM",
"RuleContractDeclaredOrchestratorWorkflow",
]
Loading
Loading