Phase 3 (persistence) + Phase 3a (i18n scaffolding) #2
Loading…
Reference in a new issue
No description provided.
Delete branch "feature/phase-3-persistence-i18n"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Phase 3 adds the persistence layer and Phase 3a adds i18n scaffolding. Review verdict: APPROVED (no blocking findings). Builds on the already-merged Phase 0-2.
What was built (per FEAT)
FEAT-001 — ORM models + Alembic initial migration (tasks.md T3.1, finishes T0.3). All 8 design-§4 models under
app/models/: ContestYear, MonthlyContest, Entry, QSO, Flag, DutchLocatorReference, AuditLogEntry, Manager.MonthlyContest.submission_closes_at= contest_date + 14 days at Europe/Amsterdam end-of-day, DST-aware via zoneinfo, stored as aware UTC;is_lockedis a derived Python@property, not a column. Entry carries the privacy split (public: own_locator, afdeling; private: email, pin_hash, operator_name, address, other_operators, declaration_*), upload bookkeeping (source_format CBR/EDI/ADIF, version, replaced_at, raw_file_path), override fields, and cached score (monthly_scoreint +qso_points_per_band/multipliers_per_bandJSON). Unique constraintuq_entry_contest_callsign_section. Yearly Standing is intentionally NOT a table. Reuses the existing Phase-1/2 enums.app/db.pylazy engine untouched. Alembicversions/0001_initial_schema.pybuilds every table/FK/unique constraint/JSON column and named enum types; downgrade drops tables + enum types.FEAT-002 — Security + audit (tasks.md T3.2, T3.3).
app/security.py: saltedhash_pin/verify_pin,hash_password/verify_password, andauthenticate_manager(session, username, password)supporting multiple managers (returns None for unknown/wrong-password/inactive).app/audit.py: singlerecord_audit(...)choke point that add+flush (does NOT commit — caller owns the txn). See hashing-scheme note below.FEAT-003 — Persistence integration (tasks.md T3.4, T3.5 + §9 invariant).
app/persistence/:raw_files.py(relative pathuploads/<year>/<month>/<callsign>-<section>-v<version>.<ext>, DB stores the relative path per ADR D11),errors.py(PinMismatchError, DuplicateEntryError),ingest.py(ingest_parsed_log— raw file written to disk FIRST as the recovery anchor, then one transaction scores + builds Entry/QSOs/Flags; caller-wins defaulting D-1/D-2), andrebuild.py(rebuild_from_raw+python -m app.persistence.rebuildCLI). The §9 invariant: first submit claims the (monthly_contest, callsign, section) slot and stores pin_hash; re-upload requires verify_pin (match -> replace QSOs/Flags, bump version, set replaced_at, new raw file; mismatch -> PinMismatchError, no change); a distinct second Entry is blocked by check-then-act plus IntegrityError -> DuplicateEntryError.FEAT-004 — i18n scaffolding (Phase 3a, tasks.md T3a.1).
app/i18n/: nl default / en selectable / safe key fallback (never raises), dependency-free.poparsing (no babel/gettext, no.mocommitted),get_translator(locale)Jinja2-ready. Prose strings translated; jargon (qso/locator/multiplier) identical in both catalogs (R30). Imports nothing from parsing/scoring/rules/reference (R31), verified in a fresh subprocess.DB verification strategy (Postgres apply status)
Two-path strategy. Path (b) SQLite + revision-coherence runs in
tests/test_migration.pyand always passes. Path (a) real Postgres:alembic upgrade headwas applied cleanly against Postgres 16 via a throwawaypython:3.12-slimcontainer joined to the compose DB network —sqlalchemy.inspectconfirmed all 8 tables +alembic_version+ the unique constraint, then compose was torn down (keeping the ./data/db bind mount).Pending: the Postgres apply was NOT run from the project venv. This host's pure-Python psycopg has no libpq (the
/opt/homebrew/opt/libpqsymlink is dangling), so.venv/bin/alembic upgrade headfails with 'no pq wrapper available'. To run alembic/live-DB from the venv on this host, install a working libpq (brew install libpq, likely exportDYLD_LIBRARY_PATH) or run inside a container. Pure-logic tests need none of this.Asserted test facts
monthly_score == 460,qso_points_per_band {2m:16, 70cm:7},multipliers_per_band {2m:14, 70cm:6}, 23 QSO rows, raw bytes round-trip; forced-commit-failure still leaves the raw file on disk; wrong PIN -> PinMismatchError (no change); matching PIN -> v2 + replaced_at + fresh QSOs + -v2 raw file; distinct 2nd Entry -> DuplicateEntryError; rebuild reproduces 460 / same per-band breakdown / 23 QSOs / same Flag set and is idempotent (preserving email + pin_hash).Flagged ambiguities / notes
>=1.7,<1.8(1.7.4), whose bcrypt backend is incompatible with the installed bcrypt 5.0.0 (reads removed__about__.__version__; self-test probes a >72-byte secret that bcrypt 5.x rejects). Resolution: the single CryptContext usespbkdf2_sha256(pure-Python, salted) for both PIN and manager-password hashing. The contract (R8/N5/R24a, design.md) mandates a salted hash, not a specific algorithm, so this satisfies it. To use bcrypt later, install bcrypt <4.1 or unpin passlib.CryptContext.hash+verifyin 0.02s with correct round-trip and rejection, so the hang was the known persistent-shell-session issue, not a code defect; the FEAT-002/003 pass evidence stands.alembic/env.py+script.py.makoalready existed and were correct; onlyversions/0001and theimport app.modelsline were missing.STILL-OPEN Phase 0-2 reminder (NOT resolved by this PR)
The Dutch Locator Reference starter list is provisional (
needs_review=True) and BES squares are 4-char only. Authoritative NL + exact BES 6-char squares are still needed from the user before the multiplier gate is trustworthy. Phase 3 persists the reference as-is; it does not finalize the data. This reminder is surfaced, not actioned.How to run the tests
Run pytest with cwd=
/tmpand absolute paths. Pure-logic tests need no Postgres/libpq; the live-DB/alembic path needs a working libpq or a container as noted above.