You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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 PlotTypeSchemais 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 Data → PlotOptions → TraceOptions → Signal → RenderSpec. 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:
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.
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
Gate criterion = the capability delta./grilling is the fallback when it will not resolve.
The gate falls out of the classification — one step, not classify-then-route.
The unguarded periphery becomes tests, not skill prose. Structural plus doc-heading; CLAUDE.md deliberately excluded.
The skill scaffolds the adapter, never the maths. Declaring the refusal exception is a required step.
Blocking checkpoint at the maths, with conditional grilling.
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.
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.
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
Uncovered along the way, neither opened: the missing derived-signal path (the gate's "no" branch, above), and loop's refusal mis-classification — loop_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.
The decision
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 whoseNAMEandSECTION_KEYdisagree, or whoseplot.pywas never writtenbuilders.pyrefuses a registered type with no buildertests/plot_types/test_boundaries.pyrefuses a shared module that learns a type's nameBetween 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 inPlotTypeSchemais time_series, so what a type overrides is exactly what makes it one.Only four of the six flags discriminate:
TIME_AXISGRID_LAYOUTHAS_COLORBARPOINT_TIMESTAMPSRESAMPLEDandUNIFIED_HOVERareFalseon 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 becauseother/exists to receive it; there is noother/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 derivedSignalthat 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.pyis an adapter, not a maths module.spectrogram_from_signalcallsspectral.spectrogram()and spends its whole body wrapping the result intoData→PlotOptions→TraceOptions→Signal→RenderSpec.spectral.pyis a 240-line leaf imported only by the two spectral types;loopis the exception, ten lines ofnp.interp/union1dinline.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.pyis written:spectral.py, or inline inplot.py.plot_assemblygrades an undeclared exception withlogger.exceptionand 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
/grillingis the fallback when it will not resolve.CLAUDE.mddeliberately excluded.deltaandcontract. Both already live in the codebase (base.py"declares only its deltas", CLAUDE.md "Critical contracts"), so they recruit priors for free.grillingvendored verbatim into.claude/skills/grilling/with its MITLICENSE.txt(© Matt Pocock,github.com/mattpocock/skills);agents/openai.yamldropped.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:
database_optionstests/unit/test_example_assets.py::TestDemoPlotTypeCoveragetutorial.mdheadingtests/plot_types/test_plot_type_is_documented.pyCONTEXT.mdglossary termtests/plot_types/test_<name>.pyCLAUDE.mdderived-type listThe 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 letsloopsmatchloopwithout hardcoding a plural.Acceptance criteria
.claude/skills/new-plot-type/SKILL.md— model-invoked, both trigger branches, within/new-datasource's length budget/grillingvendored verbatim with its licenceregistry.DERIVEDtype is configured in the demo config (asserted)registry.DERIVEDtype has a tutorial heading and aCONTEXT.mdentry (asserted)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-classification —loop_from_signalsraises a bareValueErrorfor "Signals do not have overlapping time intervals", an ordinary clinical situation, whileBUILDERdeclaresrefusals=(PlotTypeArityError,)only, so a legitimate refusal logs with a full traceback.