Repository navigation
Description
Summary
src/main.sh is the last large flat file: 1427 lines, 24 functions. It was carried out of
#931 deliberately — it is the dispatcher and the riskiest file in src/, so it deserves its
own issue rather than being that issue's tail end.
Measured on main (8f2a1c3), after #931 and #940 left src/ at 11 modules + 15 flat files.
What is in it
| Function | Lines | Concern |
|---|---|---|
cmd_test |
485 | flag parsing for the default subcommand |
exec_tests |
189 | the run lifecycle: dispatch, aggregate, report, exit code |
cmd_bench |
108 | bench flag parsing |
cmd_doc |
69 | doc flag parsing |
exec_multi_assert · exec_assert · handle_assert_exit_code · cmd_assert · is_assertion_function · is_exit_code_assertion |
213 | the standalone bashunit assert path |
watch_loop · watch_get_checksum |
73 | the --watch polling loop |
validate_config_or_exit · set_shard_or_exit · require_non_negative_int_or_exit · require_writable_path_or_exit · abort_unknown_option |
135 | input validation shared by the parsers |
cmd_init · cmd_learn · cmd_watch · cmd_upgrade |
87 | thin delegators to src/cli/ |
exec_benchmarks · cleanup · handle_stop_on_failure_sync |
62 | run helpers |
Proposed shape
Not pre-decided. Produce the responsibility map first, as every module under #931 did — but
the seams that look real from the function list:
- validation — the five
*_or_exit/abort_*helpers, called by every parser - the assert subcommand — six functions, 213 lines, entirely self-contained
- watch loop — two functions, already delegating to
src/cli/watch.sh - flag parsing —
cmd_*, which is most of the bulk - run lifecycle —
exec_tests,exec_benchmarks,cleanup,handle_stop_on_failure_sync
Open question worth answering in the map: cmd_doc, cmd_init, cmd_learn, cmd_watch and
cmd_upgrade are flag parsing for subcommands whose implementations already live in
src/cli/. Should each parser move next to its implementation, or stay together as one
parsing layer? Both are defensible; decide deliberately and record it.
Why this one is riskier than the previous seven
- It is the only caller of
src/cli/*,src/learn/,src/benchmark.shand the runner, so a
mistake here breaks every subcommand at once rather than one module. - refactor(src): group cohesive flat files into modules (assert, console, util) #940 showed subcommand moves interact with
build::embed_docsmarkers and entrypoint
ordering;cmd_docsits directly on that path. bashunitsourcessrc/main.shlast, after everything it dispatches to. Any split must
preserve that position.cmd_testat 485 lines is a singlewhile/caseover ~60 flags. Cutting it means cutting
acasestatement, which is the one shape where a mechanical line-range split is unsafe.
Constraints
All of ADR-010's, which by now have all drawn blood at least once:
- Aggregator (
src/main/index.sh) holds onlysourcelines and comments. - Derive segment boundaries from the next function's start, never a
^}$brace — a brace
inside a heredoc or acasearm ends the segment early and silently splits a function
(refactor(learn): split src/learn.sh into a src/learn/ module #938). git check-ignore -vthe new directory before committing (refactor(coverage): split src/coverage.sh into a src/coverage/ module #928 hid twelve files).- CI runs ShellCheck per file without
-x; reproduce locally with
SHELLCHECK_OPTS="-e SC1091 -e SC2155 -e SC2016". - A file-wide
# shellcheck disable=moves only to the files that need it — and check whether
it is stale first (refactor(coverage): split src/coverage.sh into a src/coverage/ module #928 and refactor(console): split results.sh into six single-purpose files #947 both found dead ones). - Grep for hardcoded
src/main.shpaths intests/,build.sh,Makefile,.github/,
.editorconfig,.gitignorebefore committing. refactor(learn): split src/learn.sh into a src/learn/ module #938 and refactor(state): split src/state.sh into a src/state/ module #946 both shipped red or vacuous
tests from a missed path; in refactor(state): split src/state.sh into a src/state/ module #946 a layering contract passed while checking nothing. - Bash 3.0+ floor; per-test paths stay fork-free.
Verification
Prove it is a relocation: the non-blank line multiset differs only by new shebangs, module
headers and source lines; function count unchanged; the built artifact's sorted code content
identical.
Then: ./bashunit tests/ · --parallel · --parallel --simple --strict · make sa ·
make lint · CI-mode ShellCheck · bash build.sh bin -v printing ✅ Build verified ✅.
Because this file owns dispatch, also smoke-test every subcommand end to end against the
built binary, not just dev mode: test, assert, doc, init, learn, watch,
upgrade, bench.
Acceptance criteria
- Responsibility map posted here and agreed before any code moves
- The
cmd_*-next-to-implementation question answered explicitly -
git diffshows only relocations — no renamed functions, no changed logic -
src/main/index.shholds onlysourcelines and comments -
bashunitstill sources the module last - Every subcommand smoke-tested against the built binary
- All suites above green, including
bash build.sh bin -v -
.claude/rules/architecture-map.mdupdated - No CHANGELOG entry — internal, no user-visible behaviour change
Do not
- Do not split
cmd_test'scasestatement across files - Do not rename functions
- Do not move
main.shearlier in the entrypoint's source order - Do not run
shfmt -w;make lintis the format gate
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsDone
Activity
Chemaclass commentedon Aug 1, 2026
Sequencing note. #949 groups the remaining 15 flat files into
system/,util/,api/,config/andbenchmark/, leavingmain.shas the only loose file insrc/. That lands first.Two things for this issue to decide once it does:
main.shbecomessrc/main/(with a realmain/index.shaggregator) or stays a thin root dispatcher after its parts distribute into existing modules —cmd_*parsers next to theirsrc/cli/implementations,exec_teststoward the runner, the*_or_exitvalidators somewhere shared.main.shtoindex.sh. Recommendation is no —index.shcurrently means exactly one thing, a pure aggregator with onlysourcelines and comments, enforced bytest_module_aggregators_hold_only_source_lines_and_comments. A rootindex.shholding 1427 lines of flag parsing would make the name mean two things depending on depth, and that test's glob (src/*/index.sh) would not even catch the drift. Ifsrc/ending up with zero loose files is the goal, option 1 above achieves it without overloading the name.Chemaclass commentedon Aug 1, 2026
Decided: no rename.
main.shkeeps its name;index.shstays reserved for pure aggregators.Chemaclass commentedon Aug 1, 2026
Responsibility map
Generated on
dda105b: 1427 lines, 24 functions, zero file-scope statements — which makesthis far safer than its size suggests. Nothing executes at source time.
Decision on the open question
cmd_*parsers stay together as one parsing layer, they do not move next to theirsrc/cli/implementations. Movingbashunit::main::cmd_docintosrc/cli/doc.shwould put twonamespaces in one file, and the alternative — renaming it — is forbidden here. The parsing
layer is a real thing: it owns flag validation and the
_or_exithelpers that every parsershares.
Proposed layout — 8 files
main/index.shmain/validate.shabort_unknown_option, the three*_or_exithelpers,set_shard_or_exitmain/test.shcmd_testmain/bench.shcmd_benchmain/subcommands.shcmd_doc,cmd_init,cmd_learn,cmd_watch,cmd_upgrademain/assert.shcmd_assert,exec_assert,exec_multi_assert,handle_assert_exit_code,is_assertion_function,is_exit_code_assertionmain/watch.shwatch_loop,watch_get_checksummain/run.shexec_tests,exec_benchmarks,cleanup,handle_stop_on_failure_synccmd_teststays one file: it is a singlewhile/caseover ~60 flags, and the issue rightlyforbids cutting a
caseacross files. One 485-line file holding one function is navigable in away a 1427-line file holding 24 is not.
Layering, from all 19 internal call edges
testcalls validate, run, watch and assert;benchcalls validate and run;watch_loopcallsexec_tests;assertandsubcommandsare self-contained.Two contract tests grep this file and must move with it
tests/unit/completions_test.sh:13derives the test-subcommand flag list fromcmd_test's parse loop (awk '/# Parse test-specific options/,/^ done$/') — thecompletions anti-drift contract.
tests/unit/completions_test.sh:23derives the doc flag list fromcmd_doc.Both will be repointed and mutation-tested, not just repointed. #946 shipped a layering
contract that passed while checking nothing because its grep target had moved.
Proceeding.
Chemaclass commentedon Aug 1, 2026
Done in #955 (merged).
src/now holds 17 modules and zero flat files.