From 2cf75349af2af48960f1f6247bea935d157a54c5 Mon Sep 17 00:00:00 2001 From: Steve Teece Date: Thu, 3 Sep 2026 10:00:48 +1000 Subject: [PATCH] Merge latest changes on dev (#375) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 * 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(), 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 * chore(027): record after_implement companion hook state (14/64 tasks) Co-Authored-By: Claude Sonnet 5 * 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. Co-Authored-By: Claude Sonnet 5 * 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 * chore(027): record after_implement companion hook state (29/64 tasks) Co-Authored-By: Claude Sonnet 5 * 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. Co-Authored-By: Claude Sonnet 5 * 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. Co-Authored-By: Claude Sonnet 5 * 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 * 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 * 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 * 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 , 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. Co-Authored-By: Claude Sonnet 5 * 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 * 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). Co-Authored-By: Claude Sonnet 5 * 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). Co-Authored-By: Claude Sonnet 5 * 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 * 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 * 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 * 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). Co-Authored-By: Claude Sonnet 5 * 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 * chore(028): record plug-display concern for Polish T091a Co-Authored-By: Claude Sonnet 5 * 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 * chore(028): sync companion after_implement context (Phase 5 / US3) Co-Authored-By: Claude Sonnet 5 * 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). Co-Authored-By: Claude Sonnet 5 * 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 * chore(028): sync companion after_implement context (Phase 7 / US5) Co-Authored-By: Claude Sonnet 5 * 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. Co-Authored-By: Claude Sonnet 5 * chore(028): sync companion after_implement context (Phase 8 / US6) Co-Authored-By: Claude Sonnet 5 * 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 * chore(028): sync companion after_implement context (Phase 9 / US7) Co-Authored-By: Claude Sonnet 5 * chore(028): companion after_implement task-sync (75/94) Co-Authored-By: Claude Sonnet 5 * 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 * 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 * chore(028): companion after_implement task-sync (85/94) Co-Authored-By: Claude Sonnet 5 * 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 * 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 * chore(028): companion after_implement task-sync (92/94) Co-Authored-By: Claude Sonnet 5 * 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 * 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 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 * 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 * 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 * 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 * 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 * 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 * 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 * docs: add Claude Code to the README tools-used list Co-Authored-By: Claude Sonnet 5 * 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 * 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 * 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()`. Microsoft.Extensions.DependencyInjection selects the greediest resolvable constructor, so it picked `SupportedLanguagesCatalog(IEnumerable resourceAssemblyNames)` — the test-only seam — and resolved the unregistered `IEnumerable` as an *empty* sequence (not null). The constructor's `?? DefaultResourceAssemblyNames` guard only covered null, so `_resourceAssemblyNames` became `[]`, `ContainsOurSatellite` probed for nothing, no `/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)` 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()` 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 * 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 on the dashboard, wizard skipped. - Setup wizard loses its language selector, its sample-data option, and the tab-bypass behaviour that only existed to support in-wizard seeding. - Startup language resolution gains a stored-preference tier between Settings.LanguageCode and the OS language. - New app self-restart capability (Windows; graceful no-op elsewhere). Supersedes the first-run-language parts of spec 027 and the in-wizard sample-data parts of specs 017/022. No DB schema change; no GL/money change. Requirements-quality checklist passes; two multi-interpretation decisions (auto-restart into the wizard; replace the inline notice with a dialog) were settled with the requester and recorded in the checklist notes rather than left as clarification markers. Co-Authored-By: Claude Sonnet 5 * docs(029): apply language changes live in-session, drop all restart prompts Revise spec 029 so a display-language change takes effect immediately in the running session on every path (first-run screen and Settings), with no application restart and no "restart required" prompt, dialog or notice anywhere. This reverses spec 027's no-in-session-switching constraint. - First-run screen: confirm applies the language in place, then continues to the wizard (Release) or seeds sample data in the pre-wizard step and goes straight to the dashboard (Debug) - no restart on either path. - Settings: a saved language change re-renders the UI at once; the inline "restart required" notice and the planned post-save restart dialog are both gone. - Removed the standalone restart-capability requirements and all non-Windows self-restart degradation; added an in-session-switching requirement group. Requirements renumbered FR-001-FR-023, no gaps. - Updated success criteria, key entities, assumptions, edge cases, out-of-scope, and the spec-027 supersedes note; refreshed the requirements checklist notes. Co-Authored-By: Claude Sonnet 5 * docs(029): plan — first-run language selection & sample-data seeding Phase 0/1 design artifacts for spec 029: research decisions (no-DB preference store, in-session CultureProvider.Switch, startup routing/ ladder changes, wizard cleanup, sample-data reuse, Settings restart- notice removal), data model, UI/service contract, and quickstart validation scenarios. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017sTJ4fm3PqXPQS4vLXzNHe * chore(029): record plan-step completion in companion state Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017sTJ4fm3PqXPQS4vLXzNHe * docs(029): tasks — first-run language selection & sample-data seeding 37 tasks across Foundational + 3 user-story phases + Polish, generated from plan.md/research.md/data-model.md/contracts. Also records the tasks-step completion in .spec-context.json (status: ready-to-implement). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01EyGsgcc7qvJL8vZLUZRttT * feat(029): foundational language-preference store + first-run screen + US1 tests Phase 2 (Foundational) + partial Phase 3 (US1) of spec 029: - ILanguagePreferenceStore (Core contract) + MauiLanguagePreferenceStore (Preferences-backed impl), registered in MauiProgram - CultureProvider.Switch(CultureInfo) — in-session live culture change, no restart, re-renders the whole ShellLayout-wrapped tree - LanguageProvider's FR-006 startup ladder gains step 2 (recorded preference), renumbering OS-language/en-AU fallback to 3/4 - New FirstRunLanguageScreen (@page "/language-select"): lists shipped languages, pre-selects the resolved default, confirm records the preference + switches culture + navigates to /setup - App.razor.cs routes to /language-select vs /setup based on whether a preference is already recorded (only while setup is incomplete) - Tests: CultureProviderTests, FirstRunLanguageScreenTests, AppRoutingTests (new); LanguageProviderTests extended; SetupWizardTests, ReviewTabTests, LanguagePickerRenderTests updated to drop the sample-data/language-tab assertions being retired; SetupWizardNoSeederTests and SampleDataTabTests deleted outright (their premise stops existing) Full solution build is green. SetupWizard/LanguageSelectionTab/SampleDataTab/ ReviewTab themselves are not yet edited (tasks.md T018-T023) — several of the above tests (e.g. Tabs_AreNeverDisabled, SeedDataCheckbox_IsAbsent) will run red until that follow-up lands, since the wizard still carries the language step, sample-data checkbox and tab-bypass mechanism this story retires. Tasks T001-T017 of specs/029-first-run-language-seed/tasks.md complete. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017iVwWMUFaY9D9L83qmVWbb * feat(029): complete US1 — retire wizard language step & sample-data option Finishes Phase 3 (US1) of spec 029, tasks T018-T023: - Delete LanguageSelectionTab and SampleDataTab outright (spec 029 FR-016/ FR-017: no language selector, no sample-data option, no variation in the wizard's step list — both retired to the new /language-select screen) - SetupWizard.razor: drop the two tabs' markup and every Disabled="@_seedWithTestData" attribute — tabs are now always enabled; drop the now-dead seeding overlay (the wizard never seeds any more, moved to FirstRunLanguageScreen) - SetupWizard.razor.cs: remove _seedWithTestData/_debugSeeder/IsTabBypassed/ HandleSeedWithTestDataChanged and the IDebugDataSeeder resolution; Next advances one tab at a time (no bypass skip); the opening-balance Finish guard is unconditional; LanguageCode now comes from the cascaded CultureProvider.CurrentCulture, not a wizard field - Drop the now-unwritten LanguageCode field from SetupFormModel - ReviewTab: drop DebugSeederAvailable/SeedWithTestData params and the "Load sample data" summary row; drop the orphaned Setup_Review_LoadSampleDataTerm resx key (all 4 culture files) Test-infrastructure fix (found via full-suite verification, not part of tasks.md but required for a green build): - CultureProviderTests.cs had un-awaited cut.InvokeAsync(...) calls, so CultureProvider.Switch's static CultureInfo mutation could still be in-flight when the test's restore ran — this alone caused ~116 failures scattered across totally unrelated tests (English-text assertions silently seeing French). Fixed by awaiting every InvokeAsync call. - FirstRunLanguageScreenTests.cs's three confirm-button tests trigger the same Switch call via a synchronous .Click() but had no culture restoration at all — fixed with the same shared CultureRestorer helper (extracted to tests/StageFright.UI.Tests/CultureRestorer.cs). - Even with correct per-test restoration, Switch's mutation of the process-wide CultureInfo.Default* statics is observable by any other test collection running concurrently — reproduced three times across separate full runs, each time corrupting a different unrelated test. Added AssemblyCollectionBehavior.cs (DisableTestParallelization) to StageFright.UI.Tests: the standard xUnit fix when many tests share global mutable state that virtually the whole suite reads (unlike the narrower per-class MoneyFormatterStateCollection precedent in StageFright.Integration.Tests, tagging every consumer here would mean tagging nearly the entire ~600-test project). Verified: 629/629 pass reliably across repeated full runs with this in place (was previously 629/629, 628/629, or 625/629 depending on scheduling luck). Full solution: dotnet build 0 warnings/0 errors; dotnet test 2062/2062 passed across all six projects (Core.Tests 751, Reports.Tests 236, Localization.Tests 28, Data.Tests 158, UI.Tests 629, Integration.Tests 260). User Story 1 (P1/MVP) is now independently complete and testable per tasks.md's Phase 3 checkpoint: a clean install shows /language-select before the wizard; confirming a language applies it immediately with no restart; the wizard never shows a language selector or sample-data option and its step list never varies; a recorded preference is never re-prompted. Tasks T018-T023 of specs/029-first-run-language-seed/tasks.md complete. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017iVwWMUFaY9D9L83qmVWbb * chore(029): fix companion step-summary misfile (specify -> implement) The previous write-context.py --step-summary call omitted --step and defaulted to "specify"; moved the summary to "implement" where it belongs. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017iVwWMUFaY9D9L83qmVWbb * feat(029): US2 — Settings language change applies live, restart notice dropped Completes Phase 4 (US2) of spec 029, tasks T024-T028: - GeneralSettingsTab.razor.cs: drop the LanguageChanged property and _initialLanguageCode field entirely — the "was this changed" check is now a one-off local comparison against _settings.LanguageCode captured right before HandleSaveAsync overwrites it, so no dedicated field is needed. After a successful SettingsService.SaveAsync where the language changed, call ILanguagePreferenceStore.Set(_selectedLanguageCode) then CultureProvider.Switch(...) — the exact same record-then-switch sequence FirstRunLanguageScreen uses, via a newly-cascaded CultureProvider - GeneralSettingsTab.razor: remove the "restart the app" alert block — there's nothing to wait for now, the change is visible the moment it's saved (FR-010/FR-020/SC-007) - Drop the now-orphaned Settings_General_LanguageRestartNotice resx key (all 4 culture files) - Tests: GeneralSettingsTabTests gains cases for changed-language save (records preference + switches culture) and unchanged-language save (does neither), plus an explicit "no restart notice ever, before/after change/save" assertion; LanguagePickerRenderTests drops the now-false "shows restart notice" case and broadens the "no notice" case to also check after a change Full solution: dotnet build 0 warnings/0 errors; dotnet test 2064/2064 passed across all six projects (631 in UI.Tests, +2 net from these tests). User Story 2 (P2) is now independently complete per tasks.md's Phase 4 checkpoint: changing the language in Settings and saving re-renders the whole app in the new language in the same interaction, no restart notice ever shown, and the choice survives a relaunch (via the already-existing Settings.LanguageCode persistence). User Stories 1 and 2 both work independently. Tasks T024-T028 of specs/029-first-run-language-seed/tasks.md complete. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017iVwWMUFaY9D9L83qmVWbb * chore(029): record US2 verification in companion state Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017iVwWMUFaY9D9L83qmVWbb * feat(029): US3 — Debug-only sample-data loading from first-run screen Phase 5 (T029-T032): FirstRunLanguageScreen now optionally resolves IDebugDataSeeder (Debug builds only) and offers a "Load sample data" RadzenSwitch. Confirming with it ticked builds a placeholder SetupRequest, calls SetupService.InitializeAsync then IDebugDataSeeder.SeedAsync under a seeding-progress overlay (same try/finally shape the deleted SetupWizard used), and lands on /dashboard with the wizard never shown. A seeding failure shows Setup_FirstRun_SeedingError and stays on the screen. Added Setup_FirstRun_SeedingError and Setup_FirstRun_SampleOrganizationPlaceholder resx keys (neutral + en-US/fr-FR/qps-ploc) — the placeholder org name is routed through a key rather than a literal because Us2LocalizationGuardTests bans bare string literals on this scanned UI surface, even for a value DebugDataSeeder immediately overwrites. Also fixes a genuine bUnit test-timing bug found while verifying this task: a synchronous Click() on the confirm button doesn't reliably await the real Task.Run seeding hop, so the two new tests use ClickAsync(new MouseEventArgs()) instead, matching the codebase's existing convention for async-submit buttons elsewhere. Full solution build: 0 warnings/0 errors. Full test suite: 2067/2067 passed across all six projects. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017iVwWMUFaY9D9L83qmVWbb * chore(029): phase 6 polish — docs refreshed, full suite green; live verification finds in-session-switch defect T033: CLAUDE.md Navigation + Localization sections updated for the /language-select screen and the live CultureProvider.Switch (no restart notice; no next-launch delay). T034: docs/localization/adding-a-language.md — language-picker references moved from "Setup Wizard language step" to /language-select (one-minute version, SetupResource table row, step 5, qps-ploc note); section 9 retitled and rewritten with the 4-tier startup ladder (adds the ILanguagePreferenceStore no-DB preference tier) and the live in-session switch for both entry points. T035: dotnet build -c Debug 0 warnings/0 errors; full test suite 2067 pass (the sole failure, EventFormTests.DoesNotRender_FeeOrPaidFields, is the CLAUDE.md-documented GUID-substring flake — green in isolation); quickstart scenarios 1–5 filters all green. T036: live CDP-driven verification on a clean install. PASS — /language-select renders before /setup; lists en-AU/en-US/fr-FR by endonym; en-AU pre-selected; choice persists to preferences.dat outside the DB; no process restart on confirm (PID stable); a relaunch with the preference recorded resolves to fr-FR (log) and opens /setup fully in French, skipping /language-select. FAIL — confirming fr-FR on /language-select does NOT re-render the running session: /setup stayed entirely English. CultureProvider.Switch has no visible effect in the real MAUI WebView2 host; the new language only appears after a full app restart (the issue #361 symptom, on the first-run path). Recorded as a blocking concern in .spec-context.json; suspected AsyncLocal CurrentUICulture pinned at startup. Needs its own debug+fix+test pass (US1 T004/T016, US2 T027). T037 not run (per user) — exercises the same CultureProvider.Switch mechanism (FR-009), blocked on the same defect. Spec left at status=implementing — NOT marked complete. Co-Authored-By: Claude Sonnet 5 * chore(029): close T037 — Settings language change manually verified live T037 (Phase 6, deferred-until-implementation live verification) is now confirmed by the user: in the real MAUI app, changing the Display language in Settings → General and saving re-renders the running session in the new language immediately, with no process restart and no "restart required" notice at any point (before the change, after the selection changes, or after the save). Verified in both light and dark themes. This is the Settings entry point (FR-009). Contrary to the T036 note's presumption that it would be "broken identically", the in-session CultureProvider.Switch re-render IS effective here in the real WebView2 host — Settings renders inside the live cascaded CultureProvider, so StateHasChanged propagates to its descendants. The T036 blocking concern is a different code path (first-run /language-select confirm → navigate to /setup, which does not re-render live in the real app) and is unaffected — it remains OPEN and still needs its own debug+fix+test pass. Spec left at status=implementing, NOT marked complete. - tasks.md: T037 checked, with the manual-verification note inline - .spec-context.json: T037 history + task_summary + verified entry; currentTask → T037; last_action refreshed - .spec-context.events.jsonl: T037 completion event appended No code changed — docs/companion-state only, so no build/test run (matches the prior companion-state-only commit 6416ba0). Co-Authored-By: Claude Sonnet 5 * fix(029): in-session language switch dead in real MAUI host — set culture globals only (T036) The T036 live check found the spec-029 in-session switch had no effect in the real WebView2 host: confirming a language on /language-select persisted it and navigated on, but /setup rendered in the OLD language; only a full process restart applied it (the issue #361 symptom). Root cause (confirmed by a deterministic ExecutionContext.Run repro): CultureInfo.CurrentCulture / CurrentUICulture are AsyncLocal-backed. MauiProgram.RunStartupSequence pinned them on the startup thread; that per-execution-context value *shadows* the DefaultThreadCurrent* globals on the Blazor renderer's own ExecutionContext. CultureProvider.Switch updated the globals (and set CurrentUICulture on the unwinding click-handler context), but every queued render kept resolving the stale pinned value. A restart worked only because startup then re-pinned the new culture. bUnit / integration tests passed because they render synchronously on the same context that calls Switch. Fix — set ONLY the process-wide statics, never the per-context override: - MauiProgram.RunStartupSequence: assign CultureInfo.DefaultThreadCurrentCulture / DefaultThreadCurrentUICulture; drop the CurrentCulture / CurrentUICulture assignments. - CultureProvider.Switch: same — assign the two DefaultThreadCurrent* globals, update the cascaded CurrentCulture property, StateHasChanged(). No per-context pin, so a second switch can't re-introduce the shadow either. With no override anywhere, CultureInfo.CurrentUICulture reads straight through to DefaultThreadCurrentUICulture on every render, so the switch is visible on the very next render regardless of which ExecutionContext runs it — the StateHasChanged() cascade re-render is unchanged. Tests: CultureProviderTests reworked — Switch_SetsTheProcessWideCultureGlobals asserts the real contract; new Switch_DoesNotPinAPerContextOverride_SoLater GlobalChangesStillTakeEffect is the regression guard (a later DefaultThread CurrentUICulture change is still observed — proof Switch pinned no override). Build 0 warnings / 0 errors; full suite 2068/2068 (UI.Tests 635, +1). Docs: CLAUDE.md Localization section; docs/localization/adding-a-language.md sec 9; spec 029 contract, data-model.md, and a dated correction to research.md Decision 2 (its "setting CurrentCulture on the calling thread reaches the next render" rationale was the flawed premise). Companion state: T036 concern marked resolved; verified entry added. Live in-app re-verification of the fix (first-run path, both themes, + a T037 re-confirm) is tracked as new task T038 — bUnit renders synchronously so it cannot reproduce the EC-boundary failure. Spec stays status=implementing until that live check passes. Co-Authored-By: Claude Sonnet 5 * fix: address PR #363 code review — simplicity & correctness cleanups - Localizer.FormatNamedPlaceholders: bind each distinct {token} to an arg once and substitute every occurrence by name, so a repeated token renders its value instead of leaking the literal "{token}" once args run out. - Enum key format (Enum__) now lives in one place — EnumLocalizationExtensions.EnumResourceKey — used by both LocalizeEnum and Localizer.Enum instead of being rebuilt in each. - SetupService.Validate now range-checks FinancialYearStartMonth (1..12), matching the existing day check; new Validation_Setup_FinancialYearStartMonthRange key added to all four ValidationResource resx files. Contract doc updated. - SupportedLanguagesCatalog: materialise the satellite-folder list eagerly so an IO/permission failure during enumeration is caught, not thrown mid-foreach out of BuildCatalog where Lazy would cache it and break the catalog forever. - FirstRunLanguageScreen sample-data path: guard InitializeAsync with IsSetupCompleteAsync so re-pressing Confirm after a failed seed retries seeding instead of dead-ending on "setup already completed". Spec 029 docs updated. - GeneralSettingsTab code-behind: resolve the save-success message through the already-injected ILocalizer, consistent with its other lookups. - CultureProvider: drop the redundant OnInitialized (field initialiser already sets CurrentCulture to the same value). - MoneyFormatter: document the process-wide-static test-isolation contract. Review items #8 (RadzenSwitch