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
#931 splits large files into modules. This is the other half: src/ still holds 36 flat
files, many of them small and clearly belonging to the same concept. Ten of them are assert_*.sh. Group by context so a directory names a concept, following ADR-010's index.sh convention.
Measured on main (c8b0b53).
Group 1 — src/assert/ (highest confidence, do first)
Ten files, 2313 lines, already aggregated together by src/assertions.sh. The concept is
established; only the directory is missing.
Now
Becomes
src/assertions.sh
src/assert/index.sh
src/assert.sh (970)
src/assert/core.sh
src/assert_arrays.sh (101)
src/assert/arrays.sh
src/assert_assertions.sh (192)
src/assert/assertions.sh
src/assert_dates.sh (185)
src/assert/dates.sh
src/assert_duration.sh (69)
src/assert/duration.sh
src/assert_files.sh (156)
src/assert/files.sh
src/assert_folders.sh (118)
src/assert/folders.sh
src/assert_json.sh (67)
src/assert/json.sh
src/assert_once.sh (156)
src/assert/once.sh
src/assert_snapshot.sh (299)
src/assert/snapshot.sh
Drop the assert_ prefix inside the directory — it carries the concept, as learn/lessons/basics.sh does. Function names do not change; only file names.
Two callers hardcode these paths and must move with them:
tests/unit/completions_test.sh:30 greps src/assert*.sh to derive the public assertion
list for the completions parity contract → becomes src/assert/*.sh.
tests/unit/build_test.sh special-cases src/assertions.sh as "the one flat-file
aggregator" outside the src/*/index.sh glob. That special case disappears — a welcome
simplification, and the reason ADR-010's original "beside" argument was withdrawn.
src/assertions.sh also sources skip_todo.sh and test_doubles.sh, which are not
assertions (skip/todo markers; spies and mocks). Leave both flat in this group, or handle
them in group 4 — do not sweep them into src/assert/ just because the aggregator lists them.
console_results.sh is also on #931's list at 828 lines. Prefer grouping first, splitting
second: move it into src/console/ here, then split it inside the module under #931 if
still warranted.
Group 3 — src/cli/ (high confidence)
The subcommand implementations, ~430 lines. main.sh is the only caller of all four,
and each is literally a bashunit <subcommand> implementation:
src/upgrade.sh (54) → src/cli/upgrade.sh
src/watch.sh (112) → src/cli/watch.sh
src/doc.sh (198) → src/cli/doc.sh
src/init.sh (66) → src/cli/init.sh
benchmark.sh looks like it belongs but src/runner/bench.sh also calls it, so it is a
shared implementation rather than purely a subcommand — it stays flat. main.sh is the
dispatcher and needs its own split under #931 first; grouping it now would only relocate a
1427-line file.
Zero hardcoded path references to any of the four.
Won't do: src/util/ and src/system/
Considered and declined, recorded here so it is not re-litigated.
The candidates were str (156), math (104), io (31), check_os (107), dependencies (42), clock (203) — 2 to 10 functions each. These are the most obviously
named files in the repo; nobody has struggled to find math.sh. Wrapping them costs an index.sh, a source line and a directory hop, and buys navigation for files that were
never hard to navigate. A two-file util/ is not a module, it is ceremony.
ADR-010's rationale is a directory for something with internal structure. A 31-line file
has none.
The effort is better spent on helpers.sh (909 lines, 30 functions, six unrelated concerns:
test-function naming, discovery, data providers, tags, base64 encoding, misc). That is a
cohesion problem, tracked on #931.
Explicitly stays flat
Not everything benefits from a directory. These are single-concept files or cross-cutting
state, and grouping them would invent a concept that does not exist:
main.sh (1427) and the subcommand files (doc, init, upgrade, watch, benchmark)
look like a src/cli/ group, but main.sh needs its own split under #931 first — grouping it
now would just move a 1427-line file. Out of scope here.
Aggregators hold only source lines and comments — enforced by test_module_aggregators_hold_only_source_lines_and_comments, which globs src/*/index.sh.
.gitignore can swallow a new module directory.coverage/ was unanchored and hid
twelve files from git with no ?? in git status. Run git check-ignore -v src/<module>/<file>.sh before committing.
CI runs ShellCheck per file without -x, so a split exposes cross-file symbol use a
monolith hid. Reproduce locally with SHELLCHECK_OPTS="-e SC1091 -e SC2155 -e SC2016".
A file-wide # shellcheck disable= moves to the files that need it, not to all of them.
Per-file .editorconfig rules are lost by a split — check before, decide deliberately.
Prove it is a relocation: the non-blank line multiset differs only by new shebangs, module
headers and source lines, and the function count is unchanged. Then the built artifact —
either byte-identical, or its sorted code content identical when grouping reorders
definitions (safe: everything is sourced before anything is invoked).
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)
Mapping posted on this issue before code moves
git diff shows only relocations — no renamed functions, no changed logic
Aggregator is src/<group>/index.sh and holds only source lines and comments
Every new file's first line is #!/usr/bin/env bash
Summary
#931 splits large files into modules. This is the other half:
src/still holds 36 flatfiles, many of them small and clearly belonging to the same concept. Ten of them are
assert_*.sh. Group by context so a directory names a concept, following ADR-010'sindex.shconvention.Measured on
main(c8b0b53).Group 1 —
src/assert/(highest confidence, do first)Ten files, 2313 lines, already aggregated together by
src/assertions.sh. The concept isestablished; only the directory is missing.
src/assertions.shsrc/assert/index.shsrc/assert.sh(970)src/assert/core.shsrc/assert_arrays.sh(101)src/assert/arrays.shsrc/assert_assertions.sh(192)src/assert/assertions.shsrc/assert_dates.sh(185)src/assert/dates.shsrc/assert_duration.sh(69)src/assert/duration.shsrc/assert_files.sh(156)src/assert/files.shsrc/assert_folders.sh(118)src/assert/folders.shsrc/assert_json.sh(67)src/assert/json.shsrc/assert_once.sh(156)src/assert/once.shsrc/assert_snapshot.sh(299)src/assert/snapshot.shDrop the
assert_prefix inside the directory — it carries the concept, aslearn/lessons/basics.shdoes. Function names do not change; only file names.Two callers hardcode these paths and must move with them:
tests/unit/completions_test.sh:30grepssrc/assert*.shto derive the public assertionlist for the completions parity contract → becomes
src/assert/*.sh.tests/unit/build_test.shspecial-casessrc/assertions.shas "the one flat-fileaggregator" outside the
src/*/index.shglob. That special case disappears — a welcomesimplification, and the reason ADR-010's original "beside" argument was withdrawn.
src/assertions.shalso sourcesskip_todo.shandtest_doubles.sh, which are notassertions (skip/todo markers; spies and mocks). Leave both flat in this group, or handle
them in group 4 — do not sweep them into
src/assert/just because the aggregator lists them.Group 2 —
src/console/(high confidence)Output rendering, 1220 lines:
src/colors.sh(57) →src/console/colors.shsrc/console_header.sh(335) →src/console/header.shsrc/console_results.sh(828) →src/console/results.shconsole_results.shis also on #931's list at 828 lines. Prefer grouping first, splittingsecond: move it into
src/console/here, then split it inside the module under #931 ifstill warranted.
Group 3 —
src/cli/(high confidence)The subcommand implementations, ~430 lines.
main.shis the only caller of all four,and each is literally a
bashunit <subcommand>implementation:src/upgrade.sh(54) →src/cli/upgrade.shsrc/watch.sh(112) →src/cli/watch.shsrc/doc.sh(198) →src/cli/doc.shsrc/init.sh(66) →src/cli/init.shbenchmark.shlooks like it belongs butsrc/runner/bench.shalso calls it, so it is ashared implementation rather than purely a subcommand — it stays flat.
main.shis thedispatcher and needs its own split under #931 first; grouping it now would only relocate a
1427-line file.
Zero hardcoded path references to any of the four.
Won't do:
src/util/andsrc/system/Considered and declined, recorded here so it is not re-litigated.
The candidates were
str(156),math(104),io(31),check_os(107),dependencies(42),clock(203) — 2 to 10 functions each. These are the most obviouslynamed files in the repo; nobody has struggled to find
math.sh. Wrapping them costs anindex.sh, asourceline and a directory hop, and buys navigation for files that werenever hard to navigate. A two-file
util/is not a module, it is ceremony.ADR-010's rationale is a directory for something with internal structure. A 31-line file
has none.
The effort is better spent on
helpers.sh(909 lines, 30 functions, six unrelated concerns:test-function naming, discovery, data providers, tags, base64 encoding, misc). That is a
cohesion problem, tracked on #931.
Explicitly stays flat
Not everything benefits from a directory. These are single-concept files or cross-cutting
state, and grouping them would invent a concept that does not exist:
bashunit.sh(public custom-assert facade) ·globals.sh(public test API) ·env.sh·state.sh·helpers.sh·clock.sh·parallel.sh·rerun.sh·test_title.shmain.sh(1427) and the subcommand files (doc,init,upgrade,watch,benchmark)look like a
src/cli/group, butmain.shneeds its own split under #931 first — grouping itnow would just move a 1427-line file. Out of scope here.
Process
Same as #931, and one group per PR:
git mvso the rename is recorded as a rename, not add+delete.index.shand the entrypoint'ssourceline.tests/,build.sh,Makefile,.github/,.editorconfig,.gitignore. refactor(learn): split src/learn.sh into a src/learn/ module #938 shipped 14 red tests from one hardcodedsrc/learn.shin a test file.Constraints (all learned the hard way)
sourcelines and comments — enforced bytest_module_aggregators_hold_only_source_lines_and_comments, which globssrc/*/index.sh..gitignorecan swallow a new module directory.coverage/was unanchored and hidtwelve files from git with no
??ingit status. Rungit check-ignore -v src/<module>/<file>.shbefore committing.-x, so a split exposes cross-file symbol use amonolith hid. Reproduce locally with
SHELLCHECK_OPTS="-e SC1091 -e SC2155 -e SC2016".# shellcheck disable=moves to the files that need it, not to all of them..editorconfigrules are lost by a split — check before, decide deliberately.^}$brace. A braceinside a heredoc ends the segment early and silently splits a function across files (refactor(learn): split src/learn.sh into a src/learn/ module #938).
Verification, per PR
Prove it is a relocation: the non-blank line multiset differs only by new shebangs, module
headers and
sourcelines, and the function count is unchanged. Then the built artifact —either byte-identical, or its sorted code content identical when grouping reorders
definitions (safe: everything is sourced before anything is invoked).
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.shand holds onlysourcelines and comments#!/usr/bin/env bashtests/ build.sh Makefile .github/)git check-ignore -vconfirms the new directory is not ignoredbash build.sh bin -v.claude/rules/architecture-map.mdmodule table updatedDo not
skip_todo.sh/test_doubles.shintosrc/assert/main.sh— it needs its own split under refactor(src): finish the module split — 9 large files still flat #931 firsttests/unit/<group>/;make testglobs one levelshfmt -w;make lintis the format gate