@@ -85,6 +85,24 @@ for step in analysis.decisions:
8585 print (step.phase, step.decision, step.reason)
8686```
8787
88+ ### Natural-language description (` describe ` )
89+
90+ ` bluenamer.describe(smiles) ` walks the same trace and renders a
91+ deterministic, multi-paragraph explanation of how the name is built.
92+ Useful for explainability views and for generating (SMILES, name,
93+ description) training tuples:
94+
95+ ``` python
96+ from bluenamer import describe
97+
98+ d = describe(" CC(=O)Nc1ccccc1" )
99+ print (d) # multi-paragraph prose
100+ d.rules_hit # ('P-44', 'P-45', 'P-41', 'P-61', 'P-67')
101+ d.components[0 ] # DescribedComponent(phase='parse', text='RDKit parsed ...')
102+ ```
103+
104+ Same input → same output. No LLM in the loop.
105+
88106### CLI
89107
90108``` bash
@@ -104,7 +122,10 @@ pip install -e ".[dev]"
104122pytest
105123
106124# run only fast tests
107- pytest -m " not slow and not dataset"
125+ pytest -m " not slow and not dataset and not golden"
126+
127+ # strict RDKit-version regression suite (also runs in the rdkit-compat CI job)
128+ pytest -m golden
108129
109130# lint and format
110131ruff check --fix src/bluenamer
@@ -113,6 +134,164 @@ ruff format src/bluenamer
113134
114135Java is required for the OPSIN-based round-trip checks (see ` py2opsin ` ).
115136
137+ ## HTTP service (Docker)
138+
139+ The ` [web] ` extra ships a FastAPI app with ` name ` , ` batch ` , ` describe `
140+ and ` healthz ` endpoints. The bundled ` Dockerfile ` includes a headless JRE
141+ so ` verify_opsin=True ` works out of the box.
142+
143+ ``` bash
144+ # build + run
145+ docker build -t bluenamer:local .
146+ docker run --rm -p 8000:8000 bluenamer:local
147+
148+ # or via compose
149+ docker compose -f docker/compose.yaml up --build
150+ ```
151+
152+ Call the API:
153+
154+ ``` bash
155+ curl -X POST localhost:8000/name -H ' content-type: application/json' \
156+ -d ' {"smiles":"CC(=O)Nc1ccccc1","include_trace":true,"verify_opsin":true}'
157+
158+ curl -X POST localhost:8000/batch -H ' content-type: application/json' \
159+ -d ' {"smiles":["CCO","c1ccccc1","CC(=O)O"],"processes":1}'
160+
161+ curl -X POST localhost:8000/describe -H ' content-type: application/json' \
162+ -d ' {"smiles":"CC(=O)Nc1ccccc1"}'
163+ ```
164+
165+ OpenAPI docs are served at ` http://localhost:8000/docs ` .
166+
167+ ## Debugging
168+
169+ Token binding metadata is currently available through the assembly decision trace when ` include_trace=True ` .
170+
171+ ``` python
172+ from blunamer import name
173+
174+ result = name(" C(C1C(C(C(C(O1)O)O)O)O)O" , include_trace = True )
175+
176+ print (result.name)
177+ # 6-(hydroxymethyl)oxane-2,3,4,5-tetraol
178+ ```
179+
180+ To inspect token spans:
181+
182+ ``` python
183+ def name_token_spans (result ):
184+ for step in reversed (result.decisions):
185+ if isinstance (step.data, dict ) and " name_token_spans" in step.data:
186+ return step.data[" name_token_spans" ]
187+ return []
188+
189+
190+ for token in name_token_spans(result):
191+ print (
192+ token[" text" ],
193+ " atoms=" , token[" atoms" ],
194+ " bonds=" , token[" bonds" ],
195+ " kind=" , token[" token_kind" ],
196+ " confidence=" , token[" confidence" ],
197+ " source=" , token[" source" ],
198+ )
199+ ```
200+
201+ Example output:
202+
203+ ``` text
204+ 6 atoms= [0, 11] bonds= [1, 11] kind= locant confidence= derived source= typed_rewrite
205+ hydroxymethyl atoms= [0, 11] bonds= [11] kind= prefix confidence= derived source= substituent_renderer
206+ oxane atoms= [1, 2, 3, 4, 5, 6] bonds= [2, 3, 4, 5, 6, 12] kind= parent confidence= derived source= typed_rewrite
207+ 2,3,4,5 atoms= [2, 3, 4, 5, 7, 8, 9, 10] bonds= [3, 4, 5, 7, 8, 9, 10] kind= locant confidence= derived source= typed_rewrite
208+ tetraol atoms= [2, 3, 4, 5, 7, 8, 9, 10] bonds= [3, 4, 5, 7, 8, 9, 10] kind= suffix confidence= derived source= renderer_suffix
209+ ```
210+
211+ The token metadata is split into ` token_kind ` , ` ownership ` , ` confidence ` , and ` source ` .
212+
213+ ### ` token_kind `
214+
215+ What grammar role the token plays.
216+
217+ | Value | Meaning / example |
218+ | ---------------- | --------------------------------------------------------------------------- |
219+ | ` parent ` | Parent skeleton token, e.g. ` ethan ` , ` benzene ` , ` spiro[...] ` . |
220+ | ` prefix ` | Prefix/substituent token, e.g. ` chloro ` , ` methyl ` , ` hydroxy ` . |
221+ | ` suffix ` | Principal suffix token, e.g. ` acid ` , ` ol ` , ` one ` , ` nitrile ` . |
222+ | ` locant ` | Locant token, e.g. ` 2 ` , ` 1,3 ` , ` N ` , ` 4a ` . |
223+ | ` charge ` | Charge-bearing name part, e.g. ` ium ` , ` oxide ` , ` ammonio ` . |
224+ | ` hydro ` | Indicated hydrogen or hydro operation, e.g. ` 1H ` , ` dihydro ` . |
225+ | ` replacement ` | Replacement prefix token, e.g. ` oxa ` , ` aza ` , ` thia ` . |
226+ | ` unsaturation ` | Unsaturation token, e.g. ` en ` , ` yn ` , ` diene ` . |
227+ | ` modifier ` | Front or suffix modifier token, e.g. stereo, hydro, or functional modifier. |
228+ | ` grammar ` | Pure grammar token, e.g. ` di ` , ` bis ` , or parentheses-bridging particles. |
229+ | ` structural ` | Structural token that does not fit a narrower kind. |
230+ | ` retained_alias ` | Token matched as a retained-name alias or context term. |
231+
232+ ### ` ownership `
233+
234+ How the token claims graph atoms.
235+
236+ | Value | Meaning / example |
237+ | ------------------------ | ------------------------------------------------------------------------------------------ |
238+ | ` exact ` | Token was intentionally emitted for these atoms. Best case. |
239+ | ` preserves_binding ` | Text matched a known binding directly after assembly. |
240+ | ` preserve_all ` | Rewrite preserved all previous atom ownership. |
241+ | ` locanted_hydro ` | Hydro token owns atoms through locants, e.g. ` 1H ` . |
242+ | ` component_locant ` | Locant belongs to a component namespace, often primed spiro/fused components. |
243+ | ` role_alias ` | Token is an alias for a graph role. |
244+ | ` retained_alias_context ` | Token matched retained parent alias context. |
245+ | ` stage_alias ` | Token inferred from all bindings of a stage, e.g. generic suffix token. |
246+ | ` grammar_scope ` | Grammar token applies to nearby graph-bound terms, not its own atom. |
247+ | ` multiplier_scope ` | Multiplier token, e.g. ` di ` , scopes over repeated graph-bound terms. |
248+ | ` operation_scope ` | Token recovered from a named operation trace. |
249+ | ` morphology_gap ` | Token is a morphology bridge inside a compound token. |
250+ | ` ambiguous ` | Best-effort broad binding. Diagnostic only. |
251+ | ` unbound ` | No reliable graph binding found. Should be treated as a problem. |
252+ | ` absorbed ` | Token was absorbed by a rewrite into another token. |
253+ | rewrite-specific values | Values such as ` retained_replace ` or ` merge_replaced_span ` ; these come from rewrite rules. |
254+
255+ ### ` confidence `
256+
257+ How strong the assignment is.
258+
259+ | Value | Meaning |
260+ | ---------- | ------------------------------------------------------------------------------------------------------------------------------------ |
261+ | ` exact ` | Renderer emitted this token with atom metadata directly. |
262+ | ` derived ` | Recovered from locants, rewrite history, context, or operation trace. |
263+ | ` fallback ` | Best-effort binding. Useful for debugging, but not proof of correct graph naming. |
264+
265+ ### ` source `
266+
267+ Where the binding came from.
268+
269+ | Value | Meaning / example |
270+ | ---------------------------------- | ---------------------------------------------------------------------------- |
271+ | ` renderer ` | Direct renderer-emitted token. Strongest source. |
272+ | ` renderer_suffix ` | Principal suffix renderer emitted it. |
273+ | ` functional_prefix_renderer ` | Functional-prefix renderer emitted it. |
274+ | ` substituent_renderer ` | Substituent renderer emitted it. |
275+ | ` default_binding ` | Built from a broader ` NameAtomBinding ` when no finer token metadata existed. |
276+ | ` typed_rewrite ` | Came through a typed post-processing rewrite; atom metadata was propagated. |
277+ | ` direct_text_match ` | Token text directly matched an existing binding. |
278+ | ` locant_fallback ` | Locant was matched back to a binding by locant metadata. |
279+ | ` charge_suffix_fallback ` | Charge token inferred from charge suffix context. |
280+ | ` indicated_hydrogen_fallback ` | ` H ` or indicated hydrogen inferred from hydro metadata. |
281+ | ` dihydro_locant_fallback ` | Dihydro locants inferred from hydro operation metadata. |
282+ | ` primed_component_locant_fallback ` | Primed/component locant inferred from component scope. |
283+ | ` role_alias_fallback ` | Token matched a known role alias. |
284+ | ` retained_alias_context ` | Token matched retained-name alias context. |
285+ | ` stage_fallback ` | Token assigned to all bindings of a stage, e.g. parent, prefix, or suffix. |
286+ | ` grammar_token ` | Pure grammar token. |
287+ | ` operation_trace ` | Binding recovered from recorded naming operation. |
288+ | ` broad_fallback ` | Last-resort plausible chemical token binding. Diagnostic only. |
289+ | ` unresolved ` | No binding found. |
290+ | ` compound_gap_bridge ` | Token bridges adjacent bound tokens inside a compound word. |
291+ | ` compound_gap_token ` | Grammar-like token found inside a compound gap. |
292+ | ` compound_gap_unresolved ` | Unresolved token inside a compound gap. |
293+ | dynamic rewrite names | Any named rewrite can appear as a source if it changed the token. |
294+
116295## License
117296
118297MIT. See ` LICENSE ` .
0 commit comments