orthonym.metrics.candidate_ledger#

Note

Internal API. Names and behaviour may change between releases.

Append-only candidate ledger (, the audit instrument’s recorder).

Every oracle this project owns answers “does the EMITTED name denote the right molecule?” — round-trip,, E1, bb_conformance. None answers “was a correct name ever BUILT, and if so what threw it away?”, which is the question that defeated PB task 5 and: three correct fixes moved a dev split by ~0 because hand-picked target lists kept landing off the mass.

This module is that missing recorder. It is deliberately modelled on metrics/abstention.py — thread-local, side-effect-only, every recording call wrapped so telemetry can never raise into naming — with three deliberate differences, each forced by a measurement (internal notes ):

  • Append-only, not first-writer-wins. abstention.py keeps one code per session because it answers “which site declined”. The selection question needs every candidate, including the ones that lost.

  • Meaningful for SUCCESSES too. abstention_code_for returns None unless the result is a failure sentinel. A ledger that did that could never see the interesting case — a row that emits a wrong name while a correct candidate was built and discarded.

  • ``scope`` is a recorded field. Measured: the pipeline builds fragment names (N,N-diethylethanamine) that are correct names for a fragment and can never round-trip to the whole input. Round-tripping them against the molecule would file every one under “wrong” and manufacture a large fake producer-correctness class, so consumers filter on scope == 'molecule'.

OFF BY DEFAULT. enable must be called explicitly, so the production naming path carries nothing but a single if not _enabled test. The naming output must be byte-identical with the ledger on and off; that contract is what makes this an instrument rather than a behaviour change, and it is asserted in tests/unit/metrics/test_candidate_ledger.py.

class orthonym.metrics.candidate_ledger.LedgerEntry(site, stage, scope, name, detail, depth)#

Bases: NamedTuple

site: str#

Alias for field number 0

stage: str#

Alias for field number 1

scope: str#

Alias for field number 2

name: str | None#

Alias for field number 3

detail: str | None#

Alias for field number 4

depth: int#

Fragment-recursion depth at record time (0 = top-level molecule).

class orthonym.metrics.candidate_ledger.Scope#

Bases: object

Whether the name names the WHOLE input or one fragment of it.

MOLECULE = 'molecule'#

A candidate for the whole input molecule. Only these may be round-tripped against the input’s InChIKey.

FRAGMENT = 'fragment'#

A substituent/fragment name. Correct fragment names never round-trip to the whole molecule, so comparing them against the input is a category error.

class orthonym.metrics.candidate_ledger.Stage#

Bases: object

What happened to the candidate. Not an Enum: these are written into JSON artifacts and compared as plain strings by the analysis scripts.

PRODUCED = 'produced'#

A producer built this name string.

GATE_REJECTED = 'gate_rejected'#

A pool/confidence gate declined it (CandidatePool.add returned None).

SUPPRESSED = 'suppressed'#

A correctness gate suppressed it (, OPSIN validity, coverage downgrade).

SUPERSEDED = 'superseded'#

A later producer replaced it without any gate firing.

EMITTED = 'emitted'#

The name the caller actually returned. Recorded EXPLICITLY rather than inferred by looking the emitted string up in the ledger, because measured: the pool held ‘(2R)-piperidine-2-carboxylic acid’ and naming emitted ‘(2R)-piperidine-2-carboxylate’. A lookup would mis-report every anion row.

orthonym.metrics.candidate_ledger.clear_ledger()#

Drop recorded entries but stay enabled. Called between molecules by a batch consumer that names more than one input per process.

orthonym.metrics.candidate_ledger.disable()#

Stop recording on this thread and drop the entries.

orthonym.metrics.candidate_ledger.enable()#

Start recording on this thread, and clear anything already recorded.

orthonym.metrics.candidate_ledger.is_enabled()#
orthonym.metrics.candidate_ledger.read_ledger()#

The entries recorded on this thread, in order. Empty when disabled.

orthonym.metrics.candidate_ledger.record_candidate(site, stage, name, scope=None, detail=None)#

Append one event. No-op unless enable was called on this thread.

scope=None (the default) resolves scope and depth from the current naming depth via:func:resolve_scope; pass an explicit scope only to override that, as the substituent-cascade hook does (it knows it produced a fragment name regardless of the depth it was called at).

Never raises: a telemetry failure must not change a name. The bare except mirrors abstention.record_abstention for the same reason.