diff --git a/docs/app-split-recipes.md b/docs/app-split-recipes.md new file mode 100644 index 00000000..fea26a01 --- /dev/null +++ b/docs/app-split-recipes.md @@ -0,0 +1,78 @@ +# App split recipes (Dart, Kotlin, TypeScript) + +Settled while clearing `focus_owner`, `kcd2_dice_solver` and +`reverse_survivors`. Referenced from `refactor_claude_todo_resume.md`. + +## Baselines — measure before editing, re-check after + +| Project | Command | Expect | +| ---------------------- | ------------------- | -------------- | +| `focus_owner` (Dart) | `flutter test` | 94 | +| `focus_owner` (Kotlin) | see below | 40, 0 failures | +| `kcd2_dice_solver` | `npm test -- --run` | 288 | +| `reverse_survivors` | `npm test -- --run` | 160 | + +Gradle prints `UP-TO-DATE` rather than a count, so read the JUnit XML, and +**pin the JDK** — the default JDK 26 breaks the AGP jlink transform: + +```bash +cd focus_owner/android +JAVA_HOME=/usr/lib/jvm/java-21-openjdk ./gradlew testDebugUnitTest --offline +grep -ho 'tests="[0-9]*"' ../build/app/test-results/testDebugUnitTest/*.xml \ + | grep -oP '\d+' | paste -sd+ | bc +``` + +## Dart: privates are library-scoped, so use `part` + +A separate file is a separate **library**, so `_Card` and friends are invisible +to it. Splitting `main.dart` with `import` + `export` produced 30 errors of the +form "The name `_Card` isn't a class". `part 'x.dart';` + `part of 'main.dart';` +keeps both files in one library and the analyzer goes straight to zero issues. + +The part file carries **no imports of its own** — it inherits the library's. + +**A `part` cannot rescue a State class.** `status_page_state.dart` stayed over +the cap: its methods call `setState` and read seven private fields, and a +`mixin on State<...>` cannot see them without declaring the state twice. + +**Splitting a widget test off its `testWidgets` group loses the binding.** +Four tests failed with "Binding has not yet been initialized" because the +group that had been initialising it implicitly moved away. Add +`TestWidgetsFlutterBinding.ensureInitialized()` to **both** halves. + +## Kotlin: no `part`, so use an internal fixture object + +Shared `private val`/`private fun` test fixtures go into an +`internal object XFixtures` in the same package, and each test class delegates +to it. Two compile failures came from missing one (`installed`) — list every +`private` member of the original class before cutting. + +`EnforcementRunner.kt` is **not splittable** as it stands: its location block +reads the private constructor property `context` and `prefs()` eleven times, +which no extension function can reach. + +## TypeScript: `tsc` does not tell you a split worked + +Three separate projects had a seam that compiled clean and failed at runtime: + +- an import cycle leaves a constant `undefined` at module-init + (`HEADSTART_POINTS`, twice, in `kcd2`); +- a moved function is simply not there (`survivorStep`, 63 failures); +- a type name that collides with a DOM global resolves to the DOM one — the + error talks about `anchorNode` rather than saying the name is missing. + +**Always run the suite.** Prune the imports the split leaves behind by +iterating on `tsc`'s own TS6133/TS6192/TS6196 output rather than guessing, but +note it cannot see a helper another file still needs. + +## Coverage: new files must land inside the include glob + +`kcd2` and `reverse_survivors` both gate at 100%. New source modules are picked +up automatically; **shared test fixtures must go in `src/test/`**, which the +coverage `exclude` names by path. Beside the source they count as production +code and break the gate — the same trap recorded for `poker-stakes`. + +## A split that empties a file must delete it + +`App.test.tsx` lost its last `describe` and vitest failed the whole run with +"No test suite found in file". Remove the shell. diff --git a/refactor_claude_todo_resume.md b/refactor_claude_todo_resume.md index f3bf94b4..839171c5 100644 --- a/refactor_claude_todo_resume.md +++ b/refactor_claude_todo_resume.md @@ -6,8 +6,9 @@ ## Where things stand -**83 files** over 250 lines (was 183). Everything is committed and pushed to -`main` at `093e2f6`; the tree is clean. +**78 files** over 250 lines (was 183). Everything is committed and pushed to +`main` at `2ce86a5`. `focus_owner/analysis_options.yaml` has an uncommitted +change from an earlier session — **leave it unstaged**, it is not ours. **`python_pkg/` is DONE — 50 violations → 0.** Do not reopen it. Cleared one package per tranche, pushing after each: `brother_printer` (15→0, suite pinned @@ -62,8 +63,12 @@ Same in TypeScript: `kcd2`'s `search.ts` and `badgeValue.ts`, and caller's globals — stricter than `shellcheck -x` on the entry, and the check that decides whether a seam is real. Run it before believing a split. -Next: the remaining `main()`-shaped shell scripts, then `focus_owner` and -`billsplit` (Kotlin+Dart, untouched so far — check for a test command first). +Next: `billsplit` (Dart, untouched), then the remaining `main()`-shaped shell +scripts. `focus_owner`'s six leftovers are four Kotlin sources that read a +private `context`, one State class, and one test one line over. + +Baselines and the per-language traps (Dart `part`, Kotlin fixture objects, the +TS import cycle) are in `docs/app-split-recipes.md`. ## The live worklist @@ -136,10 +141,12 @@ Plus `.github/workflows/file-length.yml` modelled on ## Language recipes -- `docs/python-split-recipes.md` — the seam-selection rule, the `__all__` - re-export form, the `TC001` trap, the mixin shape, and the coverage - behaviour of new sibling modules. `python_pkg` is done; read it if a stray - `.py` turns up, or for the seam rule, which generalises to any language. +- `docs/python-split-recipes.md` — the seam-selection rule (generalises to + every language), the `__all__` re-export form, the `TC001` trap and the + mixin shape. `python_pkg` is done. +- `docs/app-split-recipes.md` — Dart `part`, Kotlin fixture objects, the + TypeScript import cycle that compiles clean, and the per-project test + baselines. **Read before touching `billsplit` or `focus_owner`.** - `docs/shell-split-recipes.md` — **read this before the next `linux_configuration` split.** What replaces the pass count when there is no test suite, which scripts escape the repo, the lib shape that satisfies both @@ -154,23 +161,10 @@ Each rule in both cost a failed gate run or a revert to establish. - **Shell** — extract into `lib/*.sh` sourced by a thin entry script. See `docs/shell-split-recipes.md` for the lib shape; note it corrects the old advice to repeat `set -euo pipefail` in every lib. -- **Python** — extract cohesive helpers into sibling modules; keep the public - API stable by re-exporting. -- **TS/Dart** — extract components; re-export from the original path so no - importer changes. -- **Tests** — split by describe/test-group. Coverage must not drop, and - **verify by the pass _count_, not a green run**: a split that loses a whole - module still reports "passed". - In `python_pkg`, shared fixture builders go in the package's - **`tests/conftest.py`** — the `name-tests-test` hook rejects any other - non-`test_*.py` file under `tests/`, so a `_helpers.py` will fail the gate. - `python_pkg/syncyomi_guard/tests/conftest.py` is the precedent, and - `meta/pyproject.toml` omits `*/tests/*` from coverage by path so nothing - there is counted as production code (the opposite of the `poker-stakes` - trap below). Prefer one shared conftest over another `partN` file: the repo - already carries four, and each new one that redeclares the same fixture - scaffolding pushes jscpd toward its 2% duplication threshold — which only - fires on the real `git commit`. +- **Python / TS / Dart / Kotlin** — see the two recipe docs above. +- **Tests** — split by describe/test-group and **verify by the pass _count_, + not a green run**: a split that loses a whole module still reports "passed". + Shared fixtures go where the coverage config already excludes them. Never game the cap: no one-lining, no deleting tests, no moving code into an exempt extension, no suppressions.