Skip to content

A /new-plot-type skill — the gate and the maths, not a checklist #92

Description

@AlexisJanin

Rewritten after a grilling session (2026-08-27). The original scope — scaffold the package, register it, write config keys, tests and docs — was a checklist against a failure #93 has since made impossible. A forgotten half is now an ImportError at start-up, so a checklist would guard a locked door. The skill's job is what the code still cannot decide.

The decision

The skill is the gate and the semantic choices, not a checklist.

Two questions no amount of structure can answer — is this a plot type at all, and what maths does it draw — plus the periphery a package lands in but does not contain.

Why the original scope is obsolete

This issue argued from PSD's cost: 7 src files, +680 lines, one site still missed. #93 removed that argument:

  • registry._check_registry_is_complete() refuses a type whose NAME and SECTION_KEY disagree, or whose plot.py was never written
  • builders.py refuses a registered type with no builder
  • tests/plot_types/test_boundaries.py refuses a shared module that learns a type's name

Between them, a plot type that validates cleanly and renders nothing is now an import-time crash. What is left undecidable by code is the gate, the maths, and the documentation.

The gate: the capability delta

A plot type earns a package only by declaring a delta from time_series. Every default in PlotTypeSchema is time_series, so what a type overrides is exactly what makes it one.

Only four of the six flags discriminate:

Flag time_series loop spectrogram psd
TIME_AXIS
GRID_LAYOUT
HAS_COLORBAR
POINT_TIMESTAMPS

RESAMPLED and UNIFIED_HOVER are False on all three derived types — "I am derived" spelled twice, not a delta. A gate that counted them would accept anything that is not a raw time-series.

The gate falls out of the classification rather than preceding it, unlike /new-datasource's separate classify-then-route: the four answers that pass the gate are the four that configure the scaffold. Routing to a reference package comes after.

The "no" branch has no destination — a known gap

When no flag is a delta, the skill names the hole and stops. /new-datasource's equivalent branch works only because other/ exists to receive it; there is no other/ here.

New maths on a signal, still drawn against time and sharing a zoom with its source — a compliance curve, a moving average — has no home today. Every builder sets its own plot_type=, and nothing builds a derived Signal that renders as a time-series. Forcing it into a plot type would park it on a page section of its own, away from the signal it came from.

Worth its own issue. Not opened.

The maths is a blocking checkpoint

plot.py is an adapter, not a maths module. spectrogram_from_signal calls spectral.spectrogram() and spends its whole body wrapping the result into DataPlotOptionsTraceOptionsSignalRenderSpec. spectral.py is a 240-line leaf imported only by the two spectral types; loop is the exception, ten lines of np.interp/union1d inline.

The skill scaffolds the adapter and never invents the transform — a plot type is a way of drawing, and what it computes is a clinical claim. Two things must exist before plot.py is written:

  1. The maths function — supplied, named, or dictated by the user. Both homes are shown and neither mandated: a leaf module beside spectral.py, or inline in plot.py.
  2. Its refusal — the exception meaning a deliberate, reportable "no". plot_assembly grades an undeclared exception with logger.exception and a full traceback, so an undeclared refusal is logged as a bug the first day a clinician meets it.

Grilling here is conditional — fired only when the maths has real choices to settle (tunable parameters, a regrid decision, a condition it can refuse on).

Decisions

  1. Gate criterion = the capability delta. /grilling is the fallback when it will not resolve.
  2. The gate falls out of the classification — one step, not classify-then-route.
  3. The unguarded periphery becomes tests, not skill prose. Structural plus doc-heading; CLAUDE.md deliberately excluded.
  4. The skill scaffolds the adapter, never the maths. Declaring the refusal exception is a required step.
  5. Blocking checkpoint at the maths, with conditional grilling.
  6. Model-invoked, two trigger branches — solution-shaped ("add a plot type", "new kind of plot") and symptom-shaped ("plot X against Y", "as a heatmap"), so the gate can fire before "plot type" is the user's framing. Newcomers do not go looking for local skills.
  7. Leading words: delta and contract. Both already live in the codebase (base.py "declares only its deltas", CLAUDE.md "Critical contracts"), so they recruit priors for free.
  8. grilling vendored verbatim into .claude/skills/grilling/ with its MIT LICENSE.txt (© Matt Pocock, github.com/mattpocock/skills); agents/openai.yaml dropped.
  9. Lands on Extract a plot_types/ package — one module per plot type (#83, #91) #93 rather than waiting for the merge — 99% new files and documentation, none of it on the refactor's risky surface.

The periphery, now guarded

A package is guarded by the registry; what surrounds it was guarded by nothing. Three of the five items now go red on a type that exists only in code:

Periphery Guard
configured in the demo database_options tests/unit/test_example_assets.py::TestDemoPlotTypeCoverage
a tutorial.md heading tests/plot_types/test_plot_type_is_documented.py
a CONTEXT.md glossary term same
tests/plot_types/test_<name>.py the skill only
CLAUDE.md derived-type list the skill only

The doc guard is a weaker category than everything else in test_example_assets.py, which only ever compares registry-derived sets against disk-derived ones. It anchors on headings and bold glossary terms, never a search of the prose: "loop" appears throughout the tutorial as the datasource loop, a loop subplot's height, multi-cycle loops, so a body search would report green for a type nobody had documented. Accepted spellings come off the schema (NAME, SECTION_KEY, SHEET_NAME), which is what lets loops match loop without hardcoding a plural.

Acceptance criteria

  • .claude/skills/new-plot-type/SKILL.md — model-invoked, both trigger branches, within /new-datasource's length budget
  • The gate asks the four discriminating flags once, and stops when none is a delta
  • The maths is a blocking checkpoint whose criterion is "the function exists and its refusal type is declared"
  • /grilling vendored verbatim with its licence
  • Every registry.DERIVED type is configured in the demo config (asserted)
  • Every registry.DERIVED type has a tutorial heading and a CONTEXT.md entry (asserted)
  • Both guards green on the three existing types — no red state to drive

Relations

Follow-up to #83. Lands on #93.

Uncovered along the way, neither opened: the missing derived-signal path (the gate's "no" branch, above), and loop's refusal mis-classificationloop_from_signals raises a bare ValueError for "Signals do not have overlapping time intervals", an ordinary clinical situation, while BUILDER declares refusals=(PlotTypeArityError,) only, so a legitimate refusal logs with a full traceback.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions