Skip to content

SMonitor: the catalog is wired but not connected — every diagnostic renders an empty message #15

Description

@dprada

Notice from the SMonitor side, found while syncing SMONITOR_GUIDE.md. This
repository was carrying the guide from smonitor@0.11.5 (2026-03-10): the sync
script named five repositories while nine carried the guide, and this was one of
the four it had forgotten. Three releases of rules have just arrived in one commit.

Reading the integration against those rules turned up something that is not a
style point: the catalog is wired but not connected. Every catalog-backed
diagnostic here renders an empty message.

How this was checked

import topomt pulls dependencies that fail in the environment I had, so the
configuration was reconstructed statically from _smonitor.py and
_private/smonitor/catalog.py and run through SMonitor's own validator and
resolver. Confirm it in your environment with one command:

smonitor --validate-config --config-path topomt

What comes back

- Unknown top-level key: ERRORS
- Unknown top-level key: WARNINGS
- CODES[SIGNALS] must define a message field
- CODES[ERRORS] must define a message field
- CODES[WARNINGS] must define a message field
>>> smonitor.resolve(code="NotDigestedArgumentWarning", extra={"argument": "x", "caller": "f"})
('', None)
>>> smonitor.resolve(code="PocketeerDelaunayWarning", extra={"reason": "r"})
('', None)

Four things, in the order they break

1. CODES is indexed by the wrong keys. SMonitor looks templates up as
CODES[<the code string>]. Here CODES is

CODES = {"SIGNALS": ..., "ERRORS": ..., "WARNINGS": ...}

so Manager._codes has three entries named SIGNALS, ERRORS and WARNINGS,
and no catalog key ever resolves against it.

2. The templates are under a field name SMonitor does not read. Entries carry
"template". SMonitor reads user_message, dev_message, qa_message,
agent_message and a generic message — section 1.2 of the guide you just
received lists them, along with the fallback chain added in 0.14.0, which means
one field is now enough rather than four.

3. Catalog entries carry no code. emit_from_catalog reads entry["code"],
so events emitted through DiagnosticBundle arrive with code=None. They are
then invisible to events_by_code, to fingerprint summaries and to any QA policy
keyed on codes — which is most of what the catalog is for.

4. ERRORS and WARNINGS are not SMonitor top-level names. The recognised
ones are PROFILE, SMONITOR, PROFILES, ROUTES, FILTERS, CODES and
SIGNALS. The two extra names are reported and ignored.

SIGNALS is the part that is correct: CATALOG["signals"] is exactly the shape
extra_required contracts expect, and that half works.

While you are in there: the class shape

class NotDigestedArgumentWarning(TopoMTCatalogWarning):
    catalog_key = 'NotDigestedArgumentWarning'
    def __init__(self, argument, caller=None):
        super().__init__(extra={'argument': argument, 'caller': caller})

class PocketeerDelaunayWarning(UserTopoMTWarning):
    catalog_key = 'PocketeerDelaunayWarning'
    def __init__(self, reason):
        super().__init__(extra={'reason': reason})

This is the domain-field-first shape section 3.3.1 exists to forbid, and your
templates interpolate exactly those fields ({argument}, {caller}, {reason}).
Python rebuilds a warning as type(w)(*w.args)pickle, copy.deepcopy,
warnings.warn(text, category) and pytest-xdist between a worker and the
controller all do it — so once the templates are connected, the rebuilt instance
receives its own rendered sentence as reason and renders around its own output:

Pocketeer Delaunay tessellation failed: Pocketeer Delaunay tessellation failed: r

It cannot bite today only because nothing renders. Fixing the wiring without
fixing the shape trades an empty message for a doubled one.

TopoMTCatalogWarning.__init__(self, **kwargs) has the same root: with no
positional parameter, warnings.warn(text, TopoMTCatalogWarning) raises
TypeError. That path became reachable in 0.13.0, when warn() started raising
the Python warning as well as emitting the event (section 3.3.2).

The shape the guide asks for is message first, domain fields keyword-only:

class PocketeerDelaunayWarning(UserTopoMTWarning):
    catalog_key = 'PocketeerDelaunayWarning'

    def __init__(self, message=None, *, reason=None):
        super().__init__(message, extra={'reason': reason})

    @classmethod
    def for_reason(cls, reason):
        rendered, _ = smonitor.resolve(code=CATALOG['warnings'][cls.catalog_key]['code'],
                                       extra={'reason': reason})
        return cls(rendered, reason=reason)

Verifying the fix

Section 7 of the guide you just received is four checks, one assertion each,
as a copy-pasteable test file. Checks 1 to 3 catch everything above, and check 4
is the args round-trip. One warning from it worth repeating here: test args
idempotence, not only pickle. pickle restores the instance dictionary
afterwards, so it comes out correct even for a class written the wrong way
a pickle-only test certifies the defect it was written to catch.

type(exc)(*exc.args).args == exc.args

Happy to answer anything on the SMonitor side.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions