diff --git a/.codex/TECH_DEBT_QUEUE.md b/.codex/TECH_DEBT_QUEUE.md index 0b4d4ac1b2..0a1105b739 100644 --- a/.codex/TECH_DEBT_QUEUE.md +++ b/.codex/TECH_DEBT_QUEUE.md @@ -33,7 +33,7 @@ violation, never edit a baseline the code still triggers. `src/Sections/Humans.
/Docs/
.md` + `Docs/data-access.md`. Debt is whatever diverges from that model; find it by comparing a section against the model, not by consuming a frozen list. -- **Surface / interconnectivity baseline:** `dotnet build Humans.slnx -v quiet`, then +- **Surface / interconnectivity baseline:** `dotnet build Humans.slnx -v quiet -clp:ErrorsOnly`, then `reforge surface-score --all --top-symbols 200 --format Json` (score a **built** solution — unbuilt under-reports ~4%). Rank sections by the Section Refactor History table in `docs/architecture/maintenance-log.md`. The score is a detector, not an @@ -71,8 +71,8 @@ violation, never edit a baseline the code still triggers. *Current state* below **by rewriting it**. 2. Pick one item by the priority order. Write a one-sentence architecture thesis; if the thesis is "a number goes down", pick something else. -3. Make the smallest real refactor. Targeted section tests + `dotnet build Humans.slnx -v quiet` - per change; full `dotnet test Humans.slnx -v quiet` before any push. +3. Make the smallest real refactor. Targeted section tests + `dotnet build Humans.slnx -v quiet -clp:ErrorsOnly` + per change; full `dotnet test Humans.slnx -v quiet -clp:ErrorsOnly` before any push. 4. One coherent improvement per commit; push the branch; open/refresh the PR (`memory/process/always-open-a-pr.md`). 5. When stopping: rewrite *Current state* (including *Needs Peter*), leave the diff --git a/.codex/bug-hunt-prompt.md b/.codex/bug-hunt-prompt.md index 88b5be69b8..f719ca7469 100644 --- a/.codex/bug-hunt-prompt.md +++ b/.codex/bug-hunt-prompt.md @@ -76,7 +76,7 @@ Do not modify: ## Build Command ``` -dotnet build Humans.slnx -v q && dotnet test Humans.slnx -v q --filter "FullyQualifiedName~Application" +dotnet build Humans.slnx -v q -clp:ErrorsOnly && dotnet test Humans.slnx -v q -clp:ErrorsOnly --filter "FullyQualifiedName~Application" ``` ## Phase 1: Razor Rendering & HTML Structure *(highest frequency — 15+ historical fixes)* diff --git a/.codex/skills/humans-bug-hunt/SKILL.md b/.codex/skills/humans-bug-hunt/SKILL.md index ae0b578088..3b7139e4e8 100644 --- a/.codex/skills/humans-bug-hunt/SKILL.md +++ b/.codex/skills/humans-bug-hunt/SKILL.md @@ -27,8 +27,8 @@ Run recurring autonomous bug-hunt passes in this repository. 2. Search for one high-confidence bug at a time, using repo patterns rather than a fixed checklist. 3. Implement the smallest defensible fix. 4. Add or extend tests when practical. -5. Run targeted verification, plus `dotnet build Humans.slnx --disable-build-servers -v q`. -6. Periodically run `dotnet test Humans.slnx --no-build --disable-build-servers -v q --filter "FullyQualifiedName~Application"`. +5. Run targeted verification, plus `dotnet build Humans.slnx --disable-build-servers -v q -clp:ErrorsOnly`. +6. Periodically run `dotnet test Humans.slnx --no-build --disable-build-servers -v q -clp:ErrorsOnly --filter "FullyQualifiedName~Application"`. 7. Commit each fix separately and push the branch after verified progress. 8. Continue until remaining ideas are speculative or require forbidden areas. diff --git a/.codex/skills/humans-refactor/SKILL.md b/.codex/skills/humans-refactor/SKILL.md index 26a0b4a3f6..124fd6ffc2 100644 --- a/.codex/skills/humans-refactor/SKILL.md +++ b/.codex/skills/humans-refactor/SKILL.md @@ -161,7 +161,7 @@ Then repeat until stasis: 3. Pick the highest-leverage cohesive improvement, not just the highest scoring rule. 4. Write a candidate thesis in the run notes: what concept will be deleted, which responsibility moves to its rightful owner, or which duplicated/cross-section path disappears. If the thesis is just "the score drops", reject the candidate before editing. 5. Make the change. -6. Run targeted tests and `dotnet build Humans.slnx --disable-build-servers -v q`. +6. Run targeted tests and `dotnet build Humans.slnx --disable-build-servers -v q -clp:ErrorsOnly`. 7. Run Reforge after the change. 8. Run the score-blind architecture-review gate. 9. If accepted, commit and push. If rework/reject, improve or abandon before committing. diff --git a/.codex/skills/humans-tech-debt/SKILL.md b/.codex/skills/humans-tech-debt/SKILL.md index 37bf0fe078..85b375d457 100644 --- a/.codex/skills/humans-tech-debt/SKILL.md +++ b/.codex/skills/humans-tech-debt/SKILL.md @@ -30,9 +30,9 @@ Run recurring autonomous tech-debt reduction passes in this repository. 3. Before editing, write a one-sentence architecture thesis: what concept will be deleted, what responsibility will move to its rightful owner, or what duplication/coupling will disappear. If the thesis is "the score drops", abandon the candidate. 4. Make the smallest coherent improvement that reduces divergence, duplication, misplaced responsibility, or durable public surface. 5. Add or extend tests when practical. -6. Run targeted verification, plus `dotnet build Humans.slnx --disable-build-servers -v q`. +6. Run targeted verification, plus `dotnet build Humans.slnx --disable-build-servers -v q -clp:ErrorsOnly`. 7. Run a score-blind second pass before commit. Review only the diff, the architecture thesis, and verification. Reject the change if it would not be worth keeping without metric movement. -8. Run the full `dotnet test Humans.slnx --no-build --disable-build-servers -v quiet` gate before any push. +8. Run the full `dotnet test Humans.slnx --no-build --disable-build-servers -v quiet -clp:ErrorsOnly` gate before any push. 9. Commit each accepted improvement separately and push the branch after verified progress. 10. Continue until remaining ideas are low-value, speculative, blocked by forbidden areas, or only reducible through metric-gaming changes. diff --git a/.codex/tech-debt-prompt.md b/.codex/tech-debt-prompt.md index 01a492da87..e59a25d129 100644 --- a/.codex/tech-debt-prompt.md +++ b/.codex/tech-debt-prompt.md @@ -32,7 +32,7 @@ file's *Current state* instead of restarting discovery. `docs/sections/SECTION-TEMPLATE.md`, `docs/architecture/design-rules.md`, each section's `Docs/
.md` + `Docs/data-access.md`. Divergence from the model is the debt. -- Surface / coupling baseline: `dotnet build Humans.slnx -v quiet`, then +- Surface / coupling baseline: `dotnet build Humans.slnx -v quiet -clp:ErrorsOnly`, then `reforge surface-score --all` (built solution only). Reduce it through architecturally-real deletions; the score is a detector, never the objective. @@ -46,7 +46,7 @@ file's *Current state* instead of restarting discovery. `local/tech-debt-runs//` either way. - One coherent improvement per commit, each with a one-sentence architecture thesis that stands without score movement. Targeted section tests + build per change; - full `dotnet test Humans.slnx -v quiet` before any push. `-v quiet` always. + full `dotnet test Humans.slnx -v quiet -clp:ErrorsOnly` before any push. `-v quiet` always. - Honor every boundary in the queue file's *Boundaries* section — especially: debt only (never feature follow-ups, even fully-specced ones), no authorization/privacy shape changes, no reverting documented test-infrastructure decisions, and new public surface goes to *Needs Peter* instead of into the code. diff --git a/docs/architecture/debt-ledger.yml b/docs/architecture/debt-ledger.yml index 4f48205bcc..67c0c71875 100644 --- a/docs/architecture/debt-ledger.yml +++ b/docs/architecture/debt-ledger.yml @@ -395,10 +395,6 @@ inbox: # sweep scope entirely); PasswordGenerator CSPRNG (explicitly deferred by # Peter, 2026-04-24); BudgetRepository ResponsibleTeam .Include (covered by # grandfathered-hum0024-nav-strip). - - added: 2026-08-11 - id: CENTRAL-14 - what: "EventSettingsConfiguration.cs:44-47 declares HasDefaultValue(-25/-16/-9/-4) on four int offsets without HasSentinel — EF's int sentinel stays 0, so explicitly assigning 0 to any of these offsets is skipped and the DB default written instead (int-flavoured bool-sentinel trap). Pre-existing; surfaced by the EF migration review of the Shifts peel (peel 13, nobodies-collective/Humans#858)" - review: light - added: 2026-08-07 id: CENTRAL-12 what: "Shifts section's own Web layer (ShiftAdminController, ShiftsController, VolunteerTrackingController, ShiftViewModels.cs, ShiftAdminPageBuilder, ShiftBrowsePageBuilder, ShiftDashboardPageBuilder, ShiftBrowseMapper, ShiftFilterResolver, ShiftSignupBucketer, ShiftVolunteerSearchBuilder, DevelopmentDashboardSeeder, ShiftSignupsViewComponent) carries the EF EventSettings entity as a view-model/builder field on read-only display paths — should be BurnSettingsInfo per nobodies-collective/Humans#809's acceptance criteria. Distinct from the write/edit path (EventSettingsFormMapper), which legitimately keeps the entity. ~10 interconnected files forming one section's own display pipeline — warrants its own dedicated section-scoped PR rather than folding into #809's cross-section batch (found while working nobodies-collective/Humans#809)" @@ -583,10 +579,6 @@ inbox: id: CENTRAL-59 what: "src/Sections/Humans.Users.Contracts/IUserInfoInvalidator.cs:24 deliberately omits the IInvalidator marker (its own comment says so, debt nobodies-collective/Humans#805), so HUM0028 cannot see it and it needs no [Grandfathered] marker — one invalidator dodging the ratchet by being invisible to it. Marking it (or recording the exemption where the analyzer reads it) is small and separable from the big invalidator rework the HUM0028 theme is parked on. Found by the 2026-09-20 accepted-smell audit." review: light - - added: 2026-09-20 - id: CENTRAL-60 - what: "The pre-existing Codex prompt/skill files run dotnet with `-v quiet`/`-v q` but without the `-clp:ErrorsOnly` that memory/process/dotnet-verbosity-quiet.md requires (.codex/tech-debt-prompt.md, .codex/bug-hunt-prompt.md, .codex/TECH_DEBT_QUEUE.md, .codex/skills/humans-{tech-debt,refactor,bug-hunt}/SKILL.md). Without it the obsolete-warning wall floods the context of every Codex run that follows them, which costs most on a time-budgeted unattended run. Mechanical fix, no judgement needed. Found 2026-09-20 while adding the flag to the nightly debt runner in peterdrier/Humans#1759, which deliberately did not widen to these files." - review: light - added: 2026-09-20 id: CENTRAL-61 what: "Two pre-existing Codex runner scripts use `git -C `, which memory/process/never-use-git-dash-c.md marks a HARD RULE with no exceptions: .codex/run-weekly-bug-hunt.sh and .codex/cleanup-merged-bug-hunt-worktrees.sh. Fix is mechanical — run each group in a `(cd \"$dir\" && git ...)` subshell, as .codex/cron/run-daily-debt.sh now does. Worth noting the atom's stated rationale (a wrong shell folder; defeating Bash allowlist prefixes) is about agent-issued commands, so Peter may want to say whether the rule binds committed scripts at all rather than have it re-found every audit. Found 2026-09-20 while fixing the same violation in the nightly debt runner in peterdrier/Humans#1759." diff --git a/src/Sections/Humans.Camps/Data/CampRepository.Roles.cs b/src/Sections/Humans.Camps/Data/CampRepository.Roles.cs index 2759ac4ad2..082449ef4e 100644 --- a/src/Sections/Humans.Camps/Data/CampRepository.Roles.cs +++ b/src/Sections/Humans.Camps/Data/CampRepository.Roles.cs @@ -135,7 +135,6 @@ public async Task> GetAssignmentsForSeasonAsyn .Include(a => a.Definition) .Include(a => a.CampMember) .Where(a => a.CampSeasonId == campSeasonId) - .OrderBy(a => a.Definition.SortOrder).ThenBy(a => a.AssignedAt) .ToListAsync(ct); } diff --git a/src/Sections/Humans.Email/Docs/debt.yml b/src/Sections/Humans.Email/Docs/debt.yml index f5d60efc27..a911a63056 100644 --- a/src/Sections/Humans.Email/Docs/debt.yml +++ b/src/Sections/Humans.Email/Docs/debt.yml @@ -2,8 +2,4 @@ # /debt-sweep pools every src/Sections/*/Docs/debt.yml into the same inbox as the central ledger. version: 1 next_id: 3 -inbox: - - added: 2026-09-11 - id: EMAIL-2 - what: "src/Sections/Humans.Email/Services/EmailRenderer.cs HTML-encodes these subject lines: Email_IssueComment_Subject, Email_CoordinatorRotaMessage_Subject, Email_CoordinatorTeamRotasMessage_Subject, Email_GoogleGroupRemoval_LossOfAccess_Subject, Email_GoogleDriveRemoval_LossOfAccess_Subject, Email_GoogleAccessRemoval_SecondaryCleanup_Subject. A Subject header is plain text, so a title containing & or < reaches the recipient as R&D. The assembly-vote subjects had the same bug and were fixed in peterdrier/Humans#1649; these are left alone as out of that PR's scope." - review: light +inbox: [] diff --git a/src/Sections/Humans.Events/Controllers/EventsController.cs b/src/Sections/Humans.Events/Controllers/EventsController.cs index 255285e653..3023a8fe56 100644 --- a/src/Sections/Humans.Events/Controllers/EventsController.cs +++ b/src/Sections/Humans.Events/Controllers/EventsController.cs @@ -9,6 +9,7 @@ using Humans.Events.Models; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Localization; using NodaTime; using static Humans.Events.Helpers.EventsLookupHelpers; using static Humans.Events.Helpers.EventsTimeHelpers; @@ -26,7 +27,8 @@ internal sealed class EventsController( ICampServiceRead camps, IAuthorizationService authorizationService, IClock clock, - ILogger logger) : HumansCampControllerBase(users, camps, authorizationService) + ILogger logger, + IStringLocalizer localizer) : HumansCampControllerBase(users, camps, authorizationService) { [HttpGet("MySubmissions")] public async Task MySubmissions() @@ -120,7 +122,7 @@ public async Task Submit() var guideSettings = await guide.GetGuideSettingsAsync(); if (!IsSubmissionOpen(guideSettings)) { - SetError("The submission window is not currently open."); + SetError(localizer["Events_SubmissionWindowClosed"].Value); return RedirectToAction(nameof(MySubmissions)); } @@ -140,7 +142,7 @@ public async Task Create(IndividualEventFormViewModel model) var guideSettings = await guide.GetGuideSettingsAsync(); if (!IsSubmissionOpen(guideSettings)) { - SetError("The submission window is not currently open."); + SetError(localizer["Events_SubmissionWindowClosed"].Value); return RedirectToAction(nameof(MySubmissions)); } @@ -180,7 +182,7 @@ public async Task Create(IndividualEventFormViewModel model) logger.LogInformation("User {UserId} submitted individual event '{Title}'", user.Id, model.Title); - SetSuccess($"Event \"{model.Title}\" submitted for review."); + SetSuccess(FormatEventFeedback("Events_SubmittedForReview", model.Title)); return RedirectToAction(nameof(MySubmissions)); } @@ -197,14 +199,14 @@ public async Task Edit(Guid eventId) if (!guideEvent.CanBeEditedBySubmitter) { - SetError("This event cannot be edited in its current state."); + SetError(localizer["Events_NotEditable"].Value); return RedirectToAction(nameof(MySubmissions)); } var guideSettings = await guide.GetGuideSettingsAsync(); if (guideSettings == null) { - SetError("Guide settings not configured."); + SetError(localizer["Events_GuideSettingsUnavailable"].Value); return RedirectToAction(nameof(MySubmissions)); } @@ -246,14 +248,14 @@ public async Task Update(Guid eventId, IndividualEventFormViewMod if (!guideEvent.CanBeEditedBySubmitter) { - SetError("This event cannot be edited in its current state."); + SetError(localizer["Events_NotEditable"].Value); return RedirectToAction(nameof(MySubmissions)); } var guideSettings = await guide.GetGuideSettingsAsync(); if (guideSettings == null) { - SetError("Guide settings not configured."); + SetError(localizer["Events_GuideSettingsUnavailable"].Value); return RedirectToAction(nameof(MySubmissions)); } @@ -290,7 +292,7 @@ public async Task Update(Guid eventId, IndividualEventFormViewMod logger.LogInformation("User {UserId} updated event '{Title}' ({EventId})", user.Id, model.Title, eventId); - SetSuccess($"Event \"{model.Title}\" resubmitted for review."); + SetSuccess(FormatEventFeedback("Events_ResubmittedForReview", model.Title)); return RedirectToAction(nameof(MySubmissions)); } @@ -306,14 +308,14 @@ public async Task Withdraw(Guid eventId) if (!guideEvent.CanBeWithdrawnBySubmitter) { - SetError("This event cannot be withdrawn in its current state."); + SetError(localizer["Events_NotWithdrawable"].Value); return RedirectToAction(nameof(MySubmissions)); } await guide.WithdrawEventAsync(guideEvent); logger.LogInformation("User {UserId} withdrew event '{Title}' ({EventId})", user.Id, guideEvent.Title, eventId); - SetSuccess($"Event \"{guideEvent.Title}\" withdrawn."); + SetSuccess(FormatEventFeedback("Events_Withdrawn", guideEvent.Title)); return RedirectToAction(nameof(MySubmissions)); } @@ -527,7 +529,7 @@ public async Task BarrioSubmit(string slug) var guideSettings = await guide.GetGuideSettingsAsync(); if (!IsSubmissionOpen(guideSettings)) { - SetError("The submission window is not currently open."); + SetError(localizer["Events_SubmissionWindowClosed"].Value); return RedirectToAction(nameof(MySubmissions)); } @@ -547,7 +549,7 @@ public async Task BarrioCreate(string slug, CampEventFormViewMode var guideSettings = await guide.GetGuideSettingsAsync(); if (!IsSubmissionOpen(guideSettings)) { - SetError("The submission window is not currently open."); + SetError(localizer["Events_SubmissionWindowClosed"].Value); return RedirectToAction(nameof(MySubmissions)); } @@ -586,7 +588,7 @@ public async Task BarrioCreate(string slug, CampEventFormViewMode await guide.SubmitEventAsync(guideEvent, viewUrl); logger.LogInformation("User {UserId} submitted barrio event '{Title}' for camp {CampId}", user.Id, model.Title, camp.Id); - SetSuccess($"Event \"{model.Title}\" submitted for review."); + SetSuccess(FormatEventFeedback("Events_SubmittedForReview", model.Title)); return RedirectToAction(nameof(MySubmissions)); } @@ -601,7 +603,7 @@ public async Task BarrioEdit(string slug, Guid eventId) if (!guideEvent.CanBeEditedBySubmitter) { - SetError("This event cannot be edited in its current state."); + SetError(localizer["Events_NotEditable"].Value); return RedirectToAction(nameof(MySubmissions)); } @@ -642,7 +644,7 @@ public async Task BarrioUpdate(string slug, Guid eventId, CampEve if (!guideEvent.CanBeEditedBySubmitter) { - SetError("This event cannot be edited in its current state."); + SetError(localizer["Events_NotEditable"].Value); return RedirectToAction(nameof(MySubmissions)); } @@ -684,7 +686,7 @@ public async Task BarrioUpdate(string slug, Guid eventId, CampEve logger.LogInformation("User {UserId} updated barrio event '{Title}' ({EventId})", user.Id, model.Title, eventId); - SetSuccess($"Event \"{model.Title}\" resubmitted for review."); + SetSuccess(FormatEventFeedback("Events_ResubmittedForReview", model.Title)); return RedirectToAction(nameof(MySubmissions)); } @@ -700,14 +702,14 @@ public async Task BarrioWithdraw(string slug, Guid eventId) if (!guideEvent.CanBeWithdrawnBySubmitter) { - SetError("This event cannot be withdrawn in its current state."); + SetError(localizer["Events_NotWithdrawable"].Value); return RedirectToAction(nameof(MySubmissions)); } await guide.WithdrawEventAsync(guideEvent); logger.LogInformation("User {UserId} withdrew barrio event '{Title}' ({EventId})", user.Id, guideEvent.Title, eventId); - SetSuccess($"Event \"{guideEvent.Title}\" withdrawn."); + SetSuccess(FormatEventFeedback("Events_Withdrawn", guideEvent.Title)); return RedirectToAction(nameof(MySubmissions)); } @@ -732,13 +734,13 @@ public async Task BulkUploadImport(string slug, IFormFile? file) var guideSettings = await guide.GetGuideSettingsAsync(); if (!IsSubmissionOpen(guideSettings)) { - SetError("The submission window is not currently open."); + SetError(localizer["Events_SubmissionWindowClosed"].Value); return RedirectToAction(nameof(MySubmissions)); } if (file == null || file.Length == 0) { - SetError("Please select a CSV file to upload."); + SetError(localizer["Events_UploadSelectCsv"].Value); return RedirectToAction(nameof(MySubmissions)); } @@ -762,7 +764,7 @@ public async Task BulkUploadImport(string slug, IFormFile? file) if (rows.Count == 0) { - SetError("The CSV had no event rows. Add at least one row below the header and try again."); + SetError(localizer["Events_UploadNoRows"].Value); return RedirectToAction(nameof(MySubmissions)); } @@ -782,7 +784,11 @@ public async Task BulkUploadImport(string slug, IFormFile? file) logger.LogInformation( "Bulk upload by user {UserId} for camp {CampId}: {Created} created, {Updated} updated.", user.Id, camp.Id, result.CreatedCount, result.UpdatedCount); - SetSuccess($"Bulk upload complete — {result.CreatedCount} created, {result.UpdatedCount} updated."); + SetSuccess(string.Format( + System.Globalization.CultureInfo.CurrentCulture, + localizer["Events_UploadComplete"].Value, + result.CreatedCount, + result.UpdatedCount)); return RedirectToAction(nameof(MySubmissions)); } @@ -790,6 +796,9 @@ public async Task BulkUploadImport(string slug, IFormFile? file) // camp is non-null at every call site; Slug is always set, so a name always resolves. private static string ResolveCampDisplayName(CampInfo camp) => ResolveCampName(camp)!; + private string FormatEventFeedback(string resourceKey, string title) => + string.Format(localizer[resourceKey].Value, title); + private async Task BuildBarrioFormAsync(string slug, CampInfo camp, EventSettingsInfo burn) { var model = new CampEventFormViewModel diff --git a/src/Sections/Humans.Events/Docs/debt.yml b/src/Sections/Humans.Events/Docs/debt.yml index 35d254b5e3..6f2b21adaa 100644 --- a/src/Sections/Humans.Events/Docs/debt.yml +++ b/src/Sections/Humans.Events/Docs/debt.yml @@ -30,5 +30,5 @@ inbox: review: light - added: 2026-09-09 id: EVENTS-7 - what: "Member-facing views ship hardcoded English: Views/Events/IndividualEventForm.cshtml (no Localizer reference at all), Views/Events/BarrioEventForm.cshtml (a stray Localizer reference aside, its labels, hints and buttons are literals) and Views/Shared/Components/EventsCard/Default.cshtml ('Events' header, 'Recurring event' tooltip). Browse, Schedule and MySubmissions are fully localized; the submit forms never were. Fix is a resx backfill across every supported culture plus the view edits — its own PR, not a side effect. Found by /section-doctor on Events 2026-09-09." + what: "Submit-form field labels still render hardcoded English: IndividualEventForm.cshtml and BarrioEventForm.cshtml draw their field labels via