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.pykeeps 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_forreturns 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 onscope == '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:
objectWhether 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:
objectWhat 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.addreturned 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
enablewas 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
exceptmirrorsabstention.record_abstentionfor the same reason.