Implementation plan — the road to "no TOML for common cases"¶
Status: Phases 0–2 shipped; sacred/glue apply, jm regenerate, and
the --impl line-range form landed in v0.14. This document is the
coordination point for the work the design docs sketch out: the
decision tree, the
template gallery, and the
bind design. The interactive wizard was cut (retired
before shipping; the design doc lives in git history). Read the
design docs for the why; this one is the what, in what order, how do
we know we're done.
North-star goal¶
A user who knows DSP can ship a Python C extension without ever
learning (a) the just-makeit TOML schema, (b) the CPython
binding ABI, or (c) the CMake details. They pick a preset, type
one CLI command, open _core.c, replace the /* TODO */ markers, and
jm build && jm test. Everything else — the manifest, the binding,
the CMakeLists, the tests, the bench, the type stub — is generated and
stays in sync.
This goal does not retire TOML. The manifest stays as the on-disk
truth and jm apply keeps materialising from it under the sacred/glue
contract; advanced users who prefer hand-authoring TOML are
first-class. What changes is that the CLI becomes a complete
alternative — every common feature reachable through TOML is also
reachable through a flag, and the templates document the type allowlist
for each slot so neither path surprises.
What stays¶
The load-bearing primitives nothing replaces:
- Renderer (
_render.py+ the templates undersrc/just_makeit/templates/). Already covers every shape we ship; both the TOML flow andjm bindfeed it the samecontext: dict[str, str]. - Context builders (
_context/_state.py,_methods.py,_step.py,_sample.py,_parse.py).make_state_ctx,make_methods_ctx,make_step_ctx,make_sample_ctx,make_properties_ctx,make_perf_ctx. These are the canonical translation from "what the user wants" into "what the renderer needs." - Type registry (
_types._CTYPE_META,_ARRAY_DTYPE). The accepted-types vocabulary, now documented per slot indocs/types.md. - TOML as the manifest format.
just-makeit.tomlis the durable description;jm applymaterialises from it. Every flag added in this plan still round-trips through TOML. - Existing CLI verbs:
new,object,module,method,property,function,add,perf,apply,regenerate,remove,build,test,bench,app,script,upgrade. Behaviour is preserved for every flag they accept today. - The gallery shapes as the canonical taxonomy of components.
Every new CLI flag and every
jm bindparse case routes through one of them.
What goes¶
Things we are committing to retire from the user-facing surface. They keep working in TOML for as long as the schema needs them; they stop being the recommended path the moment their CLI counterpart ships.
- TOML-only knobs without CLI parity:
out_type,out_divisor,variable_output,result_fields,multi_output,init_params,create_impl/reset_impl/destroy_impl,extra_link_libs(per-component),extra_include_dirs,extra_types,find_packages,pkg_modules,c_deps,result_fields. Every one gets a flag in Phase 2. just-makeit.toml-first explanations in onboarding docs. The decision tree, template gallery, and quick reference become the landing pages;docs/configuration.mdstays as a reference for advanced users but stops being a prerequisite for getting started.jm wizardentirely. Cut, not deferred. The preset pages plus single-shot CLI cover the same need without a parallel interactive surface to learn, test, and document.- Implicit fall-throughs. When the user passes a type a slot doesn't accept, the CLI errors with the slot's full allowlist (and a link to the relevant gallery page). No more "silently compiled, failed at runtime" surprises.
What changes¶
Refactors that aren't strictly additive:
- Shared type-slot validator. A new helper —
_types.validate_slot(slot: str, type_str: str) -> None— used by every CLI handler and the bind parser. Single error format:"--state field type 'const char *' is not legal here. Allowed: float, double, int, ... See docs/types.md#state-variable-types." - Presets become first-class. Each gallery shape gets a top-level
--presetflag onjm object(andjm functionis its own verb). The existing--no-step/--arg-type voidcombos still work; the preset is a labelled bundle. Working presets:processor(default),generator,consumer,reader.blockwiseis excluded — array return (T[] -> T[]) is unsupported and now errors cleanly. Variable-output is a capability flag, not a preset. jm bindbecomes a top-level workflow, not just a debug tool. Bind goes from "parse for the processor shape" to "parse for every working shape, with--checkrunning in CI on every example."
Phases¶
Phase 0 — fixes and conventions (shipped, v0.13.22)¶
Done. Closed seven bugs (gh-65, gh-66, gh-68, gh-69, gh-70, gh-71,
gh-72), added extra_include_dirs and --out-param, wrote the
decision tree. Establishes the type-slot
conventions the rest of the work consumes.
Phase 1 — gallery and bind MVP (shipped)¶
Done:
- Template gallery — preset pages with
Concrete-types tables (
Accepts | Rejects | Default). - bind design.
- Type-slot docs (
docs/types.md) covering all five slots with explicit allowlists. jm bindMVP for the processor shape — byte-identical round-trip.running_statsusesjm bindend-to-end.
Phase 2 — CLI parity for every TOML field (shipped, v0.13.23)¶
Every common TOML-only knob got a flag. Each row was one PR.
| New flag | TOML it replaces | Affects |
|---|---|---|
--init-param name:T[:D] |
[[obj.init_params]] |
jm object, jm method |
--out-type T |
out_type (methods + functions) |
jm method, jm function |
--out-divisor N |
out_divisor (methods) |
jm method |
--variable-output + --max-out N |
variable_output = true, sibling _max_out declaration |
jm object, jm method |
--multi-output T,T,... |
multi_output |
jm method |
--result-field name:T (repeatable) |
result_fields = [...] |
jm method, jm function |
--impl SLOT::FN / --impl file::N:M |
create_impl / reset_impl / destroy_impl / impl |
jm object, jm method, jm function |
--find-package NAME |
[project] find_packages |
jm new |
--pkg-module NAME |
[project] pkg_modules |
jm new |
--c-dep DIR |
[project] c_deps |
jm new |
--extra-include-dirs DIR |
per-component / per-module extra_include_dirs |
jm object, jm module |
--extra-types NAME,NAME |
[module] extra_types |
jm module |
Also in this phase: the shared validate_slot() helper, applied to
every existing and new flag, with regression tests for every slot's
rejection cases.
Success bar: a one-page table in docs/configuration.md listing every
TOML field has a "Reachable via CLI" column, and every row is ✓.
Phase 3 — the presets, bind for all of them¶
Two threads run in parallel; each delivers one piece of the "pick a shape, scaffold it" experience.
Thread 3a: Presets. (shipped — see the working presets below)
- [x]
--preset processor(alias for current defaults; documented) - [ ]
--preset blockwise— excluded. Array return (T[] -> T[]) is unsupported; the CLI errors cleanly on it. Array input (--arg-type "T[]") works. - [x]
--preset generator—--arg-type void; emits asteps(n)generator skeleton (void-arg defaults to a complex return). - [x]
--preset consumer—--return-type void; emits an accumulator skeleton. - [x]
--preset reader—--no-step --init-param filepath:"const char *"plusread()/seek()/close()methods registered withopen()/stat()already wired.
Variable-output (event-emitter shape) is a capability, not a preset:
--variable-output --max-out N with repeatable --result-field name:T
on any output-producing preset.
→ Current focus.
Thread 3b: Bind expansion.
- [ ] Parse methods (
<comp>_<verb>(...)declarations). - [ ] Parse init_params (ctor params that aren't state fields).
- [ ] Parse output-param naming (
out/output/dst/dest). - [ ] Parse variable-output (
_max_outsibling pairing). - [ ] Parse opaque state (forward decl in header, definition in
.c). - [ ]
jm bind --checkruns in CI for every bundled example.
Thread 3c (Wizard) was cut in the v0.13.23 retrospective for maintenance burden and the small finite pattern set. The gallery's preset pages + CLI commands cover the same need without a parallel interactive surface.
Success bar:
- Every preset, run with the documented CLI command, produces a
project where
jm build && jm testpasses immediately. jm bind --checkis green on every bundled example.
Phase 4 — third-party importability, libclang fallback, docs reordering¶
Polish and proof. By the end of this phase the project's pitch becomes "write your DSP in C; we make it Python."
- [ ] Add a bundled example that imports a real third-party header
(a single-file DSP library, shimmed to follow the contract) via
jm bind. Pick something small and well-known. - [ ] libclang fallback for
jm bindwhen regex parsing fails. Optional dependency (pip install just-makeit[bind-robust]). - [ ] Re-landscape
docs/so the decision tree and template gallery are the onboarding path.docs/configuration.mdstays but moves to a reference section. - [ ] Contract-versioning comment in
_core.h(// jm-bind: contract-1) so bind can warn cleanly on older shapes.
Success bar: a new user opens docs/, picks --preset reader, runs
the CLI directly, and ships a working Python extension in under five
minutes without opening any TOML file.
Bind acceptance matrix¶
Living table. A row goes green when:
- The preset's CLI flag works (
jm object NAME --preset Xproduces the canonical scaffold). jm bind NAMEagainst the resulting_core.hproduces an_ext.cbyte-identical to the scaffolded one.jm testpasses against the bound binding.jm bind --checkis wired into the bundled example's CI.
| Preset | Phase 1 (MVP) | Phase 3 (full) |
|---|---|---|
processor |
byte-identical ✅ (single + multi-field) | — |
blockwise |
excluded (array return unsupported) | excluded |
generator |
n/a | pending |
consumer |
n/a | pending |
reader |
n/a | pending |
function |
n/a | pending |
Risks¶
- Convention rigidity. The contract
jm binddepends on (<comp>_state_t, lifecycle trio naming, output-param naming) will feel limiting to users importing existing C. Mitigation: document an escape hatch —--annotate FUNC:OUT:nameonjm bindto override naming heuristics per call without changing the source. - TOML-only features keep accruing. Easy to add an experimental knob to TOML "just for now." Mitigation: every TOML key added after Phase 2 requires a parallel CLI flag in the same PR.
Explicit non-goals¶
- Replacing
jm applywith bind. Bind reads C; apply reads TOML. Both stay; they're two front-ends to one renderer. - Any interactive surface. No wizard, no TUI, no GUI. The CLI plus the inspectable preset pages are the whole interface.
- Cross-language bindings. Rust / C++ wrappers stay future work. Bind producing CPython only.
- Schema migrations beyond what
jm upgradealready does. No breaking changes to TOML in this arc. - Performance tuning. The renderer's
<<...>>substitution is already negligible; bind's regex pass is microseconds. Worry later.
How to use this document¶
- Reviewing a PR: identify which phase + which bullet it lands. PR description should cite the phase number and the success-bar line it advances.
- Adding a new TOML key: don't. Open an issue describing the need; the response is either "do this with existing keys" or "add a parallel CLI flag in the same PR."
- Adding a new preset: it's an extension of Phase 3 thread 3a. Add a gallery page first (Concrete types table mandatory), then the CLI flag, then the bind parse rule.
- Marking work done: tick the relevant checkbox; update the bind acceptance matrix.
The matrix and the per-phase checklists are the canonical "are we done" signals — not commit counts, not lines of code, not "the design is complete." Done means user-visible behaviour matches the success bar.