Skip to content

Merge latest changes on dev (#375) - #376

Merged
SteveTeece merged 1 commit into
devfrom
master
Sep 3, 2026
Merged

Merge latest changes on dev (#375)#376
SteveTeece merged 1 commit into
devfrom
master

Conversation

@SteveTeece

Copy link
Copy Markdown
Owner
  • spec(027): add localization support spec, plan, and tasks

Feature 027 introduces per-language resource files (Australian English baseline) so every screen, report, menu, tile, exception message and user-facing enum value reads its text via IStringLocalizer, plus a persisted, OS-aware Settings.LanguageCode selection.

Adds the full spec-kit artifact set:

  • spec.md - 3 prioritized user stories, FR-001..FR-024, SC-001..SC-010
  • plan.md, research.md - IStringLocalizer + ~12 area .resx markers, missing-key logging decorator, MoneyFormatter (fixed AUD), startup culture ladder (explicit -> OS language -> en-AU)
  • data-model.md - Settings.LanguageCode column + migration outline
  • contracts/ - localization interfaces + resource-key catalog/guards
  • quickstart.md, checklists/requirements.md
  • tasks.md - 64 tasks (T001-T064) in 6 wave-grouped phases

Bumps .specify/feature.json to specs/027-localization-support.

  • feat(027): localization Setup + Foundational infrastructure (T001-T014)

Phase 1 (Setup):

  • Add Microsoft.Extensions.Localization 10.0.10 to Directory.Packages.props
  • Scaffold StageFright.Localization.Tests guard-test project (refs Core/UI/Reports)
  • Add version-less PackageReference + NeutralLanguage=en-AU to Core/Reports/UI csproj

Phase 2 (Foundational):

  • 12 area resource marker classes + empty neutral .resx: NavigationResource, ValidationResource, EnumsResource (Core); ReportsResource (Reports); Shared/Dashboard/Members/Rehearsals/Events/Finance/Settings/SetupResource (UI)
  • ILocalizer facade + Localizer impl (Get/Get-with-args/Plural/Enum), with named-placeholder substitution by order of first appearance (FR-010)
  • MissingKeyLoggingLocalizerFactory: IStringLocalizerFactory decorator that logs a Warning and falls back to neutral on a missing key (FR-008/FR-009)
  • EnumLocalizationExtensions.LocalizeEnum: Enum__ lookup via a factory holder set once at startup (FR-024)
  • MoneyFormatter.Format/FormatWithCode: fixed "$"/"AUD" symbol, culture-driven separators/grouping (FR-015)
  • Guard-test scanning infrastructure (LocalizerKeyUsageScanner, ResxKeyScanner, PlaceholderTokenScanner, UserFacingEnumScanner) — no assertions yet
  • LocalizedTestContext bUnit base wiring AddLocalization() + LocalizeEnum
  • DI wiring in MauiProgram: AddLocalization(), decorated IStringLocalizerFactory, AddScoped<ILocalizer, Localizer>(), EnumLocalizationExtensions.UseFactory at startup

Full solution build: 0 warnings, 0 errors. Full test suite: 1693 existing tests pass unchanged. User stories 1-3 and Polish (T015-T064) remain undone.

  • chore(027): record after_implement companion hook state (14/64 tasks)

  • feat(027): localize navigation shell + Members module (US1, T015-T028)

Phase 3 / User Story 1 — the extraction pattern proven end-to-end on the navigation shell plus the complete Members module. Same en-AU wording, now sourced from IStringLocalizer resources.

Resources (T017-T021):

  • NavigationResource / MembersResource / ValidationResource / EnumsResource / SharedResource .resx populated with verbatim en-AU entries (labels, headings, buttons, placeholders, aria-label/alt/title, plural pair, enum display text).

Call sites (T022-T028):

  • ShellLayout, ThemeProvider (no-op — no user text), MemberMenuItemProvider, MemberValidationService, MemberList/MemberDetail/MemberForm (.razor+.razor.cs), MembersTile, MembersDashboardTileProvider read text via IStringLocalizer.
  • MemberStatus + Theme render through LocalizeEnum() (FR-024); fee amounts via MoneyFormatter.Format (FR-015); parameterised strings via ILocalizer helper methods in code-behind.
  • Completed the T011 gap: @using StageFright.Core.Localization in _Imports.razor.
  • DI unchanged — all three localizer-taking registrations resolve from AddLocalization() + the decorating factory.

Tests (T015-T016):

  • New StageFright.Localization.Tests US1 guard suite: baseline completeness (+plural pairing), MemberStatus/Theme enum-key coverage, residual-literal scan over the US1 slice, no-raw-enum-render, no-"C"-currency-format, missing-key warning + neutral fallback.
  • New FR-018 bUnit tests assert US1 text resolves via resource keys / LocalizeEnum.
  • 5 existing bUnit classes moved to LocalizedTestContext; it now also registers ILocalizer. StubStringLocalizer / RealStringLocalizer test helpers added for the constructor-injection sites.

Full build clean; 1706 tests pass (Localization 6, Reports 178, Core 597, UI 587, Data 152, Integration 186). US2/US3/Polish not started.

  • feat(027): localize Rehearsals module (US2, T032)

Move every user-facing literal in Pages/Rehearsals/** into RehearsalsResource.resx (en-AU verbatim) read through IStringLocalizer at render time: RehearsalList, RehearsalForm and AttendanceGrid .razor

  • .razor.cs, including aria-label/placeholder text (FR-001). The attendance fee now formats through MoneyFormatter, not ToString("C") (FR-015). Parameterised strings (search term, dates, member names, attendance rate) resolve via ILocalizer.Get in code-behind helpers.

RehearsalFormModel.Date drops its dead DataAnnotations ErrorMessage literal (RequiredAttribute never fires on a non-nullable DateTime). Three catch blocks stop surfacing raw ex.Message, matching the US1 MemberForm precedent.

Tests: new Rehearsals-scoped guard suite (baseline completeness, residual-literal incl. accessibility attrs, no-"C"-currency) as an interim stand-in for the still-pending repo-wide T029/T030; new FR-018 render tests; RehearsalListTests + AttendanceGridTests flipped to LocalizedTestContext (resx values byte-identical to prior copy).

Full rebuild clean (0 warnings). 1714 tests pass (+8 from the US1 baseline). 29/64 tasks; US2 continues with T029/T030 + T031/T033-T045.

  • chore(027): record after_implement companion hook state (29/64 tasks)

  • feat(027): localize Dashboard page + all dashboard tiles (US2, T031)

Dashboard page chrome, the six remaining dashboard tile bodies (Events, Rehearsals, AttendanceTrend, Finance, CashFlow, OutstandingBalances) and their tile providers now read every user-facing string from DashboardResource / EventsResource / FinanceResource / RehearsalsResource / SharedResource. Chart dataset legend labels localized; tile fee/balance amounts moved from .ToString("C") to MoneyFormatter (FR-015). Tile providers ctor-inject IStringLocalizer (matching MembersDashboardTile provider). bUnit tile/dashboard test classes flipped to LocalizedTestContext; provider-test + V8 integration ctors updated.

  • feat(027): localize remaining menu providers + author enum resource keys (US2, T038 + T042 groundwork)

All six navigation menu providers (Dashboard, Rehearsals, Events, Finance, Settings, Reports) ctor-inject IStringLocalizer and source Title/ShortLabel/sub-item labels from NavigationResource.resx (matching MemberMenuItemProvider). EnumsResource.resx gains Enum__ keys for FeeType, PaymentMethod, PaymentType, AccountType, TaxCode, ReconciliationStatus and JournalEntryType (call-site LocalizeEnum swaps land with each module + T042). Core.Tests gains a RealStringLocalizer fixture; the two affected menu-provider test ctors updated.

  • feat(027): localize shared components (US2, T037)

AddAccountForm, BorderedListBox, OpeningBalanceEntryForm, ReactivationForgivenessDialog, ReportViewer and TileRenderer now read all user-facing text (labels, headings, buttons, placeholders, aria-labels, status/error messages, plural button labels) from SharedResource.resx. AccountType options and account-type column render via LocalizeEnum; forgiveness fee amounts and report money via MoneyFormatter; parameterised strings resolved in code-behind helpers. SubmitButtonText / EmptyText params made nullable with a localized fallback. 16 bUnit test classes that render these components flipped to LocalizedTestContext. Full suite green (1714).

  • feat(027): localize Events / AGM module (US2, T033)

All 10 Events/AGM screens (EventList, EventForm, EventDetail, ParticipationGrid, AgmList, AgmDetail, ScheduleAgm, RecordAgm, RecordSpecialElection, AgmAttendanceGrid) plus AgmAttendanceReportPrinter read every user-facing string, aria-label, badge, status text, validation and print message from EventsResource.resx / SharedResource. Parameterised strings via ILocalizer code-behind helpers; EventFormModel data-annotation messages dropped to bare [Required] (dead on non-nullable value types, matching US1). 8 Events bUnit test classes flipped to LocalizedTestContext. Full suite green.

  • feat(027): localize Settings page + tabs (US2, T035)

SettingsPage tab titles/error-boundary text and the General, Sales Tax, Committee, Event Types and Backup & Restore tabs read all user-facing strings, labels, help text, badges, aria-labels, status/error/success messages and validation from SettingsResource.resx / SharedResource. Theme label via LocalizeEnum; TaxCode options via LocalizeEnum (value stays the invariant token); audit-retention option pluralised via ILocalizer.Plural; fee input adornments fixed to "$" per FR-015 (was culture CurrencySymbol). Language picker row deliberately left for US3/T056. 5 Settings bUnit test classes flipped to LocalizedTestContext. Full suite green.

  • feat(027): localize Setup Wizard (US2, T036)

SetupWizard shell + all 9 tab components (GeneralAppearance, MembershipFees, ThemeSelection, SalesTax, SampleData, ChartOfAccounts, OpeningBalances, Committee, Review) read every user-facing string, label, help text, placeholder, aria-label, badge and validation/error message from SetupResource.resx. Review money via MoneyFormatter; Theme + TaxCode via LocalizeEnum (wizard's Light/Dark keeps invariant option values per spec 017 FR-022); audit-retention & review lines pluralised via ILocalizer.Plural; fee adornments fixed to "$". Language step deliberately left for US3/T057. 5 Setup tab bUnit test classes flipped to LocalizedTestContext. Full suite green. feat(027): localize StartupError page + Reports page shell (US2, T043) StartupError.razor (database recovery page) and ReportsPage.razor read their headings, body text, buttons, prompts and error messages from SharedResource.resx. MainPage.xaml / App.xaml carry no user-facing text. Remaining Pages/** (Finance) is T034. feat(027): localize Finance module (US2, T034) Move every user-facing literal in the 13 Finance pages + code-behinds into FinanceResource.resx (en-AU baseline). Fee/payment/account/tax/ reconciliation-status enums render via LocalizeEnum; every displayed amount goes through MoneyFormatter (FR-015) instead of ToString("C") / FormatString="{0:C}". aria-labels, placeholders and the embedded-link "no accounts configured" warnings are localized too (FR-001). Nine Finance bUnit test classes flipped to LocalizedTestContext so IStringLocalizer resolves; neutral values are byte-identical to the prior hardcoded copy so assertions stay green. Full solution build clean; full suite 1714 passing. feat(027): localize AgeCalculationService DOB/age messages (US2, T041 groundwork) The US1-deferred piece of T041: AgeCalculationService.ValidateDateOfBirth now sources its three user-facing messages from ValidationResource via the ILocalizer facade (named {Age}/{MaxYears}/{MinimumAge} placeholders), so the service is DI-constructed with ILocalizer. Call-site fan-out handled: MemberList/MemberDetail switch from new() to an injected AgeCalculationService (registered in LocalizedTestContext for bUnit); Core/Integration/Reports test projects each gain a RealLocalizer fixture and pass it to the ~12 new AgeCalculationService(...) sites. The remaining T041 work — service-thrown ValidationException/DomainValidation- Exception message text across ~18 Core services — is not in this commit. Full solution build clean; full suite 1714 passing. chore(027): record companion context after T034 + T041 groundwork feat(027): localize the 11 report providers (US2, T039 + T030) Route every report provider's user-facing text through the ILocalizer facade against a new populated ReportsResource.resx (~120 en-AU entries, transcribed verbatim per FR-004): report names, filter labels, titles/subtitles (named-placeholder formatted), section headings, column headers, subtotal/total + inline row labels, and the GL-imbalance exception message. Enum-valued cells (MemberStatus, ReconciliationStatus) resolve via ILocalizer.Enum (FR-024). Filter option VALUES stay culture-invariant tokens (Options, unchanged — still compared in GenerateAsync and persisted); the localised option LABEL is carried on a new parallel ReportFilterDefinition.OptionLabels list, rendered by ReportViewer with fallback to the token. Report amounts keep decimal.ToString("F2") — providers never used "C", and adding a $ symbol/grouping would break SC-002 render parity. Report renderers (T040) and service exception text (T041) remain. T030: ReportsResourceLabelTests (names/headers/section+total labels resolve via ReportsResource, no raw-key/blank leak — FR-006) and PluginTextNonEnglishCultureTests (TestPlugin English report + tile text renders unchanged under fr-FR, run on a dedicated thread so the culture never leaks into parallel tests — FR-020). Build 0 warnings / 0 errors; full suite 1733 pass / 0 fail (Reports 178->195, Integration 186->188). chore(027): record after_implement companion context (39/64 tasks) feat(027): localize report PDF renderer chrome (US2, T040) + enum/currency sweeps (T042, T044) T040: all 6 PDF report renderers now source their page chrome from ReportsResource.resx (~21 en-AU keys, verbatim per FR-004): PdfReportRenderer: "Generated: {DateTime} UTC", footer "Page"/"of" AttendanceRollPdfRenderer: title, "Rehearsal: {Date} at {Time}", Name/Present/Pd headers AgmResultsPdfRenderer: title, meeting/attendance lines, "Elected Positions", "No positions recorded.", bold "{Label}: " position prefix, "General Committee Member:" CheckboxSheetPdfBuilder: shared "Name" column + footer; gains an ILocalizer param and a nameColumnHeader arg threaded to BuildMemberTable Event/AgmAttendanceSheetPdfRenderer: titles, date lines, Participated/Attended headers The 5 concrete renderers ctor-inject ILocalizer (resolves from the existing AddScoped<ILocalizer, Localizer>() - no MauiProgram change). CsvReportExporter has no user-facing literals and is untouched. 8 direct test-construction sites now pass RealLocalizer.Instance; ReportsResourceLabelTests gains 22 chrome-key + placeholder assertions (FR-006). T042: verified the user-facing enum sweep is complete - EnumsResource.resx holds every Enum__ key for all 9 user-facing enums. Fixed the one residual raw display site (the item deferred from US1): MemberDetail Fee Payment History FeeType column now uses fee.FeeType.LocalizeEnum(). T044: verified zero decimal.ToString("C")/{0:C}/FormatString display sites remain in UI or Reports. The one user-facing residual - BankReconciliationService.cs:153 ReconciliationException message - is exception-message text owned by T041. Full solution build: 0 warnings / 0 errors. Full test suite: 1755 passed / 0 failed (Reports 217, up from 195). US2 remaining: T041 (user-facing exception Message text across ~18 Core services - high blast radius, own session), T045 (DI recheck after T041's ctor changes), T029 (repo-wide guard suite - blocked until T041). feat(027): localize non-Finance service exception messages (US2, T041 part 1) Route user-facing ValidationException / DataIntegrityException / ImportException Message text through ValidationResource for the eight non-Finance Core services: AgmService, EventService, EventTypeService, CommitteeOfficeHolderTypeService, AttendanceService, SettingsService, SetupService, BackupService. 34 new en-AU keys in ValidationResource.resx (verbatim wording, FR-004); named-placeholder keys for {Year}/{SchemaVersion}/{SupportedMajorVersion}/{EntityType}. Each service takes the ILocalizer facade via a required ctor param (matches the AgeCalculationService T041-groundwork precedent; only path that substitutes named tokens). All eight are plain AddScoped<I,Impl> registrations so DI auto-resolves the new param -- no MauiProgram change. BackupService DeserializeAndValidate / ValidateVersion / ValidateCompleteness change from private static to instance to read _localizer. EntityNotFoundException built-in message and DataAccessException wrappers stay English (diagnostic per FR-007 -- UI never shows them verbatim). 20 test call-sites updated to pass RealLocalizer.Instance; new RealLocalizer fixture added to StageFright.Data.Tests. Interim guard Us2ExceptionMessageGuardTests (baseline completeness + no residual literal) scoped to the eight files. Full build 0 warnings / 0 errors. Full suite 1757 pass. Remaining for US2: T041 Finance services (incl. BankReconciliationService {Difference:C}), T045 DI recheck, T029 repo-wide guard suite. feat(027): finish US2 — localize Finance exception messages + repo-wide guard (T041, T029, T045) T041 (Finance half): route user-facing ValidationException / ReconciliationException Message text through ValidationResource for all 9 Finance services — AccountService, BankReconciliationService, ExpensePaymentService, PaymentService, FeeService, GeneralJournalService, IncomeEntryService, BankDepositService, OpeningBalanceService. 39 new verbatim en-AU ValidationResource keys (FR-004) + reuse of Validation_Settings_NotConfigured; named-token keys for {Name} (Account duplicate), {Date} (statement date), {Difference}/{Zero} (reconciliation finalise). BankReconciliationService finalise message: the {workspace.Difference:C} + literal "$0.00" (the T044-flagged C-format residual) now routes both amounts through MoneyFormatter.Format — byte-identical under en-AU, fixed-AUD under any culture (FR-015). Each service takes the ILocalizer facade via a required ctor param (matches the non-Finance T041 precedent). All 9 are plain AddScoped<I,Impl> so DI auto-resolves the new param — no MauiProgram change (T045). ~30 test ctor sites pass RealLocalizer.Instance (Core.Tests, Data.Tests, Integration.Tests). EntityNotFoundException / GLBalanceException wrappers stay English (diagnostic per FR-007). Us2ExceptionMessageGuardTests extended to cover the Finance services. T029: new repo-wide Us2LocalizationGuardTests (8 facts) — baseline key completeness across the US1+US2 surface, Enum__ coverage for all 9 user-facing enums no-raw-enum-render, residual-literal scan over the UI display surface, orphan-satellite keys, plural-pair + placeholder parity, cross-culture token parity, and no-"C"-currency repo-wide. Report-provider filter option-VALUE tokens (guarded by ReportsResourceLabelTests, T030) and DataAnnotations ErrorMessage / ValidationResult literals (compile-time constant, resolved in T060) are documented exclusions. resource-key-catalog.md §3 updated to reflect the implemented guard layout and exclusions. Full rebuild 0 warnings / 0 errors. Full suite 1765 pass (Localization 19, Reports 217, Core 597, UI 592, Data 152, Integration 188). US2 (Phase 4) complete — 45/64 tasks. feat(027): US3 — persisted, OS-aware display language + pickers (T046-T059) Adds the language-selection slice on top of the US1/US2 extraction: Settings.LanguageCode (nullable, presentation-only) + hand-written EF migration AddLanguageCodeToSettings (dotnet ef can't build the MAUI startup project here); SchemaVersion left at 1.1.0 (unchanged for the last ~6 settings migrations; asserted verbatim by backup tests). ILanguageProvider FR-023 startup ladder (explicit choice -> OS display language by exact then parent-language match -> en-AU), wired in MauiProgram to set the process culture before the first Blazor render; ISystemCultureProvider seam (SystemCultureProvider in the App layer). SupportedLanguagesCatalog: runtime discovery of shipped resource cultures by scanning satellite folders for the StageFright assemblies; qps-* pseudo-locales excluded; endonyms from CultureInfo.NativeName. Settings General tab + Setup Wizard language pickers (endonym , active-marked, inline restart notice at the point of change - FR-021); choice flows through SetupRequest -> Settings at first-run Finish.

  • CultureProvider cascading seam wrapping ShellLayout (parallel to ThemeProvider; no v1 behaviour change) - named CultureProvider to avoid colliding with the Core LanguageProvider service.
  • qps-ploc test pseudo-locale: scripts/generate-pseudo-locale.py + 12 regenerable .qps-ploc.resx fixtures (bracketed/accented, {tokens} preserved, 3 keys omitted to exercise per-key fallback).
  • Tests (+40): LanguageProvider ladder, catalog runtime discovery + qps-* exclusion, migration round-trip, V21 startup/SC-006 no-data- change, qps-ploc e2e, language-picker bUnit. LocalizedTestContext registers the new language services.
  • Full rebuild: 0 warnings / 0 errors. Full suite: 1805 pass, 0 fail (Localization 23, Core 620, Reports 217, UI 599, Data 154, Integration 192). Polish (T060-T064) remains.

    • feat(027): Polish — repo-wide literal guard + localize App.razor + docs (T060-T064)

    T060: fold the interim per-slice residual-literal scans into one repo-wide guard (Us2LocalizationGuardTests.Should_HaveNoUserFacingLiteral_When_AppSurfaceScanned) covering all of StageFright.UI + every user-facing exception Message literal in StageFright.Core. The widened scan flagged App.razor (project root, outside the old 4-dir scope): localized its error-boundary + not-found chrome to 6 new Shared_App_* keys (neutral + qps-ploc). DataAnnotations ErrorMessage args are frozen as the one documented carve-out.

    T061: docs/localization/adding-a-language.md — translator/maintainer guide (FR-022, SC-009). T062: CLAUDE.md gains a Localization section + module-list entry + money-column data-grid note; resource-key-catalog.md updated to past tense; spec 007 account-balance contract gets a forward-note (its {0:C} columns moved to MoneyFormatter). T063/T064: full rebuild 0 warn / 0 err; full suite 1805 pass (Localization 23).

    Spec 027 status: completed (64/64 tasks).

    • feat(027): add American English (en-US) resource set

    Drop-in en-US satellite for all 12 area .resx files, translated from the en-AU neutral baseline. The runtime SupportedLanguagesCatalog discovers it automatically — no code change — so it now appears in Settings ▸ General and the Setup Wizard language step.

    Divergence from en-AU is spelling + vocabulary only (the two dialects are otherwise identical across this corpus):

    • organisation → organization (Settings/Setup labels, Tax Summary subtitle)
    • finalise / finalised → finalize / finalized (Finance button, reconciliation validation messages, ReconciliationStatus enum label)
    • cheque → check (PaymentMethod enum label)
    • cancelled → canceled (report-generation message)
    • "financial year" → "fiscal year" (Settings + opening-balances wizard copy)

    Keys, {named} placeholders and docs are byte-identical to neutral, so the Us2 orphan-key / placeholder-parity guards pass.

    Also make three previously culture-fragile bUnit assertions resolve their expected text through the same IStringLocalizer the component uses, so they hold under a US UI culture (GitHub CI runs windows-latest = en-US):

    • ReconciliationWorkspaceTests: assert ReconciliationStatus.Finalised.LocalizeEnum()
    • SetupWizardTests: assert the localized "Organisation Settings" tab title
    • refresh the now-stale "v1 ships only en-AU" note in SupportedLanguagesCatalog

    Full suite green under both the default culture and -culture en-US (UI 599, Core 620, Data 154, Reports 217, Integration 192, Localization 23).

    • feat(027): add French (fr-FR) resource set

    Full French translation of all 12 area .resx files, dropped in beside the en-AU neutral baseline. SupportedLanguagesCatalog discovers fr-FR at runtime — no code change — so "français (France)" now appears in Settings ▸ General and the Setup Wizard language step, and an fr / fr-FR operating system resolves to it on first run. Selecting it renders the whole app in French with French number/date formatting; the currency stays "$" / "AUD" (MoneyFormatter).

    • ~1,000 strings translated; keys, {named} placeholders, _One/_Other plural pairs and docs are byte-identical to neutral, so the Us2 orphan-key / placeholder-parity / plural-pairing guards pass.
    • AGM → "AGA" (assemblée générale annuelle); accounting terms use standard French (plan comptable, grand livre, bilan, compte de résultat, rapprochement bancaire, produits/charges, capitaux propres, écriture de journal…).
    • Embedded-HTML values keep their markup; significant leading/trailing spaces preserved.

    No test changes needed: the only test that deliberately runs under fr-FR (PluginTextNonEnglishCultureTests) still passes, and every other test runs under the machine culture (en-GB locally / en-US on CI) where the fr-FR satellite is not loaded.

    Full suite green under both the default culture and -culture en-US (Core 620, Data 154, Reports 217, UI 599, Integration 192, Localization 23).

    • spec(028): add international accounting-practice readiness spec, plan, and tasks

    Feature 028 readies the finance module for community/amateur-theatre groups outside Australia, judged against universal double-entry and bookkeeping good practice (not formal IFRS/GAAP). Sourced from GitHub issue #341 and sub-issues #342-#352.

    Adds the full spec-kit artifact set:

    • spec.md - 10 prioritized user stories (currency, regional number entry, statement consistency, basis-of-accounting disclosure, bank reconciliation, prior-year period locks, financial-year start, audit-history retention, written policies, sales-tax i18n spike), FR-001..FR-033, SC-001..SC-013
    • plan.md, research.md - configurable org currency replacing hard-coded AUD, culture-safe money parsing, period-lock service, report integrity checks + disclosures
    • data-model.md - currency/period-lock/policy fields + migration outline
    • contracts/ - currency-formatting, period-lock, reports, and settings-and-setup contracts
    • checklists/requirements.md
    • tasks.md - 94 tasks (T001-T094) across 13 phases

    Bumps .specify/feature.json to specs/028-international-accounting-standards.

    • feat(028): foundational currency primitives + Settings schema (T001-T008)

    Phase 1 baseline confirmed green (1805 tests). Phase 2 lays the shared foundation every user-story phase builds on — no user-visible behaviour change yet; an AUD dataset formats byte-identically.

    • SupportedCurrency record + CurrencyCatalog (curated ISO 4217 seed set: AUD/USD/EUR/GBP/NZD/CAD 2-digit, JPY 0-digit, KWD/BHD 3-digit) with case-insensitive TryGet (false + Default, no throw) and Get (throws ValidationException on an unknown code). Mirrors SupportedLanguagesCatalog.
    • MoneyFormatter gains Configure(SupportedCurrency); Format/FormatWithCode now use the configured symbol/code and MinorUnitDigits while grouping and placement still follow CultureInfo.CurrentCulture. Falls back to CurrencyCatalog.Default (AUD/$/2) before Configure is called.
    • MauiProgram configures MoneyFormatter for Settings.CurrencyCode right after the display culture is applied; failure leaves it on AUD default.
    • Settings entity + EF config: add CurrencyCode ("AUD"), FinancialYearStartDay (1), ClosedThroughDate (null); AuditRetentionYears default 1 -> 5 (range 1-7 unchanged).
    • Migration AddInternationalAccountingSettings: adds the three columns (NOT NULL DEFAULT backfills the existing row), alters only the AuditRetentionYears column default (no UpdateData, so a configured value is preserved).

    Build 0W/0E; all 1805 tests green.

    • feat(028): US1 configurable currency — MoneyFormatter drives all money display (T009-T028)

    Phase 3 / User Story 1 (spec 028). Every screen, report, PDF and CSV amount now renders in the organisation's configured currency (Settings.CurrencyCode, ISO 4217, default AUD) with the right symbol and minor-unit precision; grouping/placement still follow the active culture. An AUD dataset is byte-identical to the pre-028 output.

    • TaxCalculator.SplitInclusive: optional minorUnitDigits (default 2); all callers pass CurrencyCatalog.Get(settings.CurrencyCode).MinorUnitDigits.
    • First-run setup: mandatory #setup-currency picker on GeneralAppearanceTab bound to SetupFormModel.CurrencyCode; SetupService validates it against CurrencyCatalog (Validation_Setup_CurrencyUnknown) and persists the normalised code.
    • SettingsService.SaveAsync rejects a post-setup currency change (Validation_Settings_CurrencyImmutable) — currency is fixed after setup.
    • All StageFright.Reports providers + JournalEntryPage totals route money through MoneyFormatter.Format instead of ToString("F2")/"N2".
    • New resource keys (neutral + en-US + fr-FR): Validation_Setup_CurrencyUnknown, Validation_Settings_CurrencyImmutable, Setup_General_CurrencyLabel/Help.
    • Tests: CurrencyCatalogTests, MoneyFormatterTests, TaxCalculator 0/2/3-digit theory, Setup/Settings currency cases, CurrencyPickerTests (bUnit), AudZeroDriftTests + V28_CurrencyConfigurationTests (integration acceptance). MoneyFormatterState collection serialises MoneyFormatter.Configure-sensitive integration tests.
    • Updated ~30 pre-existing report/UI/integration assertions to MoneyFormatter.Format(...).
    • Docs: CLAUDE.md Localization note + spec 027 superseding notes for the fixed-$/AUD prose.

    Full rebuild: 0W/0E. Tests: 1868 pass (was 1805).

    • feat(028): US2 locale-safe money entry — MoneyInput.Parse for numeric inputs (T029-T035)

    Phase 4 / User Story 2 (spec 028). The manual journal and opening-balance forms hand-rolled decimal.TryParse(value, NumberStyles.Number, CultureInfo.CurrentCulture) on the value of an — which the browser always serialises invariant ("1.50"). Under fr-FR / de-DE the period was read as a thousands separator and 1.50 posted to the ledger as 150 (FR-007…FR-009 / live data bug).

    • New MoneyInput.Parse(string?): invariant decimal.TryParse with NumberStyles.AllowDecimalPoint | AllowLeadingSign; 0m for null/blank/unparseable. The single shared money-entry parse helper.
    • JournalEntryPage.ParseAmount and OpeningBalanceEntryForm.SetAmount now delegate to MoneyInput.Parse; removed the CurrentCulture parse and the now-unused System.Globalization using from both.
    • Tests: MoneyInputTests (unit, fr-FR/de-DE/en-AU), JournalEntryPageLocaleTests + OpeningBalanceEntryFormLocaleTests (bUnit — entered amount stored exactly), MoneyInputGuardTests (repo-wide: no UI money field parses with CurrentCulture; both handlers route through MoneyInput.Parse), V28_LocaleSafeMoneyEntryTests (integration acceptance — real journal/opening-balance services over SQLite, fr-FR/de-DE input stored exact to the cent and identical to en-AU).

    Build: 0W/0E. Tests: 1895 pass (was 1868, +27).

    • chore(028): sync companion after_implement context (Phase 4 / US2)

    after_implement task-sync: 35/94 tasks complete, status stays implementing.

    • chore(028): record plug-display concern for Polish T091a

    • feat(028): US3 statement integrity — Balance Sheet / Trial Balance guards (T036-T040)

    TrialBalanceReportProvider drops the 0.01 tolerance band: the imbalance check is now totalDebits != totalCredits, so any non-zero difference (including one cent) refuses to generate — no tolerance band (FR-011).

    BalanceSheetReportProvider appends an explicit emphasized out-of-balance line (new Reports_BalanceSheet_OutOfBalance key + MoneyFormatter.Format of the difference) whenever total assets != total liabilities + equity, so a corrupted ledger never yields a clean statement (FR-010).

    Reworded Reports_TrialBalance_GLImbalanceError across neutral/en-US/fr-FR to drop the tolerance implication and the doubled literal $ (the {Debits}/{Credits} args carry the configured currency symbol since T024); dropped the stale FR-034 doc-comment reference.

    Tests: 5 new Reports provider tests + V28_StatementIntegrityTests (5 acceptance cases). Full suite 1903 pass, build 0W/0E.

    • chore(028): sync companion after_implement context (Phase 5 / US3)

    • feat(028): US4 basis-of-accounting disclosure on financial statements (T041-T047)

    ReportData gains an optional BasisOfAccounting string. PdfReportRenderer renders it as a grey line below the "Generated:" line; CsvReportExporter appends it as a trailing self-labelled note record after the grand total; ReportViewer shows it in a

    directly beneath the subtitle — each only when non-null.

    New shared Reports_Common_BasisOfAccounting (neutral / en-US / fr-FR) with hybrid wording that names both the accrual treatment of member fees and the cash treatment of all other income and expenditure — never a single blanket basis (FR-012). All eight financial-statement providers (Income Statement, Balance Sheet, Trial Balance, Tax Summary — both return paths, Account Register, General Ledger, Bank Reconciliation, Member Account Summary) set it; Member List and Committee leave it null.

    Tests: BasisOfAccountingTests (Reports), ReportViewerBasisTests (UI/bUnit), V28_BasisOfAccountingDisclosureTests (integration acceptance, US4 AC-1..AC-2). reports-contract.md CSV rendering row synced to the shipped single-column note.

    Build 0W/0E; touched-project suites green (Reports 235, UI 613, Integration 215, Localization 25).

    • feat(028): US5 conventional bank reconciliation (T048-T051)

    Rewrite BankReconciliationReportProvider to the standard adjusted-balance layout — one section per bank account: balance per bank statement, add outstanding deposits (listed + summed), less outstanding payments (listed + summed), adjusted bank balance, balance per general ledger (GetAccountBalanceAsync at the statement date), and a Reconciled residual row that is zero on a tied statement (FR-013…FR-015, SC-008). All amounts route through MoneyFormatter.

    • ReportsResource neutral/en-US/fr-FR/qps-ploc: add BalancePerBankStatement, AddOutstandingDeposits, LessOutstandingPayments, AdjustedBankBalance, BalancePerGeneralLedger, Reconciled; drop the 10 now-dead cleared/summary keys (provider was their only consumer).
    • BankReconciliationServiceTests: +2 FR-015 tests (finalise rejected over the half-cent tolerance with no state change; finalised reconciliation rejects toggle-clear and re-finalise).
    • V28_ConventionalBankReconciliationTests: new integration acceptance (real SQLite + migrations) for US5 AC-1…AC-3.
    • Sync contracts/reports-contract.md to the shipped layout.

    Full suite green: Core 684, Reports 236, Localization 25, UI 613, Data 154, Integration 220. Build 0W/0E.

    • chore(028): sync companion after_implement context (Phase 7 / US5)

    • feat(028): US6 closed-period lock (T052-T064)

    Reject any financial transaction dated on or before Settings.ClosedThroughDate so a reported prior period cannot be altered by a back-dated entry (FR-016/017), while first-run setup opening balances stay permitted (FR-018).

    • ClosedPeriodException (sealed, §5.2 shape) + IClosedPeriodGuard / ClosedPeriodGuard (reads the Settings singleton; no-op pre-setup or while nothing is closed).
    • GLRepository consults the guard on the earliest line date before the write, at the single GL choke point; AddPairAsync is covered by delegation. UnitOfWork passes ClosedPeriodException through unwrapped like GLBalanceException, so a rejection rolls the whole operation back — no business row, no ledger line.
    • General settings tab gains a "close periods through " control (#settings-close-through-date) + a confirmation checkbox; the closed-through date only advances when both are set. SettingsRepository.SaveAsync now maps ClosedThroughDate on the detached-update path.
    • All 7 finance posting forms catch ClosedPeriodException and show Validation_ClosedPeriod_PostingRejected, staying re-submittable.
    • Tests: ClosedPeriodGuard unit (boundary/null cases), ClosedPeriodLock integration sweep (journal/expense/income/deposit/payment/fee-accrual/ forgiveness all reject in-period and post after), SetupService FR-018, ClosePeriodControl bUnit. 57 GLRepository test construction sites updated for the new required guard dependency.

    Build 0W/0E; suite green (Core 691, Integration 235, Data 154, UI 616, Reports 236, Localization 25) — one pre-existing random-GUID "fee" substring flake in EventListTests, passes on re-run.

    • chore(028): sync companion after_implement context (Phase 8 / US6)

    • feat(028): US7 financial-year start as a setup decision (T065-T075)

    FinancialYearCalculator.GetRange/GetPreviousRange gain an optional startDay (default 1, preserving every existing caller); the year now pivots on the (month, day) anchor. The four FY-preset report providers (TrialBalance, BalanceSheet, IncomeStatement, TaxSummary) and OpeningBalancesWizard pass settings.FinancialYearStartDay through.

    SetupRequest / SetupFormModel / SetupService carry FinancialYearStartMonth

    • FinancialYearStartDay; SetupService validates the day to 1..28 (Validation_Setup_FinancialYearStartDayRange) and persists both. The setup General tab gains mandatory month + day pickers (#setup-fy-start-month / #setup-fy-start-day) and the Settings General tab gains a matching day picker beside the existing month one.

    Tests: FinancialYearCalculatorTests (non-first-of-month / February anchors, UTC end-of-day, default-to-1 equivalence), FinancialYearStartTests (integration — setup persists a non-first-of-month start; every FY-preset report honours the anchor; AU July-1 dataset unchanged), FinancialYearStartPickerTests (bUnit), and SetupServiceTests additions. Build 0W/0E; 1986 tests pass.

    T076 (FR-022 sub-twelve-month first-year follow-on issue) is recorded in spec.md Assumptions; the GitHub issue itself still needs to be created.

    • chore(028): sync companion after_implement context (Phase 9 / US7)

    • chore(028): companion after_implement task-sync (75/94)

    • feat(028): US8 audit retention, coverage & purge-failure surfacing (T077-T083)

    Phase 10 of spec 028 (International accounting-practice readiness), User Story 8:

    • T080: raise the audit-retention default 1 -> 5 at the remaining declaration sites (SetupFormModel, SetupRequest); SetupService flows it through unchanged. Entity/config/migration defaults were already 5 (T003/T005/T006).
    • T081: AttendanceService.RecordBatchAsync now writes an AuditTrailEntry for the attendance-fee accrual and, when paid at creation, the auto-payment - inside the existing UnitOfWork transaction (FR-026).
    • T082: a failed startup audit purge is no longer swallowed. AuditTrailService.PurgeOlderThanAsync propagates; IStartupDiagnosticService gains a non-fatal warning channel (HasStartupWarning / StartupWarning / RecordWarning); MauiProgram's purge catch logs AND records the warning, and the retention fallback follows the new 5-year default. Startup still continues and the user is not routed to the blocking recovery page (FR-025).
    • T083: a dismissible Dashboard banner surfaces the warning (Shared_StartupWarning_* in neutral / en-US / fr-FR).

    Tests: T077 migration default (fresh -> 5, existing value preserved), T078 AttendanceService audit coverage, T079 purge-failure propagation + diagnostic surfacing, plus 3 Dashboard banner bUnit tests. Reconciled the two SetupService default-flow tests and AuditTrailServiceTests to the new behaviour. data-model.md section 10 updated. Full build 0W/0E; 1999 tests pass.

    • docs(028): US9 accounting policies + living-spec currency (T084-T086a)

    Phase 11 / User Story 9 — "Have the accounting policies written down":

    • T084: new docs/accounting-policies.md — basis of accounting (hybrid accrual/cash), revenue recognition, rounding to the currency minor unit, single-currency policy, record immutability + reversing-entry corrections, period locking, financial year, audit-trail retention; states the reports are unaudited management accounts (FR-027, SC-012).
    • T085: capabilities/finance/spec.md de-drafted; every retired registration-based tax reference (IsGstRegistered, per-fee GstCode, gross ÷ 11, "GST Collected", "GST clearing accounts", "net of GST") rewritten to the spec-016 model — Settings.IsTaxApplicable / TaxRate / TaxCode (Taxable, TaxExempt, Excluded), Tax Collected account 2310 (FR-028).
    • T086: capabilities/audit-trail/spec.md retention figure corrected from a fixed 12 months to a configurable 1-7 years defaulting to 5 on new datasets, existing values preserved (stale after T003's default change).
    • T086a: spec.md Assumptions notes the pre-existing stale capabilities/settings/spec.md ABN/GST wording as a carried-forward follow-up, outside FR-028's finance-only scope.

    Markdown-only phase — no code change; full rebuild + Localization.Tests run in Phase 13 (T090/T091). Deferred T076 and Phases 12-13 remain.

    • chore(028): companion after_implement task-sync (85/94)

    • docs(028): US10 sales-tax internationalisation assessment spike (T087, T089)

    Phase 12 / User Story 10 (P3, spike) — FR-029, FR-030, FR-033.

    • T087: new docs/assessments/sales-tax-internationalisation.md — for each of the four required points, an in-scope/out-of-scope decision with a rough size: rate history/effective-dating OUT (L); tax-exclusive entry IN (M); balance-sheet classification of recoverable input tax (accounts 2310/2320) IN (S–M); multiple simultaneous rates/jurisdictions OUT (XL). The two in-scope points carry full ready-to-file follow-on issue specs.
    • T089: FR-033 verification — git diff master...HEAD over every tax-adjacent file confirms the only tax-path change on this branch is the optional minorUnitDigits rounding param on TaxCalculator.SplitInclusive (default 2 → AUD byte-identical). TaxCode enum, 2310/2320 accounts, GL line structure, tax-inclusive entry model and Tax Summary net arithmetic are untouched; AudZeroDriftTests stored-value assertions hold. Recorded in spec.md Assumptions.
    • T088 left unchecked: gh issue create is blocked by this environment's action classifier (same as T076). The two follow-on issues are specified verbatim in the assessment and noted in spec.md Assumptions for the maintainer to file.

    No code changed — Markdown only.

    • test(028): Phase 13 polish — currency-symbol guard + SC evidence (T090-T094)

    Phase 13 / Polish & cross-cutting validation (spec 028). Completes the implementation bar the two follow-on GitHub issues (T076, T088), which gh issue create cannot file in this environment — their full text is in spec.md Assumptions + docs/assessments/sales-tax-internationalisation.md.

    • T090: full dotnet build -t:Rebuild = 0W/0E; full dotnet test = 2002 pass (Core 714, Localization 28, Reports 236, UI 624, Data 156, Integration 244).
    • T091a: new CurrencySymbolGuardTests — repo-wide guard that no Reports provider and no UI money-display site hard-codes a "$"/"AUD" literal or formats money with ToString("C")/"{0:C}"/ToString("F2"); the 9 money-bearing providers each route through MoneyFormatter (FR-004, SC-002).
    • T091: StageFright.Localization.Tests green (28/28) incl. the new guard.
    • T092: CLAUDE.md Reports-pipeline money note (MoneyFormatter for every provider/renderer/exporter amount); finalised the remaining stale specs/027-localization-support/spec.md FR-015 lines (Clarifications, Edge case, FR-015 itself) with "Superseded by spec 028" notes, matching the T028 precedent.
    • T093: re-ran the AUD zero-drift regression against the final build — AudZeroDriftTests + V28_CurrencyConfigurationTests 9/9 pass; identical report figures and stored monetary/tax/GL values (SC-004, FR-031, FR-032).
    • T094: walked SC-001..SC-013 — every criterion maps to a green V28_* acceptance scenario or InternationalAccounting integration test (52/52 pass) for a non-AUD, non-first-of-month org end to end; recorded specs/028-international-accounting-standards/sc-verification-evidence.md.

    Tasks: 92/94 checked. T076 + T088 stay open pending maintainer issue filing.

    gh issue create is now permitted (global settings allow rule), so the two follow-on-issue tasks left open in Phase 13 are done:

    Implements FR-022 / issue #353 as spec 028 Phase 14, on the existing 028 branch (no new branch).

    • Settings.InceptionDate (DateTime?, nullable, default null) — optional organisation founding date, captured at first-run setup; migration AddOrganisationInceptionDate (TEXT NULL).
    • FinancialYearCalculator gains first-period-aware GetRange/GetPreviousRange overloads taking DateTime? inceptionDate and returning (From, To, bool IsPartYear). A null inception, or one on the anchor, reproduces the existing 12-month behaviour exactly.
    • TrialBalance / IncomeStatement / TaxSummary / BalanceSheet providers pass settings.InceptionDate and wrap their subtitle via the new Reports_Common_PartYearSubtitle when the default FY-preset period is the part-year first period (never for a user-supplied custom range).
    • Setup: optional #setup-inception-date on GeneralAppearanceTab; SetupFormModel/SetupRequest/SetupService plumb it through.
    • Resources: Reports_Common_PartYearSubtitle and Setup_General_InceptionDate{Label,Help} in neutral/en-US/fr-FR; qps-ploc regenerated (also catches pre-existing spec-028 drift).
    • spec.md (FR-022 delivered, SC-014, Assumptions, Edge Case, Key Entity) and data-model.md updated.

    Presentation / range calculation only — no stored monetary, tax or GL value changes; AudZeroDriftTests still green. Full rebuild 0W/0E; 2019 tests pass (+8 calculator, +5 integration, +4 bUnit).

    Implements issue #354 as spec 028 Phase 15, on the existing 028 branch (no new branch). The in-scope "Issue A" from the US10 sales-tax internationalisation spike.

    • TaxEntryMode enum (Inclusive — default, every pre-[FEATURE] Support tax-exclusive amount entry (net + tax) alongside the current tax-inclusive entry #354 dataset — or Exclusive) on Settings.TaxEntryMode; migration AddTaxEntryMode (TEXT NOT NULL DEFAULT 'Inclusive', string-converted like every other enum column), so an existing row backfills to Inclusive and stays byte-identical.
    • TaxCalculator gains SplitExclusive(net, rate, digits) -> (gross, tax) with tax = round(net * rate / 100) and gross = net + tax, plus a Split(entered, mode, rate, digits) -> (gross, net, tax) dispatcher so every taxable posting service has one uniform call site.
    • FeeService / IncomeEntryService / ExpensePaymentService / AttendanceService dispatch through TaxCalculator.Split: in Exclusive mode the entered figure is the net, tax is added on top, and the receivable/bank line (plus Fee.Amount, Payment.Amount and the attendance paid-at-creation cash pair) carry the gross while the income/expense line keeps the net. Inclusive mode is unchanged.
    • ReactivationForgivenessService is deliberately untouched: it reverses a stored gross Fee.Amount, which SplitInclusive re-sums by construction, so the write-off stays balanced in either mode and no historical figure is reinterpreted.
    • Setup: SetupFormModel / SetupRequest / SetupService plumb TaxEntryMode through; SalesTaxTab and TaxSettingsTab render an Inclusive/Exclusive selector while tax applies (reset to Inclusive on toggle-off); SettingsService.SaveAsync forces Inclusive when tax is off; GeneralSettingsTab merges TaxEntryMode from the fresh fetch.
    • RecordIncome / ExpensePaymentPage: the tax hint picks Finance_Common_TaxExclusiveHint vs the existing inclusive hint, and the Amount label reflects the mode.
    • Resources: Finance_Common_TaxExclusiveHint / AmountLabelTax{Inclusive,Exclusive}, Settings_/Setup_Tax_EntryModeLabel, Enum_TaxEntryMode_{Inclusive,Exclusive} in neutral / en-US / fr-FR; qps-ploc regenerated; Us2LocalizationGuardTests.UserFacingEnums gains typeof(TaxEntryMode).
    • spec.md (Assumptions [FEATURE] Support tax-exclusive amount entry (net + tax) alongside the current tax-inclusive entry #354-delivered + FR-033 verification extended, SC-015), data-model.md (Settings.TaxEntryMode row, AddTaxEntryMode migration, Phase 15 section), the sales-tax assessment (Point 2 delivered) and CLAUDE.md updated.

    Additive and opt-in — no GL line-structure, 2310/2320 or TaxCode change; Inclusive mode and AudZeroDriftTests are byte-identical. Full rebuild 0W/0E; 2053 tests pass (+28 calculator, +4 service, +5 integration, +1 migration, +3 bUnit).

    Implements issue #355 as spec 028 Phase 16, on the existing 028 branch (no new branch). The in-scope "Issue B" from the US10 sales-tax internationalisation spike (assessment Point 3), option 1 (preferred).

    • Seed: the system account 2320 is re-typed from AccountType.Liability "Tax Paid" to AccountType.Asset "Tax Receivable" — tax paid on purchases is recoverable from the tax authority, so it is an asset (a receivable). 2310 "Tax Collected" (owed to the authority) stays a Liability.
    • Migration ReclassifyInputTaxAsReceivable: UpdateData on Name + Type of the seed row; Down restores "Tax Paid" / "Liability". Paired Designer and model snapshot updated. No column added or dropped.
    • Number kept as 2320, a documented asset exception. Renumbering into the asset range would desync the denormalised Transaction.GLAccount string snapshot on every historical ledger row. GetNextAccountNumberAsync is unaffected (it already excludes system accounts and 2320 is outside its 1000-1999 asset scan window).
    • No provider code change. BalanceSheetReportProvider and TrialBalanceReportProvider section purely by AccountType, so a net-refundable org's recoverable tax now presents under Assets (debit-normal, positive) and a net-payable org's tax owed under Liabilities via 2310, with the Trial Balance still tying exactly.
    • TaxSummaryReportProvider unchanged: taxOnPurchases / net are computed from directional GL movements (GetAccountMovementsAsync), not the account's classification, so the sign convention (net = tax on sales
      • tax on purchases) needed no flip — only a comment refresh.
    • OpeningBalanceService unchanged in code — ToNormalSide keys off account.Type, so a positive carried-over balance for 2320 now posts debit-normal. OpeningBalanceServiceTests updated to match.
    • SystemAccounts / AccountNumberAssignmentService / Account doc-comments refreshed to "Tax Receivable"; the SystemAccounts.TaxPaid* C# identifiers are retained for continuity.
    • Tests: ReclassifyInputTaxAsReceivableMigrationTests (pre-migration Liability/"Tax Paid" -> post Asset/"Tax Receivable", number kept); RecoverableInputTaxClassificationTests (net-refundable -> Assets; net-payable -> Liabilities; Trial Balance ties; exactly 3 sections).
    • spec.md (Assumptions [FEATURE] Classify recoverable input tax (account 2320) correctly on the Balance Sheet #355-delivered bullet, FR-033 verification extended, SC-016, Verbatim Constraints), data-model.md (intro, migration entry, new section 13), the sales-tax assessment (Point 3 / Issue B / Summary / filing status marked delivered) and CLAUDE.md updated.

    Classification and presentation only — no stored monetary amount, no TaxCode value, no ledger line moves. Full rebuild 0W/0E; 2057 tests pass (+2 migration/integration test files), AudZeroDriftTests byte-identical.

    capabilities/settings/spec.md still described the retired registration-based tax model (IsGstRegistered, per-fee GstCode, a "GST / BAS" tab, the ATO ABN checksum). Spec 016 replaced that with Settings.IsTaxApplicable / TaxRate / AnnualFeeTaxCode / AttendanceFeeTaxCode, and spec 028 (#354) added TaxEntryMode; the settings living spec had been stale since spec 016 because 028 FR-028 scoped only the finance living spec.

    • Purpose: "GST treatment" -> "sales-tax treatment"; add currency + FY start.
    • First-run validation requirement: drop ABN; list the real setup-time checks, incl. "tax rate > 0 only when sales tax applies", with a scenario.
    • Remove the whole "ABN ... checksum-valid" requirement and its two scenarios (the Abn column was dropped in the GenericSalesTax migration).
    • Independent-tabs requirement + scenarios: "GST/BAS tab" -> "Sales Tax tab".
    • Replace "GST registration controls whether GST codes apply" with "Sales-tax applicability controls whether a rate and tax codes apply", matching SetupService.InitializeAsync and SettingsService.SaveAsync (force rate/codes null + TaxEntryMode=Inclusive when not applicable; reject rate <= 0 when applicable); resolves the stale NEEDS CLARIFICATION.

    Docs-only; no code change.

    Follow-on to the settings living-spec cleanup: the same retired registration-based tax vocabulary (spec 016's GstCode -> TaxCode / IsGstRegistered -> IsTaxApplicable rename, and the "GST Collected/Paid" account names) was still present in five more capability living specs.

    • domain-model: Transaction.GstCode / Fee.GstCode -> .TaxCode; Settings.IsGstRegistered -> IsTaxApplicable; scenario retitled.
    • data-access: seeded-system-account list "GST Collected/Paid" ->
      "Tax Collected, Tax Receivable" (current seed names; 2320 became "Tax Receivable" in the ReclassifyInputTaxAsReceivable migration, [FEATURE] Classify recoverable input tax (account 2320) correctly on the Balance Sheet #355).
    • app-host: setup-wizard requirement "GST treatment" -> "sales-tax treatment (applicability, rate, and per-fee tax codes)"; dropped the ABN-era "and tax details" from organisation identity.
    • rehearsals: attendance accrual "plus GST Collected when the fee is taxable" -> "plus a Tax Collected credit when the fee is taxable and sales tax applies".
    • reports-pipeline: GL-report example "BAS Summary" -> "Tax Summary" (the actual TaxSummaryReportProvider name).
    • finance: same line also fixed a now-false claim — per OpeningBalanceService, only Opening Balance Equity is excluded as an opening-balance entry target; Member Receivable and the tax clearing accounts (2310/2320) are deliberately eligible so carried-over balances can be seeded. "Tax Paid 2320" -> "Tax Receivable 2320".

    Docs-only; no code change.

    • chore(028): seed a complete 2026 — rehearsals to year-end, 65% attendance on last 3

    Advance the debug seeder's reference "today" from 27 Oct 2026 to 31 Dec 2026 so both seeded calendar years are fully settled: every 2026 rehearsal now has attendance recorded and its door cash banked, all annual fees are paid (bar the two deliberate non-payers), and the year-end quarterly bank fee is posted — nothing is left scheduled-but-unrecorded. The AGM is already dated late October and the setup wizard's default currency/language (AUD, Australian English) are unchanged.

    The last three rehearsals of 2026 (early-to-mid December) now model an end-of-year turnout dip: a flat 65% per-member attendance chance instead of each member's usual 85–100% profile rate.

    DEBUG-only fixture code (no test coverage, product behaviour untouched), so no build/test run.

    • chore(028): anchor seed data to the real run date so future dates stay unheld

    The prior commit froze the seeder at 31 Dec 2026, which marked rehearsals that have not happened yet as held — unrealistic. Anchor SeedCurrentDate to DateTime.UtcNow.Date instead: every dated write in the seeder is already gated on "> SeedCurrentDate", so future rehearsals, the spring concert and unpaid future bills now stay scheduled-only whenever the sample data is generated.

    • SeedRehearsalsAsync: the 65% turnout dip now applies to the three most-recently-held 2026 rehearsals (computed from heldCount), not the last three by calendar position; future rehearsals are scheduled with no attendance.
    • SeedAgmAsync: always ScheduleAsync the AGM so a late-October record exists, but only RecordAsync (attendance + committee election) once its date has passed. Today the 2026 AGM is a scheduled, not-yet-held record.
    • Doc-comments and the RNG comment updated to match.

    DEBUG-only fixture code; not compiled or run (per instruction to skip build/test).

    • docs: add Claude Code to the README tools-used list

    • Add script to remove sample data

    • chore(seed): generate organisation name and fee settings in the debug data seeder

    The debug data seeder read Settings.OrganizationName / AnnualFee / AttendanceFee from whatever the setup wizard captured. A coordinator who left the fees at zero got a hollow sample dataset: SeedAnnualFeesAsync was a no-op and the door-cash -> petty-cash -> bank sweep in SeedRehearsalsAsync moved nothing.

    SeedAsync now stamps a generated organisation name ("Clarence Valley Community Choir") and fee schedule -- annual $120, per-rehearsal $2, February membership renewal, October committee renewal, six-seat general committee -- over the wizard inputs before it uses them, mirroring how sample data already supplies its own accounts, opening balances and committee/AGM history (spec 022). Currency, language and sales-tax treatment stay as configured. The ISettingsService.SaveAsync audit entry is suppressed by the enclosing AuditTrailSuppressionScope like every other seeded write.

    Updates the DebugDataSeeder class summary and the app-host living spec's "Optional sample-data seeding" section.

    • fix: point delete-database.cmd at the real MAUI app-data path

    The script deleted nothing: it targeted %AppData% (Roaming) with forward-slash separators, which del misparses as switches. The MAUI app writes stagefright.db to FileSystem.AppDataDirectory, which on the unpackaged Windows head resolves to
    %LOCALAPPDATA%\StageFright Community\com.stagefright.community\Data.

    Rewrite the script to:

    • use %LOCALAPPDATA% with backslash separators and quoted paths
    • remove the SQLite WAL sidecars (stagefright.db-wal / -shm) too
    • also clear a repo-root design_time.db left by "dotnet ef" runs
    • guard every delete with "if exist" and report whether anything went

    Also note the concrete Windows path and the script in docs/SETUP.md's "Reset the Database" section.

    The Settings ▸ General and Setup Wizard display-language pickers listed only "English (Australia)" even though the app now ships en-US and fr-FR satellite resource sets (added on top of spec 027).

    Root cause: MauiProgram registered the catalog as
    AddSingleton<ISupportedLanguagesCatalog, SupportedLanguagesCatalog>(). Microsoft.Extensions.DependencyInjection selects the greediest resolvable constructor, so it picked SupportedLanguagesCatalog(IEnumerable<string> resourceAssemblyNames) — the test-only seam — and resolved the unregistered IEnumerable<string> as an empty sequence (not null). The constructor's ?? DefaultResourceAssemblyNames guard only covered null, so _resourceAssemblyNames became [], ContainsOurSatellite probed for nothing, no <culture>/StageFright.*.resources.dll folder ever matched, and only the hard-coded en-AU baseline survived. It was masked until en-US/fr-FR shipped because en-AU was previously the only set anyway; every test used new SupportedLanguagesCatalog() directly or a fake catalog, so none exercised the DI path.

    • MauiProgram: register with an explicit factory (_ => new SupportedLanguagesCatalog()) so the documented parameterless constructor is used.
    • SupportedLanguagesCatalog: the (IEnumerable<string>) constructor now treats an empty sequence like null and falls back to probing Core/UI/Reports — defence in depth against any DI/empty-list caller.
    • Tests: SupportedLanguagesCatalogTests gains an empty-list fallback case; StartupSequenceTests gains a DI-container regression that resolves the catalog the "natural" AddSingleton<TService,TImpl>() way and asserts en-US and fr-FR are discovered (both fail without the fix, reproducing the "en-AU only" list).
    • Refreshed three now-stale "en-AU is the only shipped set" comments in the picker area (GeneralSettingsTab, LanguagePickerRenderTests, V21_LocalizationStartupTests).

    Verified in the running MAUI app via CDP: the Setup Wizard language select now offers en-AU / en-US / fr-FR. Full rebuild 0W/0E; 2059 tests pass.

    New spec-kit feature on branch 029-first-run-language-seed. Addresses issue #361 (a saved language change is invisible until restart, with no prompt) and folds in a requested extension: move the Debug-only sample-data choice onto the same pre-wizard screen and strip both the language and sample-data steps out of the setup wizard.

    Scope captured in spec.md:

    • P1: a dedicated first-run language screen before the setup wizard; the choice is persisted outside the database and applied by one automatic restart.
    • P2: Settings language change shows a post-save "restart now / later" dialog instead of the transient inline notice.
    • P3 (Debug only): a "load sample data" option on the first-run screen that seeds before restarting and lands o…

    * spec(027): add localization support spec, plan, and tasks
    
    Feature 027 introduces per-language resource files (Australian English
    baseline) so every screen, report, menu, tile, exception message and
    user-facing enum value reads its text via IStringLocalizer, plus a
    persisted, OS-aware Settings.LanguageCode selection.
    
    Adds the full spec-kit artifact set:
    - spec.md - 3 prioritized user stories, FR-001..FR-024, SC-001..SC-010
    - plan.md, research.md - IStringLocalizer + ~12 area .resx markers,
      missing-key logging decorator, MoneyFormatter (fixed AUD), startup
      culture ladder (explicit -> OS language -> en-AU)
    - data-model.md - Settings.LanguageCode column + migration outline
    - contracts/ - localization interfaces + resource-key catalog/guards
    - quickstart.md, checklists/requirements.md
    - tasks.md - 64 tasks (T001-T064) in 6 wave-grouped phases
    
    Bumps .specify/feature.json to specs/027-localization-support.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * feat(027): localization Setup + Foundational infrastructure (T001-T014)
    
    Phase 1 (Setup):
    - Add Microsoft.Extensions.Localization 10.0.10 to Directory.Packages.props
    - Scaffold StageFright.Localization.Tests guard-test project (refs Core/UI/Reports)
    - Add version-less PackageReference + NeutralLanguage=en-AU to Core/Reports/UI csproj
    
    Phase 2 (Foundational):
    - 12 area resource marker classes + empty neutral .resx: NavigationResource,
      ValidationResource, EnumsResource (Core); ReportsResource (Reports);
      Shared/Dashboard/Members/Rehearsals/Events/Finance/Settings/SetupResource (UI)
    - ILocalizer facade + Localizer impl (Get/Get-with-args/Plural/Enum), with
      named-placeholder substitution by order of first appearance (FR-010)
    - MissingKeyLoggingLocalizerFactory: IStringLocalizerFactory decorator that logs
      a Warning and falls back to neutral on a missing key (FR-008/FR-009)
    - EnumLocalizationExtensions.LocalizeEnum: Enum_<Type>_<Member> lookup via a
      factory holder set once at startup (FR-024)
    - MoneyFormatter.Format/FormatWithCode: fixed "$"/"AUD" symbol, culture-driven
      separators/grouping (FR-015)
    - Guard-test scanning infrastructure (LocalizerKeyUsageScanner, ResxKeyScanner,
      PlaceholderTokenScanner, UserFacingEnumScanner) — no assertions yet
    - LocalizedTestContext bUnit base wiring AddLocalization() + LocalizeEnum
    - DI wiring in MauiProgram: AddLocalization(), decorated IStringLocalizerFactory,
      AddScoped<ILocalizer, Localizer>(), EnumLocalizationExtensions.UseFactory at startup
    
    Full solution build: 0 warnings, 0 errors. Full test suite: 1693 existing tests
    pass unchanged. User stories 1-3 and Polish (T015-T064) remain undone.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * chore(027): record after_implement companion hook state (14/64 tasks)
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * feat(027): localize navigation shell + Members module (US1, T015-T028)
    
    Phase 3 / User Story 1 — the extraction pattern proven end-to-end on the
    navigation shell plus the complete Members module. Same en-AU wording, now
    sourced from IStringLocalizer<T> resources.
    
    Resources (T017-T021):
    - NavigationResource / MembersResource / ValidationResource / EnumsResource /
      SharedResource .resx populated with verbatim en-AU entries (labels, headings,
      buttons, placeholders, aria-label/alt/title, plural pair, enum display text).
    
    Call sites (T022-T028):
    - ShellLayout, ThemeProvider (no-op — no user text), MemberMenuItemProvider,
      MemberValidationService, MemberList/MemberDetail/MemberForm (.razor+.razor.cs),
      MembersTile, MembersDashboardTileProvider read text via IStringLocalizer.
    - MemberStatus + Theme render through LocalizeEnum() (FR-024); fee amounts via
      MoneyFormatter.Format (FR-015); parameterised strings via ILocalizer helper
      methods in code-behind.
    - Completed the T011 gap: @using StageFright.Core.Localization in _Imports.razor.
    - DI unchanged — all three localizer-taking registrations resolve from
      AddLocalization() + the decorating factory.
    
    Tests (T015-T016):
    - New StageFright.Localization.Tests US1 guard suite: baseline completeness
      (+plural pairing), MemberStatus/Theme enum-key coverage, residual-literal scan
      over the US1 slice, no-raw-enum-render, no-"C"-currency-format, missing-key
      warning + neutral fallback.
    - New FR-018 bUnit tests assert US1 text resolves via resource keys / LocalizeEnum.
    - 5 existing bUnit classes moved to LocalizedTestContext; it now also registers
      ILocalizer. StubStringLocalizer / RealStringLocalizer test helpers added for
      the constructor-injection sites.
    
    Full build clean; 1706 tests pass (Localization 6, Reports 178, Core 597,
    UI 587, Data 152, Integration 186). US2/US3/Polish not started.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * feat(027): localize Rehearsals module (US2, T032)
    
    Move every user-facing literal in Pages/Rehearsals/** into
    RehearsalsResource.resx (en-AU verbatim) read through IStringLocalizer
    at render time: RehearsalList, RehearsalForm and AttendanceGrid .razor
    + .razor.cs, including aria-label/placeholder text (FR-001). The
    attendance fee now formats through MoneyFormatter, not ToString("C")
    (FR-015). Parameterised strings (search term, dates, member names,
    attendance rate) resolve via ILocalizer.Get in code-behind helpers.
    
    RehearsalFormModel.Date drops its dead DataAnnotations ErrorMessage
    literal (RequiredAttribute never fires on a non-nullable DateTime).
    Three catch blocks stop surfacing raw ex.Message, matching the US1
    MemberForm precedent.
    
    Tests: new Rehearsals-scoped guard suite (baseline completeness,
    residual-literal incl. accessibility attrs, no-"C"-currency) as an
    interim stand-in for the still-pending repo-wide T029/T030; new
    FR-018 render tests; RehearsalListTests + AttendanceGridTests flipped
    to LocalizedTestContext (resx values byte-identical to prior copy).
    
    Full rebuild clean (0 warnings). 1714 tests pass (+8 from the US1
    baseline). 29/64 tasks; US2 continues with T029/T030 + T031/T033-T045.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * chore(027): record after_implement companion hook state (29/64 tasks)
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * feat(027): localize Dashboard page + all dashboard tiles (US2, T031)
    
    Dashboard page chrome, the six remaining dashboard tile bodies (Events,
    Rehearsals, AttendanceTrend, Finance, CashFlow, OutstandingBalances) and
    their tile providers now read every user-facing string from
    DashboardResource / EventsResource / FinanceResource / RehearsalsResource
    / SharedResource. Chart dataset legend labels localized; tile fee/balance
    amounts moved from .ToString("C") to MoneyFormatter (FR-015). Tile
    providers ctor-inject IStringLocalizer<T> (matching MembersDashboardTile
    provider). bUnit tile/dashboard test classes flipped to
    LocalizedTestContext; provider-test + V8 integration ctors updated.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * feat(027): localize remaining menu providers + author enum resource keys (US2, T038 + T042 groundwork)
    
    All six navigation menu providers (Dashboard, Rehearsals, Events, Finance,
    Settings, Reports) ctor-inject IStringLocalizer<NavigationResource> and
    source Title/ShortLabel/sub-item labels from NavigationResource.resx
    (matching MemberMenuItemProvider). EnumsResource.resx gains
    Enum_<Type>_<Member> keys for FeeType, PaymentMethod, PaymentType,
    AccountType, TaxCode, ReconciliationStatus and JournalEntryType (call-site
    LocalizeEnum swaps land with each module + T042). Core.Tests gains a
    RealStringLocalizer fixture; the two affected menu-provider test ctors updated.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * feat(027): localize shared components (US2, T037)
    
    AddAccountForm, BorderedListBox, OpeningBalanceEntryForm,
    ReactivationForgivenessDialog, ReportViewer and TileRenderer now read all
    user-facing text (labels, headings, buttons, placeholders, aria-labels,
    status/error messages, plural button labels) from SharedResource.resx.
    AccountType options and account-type column render via LocalizeEnum;
    forgiveness fee amounts and report money via MoneyFormatter; parameterised
    strings resolved in code-behind helpers. SubmitButtonText / EmptyText
    params made nullable with a localized fallback. 16 bUnit test classes that
    render these components flipped to LocalizedTestContext. Full suite green
    (1714).
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * feat(027): localize Events / AGM module (US2, T033)
    
    All 10 Events/AGM screens (EventList, EventForm, EventDetail,
    ParticipationGrid, AgmList, AgmDetail, ScheduleAgm, RecordAgm,
    RecordSpecialElection, AgmAttendanceGrid) plus AgmAttendanceReportPrinter
    read every user-facing string, aria-label, badge, status text, validation
    and print message from EventsResource.resx / SharedResource. Parameterised
    strings via ILocalizer code-behind helpers; EventFormModel data-annotation
    messages dropped to bare [Required] (dead on non-nullable value types,
    matching US1). 8 Events bUnit test classes flipped to LocalizedTestContext.
    Full suite green.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * feat(027): localize Settings page + tabs (US2, T035)
    
    SettingsPage tab titles/error-boundary text and the General, Sales Tax,
    Committee, Event Types and Backup & Restore tabs read all user-facing
    strings, labels, help text, badges, aria-labels, status/error/success
    messages and validation from SettingsResource.resx / SharedResource. Theme
    label via LocalizeEnum; TaxCode options via LocalizeEnum (value stays the
    invariant token); audit-retention option pluralised via ILocalizer.Plural;
    fee input adornments fixed to "$" per FR-015 (was culture CurrencySymbol).
    Language picker row deliberately left for US3/T056. 5 Settings bUnit test
    classes flipped to LocalizedTestContext. Full suite green.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * feat(027): localize Setup Wizard (US2, T036)
    
    SetupWizard shell + all 9 tab components (GeneralAppearance, MembershipFees,
    ThemeSelection, SalesTax, SampleData, ChartOfAccounts, OpeningBalances,
    Committee, Review) read every user-facing string, label, help text,
    placeholder, aria-label, badge and validation/error message from
    SetupResource.resx. Review money via MoneyFormatter; Theme + TaxCode via
    LocalizeEnum (wizard's Light/Dark <select> keeps invariant option values
    per spec 017 FR-022); audit-retention & review lines pluralised via
    ILocalizer.Plural; fee adornments fixed to "$". Language step deliberately
    left for US3/T057. 5 Setup tab bUnit test classes flipped to
    LocalizedTestContext. Full suite green.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * feat(027): localize StartupError page + Reports page shell (US2, T043)
    
    StartupError.razor (database recovery page) and ReportsPage.razor read
    their headings, body text, buttons, prompts and error messages from
    SharedResource.resx. MainPage.xaml / App.xaml carry no user-facing text.
    Remaining Pages/** (Finance) is T034.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * feat(027): localize Finance module (US2, T034)
    
    Move every user-facing literal in the 13 Finance pages + code-behinds
    into FinanceResource.resx (en-AU baseline). Fee/payment/account/tax/
    reconciliation-status enums render via LocalizeEnum; every displayed
    amount goes through MoneyFormatter (FR-015) instead of ToString("C") /
    FormatString="{0:C}". aria-labels, placeholders and the embedded-link
    "no accounts configured" warnings are localized too (FR-001).
    
    Nine Finance bUnit test classes flipped to LocalizedTestContext so
    IStringLocalizer<FinanceResource> resolves; neutral values are
    byte-identical to the prior hardcoded copy so assertions stay green.
    
    Full solution build clean; full suite 1714 passing.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * feat(027): localize AgeCalculationService DOB/age messages (US2, T041 groundwork)
    
    The US1-deferred piece of T041: AgeCalculationService.ValidateDateOfBirth
    now sources its three user-facing messages from ValidationResource via the
    ILocalizer facade (named {Age}/{MaxYears}/{MinimumAge} placeholders), so the
    service is DI-constructed with ILocalizer.
    
    Call-site fan-out handled: MemberList/MemberDetail switch from `new()` to an
    injected AgeCalculationService (registered in LocalizedTestContext for bUnit);
    Core/Integration/Reports test projects each gain a RealLocalizer fixture and
    pass it to the ~12 `new AgeCalculationService(...)` sites.
    
    The remaining T041 work — service-thrown ValidationException/DomainValidation-
    Exception message text across ~18 Core services — is not in this commit.
    
    Full solution build clean; full suite 1714 passing.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * chore(027): record companion context after T034 + T041 groundwork
    
    * feat(027): localize the 11 report providers (US2, T039 + T030)
    
    Route every report provider's user-facing text through the ILocalizer
    facade against a new populated ReportsResource.resx (~120 en-AU entries,
    transcribed verbatim per FR-004): report names, filter labels,
    titles/subtitles (named-placeholder formatted), section headings, column
    headers, subtotal/total + inline row labels, and the GL-imbalance
    exception message. Enum-valued cells (MemberStatus, ReconciliationStatus)
    resolve via ILocalizer.Enum (FR-024).
    
    Filter option VALUES stay culture-invariant tokens (Options, unchanged —
    still compared in GenerateAsync and persisted); the localised option
    LABEL is carried on a new parallel ReportFilterDefinition.OptionLabels
    list, rendered by ReportViewer with fallback to the token.
    
    Report amounts keep decimal.ToString("F2") — providers never used "C",
    and adding a $ symbol/grouping would break SC-002 render parity. Report
    renderers (T040) and service exception text (T041) remain.
    
    T030: ReportsResourceLabelTests (names/headers/section+total labels
    resolve via ReportsResource, no raw-key/blank leak — FR-006) and
    PluginTextNonEnglishCultureTests (TestPlugin English report + tile text
    renders unchanged under fr-FR, run on a dedicated thread so the culture
    never leaks into parallel tests — FR-020).
    
    Build 0 warnings / 0 errors; full suite 1733 pass / 0 fail
    (Reports 178->195, Integration 186->188).
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * chore(027): record after_implement companion context (39/64 tasks)
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * feat(027): localize report PDF renderer chrome (US2, T040) + enum/currency sweeps (T042, T044)
    
    T040: all 6 PDF report renderers now source their page chrome from
    ReportsResource.resx (~21 en-AU keys, verbatim per FR-004):
    - PdfReportRenderer: "Generated: {DateTime} UTC", footer "Page"/"of"
    - AttendanceRollPdfRenderer: title, "Rehearsal: {Date} at {Time}", Name/Present/Pd headers
    - AgmResultsPdfRenderer: title, meeting/attendance lines, "Elected Positions",
      "No positions recorded.", bold "{Label}: " position prefix, "General Committee Member:"
    - CheckboxSheetPdfBuilder: shared "Name" column + footer; gains an ILocalizer param
      and a nameColumnHeader arg threaded to BuildMemberTable
    - Event/AgmAttendanceSheetPdfRenderer: titles, date lines, Participated/Attended headers
    The 5 concrete renderers ctor-inject ILocalizer (resolves from the existing
    AddScoped<ILocalizer, Localizer>() - no MauiProgram change). CsvReportExporter has
    no user-facing literals and is untouched. 8 direct test-construction sites now pass
    RealLocalizer.Instance; ReportsResourceLabelTests gains 22 chrome-key + placeholder
    assertions (FR-006).
    
    T042: verified the user-facing enum sweep is complete - EnumsResource.resx holds
    every Enum_<Type>_<Member> key for all 9 user-facing enums. Fixed the one residual
    raw display site (the item deferred from US1): MemberDetail Fee Payment History
    FeeType column now uses fee.FeeType.LocalizeEnum().
    
    T044: verified zero decimal.ToString("C")/{0:C}/FormatString display sites remain in
    UI or Reports. The one user-facing residual - BankReconciliationService.cs:153
    ReconciliationException message - is exception-message text owned by T041.
    
    Full solution build: 0 warnings / 0 errors. Full test suite: 1755 passed / 0 failed
    (Reports 217, up from 195).
    
    US2 remaining: T041 (user-facing exception Message text across ~18 Core services -
    high blast radius, own session), T045 (DI recheck after T041's ctor changes),
    T029 (repo-wide guard suite - blocked until T041).
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * feat(027): localize non-Finance service exception messages (US2, T041 part 1)
    
    Route user-facing ValidationException / DataIntegrityException / ImportException
    Message text through ValidationResource for the eight non-Finance Core services:
    AgmService, EventService, EventTypeService, CommitteeOfficeHolderTypeService,
    AttendanceService, SettingsService, SetupService, BackupService.
    
    - 34 new en-AU keys in ValidationResource.resx (verbatim wording, FR-004);
      named-placeholder keys for {Year}/{SchemaVersion}/{SupportedMajorVersion}/{EntityType}.
    - Each service takes the ILocalizer facade via a required ctor param (matches the
      AgeCalculationService T041-groundwork precedent; only path that substitutes named
      tokens). All eight are plain AddScoped<I,Impl> registrations so DI auto-resolves
      the new param -- no MauiProgram change.
    - BackupService DeserializeAndValidate / ValidateVersion / ValidateCompleteness
      change from private static to instance to read _localizer.
    - EntityNotFoundException built-in message and DataAccessException wrappers stay
      English (diagnostic per FR-007 -- UI never shows them verbatim).
    - 20 test call-sites updated to pass RealLocalizer.Instance; new RealLocalizer
      fixture added to StageFright.Data.Tests.
    - Interim guard Us2ExceptionMessageGuardTests (baseline completeness + no residual
      literal) scoped to the eight files.
    
    Full build 0 warnings / 0 errors. Full suite 1757 pass.
    
    Remaining for US2: T041 Finance services (incl. BankReconciliationService {Difference:C}),
    T045 DI recheck, T029 repo-wide guard suite.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * feat(027): finish US2 — localize Finance exception messages + repo-wide guard (T041, T029, T045)
    
    T041 (Finance half): route user-facing ValidationException / ReconciliationException
    Message text through ValidationResource for all 9 Finance services — AccountService,
    BankReconciliationService, ExpensePaymentService, PaymentService, FeeService,
    GeneralJournalService, IncomeEntryService, BankDepositService, OpeningBalanceService.
    
    - 39 new verbatim en-AU ValidationResource keys (FR-004) + reuse of
      Validation_Settings_NotConfigured; named-token keys for {Name} (Account duplicate),
      {Date} (statement date), {Difference}/{Zero} (reconciliation finalise).
    - BankReconciliationService finalise message: the {workspace.Difference:C} + literal
      "$0.00" (the T044-flagged C-format residual) now routes both amounts through
      MoneyFormatter.Format — byte-identical under en-AU, fixed-AUD under any culture (FR-015).
    - Each service takes the ILocalizer facade via a required ctor param (matches the
      non-Finance T041 precedent). All 9 are plain AddScoped<I,Impl> so DI auto-resolves
      the new param — no MauiProgram change (T045).
    - ~30 test ctor sites pass RealLocalizer.Instance (Core.Tests, Data.Tests,
      Integration.Tests). EntityNotFoundException / GLBalanceException wrappers stay
      English (diagnostic per FR-007).
    - Us2ExceptionMessageGuardTests extended to cover the Finance services.
    
    T029: new repo-wide Us2LocalizationGuardTests (8 facts) — baseline key completeness
    across the US1+US2 surface, Enum_<Type>_<Member> coverage for all 9 user-facing enums
    + no-raw-enum-render, residual-literal scan over the UI display surface, orphan-satellite
    keys, plural-pair + placeholder parity, cross-culture token parity, and no-"C"-currency
    repo-wide. Report-provider filter option-VALUE tokens (guarded by ReportsResourceLabelTests,
    T030) and DataAnnotations ErrorMessage / ValidationResult literals (compile-time constant,
    resolved in T060) are documented exclusions.
    
    resource-key-catalog.md §3 updated to reflect the implemented guard layout and exclusions.
    
    Full rebuild 0 warnings / 0 errors. Full suite 1765 pass (Localization 19, Reports 217,
    Core 597, UI 592, Data 152, Integration 188). US2 (Phase 4) complete — 45/64 tasks.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * feat(027): US3 — persisted, OS-aware display language + pickers (T046-T059)
    
    Adds the language-selection slice on top of the US1/US2 extraction:
    
    - Settings.LanguageCode (nullable, presentation-only) + hand-written
      EF migration AddLanguageCodeToSettings (dotnet ef can't build the MAUI
      startup project here); SchemaVersion left at 1.1.0 (unchanged for the
      last ~6 settings migrations; asserted verbatim by backup tests).
    - ILanguageProvider FR-023 startup ladder (explicit choice -> OS display
      language by exact then parent-language match -> en-AU), wired in
      MauiProgram to set the process culture before the first Blazor render;
      ISystemCultureProvider seam (SystemCultureProvider in the App layer).
    - SupportedLanguagesCatalog: runtime discovery of shipped resource
      cultures by scanning satellite folders for the StageFright assemblies;
      qps-* pseudo-locales excluded; endonyms from CultureInfo.NativeName.
    - Settings General tab + Setup Wizard language pickers (endonym <select>,
      active-marked, inline restart notice at the point of change - FR-021);
      choice flows through SetupRequest -> Settings at first-run Finish.
    - CultureProvider cascading seam wrapping ShellLayout (parallel to
      ThemeProvider; no v1 behaviour change) - named CultureProvider to
      avoid colliding with the Core LanguageProvider service.
    - qps-ploc test pseudo-locale: scripts/generate-pseudo-locale.py + 12
      regenerable <Marker>.qps-ploc.resx fixtures (bracketed/accented,
      {tokens} preserved, 3 keys omitted to exercise per-key fallback).
    - Tests (+40): LanguageProvider ladder, catalog runtime discovery +
      qps-* exclusion, migration round-trip, V21 startup/SC-006 no-data-
      change, qps-ploc e2e, language-picker bUnit. LocalizedTestContext
      registers the new language services.
    
    Full rebuild: 0 warnings / 0 errors. Full suite: 1805 pass, 0 fail
    (Localization 23, Core 620, Reports 217, UI 599, Data 154, Integration 192).
    Polish (T060-T064) remains.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * feat(027): Polish — repo-wide literal guard + localize App.razor + docs (T060-T064)
    
    T060: fold the interim per-slice residual-literal scans into one repo-wide guard
    (Us2LocalizationGuardTests.Should_HaveNoUserFacingLiteral_When_AppSurfaceScanned)
    covering all of StageFright.UI + every user-facing exception Message literal in
    StageFright.Core. The widened scan flagged App.razor (project root, outside the old
    4-dir scope): localized its error-boundary + not-found chrome to 6 new Shared_App_*
    keys (neutral + qps-ploc). DataAnnotations ErrorMessage args are frozen as the one
    documented carve-out.
    
    T061: docs/localization/adding-a-language.md — translator/maintainer guide (FR-022, SC-009).
    T062: CLAUDE.md gains a Localization section + module-list entry + money-column data-grid
    note; resource-key-catalog.md updated to past tense; spec 007 account-balance contract
    gets a forward-note (its {0:C} columns moved to MoneyFormatter).
    T063/T064: full rebuild 0 warn / 0 err; full suite 1805 pass (Localization 23).
    
    Spec 027 status: completed (64/64 tasks).
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * feat(027): add American English (en-US) resource set
    
    Drop-in en-US satellite for all 12 area .resx files, translated from the
    en-AU neutral baseline. The runtime SupportedLanguagesCatalog discovers it
    automatically — no code change — so it now appears in Settings ▸ General
    and the Setup Wizard language step.
    
    Divergence from en-AU is spelling + vocabulary only (the two dialects are
    otherwise identical across this corpus):
    - organisation → organization  (Settings/Setup labels, Tax Summary subtitle)
    - finalise / finalised → finalize / finalized  (Finance button, reconciliation
      validation messages, ReconciliationStatus enum label)
    - cheque → check  (PaymentMethod enum label)
    - cancelled → canceled  (report-generation message)
    - "financial year" → "fiscal year"  (Settings + opening-balances wizard copy)
    
    Keys, {named} placeholders and <comment> docs are byte-identical to neutral,
    so the Us2 orphan-key / placeholder-parity guards pass.
    
    Also make three previously culture-fragile bUnit assertions resolve their
    expected text through the same IStringLocalizer the component uses, so they
    hold under a US UI culture (GitHub CI runs windows-latest = en-US):
    - ReconciliationWorkspaceTests: assert ReconciliationStatus.Finalised.LocalizeEnum()
    - SetupWizardTests: assert the localized "Organisation Settings" tab title
    - refresh the now-stale "v1 ships only en-AU" note in SupportedLanguagesCatalog
    
    Full suite green under both the default culture and -culture en-US
    (UI 599, Core 620, Data 154, Reports 217, Integration 192, Localization 23).
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * feat(027): add French (fr-FR) resource set
    
    Full French translation of all 12 area .resx files, dropped in beside the
    en-AU neutral baseline. SupportedLanguagesCatalog discovers fr-FR at runtime —
    no code change — so "français (France)" now appears in Settings ▸ General and
    the Setup Wizard language step, and an fr / fr-FR operating system resolves to
    it on first run. Selecting it renders the whole app in French with French
    number/date formatting; the currency stays "$" / "AUD" (MoneyFormatter).
    
    - ~1,000 <value> strings translated; keys, {named} placeholders, _One/_Other
      plural pairs and <comment> docs are byte-identical to neutral, so the Us2
      orphan-key / placeholder-parity / plural-pairing guards pass.
    - AGM → "AGA" (assemblée générale annuelle); accounting terms use standard
      French (plan comptable, grand livre, bilan, compte de résultat, rapprochement
      bancaire, produits/charges, capitaux propres, écriture de journal…).
    - Embedded-HTML values keep their <a href> markup; significant leading/trailing
      spaces preserved.
    
    No test changes needed: the only test that deliberately runs under fr-FR
    (PluginTextNonEnglishCultureTests) still passes, and every other test runs
    under the machine culture (en-GB locally / en-US on CI) where the fr-FR
    satellite is not loaded.
    
    Full suite green under both the default culture and -culture en-US
    (Core 620, Data 154, Reports 217, UI 599, Integration 192, Localization 23).
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * spec(028): add international accounting-practice readiness spec, plan, and tasks
    
    Feature 028 readies the finance module for community/amateur-theatre
    groups outside Australia, judged against universal double-entry and
    bookkeeping good practice (not formal IFRS/GAAP). Sourced from GitHub
    issue #341 and sub-issues #342-#352.
    
    Adds the full spec-kit artifact set:
    - spec.md - 10 prioritized user stories (currency, regional number
      entry, statement consistency, basis-of-accounting disclosure, bank
      reconciliation, prior-year period locks, financial-year start,
      audit-history retention, written policies, sales-tax i18n spike),
      FR-001..FR-033, SC-001..SC-013
    - plan.md, research.md - configurable org currency replacing hard-coded
      AUD, culture-safe money parsing, period-lock service, report
      integrity checks + disclosures
    - data-model.md - currency/period-lock/policy fields + migration outline
    - contracts/ - currency-formatting, period-lock, reports, and
      settings-and-setup contracts
    - checklists/requirements.md
    - tasks.md - 94 tasks (T001-T094) across 13 phases
    
    Bumps .specify/feature.json to specs/028-international-accounting-standards.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * feat(028): foundational currency primitives + Settings schema (T001-T008)
    
    Phase 1 baseline confirmed green (1805 tests). Phase 2 lays the shared
    foundation every user-story phase builds on — no user-visible behaviour
    change yet; an AUD dataset formats byte-identically.
    
    - SupportedCurrency record + CurrencyCatalog (curated ISO 4217 seed set:
      AUD/USD/EUR/GBP/NZD/CAD 2-digit, JPY 0-digit, KWD/BHD 3-digit) with
      case-insensitive TryGet (false + Default, no throw) and Get (throws
      ValidationException on an unknown code). Mirrors SupportedLanguagesCatalog.
    - MoneyFormatter gains Configure(SupportedCurrency); Format/FormatWithCode
      now use the configured symbol/code and MinorUnitDigits while grouping and
      placement still follow CultureInfo.CurrentCulture. Falls back to
      CurrencyCatalog.Default (AUD/$/2) before Configure is called.
    - MauiProgram configures MoneyFormatter for Settings.CurrencyCode right
      after the display culture is applied; failure leaves it on AUD default.
    - Settings entity + EF config: add CurrencyCode ("AUD"),
      FinancialYearStartDay (1), ClosedThroughDate (null); AuditRetentionYears
      default 1 -> 5 (range 1-7 unchanged).
    - Migration AddInternationalAccountingSettings: adds the three columns
      (NOT NULL DEFAULT backfills the existing row), alters only the
      AuditRetentionYears column default (no UpdateData, so a configured value
      is preserved).
    
    Build 0W/0E; all 1805 tests green.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * feat(028): US1 configurable currency — MoneyFormatter drives all money display (T009-T028)
    
    Phase 3 / User Story 1 (spec 028). Every screen, report, PDF and CSV amount now
    renders in the organisation's configured currency (Settings.CurrencyCode, ISO 4217,
    default AUD) with the right symbol and minor-unit precision; grouping/placement still
    follow the active culture. An AUD dataset is byte-identical to the pre-028 output.
    
    - TaxCalculator.SplitInclusive: optional minorUnitDigits (default 2); all callers pass
      CurrencyCatalog.Get(settings.CurrencyCode).MinorUnitDigits.
    - First-run setup: mandatory #setup-currency picker on GeneralAppearanceTab bound to
      SetupFormModel.CurrencyCode; SetupService validates it against CurrencyCatalog
      (Validation_Setup_CurrencyUnknown) and persists the normalised code.
    - SettingsService.SaveAsync rejects a post-setup currency change
      (Validation_Settings_CurrencyImmutable) — currency is fixed after setup.
    - All StageFright.Reports providers + JournalEntryPage totals route money through
      MoneyFormatter.Format instead of ToString("F2")/"N2".
    - New resource keys (neutral + en-US + fr-FR): Validation_Setup_CurrencyUnknown,
      Validation_Settings_CurrencyImmutable, Setup_General_CurrencyLabel/Help.
    - Tests: CurrencyCatalogTests, MoneyFormatterTests, TaxCalculator 0/2/3-digit theory,
      Setup/Settings currency cases, CurrencyPickerTests (bUnit), AudZeroDriftTests +
      V28_CurrencyConfigurationTests (integration acceptance). MoneyFormatterState
      collection serialises MoneyFormatter.Configure-sensitive integration tests.
    - Updated ~30 pre-existing report/UI/integration assertions to MoneyFormatter.Format(...).
    - Docs: CLAUDE.md Localization note + spec 027 superseding notes for the fixed-$/AUD prose.
    
    Full rebuild: 0W/0E. Tests: 1868 pass (was 1805).
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * feat(028): US2 locale-safe money entry — MoneyInput.Parse for numeric inputs (T029-T035)
    
    Phase 4 / User Story 2 (spec 028). The manual journal and opening-balance forms
    hand-rolled decimal.TryParse(value, NumberStyles.Number, CultureInfo.CurrentCulture)
    on the value of an <input type="number"> — which the browser always serialises
    invariant ("1.50"). Under fr-FR / de-DE the period was read as a thousands
    separator and 1.50 posted to the ledger as 150 (FR-007…FR-009 / live data bug).
    
    - New MoneyInput.Parse(string?): invariant decimal.TryParse with
      NumberStyles.AllowDecimalPoint | AllowLeadingSign; 0m for null/blank/unparseable.
      The single shared money-entry parse helper.
    - JournalEntryPage.ParseAmount and OpeningBalanceEntryForm.SetAmount now delegate
      to MoneyInput.Parse; removed the CurrentCulture parse and the now-unused
      System.Globalization using from both.
    - Tests: MoneyInputTests (unit, fr-FR/de-DE/en-AU), JournalEntryPageLocaleTests +
      OpeningBalanceEntryFormLocaleTests (bUnit — entered amount stored exactly),
      MoneyInputGuardTests (repo-wide: no UI money field parses with CurrentCulture;
      both handlers route through MoneyInput.Parse), V28_LocaleSafeMoneyEntryTests
      (integration acceptance — real journal/opening-balance services over SQLite,
      fr-FR/de-DE input stored exact to the cent and identical to en-AU).
    
    Build: 0W/0E. Tests: 1895 pass (was 1868, +27).
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * chore(028): sync companion after_implement context (Phase 4 / US2)
    
    after_implement task-sync: 35/94 tasks complete, status stays implementing.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * chore(028): record plug-display concern for Polish T091a
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * feat(028): US3 statement integrity — Balance Sheet / Trial Balance guards (T036-T040)
    
    TrialBalanceReportProvider drops the 0.01 tolerance band: the imbalance
    check is now `totalDebits != totalCredits`, so any non-zero difference
    (including one cent) refuses to generate — no tolerance band (FR-011).
    
    BalanceSheetReportProvider appends an explicit emphasized out-of-balance
    line (new `Reports_BalanceSheet_OutOfBalance` key + `MoneyFormatter.Format`
    of the difference) whenever total assets != total liabilities + equity, so
    a corrupted ledger never yields a clean statement (FR-010).
    
    Reworded `Reports_TrialBalance_GLImbalanceError` across neutral/en-US/fr-FR
    to drop the tolerance implication and the doubled literal `$` (the
    `{Debits}`/`{Credits}` args carry the configured currency symbol since
    T024); dropped the stale FR-034 doc-comment reference.
    
    Tests: 5 new Reports provider tests + V28_StatementIntegrityTests (5
    acceptance cases). Full suite 1903 pass, build 0W/0E.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * chore(028): sync companion after_implement context (Phase 5 / US3)
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * feat(028): US4 basis-of-accounting disclosure on financial statements (T041-T047)
    
    ReportData gains an optional BasisOfAccounting string. PdfReportRenderer renders
    it as a grey line below the "Generated:" line; CsvReportExporter appends it as a
    trailing self-labelled note record after the grand total; ReportViewer shows it
    in a <p> directly beneath the subtitle — each only when non-null.
    
    New shared Reports_Common_BasisOfAccounting (neutral / en-US / fr-FR) with hybrid
    wording that names both the accrual treatment of member fees and the cash
    treatment of all other income and expenditure — never a single blanket basis
    (FR-012). All eight financial-statement providers (Income Statement, Balance
    Sheet, Trial Balance, Tax Summary — both return paths, Account Register, General
    Ledger, Bank Reconciliation, Member Account Summary) set it; Member List and
    Committee leave it null.
    
    Tests: BasisOfAccountingTests (Reports), ReportViewerBasisTests (UI/bUnit),
    V28_BasisOfAccountingDisclosureTests (integration acceptance, US4 AC-1..AC-2).
    reports-contract.md CSV rendering row synced to the shipped single-column note.
    
    Build 0W/0E; touched-project suites green (Reports 235, UI 613, Integration 215,
    Localization 25).
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * feat(028): US5 conventional bank reconciliation (T048-T051)
    
    Rewrite BankReconciliationReportProvider to the standard adjusted-balance
    layout — one section per bank account: balance per bank statement, add
    outstanding deposits (listed + summed), less outstanding payments
    (listed + summed), adjusted bank balance, balance per general ledger
    (GetAccountBalanceAsync at the statement date), and a Reconciled residual
    row that is zero on a tied statement (FR-013…FR-015, SC-008). All amounts
    route through MoneyFormatter.
    
    - ReportsResource neutral/en-US/fr-FR/qps-ploc: add BalancePerBankStatement,
      AddOutstandingDeposits, LessOutstandingPayments, AdjustedBankBalance,
      BalancePerGeneralLedger, Reconciled; drop the 10 now-dead cleared/summary
      keys (provider was their only consumer).
    - BankReconciliationServiceTests: +2 FR-015 tests (finalise rejected over
      the half-cent tolerance with no state change; finalised reconciliation
      rejects toggle-clear and re-finalise).
    - V28_ConventionalBankReconciliationTests: new integration acceptance
      (real SQLite + migrations) for US5 AC-1…AC-3.
    - Sync contracts/reports-contract.md to the shipped layout.
    
    Full suite green: Core 684, Reports 236, Localization 25, UI 613,
    Data 154, Integration 220. Build 0W/0E.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * chore(028): sync companion after_implement context (Phase 7 / US5)
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * feat(028): US6 closed-period lock (T052-T064)
    
    Reject any financial transaction dated on or before Settings.ClosedThroughDate
    so a reported prior period cannot be altered by a back-dated entry (FR-016/017),
    while first-run setup opening balances stay permitted (FR-018).
    
    - ClosedPeriodException (sealed, §5.2 shape) + IClosedPeriodGuard / ClosedPeriodGuard
      (reads the Settings singleton; no-op pre-setup or while nothing is closed).
    - GLRepository consults the guard on the earliest line date before the write, at
      the single GL choke point; AddPairAsync is covered by delegation. UnitOfWork
      passes ClosedPeriodException through unwrapped like GLBalanceException, so a
      rejection rolls the whole operation back — no business row, no ledger line.
    - General settings tab gains a "close periods through <date>" control
      (#settings-close-through-date) + a confirmation checkbox; the closed-through
      date only advances when both are set. SettingsRepository.SaveAsync now maps
      ClosedThroughDate on the detached-update path.
    - All 7 finance posting forms catch ClosedPeriodException and show
      Validation_ClosedPeriod_PostingRejected, staying re-submittable.
    - Tests: ClosedPeriodGuard unit (boundary/null cases), ClosedPeriodLock
      integration sweep (journal/expense/income/deposit/payment/fee-accrual/
      forgiveness all reject in-period and post after), SetupService FR-018,
      ClosePeriodControl bUnit. 57 GLRepository test construction sites updated
      for the new required guard dependency.
    
    Build 0W/0E; suite green (Core 691, Integration 235, Data 154, UI 616,
    Reports 236, Localization 25) — one pre-existing random-GUID "fee" substring
    flake in EventListTests, passes on re-run.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * chore(028): sync companion after_implement context (Phase 8 / US6)
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * feat(028): US7 financial-year start as a setup decision (T065-T075)
    
    FinancialYearCalculator.GetRange/GetPreviousRange gain an optional
    startDay (default 1, preserving every existing caller); the year now
    pivots on the (month, day) anchor. The four FY-preset report providers
    (TrialBalance, BalanceSheet, IncomeStatement, TaxSummary) and
    OpeningBalancesWizard pass settings.FinancialYearStartDay through.
    
    SetupRequest / SetupFormModel / SetupService carry FinancialYearStartMonth
    + FinancialYearStartDay; SetupService validates the day to 1..28
    (Validation_Setup_FinancialYearStartDayRange) and persists both. The
    setup General tab gains mandatory month + day pickers
    (#setup-fy-start-month / #setup-fy-start-day) and the Settings General
    tab gains a matching day picker beside the existing month one.
    
    Tests: FinancialYearCalculatorTests (non-first-of-month / February
    anchors, UTC end-of-day, default-to-1 equivalence), FinancialYearStartTests
    (integration — setup persists a non-first-of-month start; every FY-preset
    report honours the anchor; AU July-1 dataset unchanged),
    FinancialYearStartPickerTests (bUnit), and SetupServiceTests additions.
    Build 0W/0E; 1986 tests pass.
    
    T076 (FR-022 sub-twelve-month first-year follow-on issue) is recorded in
    spec.md Assumptions; the GitHub issue itself still needs to be created.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * chore(028): sync companion after_implement context (Phase 9 / US7)
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * chore(028): companion after_implement task-sync (75/94)
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * feat(028): US8 audit retention, coverage & purge-failure surfacing (T077-T083)
    
    Phase 10 of spec 028 (International accounting-practice readiness), User Story 8:
    
    - T080: raise the audit-retention default 1 -> 5 at the remaining declaration
      sites (SetupFormModel, SetupRequest); SetupService flows it through unchanged.
      Entity/config/migration defaults were already 5 (T003/T005/T006).
    - T081: AttendanceService.RecordBatchAsync now writes an AuditTrailEntry for the
      attendance-fee accrual and, when paid at creation, the auto-payment - inside
      the existing UnitOfWork transaction (FR-026).
    - T082: a failed startup audit purge is no longer swallowed.
      AuditTrailService.PurgeOlderThanAsync propagates; IStartupDiagnosticService
      gains a non-fatal warning channel (HasStartupWarning / StartupWarning /
      RecordWarning); MauiProgram's purge catch logs AND records the warning, and
      the retention fallback follows the new 5-year default. Startup still continues
      and the user is not routed to the blocking recovery page (FR-025).
    - T083: a dismissible Dashboard banner surfaces the warning
      (Shared_StartupWarning_* in neutral / en-US / fr-FR).
    
    Tests: T077 migration default (fresh -> 5, existing value preserved),
    T078 AttendanceService audit coverage, T079 purge-failure propagation +
    diagnostic surfacing, plus 3 Dashboard banner bUnit tests. Reconciled the two
    SetupService default-flow tests and AuditTrailServiceTests to the new behaviour.
    data-model.md section 10 updated. Full build 0W/0E; 1999 tests pass.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * docs(028): US9 accounting policies + living-spec currency (T084-T086a)
    
    Phase 11 / User Story 9 — "Have the accounting policies written down":
    
    - T084: new docs/accounting-policies.md — basis of accounting (hybrid
      accrual/cash), revenue recognition, rounding to the currency minor unit,
      single-currency policy, record immutability + reversing-entry corrections,
      period locking, financial year, audit-trail retention; states the reports
      are unaudited management accounts (FR-027, SC-012).
    - T085: capabilities/finance/spec.md de-drafted; every retired
      registration-based tax reference (IsGstRegistered, per-fee GstCode,
      gross ÷ 11, "GST Collected", "GST clearing accounts", "net of GST")
      rewritten to the spec-016 model — Settings.IsTaxApplicable / TaxRate /
      TaxCode (Taxable, TaxExempt, Excluded), Tax Collected account 2310
      (FR-028).
    - T086: capabilities/audit-trail/spec.md retention figure corrected from a
      fixed 12 months to a configurable 1-7 years defaulting to 5 on new
      datasets, existing values preserved (stale after T003's default change).
    - T086a: spec.md Assumptions notes the pre-existing stale
      capabilities/settings/spec.md ABN/GST wording as a carried-forward
      follow-up, outside FR-028's finance-only scope.
    
    Markdown-only phase — no code change; full rebuild + Localization.Tests
    run in Phase 13 (T090/T091). Deferred T076 and Phases 12-13 remain.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * chore(028): companion after_implement task-sync (85/94)
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * docs(028): US10 sales-tax internationalisation assessment spike (T087, T089)
    
    Phase 12 / User Story 10 (P3, spike) — FR-029, FR-030, FR-033.
    
    - T087: new docs/assessments/sales-tax-internationalisation.md — for each of the
      four required points, an in-scope/out-of-scope decision with a rough size:
      rate history/effective-dating OUT (L); tax-exclusive entry IN (M);
      balance-sheet classification of recoverable input tax (accounts 2310/2320) IN
      (S–M); multiple simultaneous rates/jurisdictions OUT (XL). The two in-scope
      points carry full ready-to-file follow-on issue specs.
    - T089: FR-033 verification — git diff master...HEAD over every tax-adjacent file
      confirms the only tax-path change on this branch is the optional minorUnitDigits
      rounding param on TaxCalculator.SplitInclusive (default 2 → AUD byte-identical).
      TaxCode enum, 2310/2320 accounts, GL line structure, tax-inclusive entry model
      and Tax Summary net arithmetic are untouched; AudZeroDriftTests stored-value
      assertions hold. Recorded in spec.md Assumptions.
    - T088 left unchecked: `gh issue create` is blocked by this environment's action
      classifier (same as T076). The two follow-on issues are specified verbatim in
      the assessment and noted in spec.md Assumptions for the maintainer to file.
    
    No code changed — Markdown only.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * test(028): Phase 13 polish — currency-symbol guard + SC evidence (T090-T094)
    
    Phase 13 / Polish & cross-cutting validation (spec 028). Completes the
    implementation bar the two follow-on GitHub issues (T076, T088), which
    gh issue create cannot file in this environment — their full text is in
    spec.md Assumptions + docs/assessments/sales-tax-internationalisation.md.
    
    - T090: full dotnet build -t:Rebuild = 0W/0E; full dotnet test = 2002 pass
      (Core 714, Localization 28, Reports 236, UI 624, Data 156, Integration 244).
    - T091a: new CurrencySymbolGuardTests — repo-wide guard that no Reports
      provider and no UI money-display site hard-codes a "$"/"AUD" literal or
      formats money with ToString("C")/"{0:C}"/ToString("F2"); the 9 money-bearing
      providers each route through MoneyFormatter (FR-004, SC-002).
    - T091: StageFright.Localization.Tests green (28/28) incl. the new guard.
    - T092: CLAUDE.md Reports-pipeline money note (MoneyFormatter for every
      provider/renderer/exporter amount); finalised the remaining stale
      specs/027-localization-support/spec.md FR-015 lines (Clarifications, Edge
      case, FR-015 itself) with "Superseded by spec 028" notes, matching the
      T028 precedent.
    - T093: re-ran the AUD zero-drift regression against the final build —
      AudZeroDriftTests + V28_CurrencyConfigurationTests 9/9 pass; identical
      report figures and stored monetary/tax/GL values (SC-004, FR-031, FR-032).
    - T094: walked SC-001..SC-013 — every criterion maps to a green V28_*
      acceptance scenario or InternationalAccounting integration test (52/52
      pass) for a non-AUD, non-first-of-month org end to end; recorded
      specs/028-international-accounting-standards/sc-verification-evidence.md.
    
    Tasks: 92/94 checked. T076 + T088 stay open pending maintainer issue filing.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * chore(028): companion after_implement task-sync (92/94)
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * chore(028): file follow-on issues #353-#356, close #341 + sub-issues; mark spec complete
    
    gh issue create is now permitted (global settings allow rule), so the two
    follow-on-issue tasks left open in Phase 13 are done:
    
    - Filed against parent #341: #353 (FR-022 sub-twelve-month first FY, T076),
      #354 (tax-exclusive entry) and #355 (recoverable input tax 2320
      classification) from the US10 spike (T088), and #356 (stale
      capabilities/settings/spec.md tax wording, T086a).
    - Closed #341 and all eleven sub-issues #342-#352, each with a comment
      naming the delivering user story / tasks. Delivery is on this branch,
      pending merge to master.
    - spec.md Assumptions, docs/assessments/sales-tax-internationalisation.md
      (_pending_ -> #354/#355) and sc-verification-evidence.md updated with the
      filed issue numbers.
    - T076 + T088 checked -> 94/94 tasks; write-context.py --mark-complete run,
      spec 028 now status: completed. --fold-living-spec is a clean no-op
      (US9 edited the finance/audit-trail living specs in place).
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * feat(028): sub-twelve-month first financial year, labelled a part-year (#353)
    
    Implements FR-022 / issue #353 as spec 028 Phase 14, on the existing
    028 branch (no new branch).
    
    - Settings.InceptionDate (DateTime?, nullable, default null) — optional
      organisation founding date, captured at first-run setup; migration
      AddOrganisationInceptionDate (TEXT NULL).
    - FinancialYearCalculator gains first-period-aware GetRange/GetPreviousRange
      overloads taking DateTime? inceptionDate and returning
      (From, To, bool IsPartYear). A null inception, or one on the anchor,
      reproduces the existing 12-month behaviour exactly.
    - TrialBalance / IncomeStatement / TaxSummary / BalanceSheet providers pass
      settings.InceptionDate and wrap their subtitle via the new
      Reports_Common_PartYearSubtitle when the default FY-preset period is the
      part-year first period (never for a user-supplied custom range).
    - Setup: optional #setup-inception-date <InputDate> on GeneralAppearanceTab;
      SetupFormModel/SetupRequest/SetupService plumb it through.
    - Resources: Reports_Common_PartYearSubtitle and
      Setup_General_InceptionDate{Label,Help} in neutral/en-US/fr-FR;
      qps-ploc regenerated (also catches pre-existing spec-028 drift).
    - spec.md (FR-022 delivered, SC-014, Assumptions, Edge Case, Key Entity)
      and data-model.md updated.
    
    Presentation / range calculation only — no stored monetary, tax or GL
    value changes; AudZeroDriftTests still green. Full rebuild 0W/0E;
    2019 tests pass (+8 calculator, +5 integration, +4 bUnit).
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * feat(028): tax-exclusive amount entry (net + tax), alongside tax-inclusive (#354)
    
    Implements issue #354 as spec 028 Phase 15, on the existing 028 branch
    (no new branch). The in-scope "Issue A" from the US10 sales-tax
    internationalisation spike.
    
    - TaxEntryMode enum (Inclusive — default, every pre-#354 dataset — or
      Exclusive) on Settings.TaxEntryMode; migration AddTaxEntryMode
      (TEXT NOT NULL DEFAULT 'Inclusive', string-converted like every other
      enum column), so an existing row backfills to Inclusive and stays
      byte-identical.
    - TaxCalculator gains SplitExclusive(net, rate, digits) -> (gross, tax)
      with tax = round(net * rate / 100) and gross = net + tax, plus a
      Split(entered, mode, rate, digits) -> (gross, net, tax) dispatcher so
      every taxable posting service has one uniform call site.
    - FeeService / IncomeEntryService / ExpensePaymentService /
      AttendanceService dispatch through TaxCalculator.Split: in Exclusive
      mode the entered figure is the net, tax is added on top, and the
      receivable/bank line (plus Fee.Amount, Payment.Amount and the
      attendance paid-at-creation cash pair) carry the gross while the
      income/expense line keeps the net. Inclusive mode is unchanged.
    - ReactivationForgivenessService is deliberately untouched: it reverses
      a stored gross Fee.Amount, which SplitInclusive re-sums by
      construction, so the write-off stays balanced in either mode and no
      historical figure is reinterpreted.
    - Setup: SetupFormModel / SetupRequest / SetupService plumb TaxEntryMode
      through; SalesTaxTab and TaxSettingsTab render an Inclusive/Exclusive
      selector while tax applies (reset to Inclusive on toggle-off);
      SettingsService.SaveAsync forces Inclusive when tax is off;
      GeneralSettingsTab merges TaxEntryMode from the fresh fetch.
    - RecordIncome / ExpensePaymentPage: the tax hint picks
      Finance_Common_TaxExclusiveHint vs the existing inclusive hint, and
      the Amount label reflects the mode.
    - Resources: Finance_Common_TaxExclusiveHint /
      AmountLabelTax{Inclusive,Exclusive}, Settings_/Setup_Tax_EntryModeLabel,
      Enum_TaxEntryMode_{Inclusive,Exclusive} in neutral / en-US / fr-FR;
      qps-ploc regenerated; Us2LocalizationGuardTests.UserFacingEnums gains
      typeof(TaxEntryMode).
    - spec.md (Assumptions #354-delivered + FR-033 verification extended,
      SC-015), data-model.md (Settings.TaxEntryMode row, AddTaxEntryMode
      migration, Phase 15 section), the sales-tax assessment (Point 2
      delivered) and CLAUDE.md updated.
    
    Additive and opt-in — no GL line-structure, 2310/2320 or TaxCode change;
    Inclusive mode and AudZeroDriftTests are byte-identical. Full rebuild
    0W/0E; 2053 tests pass (+28 calculator, +4 service, +5 integration,
    +1 migration, +3 bUnit).
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * feat(028): classify recoverable input tax (2320) as a Balance Sheet asset (#355)
    
    Implements issue #355 as spec 028 Phase 16, on the existing 028 branch
    (no new branch). The in-scope "Issue B" from the US10 sales-tax
    internationalisation spike (assessment Point 3), option 1 (preferred).
    
    - Seed: the system account 2320 is re-typed from AccountType.Liability
      "Tax Paid" to AccountType.Asset "Tax Receivable" — tax paid on
      purchases is recoverable from the tax authority, so it is an asset
      (a receivable). 2310 "Tax Collected" (owed to the authority) stays a
      Liability.
    - Migration ReclassifyInputTaxAsReceivable: UpdateData on Name + Type of
      the seed row; Down restores "Tax Paid" / "Liability". Paired Designer
      and model snapshot updated. No column added or dropped.
    - Number kept as 2320, a documented asset exception. Renumbering into
      the asset range would desync the denormalised Transaction.GLAccount
      string snapshot on every historical ledger row.
      GetNextAccountNumberAsync is unaffected (it already excludes system
      accounts and 2320 is outside its 1000-1999 asset scan window).
    - No provider code change. BalanceSheetReportProvider and
      TrialBalanceReportProvider section purely by AccountType, so a
      net-refundable org's recoverable tax now presents under Assets
      (debit-normal, positive) and a net-payable org's tax owed under
      Liabilities via 2310, with the Trial Balance still tying exactly.
    - TaxSummaryReportProvider unchanged: taxOnPurchases / net are computed
      from directional GL movements (GetAccountMovementsAsync), not the
      account's classification, so the sign convention (net = tax on sales
      - tax on purchases) needed no flip — only a comment refresh.
    - OpeningBalanceService unchanged in code — ToNormalSide keys off
      account.Type, so a positive carried-over balance for 2320 now posts
      debit-normal. OpeningBalanceServiceTests updated to match.
    - SystemAccounts / AccountNumberAssignmentService / Account doc-comments
      refreshed to "Tax Receivable"; the SystemAccounts.TaxPaid* C#
      identifiers are retained for continuity.
    - Tests: ReclassifyInputTaxAsReceivableMigrationTests (pre-migration
      Liability/"Tax Paid" -> post Asset/"Tax Receivable", number kept);
      RecoverableInputTaxClassificationTests (net-refundable -> Assets;
      net-payable -> Liabilities; Trial Balance ties; exactly 3 sections).
    - spec.md (Assumptions #355-delivered bullet, FR-033 verification
      extended, SC-016, Verbatim Constraints), data-model.md (intro,
      migration entry, new section 13), the sales-tax assessment (Point 3 /
      Issue B / Summary / filing status marked delivered) and CLAUDE.md
      updated.
    
    Classification and presentation only — no stored monetary amount, no
    TaxCode value, no ledger line moves. Full rebuild 0W/0E; 2057 tests
    pass (+2 migration/integration test files), AudZeroDriftTests
    byte-identical.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * docs(028): retire ABN/GST wording from settings living spec (#356)
    
    capabilities/settings/spec.md still described the retired registration-based
    tax model (IsGstRegistered, per-fee GstCode, a "GST / BAS" tab, the ATO ABN
    checksum). Spec 016 replaced that with Settings.IsTaxApplicable / TaxRate /
    AnnualFeeTaxCode / AttendanceFeeTaxCode, and spec 028 (#354) added
    TaxEntryMode; the settings living spec had been stale since spec 016 because
    028 FR-028 scoped only the finance living spec.
    
    - Purpose: "GST treatment" -> "sales-tax treatment"; add currency + FY start.
    - First-run validation requirement: drop ABN; list the real setup-time
      checks, incl. "tax rate > 0 only when sales tax applies", with a scenario.
    - Remove the whole "ABN ... checksum-valid" requirement and its two
      scenarios (the Abn column was dropped in the GenericSalesTax migration).
    - Independent-tabs requirement + scenarios: "GST/BAS tab" -> "Sales Tax tab".
    - Replace "GST registration controls whether GST codes apply" with
      "Sales-tax applicability controls whether a rate and tax codes apply",
      matching SetupService.InitializeAsync and SettingsService.SaveAsync
      (force rate/codes null + TaxEntryMode=Inclusive when not applicable;
      reject rate <= 0 when applicable); resolves the stale NEEDS CLARIFICATION.
    
    Docs-only; no code change.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * docs(028): retire ABN/GST wording across the other living specs (#356)
    
    Follow-on to the settings living-spec cleanup: the same retired
    registration-based tax vocabulary (spec 016's GstCode -> TaxCode /
    IsGstRegistered -> IsTaxApplicable rename, and the "GST Collected/Paid"
    account names) was still present in five more capability living specs.
    
    - domain-model: `Transaction.GstCode` / `Fee.GstCode` -> `.TaxCode`;
      `Settings.IsGstRegistered` -> `IsTaxApplicable`; scenario retitled.
    - data-access: seeded-system-account list "GST Collected/Paid" ->
      "Tax Collected, Tax Receivable" (current seed names; 2320 became
      "Tax Receivable" in the ReclassifyInputTaxAsReceivable migration, #355).
    - app-host: setup-wizard requirement "GST treatment" -> "sales-tax
      treatment (applicability, rate, and per-fee tax codes)"; dropped the
      ABN-era "and tax details" from organisation identity.
    - rehearsals: attendance accrual "plus GST Collected when the fee is
      taxable" -> "plus a Tax Collected credit when the fee is taxable and
      sales tax applies".
    - reports-pipeline: GL-report example "BAS Summary" -> "Tax Summary"
      (the actual TaxSummaryReportProvider name).
    - finance: same line also fixed a now-false claim — per
      OpeningBalanceService, only Opening Balance Equity is excluded as an
      opening-balance entry target; Member Receivable and the tax clearing
      accounts (2310/2320) are deliberately eligible so carried-over
      balances can be seeded. "Tax Paid 2320" -> "Tax Receivable 2320".
    
    Docs-only; no code change.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * chore(028): seed a complete 2026 — rehearsals to year-end, 65% attendance on last 3
    
    Advance the debug seeder's reference "today" from 27 Oct 2026 to 31 Dec 2026 so
    both seeded calendar years are fully settled: every 2026 rehearsal now has
    attendance recorded and its door cash banked, all annual fees are paid (bar the
    two deliberate non-payers), and the year-end quarterly bank fee is posted —
    nothing is left scheduled-but-unrecorded. The AGM is already dated late October
    and the setup wizard's default currency/language (AUD, Australian English) are
    unchanged.
    
    The last three rehearsals of 2026 (early-to-mid December) now model an
    end-of-year turnout dip: a flat 65% per-member attendance chance instead of each
    member's usual 85–100% profile rate.
    
    DEBUG-only fixture code (no test coverage, product behaviour untouched), so no
    build/test run.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * chore(028): anchor seed data to the real run date so future dates stay unheld
    
    The prior commit froze the seeder at 31 Dec 2026, which marked rehearsals that
    have not happened yet as held — unrealistic. Anchor SeedCurrentDate to
    DateTime.UtcNow.Date instead: every dated write in the seeder is already gated on
    "> SeedCurrentDate", so future rehearsals, the spring concert and unpaid future
    bills now stay scheduled-only whenever the sample data is generated.
    
    - SeedRehearsalsAsync: the 65% turnout dip now applies to the three
      most-recently-*held* 2026 rehearsals (computed from heldCount), not the last
      three by calendar position; future rehearsals are scheduled with no attendance.
    - SeedAgmAsync: always ScheduleAsync the AGM so a late-October record exists, but
      only RecordAsync (attendance + committee election) once its date has passed.
      Today the 2026 AGM is a scheduled, not-yet-held record.
    - Doc-comments and the RNG comment updated to match.
    
    DEBUG-only fixture code; not compiled or run (per instruction to skip build/test).
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * docs: add Claude Code to the README tools-used list
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * Add script to remove sample data
    
    * chore(seed): generate organisation name and fee settings in the debug data seeder
    
    The debug data seeder read Settings.OrganizationName / AnnualFee /
    AttendanceFee from whatever the setup wizard captured. A coordinator who
    left the fees at zero got a hollow sample dataset: SeedAnnualFeesAsync
    was a no-op and the door-cash -> petty-cash -> bank sweep in
    SeedRehearsalsAsync moved nothing.
    
    SeedAsync now stamps a generated organisation name ("Clarence Valley
    Community Choir") and fee schedule -- annual $120, per-rehearsal $2,
    February membership renewal, October committee renewal, six-seat general
    committee -- over the wizard inputs before it uses them, mirroring how
    sample data already supplies its own accounts, opening balances and
    committee/AGM history (spec 022). Currency, language and sales-tax
    treatment stay as configured. The ISettingsService.SaveAsync audit entry
    is suppressed by the enclosing AuditTrailSuppressionScope like every
    other seeded write.
    
    Updates the DebugDataSeeder class summary and the app-host living spec's
    "Optional sample-data seeding" section.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * fix: point delete-database.cmd at the real MAUI app-data path
    
    The script deleted nothing: it targeted %AppData% (Roaming) with
    forward-slash separators, which del misparses as switches. The MAUI
    app writes stagefright.db to FileSystem.AppDataDirectory, which on the
    unpackaged Windows head resolves to
    %LOCALAPPDATA%\StageFright Community\com.stagefright.community\Data.
    
    Rewrite the script to:
    - use %LOCALAPPDATA% with backslash separators and quoted paths
    - remove the SQLite WAL sidecars (stagefright.db-wal / -shm) too
    - also clear a repo-root design_time.db left by "dotnet ef" runs
    - guard every delete with "if exist" and report whether anything went
    
    Also note the concrete Windows path and the script in docs/SETUP.md's
    "Reset the Database" section.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * fix: language selector only offered Australian English (#360)
    
    The Settings ▸ General and Setup Wizard display-language pickers listed
    only "English (Australia)" even though the app now ships en-US and fr-FR
    satellite resource sets (added on top of spec 027).
    
    Root cause: MauiProgram registered the catalog as
    `AddSingleton<ISupportedLanguagesCatalog, SupportedLanguagesCatalog>()`.
    Microsoft.Extensions.DependencyInjection selects the greediest resolvable
    constructor, so it picked `SupportedLanguagesCatalog(IEnumerable<string>
    resourceAssemblyNames)` — the test-only seam — and resolved the
    unregistered `IEnumerable<string>` as an *empty* sequence (not null).
    The constructor's `?? DefaultResourceAssemblyNames` guard only covered
    null, so `_resourceAssemblyNames` became `[]`, `ContainsOurSatellite`
    probed for nothing, no `<culture>/StageFright.*.resources.dll` folder
    ever matched, and only the hard-coded `en-AU` baseline survived. It was
    masked until en-US/fr-FR shipped because en-AU was previously the only
    set anyway; every test used `new SupportedLanguagesCatalog()` directly or
    a fake catalog, so none exercised the DI path.
    
    - MauiProgram: register with an explicit factory
      (`_ => new SupportedLanguagesCatalog()`) so the documented parameterless
      constructor is used.
    - SupportedLanguagesCatalog: the `(IEnumerable<string>)` constructor now
      treats an empty sequence like null and falls back to probing
      Core/UI/Reports — defence in depth against any DI/empty-list caller.
    - Tests: SupportedLanguagesCatalogTests gains an empty-list fallback case;
      StartupSequenceTests gains a DI-container regression that resolves the
      catalog the "natural" `AddSingleton<TService,TImpl>()` way and asserts
      en-US and fr-FR are discovered (both fail without the fix, reproducing
      the "en-AU only" list).
    - Refreshed three now-stale "en-AU is the only shipped set" comments in
      the picker area (GeneralSettingsTab, LanguagePickerRenderTests,
      V21_LocalizationStartupTests).
    
    Verified in the running MAUI app via CDP: the Setup Wizard language
    select now offers en-AU / en-US / fr-FR. Full rebuild 0W/0E; 2059 tests
    pass.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * docs(029): spec — first-run language selection & optional sample-data seeding (#361)
    
    New spec-kit feature on branch 029-first-run-language-seed. Addresses issue
    #361 (a saved language change is invisible until restart, with no prompt) and
    folds in a requested extension: move the Debug-only sample-data choice onto the
    same pre-wizard screen and strip both the language and sample-data steps out of
    the setup wizard.
    
    Scope captured in spec.md:
    - P1: a dedicated first-run language screen before the setup wizard; the choice
      is persisted outside the database and applied by one automatic restart.
    - P2: Settings language change shows a post-save "restart now / later" dialog
      instead of the transient inline notice.
    - P3 (Debug only): a "load sample data" option on the first-run screen that
      seeds before restarting and lands o…
    @SteveTeece SteveTeece self-assigned this Sep 3, 2026
    @SteveTeece
    SteveTeece merged commit ed518f8 into dev Sep 3, 2026
    1 check passed
    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

    Labels

    None yet

    Projects

    None yet

    Development

    Successfully merging this pull request may close these issues.

    1 participant