Codifies the workflow that has worked in practice during the pre-beta hardening: derive expected values from this tree (never another implementation), narrow proof-of-shape slices with one issue per slice, the JIT-vs-interpreter two-lane test discipline, how to read the smoke harness's deterministic counts, the tier2 softlib parity rule for engine changes, and the concrete-code-level-path requirement for security reports. Defers build/layout/style to AGENTS.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
8 KiB
Contributing to TinyMUX
TinyMUX 2.14 is in alpha. No games should run it in production yet; the goal for the next several months is to raise quality — correctness, test coverage, portability — before entering beta. Contributions that move that needle are very welcome, whether you are a person or an AI agent working with a human in the loop.
This document is the contribution workflow and standards. For repository
layout, build commands, and coding style, see AGENTS.md; for the
generated-file map, see docs/generated-files.md.
Principles
- Every PR is reviewed on its merits, regardless of who wrote it. A clean track record earns goodwill, not a lighter review — good etiquette is exactly the cover a bad-faith contribution would use. Reviews read the diff.
- Derive claims from this tree, never from another implementation. Build it, run it, probe it. Expected values, error strings, and behavior come from what the current code actually does — not from a spec, another MUSH server, ICU, Python, or a source-reading guess.
- Narrow beats broad. A small, self-contained, reviewable change lands fast. A sweeping one stalls. When in doubt, slice it.
Before you start
Build the server per the "Build, Test, and Development Commands" section of
AGENTS.md, then confirm a clean smoke baseline:
cd testcases && ./tools/Makesmoke && ./tools/Smoke # results in smoke.log
The smoke tests exercise engine.so and the loadable modules, so build with the
full make install (not a netmux-only build) before running them. Add
--enable-jit at configure time to also cover the JIT/DBT path.
A green run is: every test succeeds, Dispatched: N / N expected (dispatch
equals the suite count), 0 failures, 0 crashes. See "Reading smoke results"
below — the counts depend on build config, but those three properties do not.
The contribution rhythm
This is the flow that has worked well in practice:
- Discuss first. For anything beyond a trivial fix, open an issue (or comment on the relevant tracking issue) describing the change before writing code. Cheap to redirect a plan; expensive to redirect a finished PR.
- Proof-of-shape, optionally. For work where the target is a judgment call, it's welcome to build the change locally, report the branch, the diff size, and your validation — but hold the push until the target is agreed. Zero rework if the answer is "go"; nothing wasted if it's "different target."
- One issue per slice; one PR per issue. Keep the tracking issue as a map, not a checklist that grows without bound. A big effort becomes a series of small, individually-reviewable PRs.
- Scope your commit message honestly. Use
Partially addresses #NNNwhen a slice advances a larger issue; reserveCloses #NNNfor actually completing it.
Contributing tests
The smoke corpus (testcases/*.mux) is the front line of the pre-beta hardening,
and the easiest place to start.
- Pick a real gap. Many
*_fn.muxfiles have happy-path coverage only. Boundary values, error strings, empty/null inputs, and grapheme-cluster edges are the productive seams.israt(1e10)vsisnum(1e10),right()on an emoji+modifier cluster,fmod(x,0)— each pins a real behavioral fact. - Derive every expected value by probing this tree, e.g.
muxscript -e 'think [yourexpr]'or a scratch.muxscript. Do not import an expected value from another implementation. Sanity-check Unicode results against the tables actually built here. - Two evaluation lanes. A build with
--enable-jitcompiles hot softcode through the JIT/DBT; without it, everything runs the AST interpreter. These can diverge (that's a bug — several were found this way). To exercise the compiled path, feed a runtime value (setr()), because all-constant arguments constant-fold and never reach it. An assertion that pins behavior on both lanes at once is the strongest kind. - Placement.
testcases/is for softcode-visible behavior.tests/(libmux,color_ops,db,netaddr) is for lower-level C/C++ behavior (nullptr handling, LBUF limits, byte-level UTF-8). Put a case where its subject lives. - Harness conventions. Fold a new assertion into an existing test case when
it belongs to the same semantic block; add a new
TC00Nwhen it's a distinct block (e.g. an error path alongside a positive-formatting case). Exactly one test case per file fires@trig me/tr.done— the terminal one. If you add a new terminal case, move the trigger to it; don't leave two.
Reading smoke results
- Counts are deterministic per build config, not random. A
--enable-jitbuild runs more cases (JIT-specific suites); an interpreter-only build logs those asSkipped (JIT not enabled). A stable difference between two environments is almost always this, not a flake. Report your build config alongside your counts. - The signal that matters is dispatch + failures + crashes, not the raw
succeeded count:
Dispatched: N / N expected,Failed: 0,Crashes: 0. The completeness gate names any suite that never dispatched. - A fast run with a low count and no failures may be a crash, not a pass. If
muxscript dies mid-run the harness reports it as a crash; on macOS a
~/Library/Logs/DiagnosticReports/muxscript-*.ipsalso appears. Don't read a truncated run as success. - Report harness limitations separately from real failures, so a reviewer can tell "the suite couldn't test X" from "X is broken."
Contributing engine or client code
- Verify beyond the tests. For a runtime change, confirm it builds, the database still loads, and full smoke stays green — and probe the specific behavior you changed. A passing test suite is necessary, not sufficient.
- Mind the tier2 parity copy. Some helpers in
mux/rv64/src/softlib.cdeliberately mirror host functions for JIT parity. If you change a host function's observable behavior, grep the softlib for a matching wrapper, fix it in the same change, update any test that pins the old value, and regenerate the blob. A host-only fix silently diverges the two lanes. - Never hand-edit generated files. Edit the source input and regenerate;
keep the source change in the same diff. See
docs/generated-files.md. - Keep diffs tight. Revert incidental regeneration churn (shifted
#linemarkers, autotools aux-script noise) that isn't part of your change.
Reporting bugs
- Include a concrete reproduction: the exact input, the observed result, and the
expected result — ideally a one-line
muxscript -eprobe or a short.muxscript. - If the behavior differs between the JIT and interpreter lanes, say so and give both results — that's a specific, valuable class of report.
Reporting security issues
Legitimate security reports are welcome and taken seriously. To be actionable, a report must include a concrete, code-level exploitation path: the specific function and line, the mechanism (what memory is written past what bound, or what invariant is violated), and a realistic trigger. "A scanner flagged a pattern" or an unqualified severity label is not, by itself, a finding — automated tools routinely mislabel length-bounded or heap-managed code as exploitable. A report that can't be traced to a real defect in the code will be closed with an explanation. If you found a genuine memory-safety or correctness issue, the concrete path is what lets us confirm and fix it quickly.
Etiquette
- Assume good faith; give it in return. Specific, factual review comments in both directions.
- Respond to review feedback with the change plus how you verified it. Cite the commit you built against.
- It's fine to flag something you noticed in passing (a latent bug, an adjacent gap) without turning it into scope creep — note it, and let the maintainer decide whether it becomes its own issue.
Thanks for helping make TinyMUX solid enough for beta.