Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
11 KiB
Go Rewrite — Progress Tracker
Companion to 2026-05-03-2349-go-backend-rewrite.md.
Current milestone: M2 — Pure-domain helpers Started: 2026-05-04 Last updated: 2026-05-06
How to use
- Tick a checkbox when the task's PR/commit lands. Append the SHA in the same
line:
[x] **M1.1** ... —abc1234``. - One task = one focused commit or PR. If a task balloons, split it and add sub-tasks below the parent.
- Note decisions, surprises, or blockers under "Notes & decisions" at the bottom — that's where future-you (or a contributor) will look first.
- Don't reorder milestones. Within a milestone, tasks can be done in any order unless explicitly noted.
M1 — Skeleton + tooling
Goal: make web-go serves a hello page on :8080 in parallel with make web-py on :5001. Lint clean.
- M1.1 Create
go/tree skeleton +go.modinitialized to latest stable Go - M1.2 Add
cmd/fuj/main.gowith subcommand dispatcher — stdlibflag+os.Args[1]switch - M1.3 Wire
fuj serversubcommand:net/httpServeMux on:8080, plaintext hello page - M1.4 Add Makefile targets:
go-build,go-test,go-run,go-lint - M1.5 Rename existing
make web→make web-py; addedmake web-go; keptmake webas alias - M1.6 Add
go/.golangci.yml(govet, staticcheck, errcheck, gofumpt, unused) +make go-lintclean - M1.7 Write
go/build/Dockerfile(multi-stagegolang:1.26→alpine:3); parallelbuild-gojob in Gitea CI - M1.8 Add
internal/configpackage mirroringscripts/config.py(same env var names + defaults) - M1.9 Add
internal/logging(slog, level from config) +middleware/timer.go(method/path/status/ms) - M1.10 Gate passed:
make go-build,make go-lint,make go-test,curl :8080all green; CHANGELOG entry added
Gate: ✅ make go-build succeeds, curl localhost:8080 returns hello page, make go-lint clean.
M2 — Pure-domain helpers (port leaf-first)
Goal: every pure function from the Python backend exists in Go with a parity test against captured fixtures (M3 produces fixtures in parallel — order is M2.1 → M3.1/M3.2 → M3.3+ alongside M2.2+).
Each task: port the function, write Go unit tests for fresh cases, hook into the Tier-1 parity runner.
- M2.1
domain/czech.Normalize— port czech_utils.pynormalize(NFKD + combining-mark strip + lowercase) —20ade6d - M2.2
domain/czech.ParseMonthReferences— portparse_month_references(45 month declensions, range wrap, year inference) —0a8017f - M2.3
domain/fees.CalculateFee— port attendance.pycalculate_fee(constants table) —0fc3b6d - M2.4
domain/fees.CalculateJuniorFee— portcalculate_junior_feewithExpected{Value int; Unknown bool}for the"?"sentinel —0fc3b6d - M2.5
domain/money.ParseCZK— port infer_payments.pyparse_czk_amount(Czech locale: comma decimal, dot/space thousand separators) —d24d205 - M2.6
domain/synch.GenerateSyncID— port sync_fio_to_sheets.pygenerate_sync_id(SHA-256, byte-stable hash; verify float string format against real sheet rows) - M2.7
domain/matching.BuildNameVariants+MatchMembers— port_build_name_variantsandmatch_membersfrom match_payments.py (auto vs review confidence, common-surname filter) —e596f00 - M2.8
domain/matching.InferTransactionDetails— portinfer_transaction_details(composes name + month parsing) —e596f00 - M2.9
domain/matching.FormatDate— portformat_date(handles Google Sheets serial-day numbers since 1899-12-30) —e596f00 - M2.10
domain/reconcile.Reconcile— portreconcile(three-phase allocation: greedy / proportional with float-remainder absorption / even-split fallback). The single most load-bearing function; budget extra time. —c53bf5a - M2.11
fuj feessubcommand wired up viadomain/fees+ (M4-stub) attendance loader — fail gracefully on missing IO until M4 lands - M2.12
fuj reconcilesubcommand similarly stubbed
Gate: cd go && go test -tags=parity ./tests/parity/pure/... green for every fixture in tests/fixtures/pure/.
M3 — Fixture capture + characterization framework
Goal: deterministic, PII-free fixture corpus that drives parity tests. Runs in parallel with M2 (M3.1/M3.2 unblocks M2.1).
- M3.1
scripts/capture_fixtures.py— pure-function output dumper. Reads inputs from stdin / argv, prints{"input":..., "output":...}JSON - M3.2
scripts/scrub_fixtures.py— replaces names withMember_<8hex>(deterministic per name); scrambles sender/account/VS/bank_id with stable bijection; preserves dates, amounts, exception keys - M3.3 Capture pure-fn fixtures for M2.1–M2.9 (run helper + scrubber, commit to
tests/fixtures/pure/<func>/<case>.json) - M3.4 Capture ~10 reconcile fixtures spanning every code path: greedy, proportional (float remainder), even-split, out-of-window credit, exception override,
other:purpose, junior"?", multi-person comma-split, multi-month range, unmatched. Commit totests/fixtures/reconcile/ - M3.5 Hook fixtures into Tier-1 test runner with
-tags=paritybuild constraint - M3.6 Document fixture-refresh workflow in
tests/fixtures/README.md(what to do when sheet schema changes)
Gate: tests/fixtures/ populated; M2 parity tests green; raw tmp/*.json confirmed gitignored.
M4 — IO layer behind interfaces
Goal: every external IO (Sheets, Drive, Fio, file cache) accessed through a narrow Go interface with both a real and a fake implementation.
- M4.1 Design IO interfaces (
SheetsClient,DriveClient,FioClient,FileCache) + in-memory fakes seeded from M3 fixtures - M4.2
internal/io/sheets— Google client (read + append + batchUpdate); integration test against a separate test sheet (NOT prod) - M4.3
internal/io/drive— DrivemodifiedTimeclient + integration test - M4.4
internal/io/fio— API JSON impl (token-based); parses by hardcodedcolumn0..column22indices matching fio_utils.py - M4.5
internal/io/fio— transparent-page HTML scraper usinggolang.org/x/net/htmltoken visitor; targets the second<table class="table"> - M4.6
internal/io/cache— FileCache withmodifiedTimegating + two TTL knobs + atomic writes (os.Rename) - M4.7
services/banksync.SyncToSheets+fuj syncsubcommand - M4.8
services/banksync.InferPayments+fuj infer [--dry-run]subcommand
Gate: go test -tags=integration ./internal/io/... round-trips against test sheet; default-tag tests run on fakes.
M5 — JSON-only /api/... routes
Goal: byte-equal JSON between Python and Go for every route. This is the parity contract.
- M5.1 Hand-author Go structs for
/api/adults,/api/juniors,/api/payments,/api/versionwith explicitjson:tags matching Python keys; emit JSON Schemas viagithub.com/invopop/jsonschematotests/fixtures/api-schema/ - M5.2 Implement Go handlers for
/api/*routes composingservices/*results into the JSON structs - M5.3 Add Python
/api/Xshadow endpoints in app.py:jsonify(view_model_dict)— no transformation - M5.4 Build
cmd/parity/main.go: hits both backends'/api/X, normalizes allowlist (render_time.total,build_meta), printscmp.Diff. Addmake paritytarget
Gate: For each route, make parity reports zero non-allowlisted diffs across the M3 fixture corpus.
M6 — Go-native HTML frontend
Goal: feature-equivalent UX on the Go side, designed cleanly. Not a Jinja port.
- M6.1 Template skeleton: base layout, nav (Adults/Juniors/Payments/Sync/Flush), terminal-green-on-black theme;
embed.FSfortemplates/+static/ - M6.2
/adultspage: table, name filter input, month range filter, totals row, credits/debts/unmatched sections, Pay buttons that link to/qr - M6.3
/juniorspage: same structure + per-month J/A attendance breakdown +"?"sentinel rendering - M6.4
/paymentspage: grouped-by-person ledger view - M6.5 Modal JS module (
static/js/member-detail.js): fetches/api/adults(or juniors), renders status/exceptions/transactions on row click; keyboard nav (Esc, ↑/↓) - M6.6
/qr,/sync-bank,/flush-cache,/versionpages - M6.7 Wire
embed.FSinto handlers; verify single-binary deployment includes all assets
Gate: Browser smoke on :8080: all pages render, name+month filters work, modal opens with correct data, QR loads, sync/flush work end-to-end.
M7 — Parallel-running watch period
Goal: prove parity over real time before flipping the default.
- M7.1 Add Go service to
docker-compose.ymlon different port (alongside Python container) - M7.2 Set up
parity-nightly.ymlGitea workflow: boot both, replay fixed transaction script, fail on diff - M7.3 Run
make paritydaily for 7–14 days, log any diffs; investigate and fix root cause (don't just allowlist) - M7.4 Manual feature parity check: walk through every UI feature on both sides, sign off in Notes section
Gate: Zero non-allowlisted JSON diffs over 7 consecutive days, including a sync-bank execution + flush + attendance update; user sign-off on UI feature parity.
M8 — Cutover + Python retirement
Goal: Go is the one true backend.
- M8.1 Update bookmarks, README, CLAUDE.md to point at Go (
make webaliases tomake web-go) - M8.2 Run Go-only for 2 weeks including a month-end settlement; keep Python container available but unrouted
- M8.3 Manual reconciliation review: produce a balance report on
python-finaland on Go for the same period; sign off they match - M8.4 Tag final Python image as
python-finalin registry; remove Python service fromdocker-compose.yml - M8.5 Delete app.py, scripts/, Python
Dockerfile, tests/,pyproject.toml,uv.lock - M8.6 Update CLAUDE.md to reflect Go-only state (commands, architecture, key modules); CHANGELOG entry
Gate: Two consecutive months of Go-only operation with end-of-month settlement complete; zero rollbacks.
Notes & decisions
(Add entries as you go. Format: YYYY-MM-DD — short note.)
- 2026-05-04 — Plan approved. Versioning policy: latest stable for Go and all libs at the time M1 starts. Frontends explicitly allowed to diverge between Python and Go; only the JSON API contract is parity-locked. No reverse proxy — both backends run on different ports via
make web-py/make web-go. - 2026-05-04 — M1 complete. Dockerfile base changed from
distroless/static:nonroot→alpine:3for debuggability (can tighten later). CLI dispatcher uses stdlibflag; module pathfuj-management/go. golangci-lint v1 embedded gofumpt merges all imports into one group (no stdlib/local split) — accepted as the project style.