orthonym.assembly.inner_dispatch#

Note

Internal API. Names and behaviour may change between releases.

a phase inner-dispatch substrate (DECOMP-01 + internal notes).

Mirrors a phase outer CFR routing/dispatch_table.py substrate at the INNER (post-class-routing) dispatch layer. Per internal notes the inner table is structurally identical to the outer CFR table:

  • @dataclass(frozen=True) class InnerDispatchEntry with 7 fields (handler_id, priority, predicate, handler, iupac_section, description, side_effect_inventory).

  • INNER_DISPATCH_TABLE: OrderedDict[str, InnerDispatchEntry] populated at module-import time by _register_inner(...) calls in the per-handler atomic commits 02-01..02-29 (Plan-02) + 03-01..03-09 (Plan-03).

  • _register_inner(...) private helper; raises RuntimeError if called after _INNER_REGISTRATION_FROZEN is set, or if a handler_id / priority is already registered.

  • dispatch_inner(features) -> Optional[InnerDispatchResult] first-match- wins iteration in priority order.

Plan-02 substrate commit (02-00) ships the table EMPTY; commits 02-01..02-29 each append ONE _register_inner(...) call (Tier-1 + Tier-1.5 handlers). Plan-03 ships the catch-all general_acyclic at priority 99999 (commit 03-09) — per internal notes the catch-all closes the Plan-02 fallthrough gap.

Anti-pattern hygiene: - -08 banned: silent fallthrough in inner-dispatch without explicit

general_acyclic catch-all entry at priority 99999. Plan-02 fallthrough to inline composer.py mid-tier + root branches is the deliberate Plan-02 bridge — Plan-03 closes the gap.

  • -09 banned: opt-out flag for inner dispatch (no _disable_inner_dispatch kwarg or env-var-controlled bypass).

  • -13: module-global mutable state for inner-dispatch_stats — keep per-Orthonym-instance counter mirror of a phase; stats helpers are stateful but per-instance-isolated.

  • -15 / hard invariant: side_effect_inventory == `` for every entry; ``_register_inner raises ValueError if a non-empty tuple is passed.

  • -10 banned: invent-as-you-go INNER_DISPATCH_TABLE entries not in the audit Every _register_inner(...) call MUST cite the audit row it implements.

References: - internal notes-DECOMP.md — inner-dispatch branch enumeration (39 rows;

source-of-truth for the 29 Plan-02 commits + 10 Plan-03 commits).

  • 160-internal notes — substrate shape matches a phase.

  • internal notes — analog: routing/dispatch_table.py:130-211, 696-720.

class orthonym.assembly.inner_dispatch.InnerDispatchEntry(handler_id, priority, predicate, handler, iupac_section, description, side_effect_inventory=())#

Bases: object

a phase: frozen dataclass row of INNER_DISPATCH_TABLE.

Mirrors a phase + (routing/dispatch_table.ClassDispatchEntry) at the INNER dispatch layer. The 7-field schema is locked per internal notes :

handler_id HANDLER_POLICIES key (lowercase snake_case) priority spaced int; lower = earlier; first-match-wins predicate Callable[…, bool]; MolecularFeatures → bool;

PURE per (no mutation of features / mol / module-global state)

handler Callable[…, Optional[NamingResult]]; the

handler module’s name_<handler_id> entry point

iupac_section Blue Book P-section cite (audit metadata) description one-line summary for audit logging side_effect_inventory MUST be per hard invariant; non-empty

tuples raise ValueError in _register_inner.

Per internal notes + -15: side_effect_inventory == `` is the HARD invariant; the integrity test ``test_side_effect_inventory_is_empty (Plan-04) asserts emptiness for every entry. -26 explicitly bans any predicate that mutates MolecularFeatures, mol, module-global state, or thread-local state. Handler invocations MAY call pool.add (the SOLE accepted shared-state interaction per a phase contract); recursive orthonym.name_compound(...) calls are PERMITTED in the N-oxide handler (audited per of internal notes-DECOMP.md).

handler_id: str#
priority: int#
predicate: Callable[[...], bool]#
handler: Callable[[...], Any | None]#
iupac_section: str#
description: str#
side_effect_inventory: Tuple[str, ...] = ()#
class orthonym.assembly.inner_dispatch.InnerDispatchResult(handler_id, handler, audit_record, matched_entry, result=None)#

Bases: object

a phase + a phase / -04: result of a successful dispatch_inner(features, mol, style) call.

Pre-amendment (a phase): returned the matched-entry reference; the caller (composer.py:_assemble_name_impl) invoked result.handler(...) separately and handled gate-fail (handler returning None) via inline fall-through to the legacy cascade.

