Summary
After #931 and #940, src/ is 11 modules plus 15 loose files. Individually each is small
and well named; as a listing they read as leftovers, and the root stops communicating
structure. Directories convey layering, a flat pile does not.
Group the rest so that src/ holds modules and nothing else (bar main.sh, see below).
Measured on main (8f2a1c3).
Groups
Each is backed by a real dependency cluster, not name similarity.
src/system/ — what this machine has
check_os.sh (107, leaf) · dependencies.sh (42, leaf) · io.sh (31 → dependencies) ·
clock.sh (203 → check_os, dependencies, math) — 383 lines
The true bottom layer. Nothing in it touches test state, config, console or runner.
src/util/ — pure computation
str.sh (156, leaf) · math.sh (104 → dependencies) — 260 lines
One outbound edge (math → dependencies) which just places util above system. Acyclic.
src/api/ — the test-authoring surface
globals.sh (109, leaf) · skip_todo.sh (21) · test_title.sh (5) · bashunit.sh (53) —
188 lines
What a user's test file calls: temp_file/temp_dir/current_dir/data_set, skip/todo,
set_test_title, and the custom-assert facade (assert_that, assert_once,
assertion_failed).
Assertions are the other half of this surface and stay in src/assert/, which at 11 files and
2300 lines has earned its own module. The api/index.sh comment must say so, or the split
looks arbitrary.
src/config/ — run-scoped configuration and persisted state
env.sh (754) · parallel.sh (63) · rerun.sh (124) — 941 lines
Not a junk drawer; there are real internal edges. env → rerun, parallel → env, and
rerun::is_enabled is called fromenv.sh. parallel::is_enabled is called from 11 files
across runner, coverage, console and main — a cross-cutting mode predicate, the same shape as
env.sh's 33 is_* predicates.
Note src/parallel.sh is a different concern from src/runner/parallel.sh (job-slot waiting
and the spinner, runner-internal). Same basename, different jobs — the collision #923 fixed
the build for.
env.sh is not split here; #931 recorded it as deliberately whole (51 functions but one
concern). It just moves.
src/benchmark/ — the weakest of the set, stated plainly
benchmark.sh (191, 5 functions) → annotations.sh / run.sh / report.sh
It is a distinct feature — runner/bench.sh is the file/function loop, this is the
implementation (annotation parsing, running, result printing). But three files of ~60 lines is
thin, and this is the one group where "everything in modules" costs ceremony to buy
consistency. Accept it or leave benchmark.sh flat; decide deliberately and say which.
main.sh
Stays at the root of src/ for now. Splitting it is #948, and whether it becomes src/main/
or remains a root dispatcher is that issue's call. Renaming it to index.sh is explicitly
out of scope here and should follow this work, not precede it — see the note on #948.
Process
One group per PR, same as #931 and #940:
- Post the mapping on this issue before moving code.
git mv so renames are recorded as renames.index.sh aggregator, entrypoint source line updated.- Grep for hardcoded paths before committing.
Suggested order: system → util → api → config → benchmark. System first because
everything sits on it; api third because its naming deserves a second look before it is
cemented.
Constraints
All of ADR-010's, every one of which has drawn blood at least once:
Verification, per PR
Relocation proof: 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 ✅.
Acceptance criteria (per group PR)
Do not
Summary
After #931 and #940,
src/is 11 modules plus 15 loose files. Individually each is smalland well named; as a listing they read as leftovers, and the root stops communicating
structure. Directories convey layering, a flat pile does not.
Group the rest so that
src/holds modules and nothing else (barmain.sh, see below).Measured on
main(8f2a1c3).Groups
Each is backed by a real dependency cluster, not name similarity.
src/system/— what this machine hascheck_os.sh(107, leaf) ·dependencies.sh(42, leaf) ·io.sh(31 → dependencies) ·clock.sh(203 → check_os, dependencies, math) — 383 linesThe true bottom layer. Nothing in it touches test state, config, console or runner.
src/util/— pure computationstr.sh(156, leaf) ·math.sh(104 → dependencies) — 260 linesOne outbound edge (
math→dependencies) which just places util above system. Acyclic.src/api/— the test-authoring surfaceglobals.sh(109, leaf) ·skip_todo.sh(21) ·test_title.sh(5) ·bashunit.sh(53) —188 lines
What a user's test file calls:
temp_file/temp_dir/current_dir/data_set,skip/todo,set_test_title, and the custom-assert facade (assert_that,assert_once,assertion_failed).Assertions are the other half of this surface and stay in
src/assert/, which at 11 files and2300 lines has earned its own module. The
api/index.shcomment must say so, or the splitlooks arbitrary.
src/config/— run-scoped configuration and persisted stateenv.sh(754) ·parallel.sh(63) ·rerun.sh(124) — 941 linesNot a junk drawer; there are real internal edges.
env → rerun,parallel → env, andrerun::is_enabledis called fromenv.sh.parallel::is_enabledis called from 11 filesacross runner, coverage, console and main — a cross-cutting mode predicate, the same shape as
env.sh's 33is_*predicates.Note
src/parallel.shis a different concern fromsrc/runner/parallel.sh(job-slot waitingand the spinner, runner-internal). Same basename, different jobs — the collision #923 fixed
the build for.
env.shis not split here; #931 recorded it as deliberately whole (51 functions but oneconcern). It just moves.
src/benchmark/— the weakest of the set, stated plainlybenchmark.sh(191, 5 functions) →annotations.sh/run.sh/report.shIt is a distinct feature —
runner/bench.shis the file/function loop, this is theimplementation (annotation parsing, running, result printing). But three files of ~60 lines is
thin, and this is the one group where "everything in modules" costs ceremony to buy
consistency. Accept it or leave
benchmark.shflat; decide deliberately and say which.main.shStays at the root of
src/for now. Splitting it is #948, and whether it becomessrc/main/or remains a root dispatcher is that issue's call. Renaming it to
index.shis explicitlyout of scope here and should follow this work, not precede it — see the note on #948.
Process
One group per PR, same as #931 and #940:
git mvso renames are recorded as renames.index.shaggregator, entrypointsourceline updated.Suggested order:
system→util→api→config→benchmark. System first becauseeverything sits on it;
apithird because its naming deserves a second look before it iscemented.
Constraints
All of ADR-010's, every one of which has drawn blood at least once:
sourcelines and comments.^}$brace (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).-x; reproduce withSHELLCHECK_OPTS="-e SC1091 -e SC2155 -e SC2016".# shellcheck disable=moves only to files that need it — and check whether itis 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).
.editorconfigrules are lost by a move (refactor(console): group the rendering files into a src/console/ module #942 carried one forconsole_header.sh).tests/before committing.refactor(learn): split src/learn.sh into a src/learn/ module #938 shipped 14 red tests fromone; refactor(state): split src/state.sh into a src/state/ module #946 found a layering contract that passed while checking nothing because its grep
target had moved.
Verification, per PR
Relocation proof: non-blank line multiset differs only by new shebangs, module headers and
sourcelines; 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 -vprinting✅ Build verified ✅.Acceptance criteria (per group PR)
git diffshows only relocations — no renamed functions, no changed logicsrc/<group>/index.sh, onlysourcelines and commentstests/ build.sh Makefile .github/ .editorconfig .gitignore)git check-ignore -vconfirms the directory is not ignoredbash build.sh bin -v.claude/rules/architecture-map.mdupdatedDo not
env.sh— refactor(src): finish the module split — 9 large files still flat #931 settled thatmain.shhereshfmt -w;make lintis the format gate