Add docs/app-split-recipes.md; update the handoff to 78 left
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run

Dart's library-scoped privates (use part/part of), Kotlin's lack of any
equivalent (use an internal fixture object), and the TypeScript seams
that compile clean and fail at runtime - three projects hit that last
one. Also the per-project test baselines, including that gradle prints
UP-TO-DATE rather than a count and needs JDK 21 pinned.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
This commit is contained in:
Krzysztof Rudnicki 2026-08-16 20:51:38 +02:00
parent 2ce86a5176
commit 34783a5022
2 changed files with 97 additions and 25 deletions

78
docs/app-split-recipes.md Normal file
View File

@ -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.

View File

@ -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.