Post-amendment (a phase + -04 — first-match-AND-succeeds-wins): dispatch_inner invokes the handler INTERNALLY and retries the next-priority entry on gate-fail (handler returning None). The new result: NamingResult field carries the non-None NamingResult that the chosen handler returned. The caller collapses to a single return _inner_result.result.name.

Handler contract per -04:
  • Return non-None NamingResult ⇒ “I succeeded; use this result.”

  • Return None ⇒ “I gate-failed; defer to next-priority handler.”

  • Raise Exception ⇒ surfaced as RuntimeError chained via __cause__ (no swallowing per internal notes honest-fail-on-data).

Predicate purity (a phase) is preserved verbatim: predicates report whether a handler CAN POSSIBLY apply (cheap structural check); the handler’s own gate decides whether it SHOULD apply (full IUPAC compliance check on coverage / orientation / locant feasibility / etc.).

The audit_record field is a dict of audit metadata (handler_id, priority, iupac_section) for the Plan-04 --dump-tree CLI + the inner-dispatch stats counter (stats now count SUCCESSFUL handlers only, not predicate matches per).

handler_id: str#
handler: Callable[[...], Any | None]#
audit_record: Dict[str, str]#
matched_entry: InnerDispatchEntry#
result: Any | None = None#
orthonym.assembly.inner_dispatch.freeze_inner_table()#

a phase: lock INNER_DISPATCH_TABLE against further registration.

Called by Plan-03 commit 03-10 (composer.py thinning) at the end of module import; after this call, _register_inner(...) raises RuntimeError per -09 (no opt-out / runtime modification).

Plan-02 / Plan-03 atomic commits do NOT call this — the table stays open across the migration. Plan-04 is the FIRST plan that calls freeze_inner_table after registering all 30 entries.

orthonym.assembly.inner_dispatch.dispatch_inner(features, mol=None, style='pin')#

a phase + a phase / -04: first-match-AND- succeeds-wins inner-cascade dispatch.

Iterates INNER_DISPATCH_TABLE in priority order (lowest first; OrderedDict insertion order matches priority-sorted insertion via _register_inner); for each entry whose predicate matches, INVOKES the handler internally. On non-None return, builds and returns InnerDispatchResult carrying the result. On None return (gate-fail per -04 contract), CONTINUES to the next-priority entry. Final no-match returns None.

-04 amends a phase internal notes from “first-match-wins” to “first-match-AND-succeeds-wins.” Handler contract:

  • Return non-None NamingResult ⇒ “I succeeded; use this result.”

  • Return None ⇒ “I gate-failed; defer to next-priority handler.”

  • Raise Exception ⇒ surfaced as RuntimeError chained via __cause__ (no silent swallowing).

Predicate purity (a phase) preserved verbatim: predicates report whether a handler CAN POSSIBLY apply (cheap structural check); handlers may perform internal pool.add side effects per their individual contracts.

Parameters:
  • features (Any) – MolecularFeatures instance (the upstream perception output that handlers consume).

  • mol (Any) – RDKit Mol object. Defaults to features.mol when None (preserves the pre-amendment call-site signature).

  • style (str) – PIN / IUPAC name-style flag forwarded to handlers; default "pin".

Returns:

InnerDispatchResult carrying the SUCCESSFUL handler’s NamingResult in .result, or None if no handler succeeded. Once general_acyclic catch-all ships (Plan-03-03 per internal notes), the None branch is unreachable in production.

Return type:

InnerDispatchResult | None

Per internal notes honest-fail-on-data: defensive try/except around BOTH predicate AND handler invocations surface bugs (NoneType attribute access, etc.) as RuntimeError chained via __cause__.

orthonym.assembly.inner_dispatch.get_inner_dispatch_stats()#

a phase + -13: read per-handler dispatch counters.

Returns a COPY of the in-memory counter dict so callers cannot mutate the underlying state. Plan-04 wires this into the CLI (–audit-trace flag) to surface per-handler hit rates during benchmarking.

a phase Plan-04-02: reads the per-ContextVar context dict so concurrent Orthonym instances each see their own counter snapshot.

orthonym.assembly.inner_dispatch.reset_inner_dispatch_stats()#

a phase: clear the per-handler dispatch counters.

Called by tests + Plan-04 benchmarks to isolate counter state across runs. Mirrors a phase reset_dispatch_counter per -13.

a phase Plan-04-02: clears the current ContextVar context’s dict only; other concurrent contexts keep their counters.