From 7395935f86e4a518670beff99ca6697af57113f8 Mon Sep 17 00:00:00 2001 From: Dan Moisan Date: Sat, 12 Sep 2026 17:31:06 -0400 Subject: [PATCH 1/4] Prepare issue 742 quickfiler invariant culture: research, spec, user story, and preflight-cleared atomic plan --- ...reflight-round-1-delta.2026-09-12T17-05.md | 125 +++++ ...reflight-round-2-delta.2026-09-12T17-35.md | 59 +++ .../issue.md | 126 +++++ .../plan.2026-09-12T16-09.md | 204 ++++++++ ...ler-datetime-invariant-culture-research.md | 370 +++++++++++++ .../spec.md | 486 ++++++++++++++++++ .../user-story.md | 37 ++ 7 files changed, 1407 insertions(+) create mode 100644 docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/other/preflight-round-1-delta.2026-09-12T17-05.md create mode 100644 docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/other/preflight-round-2-delta.2026-09-12T17-35.md create mode 100644 docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/issue.md create mode 100644 docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/plan.2026-09-12T16-09.md create mode 100644 docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/research/2026-09-12T16-00-quickfiler-datetime-invariant-culture-research.md create mode 100644 docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/spec.md create mode 100644 docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/user-story.md diff --git a/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/other/preflight-round-1-delta.2026-09-12T17-05.md b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/other/preflight-round-1-delta.2026-09-12T17-05.md new file mode 100644 index 000000000..38a4cab23 --- /dev/null +++ b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/other/preflight-round-1-delta.2026-09-12T17-05.md @@ -0,0 +1,125 @@ +# Preflight Round 1 Delta — Issue #742 + +Timestamp: 2026-09-12T17-05 +Reviewer signal: PREFLIGHT: REVISIONS REQUIRED +Convergence signal: CONVERGENCE: FURTHER ROUNDS LIKELY +Plan under review: docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/plan.2026-09-12T16-09.md + +## How to apply this delta + +Apply each numbered item to the plan file named above, in place. Do not create a timestamped sibling +plan file. + +For every item, report one of these dispositions: + +- `applied-verbatim` +- `applied-with-mechanical-reassembly` (used only when a command or path was wrapped for width in this + artifact and had to be rejoined onto one line) +- `not-applied-with-reason` + +Do not silently substitute your own wording for a supplied item. Substituted text is unreviewed until +the following round, so each paraphrase converts a closed defect into a new unreviewed region. If you +judge a supplied item to be wrong, leave it unapplied and state the disagreement so the orchestrator can +adjudicate it. + +## Orchestrator verification of the round-1 findings + +The orchestrator independently re-ran the central finding (D1) against this worktree on 2026-09-12 +before relaying this delta: + +- `git grep -c '[.]ToString[(]@?"[^"]*[:/.\-][^"]*"[)]' -- ` printed no line + and exited 1. +- `git grep -c '[.]ToString[(]@\?"[^"]*[:/.\-][^"]*"[)]' -- ` printed the + per-file distribution 4, 4, 1, 3, 2 and exited 0. + +The finding is confirmed. The earlier figure of 14 recorded in the planning prompt was measured with the +Grep tool, which uses an extended-regular-expression engine in which a bare question mark is a +quantifier. The plan's acceptance conditions run through basic-regular-expression mode, in which a bare +question mark is a literal character. The two engines disagree, and the plan's own engine is the one +that decides whether its acceptance conditions can fail. + +Findings D3, D4, D5 and D6 were also confirmed by the orchestrator before delegation. Item O5 was +reviewed by the reviewer and found not to be a defect; no change is required for it, and the reviewer's +reasoning is accepted. + +## Already applied by the orchestrator — do not duplicate + +The same unescaped-question-mark defect was present in the residual-sweep acceptance criterion in +spec.md. The orchestrator has already corrected spec.md to use the escaped form and has added the +two-spelling measurement to that criterion's text. Do not edit spec.md. The plan must now match the +corrected spec. + +## Delta 1 — [P0-T9], replace item 1 of the eleven-command list with + +``` + 1. `git grep -c '[.]ToString[(]@\?"[^"]*[:/.\-][^"]*"[)]' -- QuickFiler/Controllers/QfcHomeController.Metrics.cs QuickFiler/Controllers/EfcHomeController.Metrics.cs QuickFiler/Controllers/QfcItemController.ViewerSetup.cs QuickFiler/Controllers/QfcCollectionController.cs QuickFiler/Controllers/EfcItemController.cs` — expected per-file counts 4, 4, 1, 3, 2 (14 total). The `@` is escaped as `@\?` rather than `@?` because `git grep`'s default basic-regex mode treats a bare `?` as a literal character, not a quantifier; `\?` is the documented GNU basic-regex extension for "optional preceding atom." An unescaped `@?` never matches anything in this repository (no site uses a verbatim string literal), so every acceptance condition built on this pattern would read as a permanent zero-match regardless of whether the fix was applied. +``` + +## Delta 2 — [P2-T3], replace the Acceptance sentence with + +``` +Acceptance: `git grep -c '[.]ToString[(]@\?"[^"]*[:/.\-][^"]*"[)]' -- QuickFiler/Controllers/EfcHomeController.Metrics.cs` prints no line for that path and exits 1 (baseline was 4, per P0-T9 control 1). +``` + +## Delta 3 — [P2-T4], replace the Run sentence with + +``` +Run `git grep -c '[.]ToString[(]@\?"[^"]*[:/.\-][^"]*"[)]' -- QuickFiler/Controllers/QfcHomeController.Metrics.cs QuickFiler/Controllers/EfcHomeController.Metrics.cs`. +``` + +## Delta 4 — [P3-T4], replace the Run sentence with + +``` +Run `git grep -c '[.]ToString[(]@\?"[^"]*[:/.\-][^"]*"[)]' -- QuickFiler/Controllers/QfcItemController.ViewerSetup.cs QuickFiler/Controllers/QfcCollectionController.cs QuickFiler/Controllers/EfcItemController.cs`. +``` + +## Delta 5 — [P4-T5], replace the Run sentence with + +``` +Run `git grep -c '[.]ToString[(]@\?"[^"]*[:/.\-][^"]*"[)]' -- QuickFiler/Controllers/QfcHomeController.Metrics.cs QuickFiler/Controllers/EfcHomeController.Metrics.cs QuickFiler/Controllers/QfcItemController.ViewerSetup.cs QuickFiler/Controllers/QfcCollectionController.cs QuickFiler/Controllers/EfcItemController.cs`. +``` + +## Delta 6 — [P0-T3], replace the whole task with + +``` +- [ ] [P0-T3] Bootstrap the toolchain. Run `pwsh -NoProfile -Command 'nuget restore TaskMaster.sln'` followed by `pwsh -NoProfile -Command 'dotnet tool restore'`, followed by `pwsh -NoProfile -Command 'if (Get-Command dotnet-coverage -ErrorAction SilentlyContinue) { Write-Output "DOTNET_COVERAGE_PRESENT" } else { dotnet tool install --global dotnet-coverage; Write-Output "DOTNET_COVERAGE_INSTALLED" }'` (P0-T8 and P5-T4 both require `dotnet-coverage` on PATH; `dotnet-tools.json` pins only `csharpier`, so `dotnet tool restore` alone does not provide it). Acceptance: all three `EXIT_CODE` values are 0, and the third command prints exactly one of `DOTNET_COVERAGE_PRESENT` or `DOTNET_COVERAGE_INSTALLED`. Record `Timestamp:`, `Command:` (all three), `EXIT_CODE:` (all three), `Output Summary:` in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/toolchain-bootstrap.2026-09-12T16-09.md`. +``` + +## Delta 7 — [P0-T8], replace the whole task with + +``` +- [ ] [P0-T8] Baseline `QuickFiler.Test` pass/fail state and coverage figure on the unfixed tree, using the repository's coverage runner, scoped to the `QuickFiler.Test` assembly only, with the raw Cobertura output written under the repository's gitignored `coverage\` directory and deleted by this task before completion (the script's `-CoverageOutput` parameter is passed through `Join-Path $repoRoot $CoverageOutput`, which does not special-case an already-rooted path, so an absolute value produces a malformed path; a relative value avoids this). Run `pwsh -NoProfile -Command './scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot QuickFiler.Test -Configuration Debug -CoverageOutput "coverage\issue-742-coverage\baseline.cobertura.xml"; Write-Output "EXITCODE=$LASTEXITCODE"'`. This inner invocation already carries `/InIsolation` and `/TestCaseFilter:TestCategory!=LiveOutlook` (see `scripts/vscode/Invoke-MSTestWithCoverage.ps1`'s own argument construction), satisfying the plan's vstest-invocation requirements; scoping `-SearchRoot` to `QuickFiler.Test` restricts assembly discovery to `QuickFiler.Test.dll` and avoids running the rest of the repository's test suite, including the UtilitiesCS.Test shell-icon classes known to stall vstest on this machine. Acceptance: none (baseline only). Open the produced Cobertura XML at `coverage\issue-742-coverage\baseline.cobertura.xml` and transcribe the `line-rate` figure for the classes covering `QfcHomeController.Metrics.cs`, `EfcHomeController.Metrics.cs`, and the `QfcItemController` `ViewerSetup` partial, together with the overall pass/fail/skip counts vstest reports; explicitly note that `QfcCollectionController` and `EfcItemController` carry a type-level `[ExcludeFromCodeCoverage]` attribute and are absent from the report, so their baseline quality signal is the named-test pass/fail state captured here, not a coverage percentage. Delete the `coverage\issue-742-coverage\` directory after transcribing. Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` (pass/fail/skip counts plus the three transcribed line-rate figures) in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/vstest-coverage-baseline.2026-09-12T16-09.md`. +``` + +## Delta 8 — [P5-T4], replace the whole task with + +``` +- [ ] [P5-T4] Run the `QuickFiler.Test` suite with coverage enabled via the repository's coverage runner, scoped to the `QuickFiler.Test` assembly only, again writing the raw Cobertura output under the repository's gitignored `coverage\` directory and deleting it before completion (same `Join-Path`/rooted-path reasoning as P0-T8). Run `pwsh -NoProfile -Command './scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot QuickFiler.Test -Configuration Debug -CoverageOutput "coverage\issue-742-coverage\final.cobertura.xml"; Write-Output "EXITCODE=$LASTEXITCODE"'`. Acceptance: `EXITCODE` is 0 (zero failures for `QuickFiler.Test`, including the two rewritten oracle tests and the five new tests; this inner invocation already carries `/InIsolation` and `/TestCaseFilter:TestCategory!=LiveOutlook`). If this fails or a prior step in this phase rewrote a tracked file since P5-T1, restart from P5-T1. Open the produced Cobertura XML at `coverage\issue-742-coverage\final.cobertura.xml`, transcribe the post-change `line-rate` figures for the same three non-excluded classes captured in P0-T8, state the baseline-vs-final comparison for each, and confirm no regression on the lines this change touched; also transcribe the overall pass/fail/skip counts. Delete the `coverage\issue-742-coverage\` directory after transcribing. Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/qa-gates/vstest-coverage-final.2026-09-12T16-09.md`. +``` + +## Delta 9 — [P5-T6], replace the whole task with + +``` +- [ ] [P5-T6] Confirm no `.trx` or Cobertura coverage XML file has been introduced as a tracked evidence artifact. Run `pwsh -NoProfile -Command '$m = git status --porcelain --untracked-files=all -- docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence | Select-String -Pattern "\.trx$|\.cobertura\.xml$"; $m; if ($m) { exit 0 } else { exit 1 }'`. Acceptance: the command prints no line and exits 1, matching this plan's zero-match reading convention. Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/qa-gates/no-trx-cobertura-evidence.2026-09-12T16-09.md`. +``` + +## Delta 10 — SELF-REVIEW block, replace the InternalsVisibleTo bullet with + +``` +- The QuickFiler production project file (not backticked here; it is out of scope and needs no edit), `QuickFiler/Controllers/QfcHomeController.cs`, and `Properties/AssemblyInfo.cs` — re-read (grep) in this pass; confirmed both files carry `[assembly: InternalsVisibleTo("QuickFiler.Test")]`, corroborating that `QuickFiler.Test` can call the `internal` `GetItemSummary()`, `TryGetMoveReadiness`, and `ToggleExpansionStyle` members exercised by Phase 1 Test 3 and Test 4. QfcHighConfidencePreFilter.cs and the Legacy folder's IAcceleratorCallbacks file were also read; both carry only `[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]`, which targets Moq's dynamic-proxy assembly and does not grant `QuickFiler.Test` access, so they are named here in plain prose without backticks rather than cited as supporting evidence; the Legacy file is additionally out of this issue's scope per the Scope and Non-Goals section. +``` + +## Delta 11 — orchestrator addition, preamble + +Add a sentence to the plan's existing zero-match reading convention paragraph recording the +regular-expression-dialect constraint, so a later revision does not reintroduce the defect: + +``` +The residual-sweep pattern is written with `@\?` rather than `@?` because these searches run in basic-regular-expression mode, in which a bare question mark is a literal character rather than an optional-atom quantifier. Both spellings were run against the unfixed tree on 2026-09-12: the escaped form printed the 4, 4, 1, 3, 2 per-file distribution and exited 0, and the unescaped form printed no line and exited 1. Any future edit to this pattern must keep the escape, or every acceptance condition built on it becomes unfalsifiable. +``` + +## Not in scope for this delta + +Item O5 (a `git commit` with no pathspec operand) was examined and dismissed by the reviewer, on the +grounds that the preceding staging task already scopes the index precisely and no hook in this +repository requires an explicit pathspec on a commit. No change is required. diff --git a/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/other/preflight-round-2-delta.2026-09-12T17-35.md b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/other/preflight-round-2-delta.2026-09-12T17-35.md new file mode 100644 index 000000000..c589583f5 --- /dev/null +++ b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/other/preflight-round-2-delta.2026-09-12T17-35.md @@ -0,0 +1,59 @@ +# Preflight Round 2 Delta — Issue #742 + +Timestamp: 2026-09-12T17-35 +Reviewer signal: PREFLIGHT: REVISIONS REQUIRED +Convergence signal: CONVERGENCE: NO FURTHER ROUNDS EXPECTED +Plan under review: docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/plan.2026-09-12T16-09.md + +## Outcome of round 2 + +Round 2 performed a full-document pass and confirmed as correct: the rewritten evidence-artifact +discipline paragraph, the AC11 mapping, the coverage-runner self-review bullet, both corrected spec +citation ranges, all three newly added supporting citations, all seventeen acceptance-criteria mappings, +all eleven baseline discovery-count controls re-run against the tree, the residual-count arithmetic under +the corrected regular expression, and the absence of any branch-coordination task. + +It found exactly one defect, carrying two faults in a single sentence of the planner's self-review +record, both introduced by the round-1 replacement text itself: + +1. A backtick leak. The bullet backticked the non-Metrics partial-class file of the QfcHomeController + class, which this plan's own scope-boundary paragraph and the spec's non-goals section both name as + out of scope. The change-footprint harvester collects whitespace-free backticked tokens and has no + notion of polarity, so that path would have been wrongly attributed to this item's write footprint. + This is the same defect class round 1 found in the same block, reintroduced by the fix for it. +2. A path error. The bullet cited a bare relative path for the assembly-info file. The file exists only + at its project-qualified path; nothing exists at the bare path. + +## Disposition + +The reviewer supplied verbatim replacement text and stated that it had checked that replacement against +the same rule it remediates. The orchestrator applied that text verbatim, with no paraphrase and no +additional edit, rather than spending a planner round on a single-sentence textual correction. The +rationale is that the previous revision round produced five self-initiated planner edits beyond the +supplied delta, each of which became a new unreviewed region requiring another review pass; applying +reviewer-authored text directly avoids adding such a region. + +The applied region is therefore text the round-2 reviewer wrote and verified. Provenance is recorded here +because the edited passage is the planner's own declaration block and the edit was not made by the +planner. + +## Orchestrator verification after applying + +- The plan validator returns ok with no warnings, which also confirms the file retained LF line endings. +- A fixed-string search for the backticked out-of-scope path across the plan and the spec prints no line + and exits 1. +- Discovery-count control for that negative result: the same search form, run for a backticked in-scope + path, printed 8 hits in the plan and 9 in the spec and exited 0. The search can therefore match, so the + zero result is a real observation rather than a search that matches nothing for an unrelated reason. + +## Reviewer observation deliberately NOT acted on + +Round 2 recorded one non-blocking observation and explicitly declined to require a delta for it: in the +two coverage tasks, the instruction to delete the raw-output directory sits after the transcription step, +so on a failing attempt the restart clause returns control before the deletion is reached and a stray raw +file can persist on disk between attempts. The reviewer noted that the directory is ignored by version +control, that no gate can observe the condition, and that the next successful attempt overwrites and then +deletes the same path. + +No change was made. A reviewer-declined optional change is not adopted on the orchestrator's own +initiative, because an elective edit widens the delta and the substitute text would itself be unreviewed. diff --git a/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/issue.md b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/issue.md new file mode 100644 index 000000000..097bdc3aa --- /dev/null +++ b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/issue.md @@ -0,0 +1,126 @@ +# quickfiler-date-time-format-missing-invariant-culture (Issue #742) + +- Date captured: 2026-09-02 +- Author: Dan Moisan +- Status: Promoted -> docs/features/active/quickfiler-date-time-format-missing-invariant-culture/ (Issue #742) + +> Automation note: Keep the section headings below unchanged; the promotion tooling maps each of them into the GitHub bug issue template. + +- Issue: #742 +- Issue URL: https://github.com/drmoisan/TaskMaster/issues/742 +- Last Updated: 2026-09-02 +- Work Mode: full-bug + +## Summary + +Every date/time `.ToString(...)` call in `QfcHomeController.Metrics.cs`, `EfcHomeController.Metrics.cs`, +`QfcItemController.ViewerSetup.cs`, `QfcCollectionController.cs`, and `EfcItemController.cs` omits +`CultureInfo.InvariantCulture`, while the adjacent numeric fields in the same methods pass it +explicitly. The `:` character in a .NET custom format string is the `TimeSeparator` custom specifier, +not a literal colon, so it resolves to `DateTimeFormatInfo.TimeSeparator` for whichever culture is in +effect. Under a culture such as `it-IT`, `TimeSeparator` is `.` rather than `:`, so `now.ToString("HH:mm")` +can render `13.05` instead of `13:05` if the host machine's regional settings differ from the author's +assumption. + +## Environment + +- OS/version: Windows 11, Outlook VSTO add-in host +- Python version: not applicable (C# / .NET Framework 4.8) +- Command/flags used: not applicable (static code review finding) +- Data source or fixture: the session-metrics CSV emitted by the QuickFiler and EFC metrics writers, and + on-screen/exception summary strings built from `SentDate` + +## Steps to Reproduce + +1. Set the Windows regional format (or thread `CurrentCulture`) to a locale whose + `DateTimeFormatInfo.TimeSeparator` is not `:` (for example `it-IT`, whose separator is `.`). +2. Run a QuickFiler filing session or an EFC move session so the session-metrics CSV writer executes, + or trigger a code path that builds a `SentDate`/`SentTime` display string. +3. Inspect the emitted CSV time column, or the on-screen/exception summary string. + +## Expected Behavior + +Every date/time field emitted by these files renders with a fixed, invariant separator regardless of +the host machine's regional settings, matching the invariant-culture handling already applied to the +adjacent numeric fields in the same methods (per the existing comment at +`EfcHomeController.Metrics.cs:101-103`: "the metrics file is machine-read, so numeric fields are +rendered with the invariant culture rather than the operator's locale"). + +## Actual Behavior + +None of the date/time `.ToString(...)` calls in the affected files pass `CultureInfo.InvariantCulture`, +so the rendered separator character is culture-dependent. Confirmed sites: + +- `QuickFiler/Controllers/QfcHomeController.Metrics.cs`: `curDateText`/`dataLineBeg` + (`"MM/dd/yyyy"`, `"hh:mm"`/`"HH:mm"` after issue #645's fix) at lines 48 and 127. +- `QuickFiler/Controllers/EfcHomeController.Metrics.cs`: `curDateText`, `curTimeText` + (`"MM/dd/yyyy"`, `"hh:mm"`/`"HH:mm"` after issue #645's fix) at line 95-96, and the `SentDate` + field at lines 118-119 (`"MM/dd/yyyy"`, `"HH:mm:ss"`) — `SentDate` is the CSV field issue #645 cites + as the target 24-hour convention, and it is itself uncultured. +- `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs:498` — `SentDate.ToString("HH:mm")` in a + user-facing summary string. +- `QuickFiler/Controllers/QfcCollectionController.cs:1294,2300` — `SentDate.ToString("HH:mm")` in + controller/log summary strings. +- `QuickFiler/Controllers/EfcItemController.cs:612` — `SentDate.ToString("HH:mm")`, exposed as the + `SentTime` property. + +The CSV-writer sites (`QfcHomeController.Metrics.cs`, `EfcHomeController.Metrics.cs`) carry the higher +severity, since a wrong separator corrupts a machine-read artifact in the same way the numeric fields +were already protected against (see the comment cited above); the UI-facing sites +(`QfcItemController.ViewerSetup.cs`, `QfcCollectionController.cs`, `EfcItemController.cs`) are lower +severity, cosmetic-only. + +## Logs / Screenshots + +- [x] Attached minimal logs or snippet +- Snippet: none of the `.ToString("MM/dd/yyyy")`, `.ToString("hh:mm")`/`.ToString("HH:mm")`, or + `.ToString("HH:mm:ss")` calls at the sites listed above pass a `CultureInfo` argument; contrast with + the `durationText`/`durationMinutesText` calls in the same two Metrics.cs files, which do + (`.ToString("##0", CultureInfo.InvariantCulture)`). + +## Impact / Severity + +- [ ] Blocker +- [ ] High +- [x] Medium +- [ ] Low + +Medium for the two CSV-writer files (a wrong time separator on a machine-read, write-only artifact is +the same defect class already fixed for the numeric fields); Low/cosmetic for the three UI-facing +`ToString("HH:mm")` call sites. Filed as a single Medium entry since the writer sites dominate. + +## Suspected Cause / Notes + +Discovered during research for issue #645 ("Bug: quickfiler-session-metrics-twelve-hour-time-format") +while investigating whether `CultureInfo.InvariantCulture` should be added to the three `hh:mm` -> `HH:mm` +sites that issue fixes. Tracing the issue's own cited target convention (`SentDate`'s `"HH:mm:ss"`) +showed that convention is itself uncultured, which means the gap is systemic across both files rather +than isolated to the three sites issue #645 touches. Deliberately excluded from issue #645's scope: it +is a distinct defect (culture-dependent separator vs. 12-hour ambiguity), and issue #645's own +acceptance criteria do not mention culture-invariance. + +Confirmed the `H`/`h`/`m` letter specifiers in these format strings render ASCII 0-9 digits regardless +of culture (they are not digit-substituting specifiers); the only culture-dependent element in +`"HH:mm"`/`"MM/dd/yyyy"`/`"HH:mm:ss"` is the separator character, which resolves via +`DateTimeFormatInfo.DateSeparator` / `DateTimeFormatInfo.TimeSeparator`. + +## Proposed Fix / Validation Ideas + +- [x] Unit coverage areas: add or extend clock-seam tests in + `QuickFiler.Test/Controllers/QfcHomeControllerMetricsTests.cs` and + `QuickFiler.Test/Controllers/EfcHomeControllerMetricsTests.cs` that assert output is stable under a + non-`:`-separator culture (e.g. temporarily setting `CurrentCulture` to `it-IT` within the test, or + asserting the invariant-culture literal directly), plus coverage for the UI-facing sites if they have + existing tests. +- [x] Integration scenario to retest: a full `QuickFiler.Test` run after adding + `CultureInfo.InvariantCulture` to every date/time `.ToString(...)` call in the five files listed + above, confirming no existing literal assertions regress (they should not, since the default test + culture is invariant-equivalent for `:`/`.`/`-` separators in `en-US`). +- [x] Manual verification notes: a repository search for `.ToString("MM/dd/yyyy")`, + `.ToString("HH:mm")`, `.ToString("hh:mm")`, or `.ToString("HH:mm:ss")` under `QuickFiler/Controllers/` + with no adjacent `CultureInfo.InvariantCulture` argument should return no match after the fix. + +## Next Step + +- [ ] Promote to GitHub issue (bug-report template) +- [ ] Move to active fix folder / branch diff --git a/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/plan.2026-09-12T16-09.md b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/plan.2026-09-12T16-09.md new file mode 100644 index 000000000..1839f6490 --- /dev/null +++ b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/plan.2026-09-12T16-09.md @@ -0,0 +1,204 @@ +# quickfiler-date-time-format-missing-invariant-culture (Plan) + +- **Issue:** #742 +- **Parent (optional):** none +- **Owner:** drmoisan +- **Last Updated:** 2026-09-12T16-09 +- **Status:** Draft +- **Version:** 0.3 +- **Work Mode:** full-bug (acceptance criteria live exclusively in `spec.md`; `user-story.md`, if present, carries no acceptance criteria per its own note) + +**Task totals (mechanically counted, pattern `^- \[ \] \[P\d+-T\d+\]`):** Phase 0 = 10, Phase 1 = 4, Phase 2 = 4, Phase 3 = 4, Phase 4 = 5, Phase 5 = 9. Total = 36. + +**Line-anchoring convention (read before executing any fix task):** every edit task below names the literal source text it replaces, not only a line number. An earlier task in this plan can shift the line numbers of statements below it in the same file (for example, Phase 2's rewrite of the one-line interpolation at former line 48 of `QuickFiler/Controllers/QfcHomeController.Metrics.cs` grows to more than one line, moving every later line in that file down). Line numbers stated anywhere below are locating hints only, measured against the tree as read on 2026-09-12; the literal text is the authority for where to make each edit. + +**Execution-phase note (record only, not to act on during planning):** `pwsh` is refused under this Agent's worktree isolation. The execution child assigned to run this plan MUST be launched NON-ISOLATED, because every C# toolchain command below is a `pwsh -NoProfile -Command '...'` invocation and an isolated child cannot run `pwsh` at all. + +**Toolchain restart rule:** for every task in Phase 5, and for the Phase 0 baseline formatter check, if a write-mode step fails or rewrites a tracked file, restart the affected sequence from its first step before re-checking the acceptance criterion. + +**Outlook handling:** Outlook must be closed through its own UI, never killed via a process-termination command, before any rebuild step in this plan (a running Outlook process holds the QuickFiler VSTO build output locked and the rebuild fails with MSB3021). No task in this plan issues a process-kill command. + +**Evidence location:** every evidence artifact below resolves under `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence//` using kinds `baseline`, `regression-testing`, `qa-gates`, `other`. No other evidence location is used. + +**Evidence-artifact discipline (per the maintainer decision on issue #671, effective 2026-09-11):** no task commits a raw test-results file carrying the `.trx` extension, and no task commits a raw Cobertura coverage XML file, as an evidence artifact anywhere in the repository. Where a coverage-collection step needs a raw output file, it is written under the repository's gitignored `coverage\` directory (this plan uses `coverage\issue-742-coverage\`, matched by the `coverage/*` entry in `.gitignore`), the needed numeric figures are transcribed into the Markdown evidence artifact, and the raw output directory is deleted by the same task before the task is considered complete. + +**Grep-visibility convention (untracked file):** `QuickFiler.Test/Controllers/QuickFilerInvariantCultureIssue742Tests.cs` is created by this plan and remains untracked until the Phase 5 staging task. Every `git grep` command in this plan that targets that file therefore carries the `--untracked` flag. Every other file this plan edits is already tracked, so `git grep` against those files needs no `--untracked` flag. + +**Zero-match reading convention:** `git grep -c PATTERN -- path` prints no line at all, and exits `1`, when `path` has zero matching lines; it never prints a `0`. Every acceptance condition below that expects zero matches is phrased as "prints no line for that path and exits 1," never as "returns 0," per the verification note at the top of `spec.md`'s Acceptance Criteria section. The residual-sweep pattern is written with `@\?` rather than `@?` because these searches run in basic-regular-expression mode, in which a bare question mark is a literal character rather than an optional-atom quantifier. Both spellings were run against the unfixed tree on 2026-09-12: the escaped form printed the 4, 4, 1, 3, 2 per-file distribution and exited 0, and the unescaped form printed no line and exited 1. Any future edit to this pattern must keep the escape, or every acceptance condition built on it becomes unfalsifiable. + +**Scope boundary (plain prose, no backticks, per the change-footprint harvester's whitespace-free-backtick-token rule):** this change does not touch the UtilitiesCS project, the ToDoModel project, the uncompiled sources under the QuickFiler Legacy folder, the non-Metrics partial-class file of the QfcHomeController class, the sortable-key method in the EmailSorter file, the repository's editor-configuration file, its banned-symbols list, or the QuickFiler production project file, which needs no edit because no production source file is added or deleted by this change. + +**Known scheduling contention (no action required):** two sibling work items scheduled in the same run also edit `QuickFiler/Controllers/QfcCollectionController.cs` and `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` respectively. This plan contains no branch-coordination step, no rebase-onto-sibling step, and no wait-for-sibling step; that contention is resolved by the scheduler, not by this plan. + +--- + +### Phase 0 — Policy Reads and Toolchain Baseline + +- [ ] [P0-T1] Read, in this order, `CLAUDE.md`, `.claude/rules/general-code-change.md`, `.claude/rules/general-unit-test.md`, `.claude/rules/csharp.md`, and `.claude/rules/tonality.md`. Write `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/phase0-instructions-read.2026-09-12T16-09.md` containing `Timestamp:`, `Policy Order:` (the five files in the order read), and an explicit list of the five file paths. Acceptance: the artifact file exists and contains all five listed paths and both required fields. + +- [ ] [P0-T2] Confirm Outlook is closed before any rebuild in this plan. Run `pwsh -NoProfile -Command 'if (Get-Process OUTLOOK -ErrorAction SilentlyContinue) { Write-Output "OUTLOOK_RUNNING" } else { Write-Output "OUTLOOK_CLOSED" }'`. Acceptance: the printed line is exactly `OUTLOOK_CLOSED`. If `OUTLOOK_RUNNING` is printed, close Outlook through its own UI (never a process-termination command) and re-run this task before proceeding. Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/outlook-closed-confirmation.2026-09-12T16-09.md`. + +- [ ] [P0-T3] Bootstrap the toolchain. Run `pwsh -NoProfile -Command 'nuget restore TaskMaster.sln'` followed by `pwsh -NoProfile -Command 'dotnet tool restore'`, followed by `pwsh -NoProfile -Command 'if (Get-Command dotnet-coverage -ErrorAction SilentlyContinue) { Write-Output "DOTNET_COVERAGE_PRESENT" } else { dotnet tool install --global dotnet-coverage; Write-Output "DOTNET_COVERAGE_INSTALLED" }'` (P0-T8 and P5-T4 both require `dotnet-coverage` on PATH; `dotnet-tools.json` pins only `csharpier`, so `dotnet tool restore` alone does not provide it). Acceptance: all three `EXIT_CODE` values are 0, and the third command prints exactly one of `DOTNET_COVERAGE_PRESENT` or `DOTNET_COVERAGE_INSTALLED`. Record `Timestamp:`, `Command:` (all three), `EXIT_CODE:` (all three), `Output Summary:` in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/toolchain-bootstrap.2026-09-12T16-09.md`. + +- [ ] [P0-T4] Baseline formatting state, captured BEFORE any write-mode formatter task runs anywhere in this plan (a baseline taken after `csharpier format` has already repaired drift would make the Phase 5 clean-pass gate either a blanket waiver or unsatisfiable). Run the read-only `pwsh -NoProfile -Command 'dotnet tool run csharpier check .'`. Acceptance: none (baseline only). Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` (the literal exit code and whatever diff/no-diff summary the command prints) in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/csharpier-check.2026-09-12T16-09.md`. + +- [ ] [P0-T5] Baseline analyzer state. Run `pwsh -NoProfile -Command '$out = msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true 2>&1; $exit = $LASTEXITCODE; $out | Select-String -Pattern "^\s*\d+ Error\(s\)\s*$" | ForEach-Object { $_.Line.Trim() }; Write-Output "EXITCODE=$exit"'`. Acceptance: none (baseline only). Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` (the transcribed `N Error(s)` line and the exit code) in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/msbuild-analyzer.2026-09-12T16-09.md`. + +- [ ] [P0-T6] Baseline nullable/warnings-as-errors state. Run `pwsh -NoProfile -Command '$out = msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true 2>&1; $exit = $LASTEXITCODE; $out | Select-String -Pattern "^\s*\d+ Error\(s\)\s*$" | ForEach-Object { $_.Line.Trim() }; Write-Output "EXITCODE=$exit"'`. Do not add `/p:Nullable=enable`. Acceptance: none (baseline only). Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/msbuild-nullable.2026-09-12T16-09.md`. + +- [ ] [P0-T7] Build the current (unfixed) `QuickFiler.Test` assembly so a baseline test run is possible. Run `pwsh -NoProfile -Command 'msbuild TaskMaster.sln /t:Build /m /p:Configuration=Debug "/p:Platform=Any CPU"'`. Acceptance: `EXIT_CODE` is 0. Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/quickfiler-test-build.2026-09-12T16-09.md`. + +- [ ] [P0-T8] Baseline `QuickFiler.Test` pass/fail state and coverage figure on the unfixed tree, using the repository's coverage runner, scoped to the `QuickFiler.Test` assembly only, with the raw Cobertura output written under the repository's gitignored `coverage\` directory and deleted by this task before completion (the script's `-CoverageOutput` parameter is passed through `Join-Path $repoRoot $CoverageOutput`, which does not special-case an already-rooted path, so an absolute value produces a malformed path; a relative value avoids this). Run `pwsh -NoProfile -Command './scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot QuickFiler.Test -Configuration Debug -CoverageOutput "coverage\issue-742-coverage\baseline.cobertura.xml"; Write-Output "EXITCODE=$LASTEXITCODE"'`. This inner invocation already carries `/InIsolation` and `/TestCaseFilter:TestCategory!=LiveOutlook` (see `scripts/vscode/Invoke-MSTestWithCoverage.ps1`'s own argument construction), satisfying the plan's vstest-invocation requirements; scoping `-SearchRoot` to `QuickFiler.Test` restricts assembly discovery to `QuickFiler.Test.dll` and avoids running the rest of the repository's test suite, including the UtilitiesCS.Test shell-icon classes known to stall vstest on this machine. Acceptance: none (baseline only). Open the produced Cobertura XML at `coverage\issue-742-coverage\baseline.cobertura.xml` and transcribe the `line-rate` figure for the classes covering `QfcHomeController.Metrics.cs`, `EfcHomeController.Metrics.cs`, and the `QfcItemController` `ViewerSetup` partial, together with the overall pass/fail/skip counts vstest reports; explicitly note that `QfcCollectionController` and `EfcItemController` carry a type-level `[ExcludeFromCodeCoverage]` attribute and are absent from the report, so their baseline quality signal is the named-test pass/fail state captured here, not a coverage percentage. Delete the `coverage\issue-742-coverage\` directory after transcribing. Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` (pass/fail/skip counts plus the three transcribed line-rate figures) in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/vstest-coverage-baseline.2026-09-12T16-09.md`. + +- [ ] [P0-T9] Baseline discovery-count controls, re-derived against the current tree in this pass (values confirmed 2026-09-12), written BEFORE any Write Set source file is modified. Run each of these eleven `git grep` commands and transcribe the exact printed output and exit code of each: + 1. `git grep -c '[.]ToString[(]@\?"[^"]*[:/.\-][^"]*"[)]' -- QuickFiler/Controllers/QfcHomeController.Metrics.cs QuickFiler/Controllers/EfcHomeController.Metrics.cs QuickFiler/Controllers/QfcItemController.ViewerSetup.cs QuickFiler/Controllers/QfcCollectionController.cs QuickFiler/Controllers/EfcItemController.cs` — expected per-file counts 4, 4, 1, 3, 2 (14 total). The `@` is escaped as `@\?` rather than `@?` because `git grep`'s default basic-regex mode treats a bare `?` as a literal character, not a quantifier; `\?` is the documented GNU basic-regex extension for "optional preceding atom." An unescaped `@?` never matches anything in this repository (no site uses a verbatim string literal), so every acceptance condition built on this pattern would read as a permanent zero-match regardless of whether the fix was applied. + 2. `git grep -c '[{]now:' -- QuickFiler/Controllers/QfcHomeController.Metrics.cs` — expected 1. + 3. `git grep -c 'CultureInfo[.]InvariantCulture' -- QuickFiler/Controllers/QfcHomeController.Metrics.cs` — expected 4. + 4. `git grep -c 'CultureInfo[.]InvariantCulture' -- QuickFiler/Controllers/EfcHomeController.Metrics.cs` — expected 2. + 5. `git grep -c -F 'Compile Include' -- QuickFiler.Test/QuickFiler.Test.csproj` — expected 172. + 6. `git grep -c -F 'QuickFilerInvariantCultureIssue742Tests' -- QuickFiler.Test/QuickFiler.Test.csproj` — expected: prints no line, exits 1. + 7. `git grep -c -F 'using System.Globalization;' -- QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` — expected: prints no line, exits 1. + 8. `git grep -c -F 'using System.Globalization;' -- QuickFiler/Controllers/QfcCollectionController.cs` — expected: prints no line, exits 1. + 9. `git grep -c -F 'using System.Globalization;' -- QuickFiler/Controllers/EfcItemController.cs` — expected: prints no line, exits 1. + 10. `git grep -c -F 'expectedLocal.ToString("MM/dd/yyyy") + "," + expectedLocal.ToString("HH:mm") + ",";' -- QuickFiler.Test/Controllers/QfcHomeControllerMetricsTests.cs` — expected 2 (the identical expected-value construction at the currently-observed lines 243 and 278). + 11. `git grep -c -F 'CultureInfo.InvariantCulture' -- QuickFiler.Test/Controllers/QfcHomeControllerMetricsTests.cs` — expected: prints no line, exits 1. + Acceptance: `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/discovery-count-controls.2026-09-12T16-09.md` exists and records all eleven commands with `Timestamp:`, `Command:` (all eleven), `EXIT_CODE:` (each), and `Output Summary:` (each figure exactly as stated above). This artifact is the baseline evidence artifact required by the Acceptance Criteria's discovery-count-figures item, and it is written before P1-T1 (the first task that creates or modifies any Write Set file). + +- [ ] [P0-T10] Confirm the two `[ExcludeFromCodeCoverage]` attributes this plan relies on for its coverage-signal framing are present as expected. Run `git grep -c -F '[ExcludeFromCodeCoverage]' -- QuickFiler/Controllers/QfcCollectionController.cs QuickFiler/Controllers/EfcItemController.cs`. Acceptance: the command prints exactly one matching line for each of the two files (`QuickFiler/Controllers/QfcCollectionController.cs:1` and `QuickFiler/Controllers/EfcItemController.cs:1`) and `EXIT_CODE` is 0. Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/excludefromcodecoverage-confirmation.2026-09-12T16-09.md`. + +### Phase 1 — Regression Tests Demonstrating the Defect + +- [ ] [P1-T1] [expect-fail] Create `QuickFiler.Test/Controllers/QuickFilerInvariantCultureIssue742Tests.cs` containing exactly five `[TestMethod]` tests inside one `[TestClass]`, one test per production file, each following the try/finally culture-swap pattern already established at `QuickFiler.Test/Controllers/QfcHomeControllerMetricsTests.cs:183-213` and `QuickFiler.Test/Controllers/EfcHomeControllerMetricsTests.cs:136-158` (clone a `CultureInfo`, assign explicit sentinel characters — neither `/` nor `:` — to `DateTimeFormat.DateSeparator` and `DateTimeFormat.TimeSeparator`, activate it as `CultureInfo.CurrentCulture` inside a `try` block, restore the original culture in a `finally` block), asserting with FluentAssertions that production output contains `/` and/or `:` rather than the sentinel character: + - Test 1, for `QuickFiler/Controllers/QfcHomeController.Metrics.cs`: build a `QfcHomeController` with a loose `Mock` for `_formController.Groups` (mirroring the existing `BuildLooseMetricsController` helper pattern in `QfcHomeControllerMetricsTests.cs`), assign a `FakeTimeProvider` to the injectable `TimeProvider` property, call `QuickFileMetrics_WRITE` or `WriteMetricsAsync`, and assert the captured `dataLineBeg` argument contains `/` and `:`. + - Test 2, for `QuickFiler/Controllers/EfcHomeController.Metrics.cs`: call the internal static `BuildQuickFileMetricLines` directly with a literal `DateTime` argument and a one-item `List`, and assert the returned line contains `/` and `:` (both the leading date/time fields and the trailing `itemInfo.SentDate` fields). + - Test 3, for `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs`: construct a `QfcItemController` via `FormatterServices.GetUninitializedObject(typeof(QfcItemController))` (mirroring the established pattern in `QuickFiler.Test/Controllers/EfcItemControllerTests.cs:200,315`), assign a directly constructed `MailItemHelper` with `SentDate` set to the public `ItemHelper` property, call the internal `GetItemSummary()`, and assert the returned string contains `/` and `:`. + - Test 4, for `QuickFiler/Controllers/QfcCollectionController.cs`: build a `QfcCollectionController` via `QfcCollectionControllerTestSupport.CreateUninitializedController()` (the established helper in the `QfcCollectionController.TestSupport.cs` companion file used by `QfcCollectionControllerDefects468MoveTests.cs`), populate `_itemGroups` via `QfcCollectionControllerTestSupport.SetField` with one `QfcItemGroup` whose `ItemController` is a loose `Mock` exposing `.Mail` returning a loose `Mock` with `.SentOn` set (for the `TryGetMoveReadiness` site at line 235) and `.ItemHelper` returning a directly constructed `MailItemHelper` with `SentDate` set and `.IsActiveUI` returning `false` (for the `ToggleExpansionStyle` site at line 1296), and separately populate `_itemGroupsToMove` via the same helper with one `QfcItemGroup` whose `ItemController.ItemHelper.SentDate` is set (for the `GetMoveDiagnostics` site at line 2302); call `TryGetMoveReadiness`, `ToggleExpansionStyle`, and `GetMoveDiagnostics` in turn and assert each of the three outputs contains `/` and `:`. + - Test 5, for `QuickFiler/Controllers/EfcItemController.cs`: construct an `EfcItemController` via `FormatterServices.GetUninitializedObject(typeof(EfcItemController))`, assign a directly constructed `MailItemHelper` with `SentDate` set to the private `_itemInfo` field via the established `SetPrivateField` reflection helper already present in `QuickFiler.Test/Controllers/EfcItemControllerTests.cs`, read the `SentDate` and `SentTime` properties, and assert both returned strings contain `/` and `:` respectively. + No test in this file uses a temporary file, `Thread.Sleep`, or `Task.Delay`. Acceptance: `git grep --untracked -c -F '[TestMethod]' -- QuickFiler.Test/Controllers/QuickFilerInvariantCultureIssue742Tests.cs` prints the count `5` for that path and `EXIT_CODE` is 0. + +- [ ] [P1-T2] Add exactly one new line, ``, inside the single `` element that opens at line 57 of `QuickFiler.Test/QuickFiler.Test.csproj` (the element closes at line 230 and holds every other `Controllers\` compile entry, including `` at line 199; anchor the insertion adjacent to that literal line rather than by line number). Acceptance: `git grep -c -F 'QuickFilerInvariantCultureIssue742Tests' -- QuickFiler.Test/QuickFiler.Test.csproj` prints the count `1` (baseline was: prints no line, exits 1, per P0-T9 control 6), and `git grep -c -F 'Compile Include' -- QuickFiler.Test/QuickFiler.Test.csproj` prints the count `173` (baseline was 172, per P0-T9 control 5). + +- [ ] [P1-T3] Build the test project with the new file included, against the still-unfixed production tree. Run `pwsh -NoProfile -Command 'msbuild TaskMaster.sln /t:Build /m /p:Configuration=Debug "/p:Platform=Any CPU"'`. Acceptance: `EXIT_CODE` is 0 (the new test file compiles even though production is not yet fixed, because the test only reads production output rather than requiring production behavior to already be correct in order to compile). + +- [ ] [P1-T4] [expect-fail] Run `pwsh -NoProfile -Command '$vswherePath = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"; $vstestPath = (& $vswherePath -latest -products * -find "Common7\IDE\Extensions\TestPlatform\vstest.console.exe") | Select-Object -First 1; & $vstestPath "QuickFiler.Test\bin\Debug\QuickFiler.Test.dll" /TestCaseFilter:"FullyQualifiedName~QuickFilerInvariantCultureIssue742Tests" /InIsolation /Logger:"console;verbosity=normal"; Write-Output "EXITCODE=$LASTEXITCODE"'`. Acceptance: `EXITCODE` is non-zero, demonstrating the defect against unfixed production code. Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` (the printed test-run summary and the non-zero exit code observed) in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/regression-testing/expect-fail-new-tests.2026-09-12T16-09.md`. + +### Phase 2 — Fix: Session-Metrics Writer Files + +- [ ] [P2-T1] In `QuickFiler/Controllers/QfcHomeController.Metrics.cs`, replace the literal statement `dataLineBeg = $"{now:MM/dd/yyyy},{now:HH:mm},";` with two explicit two-argument `ToString` calls passing `CultureInfo.InvariantCulture`, matching the shape already used two lines below at (currently) lines 125 and 127 of the same file, concatenated with the identical comma placement so the CSV field count and order do not change: `dataLineBeg = now.ToString("MM/dd/yyyy", CultureInfo.InvariantCulture) + "," + now.ToString("HH:mm", CultureInfo.InvariantCulture) + ",";`. Do not edit the commented-out predecessor lines immediately above it (`//var curDateText = ...`, `//var curTimeText = ...`, `//dataLineBeg = ...`). Acceptance: `git grep -c '[{]now:' -- QuickFiler/Controllers/QfcHomeController.Metrics.cs` prints no line for that path and exits 1 (baseline was 1, per P0-T9 control 2). + +- [ ] [P2-T2] In `QuickFiler/Controllers/QfcHomeController.Metrics.cs`, append `, CultureInfo.InvariantCulture` as the second argument to the two literal statements `curDateText = now.ToString("MM/dd/yyyy");` and `curTimeText = now.ToString("HH:mm");` inside `WriteMetricsAsync` (located immediately below the statement P2-T1 replaced; anchor on this literal text, not on a line number, since P2-T1's edit shifts every subsequent line number in this file), changing only the argument list at each site. Acceptance: `git grep -c 'CultureInfo[.]InvariantCulture' -- QuickFiler/Controllers/QfcHomeController.Metrics.cs` prints a count strictly greater than `4` (baseline was 4, per P0-T9 control 3; the exact post-fix figure is not fixed here because CSharpier decides whether the two edited statements land on one line or wrap across two apiece in Phase 5, and a predicted figure would be a formatter artifact rather than a property of the change). + +- [ ] [P2-T3] In `QuickFiler/Controllers/EfcHomeController.Metrics.cs`, append `, CultureInfo.InvariantCulture` as the second argument to the four literal statements `currentDateTime.ToString("MM/dd/yyyy")`, `currentDateTime.ToString("HH:mm")`, `itemInfo.SentDate.ToString("MM/dd/yyyy")`, and `itemInfo.SentDate.ToString("HH:mm:ss")` (currently at lines 95, 96, 118, and 119 respectively), changing only the argument list at each site (`using System.Globalization;` is already present at line 3 of this file; no new `using` directive is added here). Acceptance: `git grep -c '[.]ToString[(]@\?"[^"]*[:/.\-][^"]*"[)]' -- QuickFiler/Controllers/EfcHomeController.Metrics.cs` prints no line for that path and exits 1 (baseline was 4, per P0-T9 control 1). + +- [ ] [P2-T4] Verify the combined result of P2-T1 through P2-T3, scoped to only the two Metrics files (the three UI-facing files are fixed in Phase 3 and are deliberately excluded from this check). Run `git grep -c '[.]ToString[(]@\?"[^"]*[:/.\-][^"]*"[)]' -- QuickFiler/Controllers/QfcHomeController.Metrics.cs QuickFiler/Controllers/EfcHomeController.Metrics.cs`. Acceptance: the command prints exactly one line, `QuickFiler/Controllers/QfcHomeController.Metrics.cs:2` (the two commented-out predecessor statements, unedited by this change), prints no line for `QuickFiler/Controllers/EfcHomeController.Metrics.cs`, and `EXIT_CODE` is 0 (baseline combined total for these two files was 4 + 4 = 8, per P0-T9 control 1). Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/regression-testing/phase2-verification.2026-09-12T16-09.md`. + +### Phase 3 — Fix: UI-Facing Files + +- [ ] [P3-T1] Add `using System.Globalization;` to `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` (placed alongside the file's existing `using System.*` directives at the top of the file, which currently has no `System.Globalization` entry), and append `, CultureInfo.InvariantCulture` as the second argument to both `ToString` calls inside the literal statement `$"Subject: {ItemHelper.Subject} sent on {ItemHelper.SentDate.ToString("MM/dd/yyyy")} at {ItemHelper.SentDate.ToString("HH:mm")} by {ItemHelper.SenderName}"` (currently at line 465, inside `GetItemSummary`), changing only the two `ToString` argument lists; the interpolation itself is unaffected because these are ordinary method calls evaluated inside an interpolation hole, not interpolated format specifiers. Acceptance: `git grep -c -F 'using System.Globalization;' -- QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` prints the count `1` (baseline was: prints no line, exits 1, per P0-T9 control 7). + +- [ ] [P3-T2] Add `using System.Globalization;` to `QuickFiler/Controllers/QfcCollectionController.cs` (placed alongside the file's existing `using System.*` directives, which currently has no `System.Globalization` entry), and append `, CultureInfo.InvariantCulture` as the second argument to all five `ToString` calls: the literal `grp.ItemController.Mail.SentOn.ToString("MM/dd/yyyy")` (currently line 235, inside `TryGetMoveReadiness`), the two calls in the literal `c.ItemHelper.SentDate.ToString("MM/dd/yyyy")` / `c.ItemHelper.SentDate.ToString("HH:mm")` (currently line 1296, inside `ToggleExpansionStyle`'s exception-message builder), and the two calls in the literal `qf.ItemHelper.SentDate.ToString("MM/dd/yyyy")` / `qf.ItemHelper.SentDate.ToString("HH:mm")` (currently line 2302, inside the `GetMoveDiagnostics` line builder), changing only the argument lists. Acceptance: `git grep -c -F 'using System.Globalization;' -- QuickFiler/Controllers/QfcCollectionController.cs` prints the count `1` (baseline was: prints no line, exits 1, per P0-T9 control 8). + +- [ ] [P3-T3] Add `using System.Globalization;` to `QuickFiler/Controllers/EfcItemController.cs` (placed alongside the file's existing `using System.*` directives, which currently has no `System.Globalization` entry), and append `, CultureInfo.InvariantCulture` as the second argument to the two literal statements `_itemInfo.SentDate.ToString("MM/dd/yyyy")` (currently line 607, the `SentDate` property getter) and `_itemInfo.SentDate.ToString("HH:mm")` (currently line 612, the `SentTime` property getter), changing only the argument lists. Acceptance: `git grep -c -F 'using System.Globalization;' -- QuickFiler/Controllers/EfcItemController.cs` prints the count `1` (baseline was: prints no line, exits 1, per P0-T9 control 9). + +- [ ] [P3-T4] Verify the combined result of P3-T1 through P3-T3, scoped to only the three UI-facing files. Run `git grep -c '[.]ToString[(]@\?"[^"]*[:/.\-][^"]*"[)]' -- QuickFiler/Controllers/QfcItemController.ViewerSetup.cs QuickFiler/Controllers/QfcCollectionController.cs QuickFiler/Controllers/EfcItemController.cs`. Acceptance: the command prints no line for any of the three paths and `EXIT_CODE` is 1 (baseline combined total for these three files was 1 + 3 + 2 = 6, per P0-T9 control 1). Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/regression-testing/phase3-verification.2026-09-12T16-09.md`. + +### Phase 4 — Test Oracle Repair and Regression Confirmation + +- [ ] [P4-T1] In `QuickFiler.Test/Controllers/QfcHomeControllerMetricsTests.cs`, rewrite the expected-value construction inside `WriteMetricsAsync_UsesInjectedClock_ForDateAndTimeStamps` (currently at lines 242-243: `var expectedDataLineBeg = expectedLocal.ToString("MM/dd/yyyy") + "," + expectedLocal.ToString("HH:mm") + ",";`, disambiguated from the identical statement in the sibling method below by the enclosing method's preceding `SetPrivateField(controller, "_stopWatch", new Stopwatch());` line, which the sibling method does not have) so that both `ToString` calls pass `CultureInfo.InvariantCulture` as their second argument, removing the self-referential uncultured oracle for this test. Acceptance: `git grep -c -F 'expectedLocal.ToString("MM/dd/yyyy") + "," + expectedLocal.ToString("HH:mm") + ",";' -- QuickFiler.Test/Controllers/QfcHomeControllerMetricsTests.cs` prints the count `1` (baseline was 2, per P0-T9 control 10; the one remaining occurrence is the untouched sibling statement inside `QuickFileMetrics_WRITE_UsesInjectedClock_ForDataLine`, fixed in P4-T2). + +- [ ] [P4-T2] In `QuickFiler.Test/Controllers/QfcHomeControllerMetricsTests.cs`, rewrite the expected-value construction inside `QuickFileMetrics_WRITE_UsesInjectedClock_ForDataLine` (the remaining occurrence of the literal statement located by P4-T1's acceptance check) so that both `ToString` calls pass `CultureInfo.InvariantCulture` as their second argument. Acceptance: `git grep -c -F 'expectedLocal.ToString("MM/dd/yyyy") + "," + expectedLocal.ToString("HH:mm") + ",";' -- QuickFiler.Test/Controllers/QfcHomeControllerMetricsTests.cs` prints no line for that path and exits 1 (baseline was 2, covering both line 243 and line 278, per P0-T9 control 10; P4-T1 already reduced it to 1). + +- [ ] [P4-T3] Build the now-fixed tree. Run `pwsh -NoProfile -Command 'msbuild TaskMaster.sln /t:Build /m /p:Configuration=Debug "/p:Platform=Any CPU"'`. Acceptance: `EXIT_CODE` is 0. + +- [ ] [P4-T4] Run `pwsh -NoProfile -Command '$vswherePath = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"; $vstestPath = (& $vswherePath -latest -products * -find "Common7\IDE\Extensions\TestPlatform\vstest.console.exe") | Select-Object -First 1; & $vstestPath "QuickFiler.Test\bin\Debug\QuickFiler.Test.dll" /TestCaseFilter:"FullyQualifiedName~QuickFilerInvariantCultureIssue742Tests|FullyQualifiedName~WriteMetricsAsync_UsesInjectedClock_ForDateAndTimeStamps|FullyQualifiedName~QuickFileMetrics_WRITE_UsesInjectedClock_ForDataLine" /InIsolation /Logger:"console;verbosity=normal"; Write-Output "EXITCODE=$LASTEXITCODE"'`. Acceptance: `EXITCODE` is 0, covering all seven named tests (the five new tests from P1-T1 plus the two rewritten oracle tests from P4-T1/P4-T2). Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` (the printed test-run summary) in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/regression-testing/phase4-confirmation.2026-09-12T16-09.md`. + +- [ ] [P4-T5] Verify the repository-wide residual count across all five production files. Run `git grep -c '[.]ToString[(]@\?"[^"]*[:/.\-][^"]*"[)]' -- QuickFiler/Controllers/QfcHomeController.Metrics.cs QuickFiler/Controllers/EfcHomeController.Metrics.cs QuickFiler/Controllers/QfcItemController.ViewerSetup.cs QuickFiler/Controllers/QfcCollectionController.cs QuickFiler/Controllers/EfcItemController.cs`. Acceptance: the command prints exactly one line, `QuickFiler/Controllers/QfcHomeController.Metrics.cs:2`, prints no line for any of the other four paths, and `EXIT_CODE` is 0 (baseline total across all five files was 14, per P0-T9 control 1). Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/regression-testing/phase4-residual-count.2026-09-12T16-09.md`. + +### Phase 5 — Final QA Loop + +- [ ] [P5-T1] Run `pwsh -NoProfile -Command 'dotnet tool run csharpier format .'` and then, as the required observation beyond the format command's own exit code, the read-only `pwsh -NoProfile -Command 'dotnet tool run csharpier check .'`. Acceptance: the `check` command's `EXIT_CODE` is 0 (no remaining diff). If `format` rewrote any tracked file, restart this task from `format` before re-running `check`. Record `Timestamp:`, `Command:` (both commands), `EXIT_CODE:` (of `check`), `Output Summary:` in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/qa-gates/csharpier-final.2026-09-12T16-09.md`. + +- [ ] [P5-T2] Run `pwsh -NoProfile -Command '$out = msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true 2>&1; $exit = $LASTEXITCODE; $out | Select-String -Pattern "^\s*\d+ Error\(s\)\s*$" | ForEach-Object { $_.Line.Trim() }; Write-Output "EXITCODE=$exit"'`. Acceptance: the transcribed summary line, once trimmed, equals exactly the literal `0 Error(s)` (anchored to the whole trimmed line, so a `10 Error(s)` line cannot satisfy this) and `EXITCODE` is 0. If the run fails or csharpier rewrote any file since P5-T1, restart from P5-T1. Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/qa-gates/msbuild-analyzer-final.2026-09-12T16-09.md`. + +- [ ] [P5-T3] Run `pwsh -NoProfile -Command '$out = msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true 2>&1; $exit = $LASTEXITCODE; $out | Select-String -Pattern "^\s*\d+ Error\(s\)\s*$" | ForEach-Object { $_.Line.Trim() }; Write-Output "EXITCODE=$exit"'`. Do not add `/p:Nullable=enable`. Acceptance: the transcribed summary line, once trimmed, equals exactly the literal `0 Error(s)` and `EXITCODE` is 0. If this fails, restart from P5-T1. Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/qa-gates/msbuild-nullable-final.2026-09-12T16-09.md`. + +- [ ] [P5-T4] Run the `QuickFiler.Test` suite with coverage enabled via the repository's coverage runner, scoped to the `QuickFiler.Test` assembly only, again writing the raw Cobertura output under the repository's gitignored `coverage\` directory and deleting it before completion (same `Join-Path`/rooted-path reasoning as P0-T8). Run `pwsh -NoProfile -Command './scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot QuickFiler.Test -Configuration Debug -CoverageOutput "coverage\issue-742-coverage\final.cobertura.xml"; Write-Output "EXITCODE=$LASTEXITCODE"'`. Acceptance: `EXITCODE` is 0 (zero failures for `QuickFiler.Test`, including the two rewritten oracle tests and the five new tests; this inner invocation already carries `/InIsolation` and `/TestCaseFilter:TestCategory!=LiveOutlook`). If this fails or a prior step in this phase rewrote a tracked file since P5-T1, restart from P5-T1. Open the produced Cobertura XML at `coverage\issue-742-coverage\final.cobertura.xml`, transcribe the post-change `line-rate` figures for the same three non-excluded classes captured in P0-T8, state the baseline-vs-final comparison for each, and confirm no regression on the lines this change touched; also transcribe the overall pass/fail/skip counts. Delete the `coverage\issue-742-coverage\` directory after transcribing. Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/qa-gates/vstest-coverage-final.2026-09-12T16-09.md`. + +- [ ] [P5-T5] Confirm the `[TestMethod]` count in the new test file remains correct after formatting. Run `git grep --untracked -c -F '[TestMethod]' -- QuickFiler.Test/Controllers/QuickFilerInvariantCultureIssue742Tests.cs`. Acceptance: the command prints the count `5` (unchanged from P1-T1; CSharpier does not add or remove attributes) and `EXIT_CODE` is 0. Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/qa-gates/test-method-count-final.2026-09-12T16-09.md`. + +- [ ] [P5-T6] Confirm no `.trx` or Cobertura coverage XML file has been introduced as a tracked evidence artifact. Run `pwsh -NoProfile -Command '$m = git status --porcelain --untracked-files=all -- docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence | Select-String -Pattern "\.trx$|\.cobertura\.xml$"; $m; if ($m) { exit 0 } else { exit 1 }'`. Acceptance: the command prints no line and exits 1, matching this plan's zero-match reading convention. Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/qa-gates/no-trx-cobertura-evidence.2026-09-12T16-09.md`. + +- [ ] [P5-T7] Verify the diff scope, scoped to the QuickFiler and QuickFiler.Test source trees only (the same change also adds Markdown evidence artifacts and updates planning documents inside this feature folder, which is expected rather than a scope violation, so a repository-wide scope would be unsatisfiable). Run `git status --porcelain --untracked-files=all -- QuickFiler QuickFiler.Test`. Acceptance: the output lists exactly these eight paths and no others: `QuickFiler/Controllers/QfcHomeController.Metrics.cs`, `QuickFiler/Controllers/EfcHomeController.Metrics.cs`, `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs`, `QuickFiler/Controllers/QfcCollectionController.cs`, `QuickFiler/Controllers/EfcItemController.cs`, `QuickFiler.Test/Controllers/QfcHomeControllerMetricsTests.cs`, `QuickFiler.Test/Controllers/QuickFilerInvariantCultureIssue742Tests.cs` (as an untracked `??` entry until P5-T8 stages it), and `QuickFiler.Test/QuickFiler.Test.csproj`. Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` (the full porcelain listing) in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/qa-gates/diff-scope-final.2026-09-12T16-09.md`. + +- [ ] [P5-T8] Stage exactly the eight Write Set paths plus this feature's own evidence folder, each named as an explicit path argument (never a bare directory-root pathspec such as `git add .` or `git add QuickFiler`, because a sibling item's queued promotion file sits under a different, unrelated tree and a broad staging span could sweep it onto this branch): `git add QuickFiler/Controllers/QfcHomeController.Metrics.cs QuickFiler/Controllers/EfcHomeController.Metrics.cs QuickFiler/Controllers/QfcItemController.ViewerSetup.cs QuickFiler/Controllers/QfcCollectionController.cs QuickFiler/Controllers/EfcItemController.cs QuickFiler.Test/Controllers/QfcHomeControllerMetricsTests.cs QuickFiler.Test/Controllers/QuickFilerInvariantCultureIssue742Tests.cs QuickFiler.Test/QuickFiler.Test.csproj docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/plan.2026-09-12T16-09.md`. Acceptance: `git status --porcelain --untracked-files=all -- QuickFiler QuickFiler.Test` now shows every one of the eight Write Set paths with a staged status character and no `??` entries remain among them. + +- [ ] [P5-T9] Commit the staged change. Run `git commit -m "Fix issue 742: append CultureInfo.InvariantCulture to QuickFiler date/time rendering"`. Acceptance: `EXIT_CODE` is 0 and `git status --porcelain --untracked-files=all -- QuickFiler QuickFiler.Test` prints no line and exits 1. + +--- + +## PLANNER-INTERNAL-REVIEW: PASS + +CITATION-TO-TREE: PASS +AC-TRACEABILITY: PASS +SCOPE-BOUNDARY: PASS + +CITATION: docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/spec.md | Acceptance Criteria section, lines 353-442 +CITATION: QuickFiler/Controllers/QfcHomeController.Metrics.cs | line 48 (`dataLineBeg = $"{now:MM/dd/yyyy},{now:HH:mm},";`), lines 125 and 127 (`curDateText`/`curTimeText` ToString calls), lines 70/73/150/153 (existing `CultureInfo.InvariantCulture` sites) +CITATION: QuickFiler/Controllers/EfcHomeController.Metrics.cs | lines 95, 96, 118, 119 (uncultured ToString calls), line 3 (`using System.Globalization;` already present) +CITATION: QuickFiler/Controllers/QfcItemController.ViewerSetup.cs | line 465 (`GetItemSummary`), lines 1-22 (no `System.Globalization` using directive) +CITATION: QuickFiler/Controllers/QfcCollectionController.cs | line 21 (`[ExcludeFromCodeCoverage]`), line 235 (`TryGetMoveReadiness`), line 1296 (`ToggleExpansionStyle`), line 2302 (`GetMoveDiagnostics`), lines 1-17 (no `System.Globalization` using directive) +CITATION: QuickFiler/Controllers/EfcItemController.cs | line 25 (`[ExcludeFromCodeCoverage]`), lines 607 and 612 (`SentDate`/`SentTime` properties), lines 1-21 (no `System.Globalization` using directive) +CITATION: QuickFiler.Test/Controllers/QfcHomeControllerMetricsTests.cs | lines 242-243 and 277-278 (identical self-referential oracle statement), line 6 (`using System.Globalization;` already present) +CITATION: QuickFiler.Test/QuickFiler.Test.csproj | lines 57 and 230 (ItemGroup boundaries), line 199 (`QfcItemController.ViewerSetupTests.cs` sibling entry) +CITATION: QuickFiler.Test/Controllers/EfcItemControllerTests.cs | lines 200 and 315 (`FormatterServices.GetUninitializedObject`), lines 220-221 (`SetPrivateField` helper usage) +CITATION: QuickFiler.Test/Controllers/QfcCollectionControllerDefects468MoveTests.cs | lines 86-93 (`CreateUninitializedController`/`SetField` helper usage) +CITATION: scripts/vscode/Invoke-MSTestWithCoverage.ps1 | lines 1-13 (parameters), lines 70-77 (`/InIsolation` and `/TestCaseFilter:TestCategory!=LiveOutlook` built into the inner vstest invocation), lines 296-303 (`Get-ChildItem` test-assembly discovery scoped to `$resolvedSearchRoot`), line 309 (`Join-Path $repoRoot $CoverageOutput` does not special-case an already-rooted `$CoverageOutput`) +CITATION: dotnet-tools.json | lines 1-13 (pins only `csharpier`; no `dotnet-coverage` entry) +CITATION: .gitignore | lines 139-145 (`*.coverage`, `*.coveragexml`, `coverage/*` with `!coverage/.gitkeep` exception) +CITATION: .claude/rules/plan-acceptance-gates.md | G2/G3/G5/G6/G7/G8/G8b wrap-tolerant assertion rules applied throughout this plan's grep- and exit-code-based acceptance conditions +CITATION: .claude/skills/atomic-plan-contract/SKILL.md | Coverage Evidence Contract, Non-Overridable Evidence Path Clause, Plan-Path Continuity Contract + +AC-INVENTORY: AC1, AC2, AC3, AC4, AC5, AC6, AC7, AC8, AC9, AC10, AC11, AC12, AC13, AC14, AC15, AC16, AC17 + +AC-MAPPING: AC1 | IMPLEMENTATION: P2-T1 | TESTS: P1-T1(Test1) | EVIDENCE: P0-T9(control 2/3)+P2-T1 acceptance +AC-MAPPING: AC2 | IMPLEMENTATION: P2-T2 | TESTS: P4-T4 | EVIDENCE: P0-T9(control 3)+P2-T2 acceptance +AC-MAPPING: AC3 | IMPLEMENTATION: P2-T3 | TESTS: P1-T1(Test2) | EVIDENCE: P0-T9(control 1/4)+P2-T3/P2-T4 acceptance +AC-MAPPING: AC4 | IMPLEMENTATION: P3-T1 | TESTS: P1-T1(Test3) | EVIDENCE: P0-T9(control 7)+P3-T1 acceptance +AC-MAPPING: AC5 | IMPLEMENTATION: P3-T2 | TESTS: P1-T1(Test4) | EVIDENCE: P0-T9(control 8)+P3-T2 acceptance +AC-MAPPING: AC6 | IMPLEMENTATION: P3-T3 | TESTS: P1-T1(Test5) | EVIDENCE: P0-T9(control 9)+P3-T3 acceptance +AC-MAPPING: AC7 | IMPLEMENTATION: P2-T1,P2-T2,P2-T3,P3-T1,P3-T2,P3-T3 | TESTS: P4-T5 | EVIDENCE: docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/regression-testing/phase4-residual-count.2026-09-12T16-09.md +AC-MAPPING: AC8 | IMPLEMENTATION: P4-T1,P4-T2 | TESTS: P4-T4 | EVIDENCE: docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/regression-testing/phase4-confirmation.2026-09-12T16-09.md +AC-MAPPING: AC9 | IMPLEMENTATION: P1-T1 | TESTS: P1-T4,P4-T4 | EVIDENCE: docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/regression-testing/expect-fail-new-tests.2026-09-12T16-09.md +AC-MAPPING: AC10 | IMPLEMENTATION: P1-T2 | TESTS: P1-T2 acceptance | EVIDENCE: P0-T9(control 5/6)+P1-T2 acceptance +AC-MAPPING: AC11 | IMPLEMENTATION: P0-T8,P5-T4 (raw output written under gitignored coverage\ and deleted) | TESTS: P5-T6 | EVIDENCE: docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/qa-gates/no-trx-cobertura-evidence.2026-09-12T16-09.md +AC-MAPPING: AC12 | IMPLEMENTATION: P0-T9 | TESTS: P0-T9 acceptance (written before P1-T1) | EVIDENCE: docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/discovery-count-controls.2026-09-12T16-09.md +AC-MAPPING: AC13 | IMPLEMENTATION: P5-T1 | TESTS: P5-T1 acceptance | EVIDENCE: docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/qa-gates/csharpier-final.2026-09-12T16-09.md +AC-MAPPING: AC14 | IMPLEMENTATION: P5-T2 | TESTS: P5-T2 acceptance | EVIDENCE: docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/qa-gates/msbuild-analyzer-final.2026-09-12T16-09.md +AC-MAPPING: AC15 | IMPLEMENTATION: P5-T3 | TESTS: P5-T3 acceptance | EVIDENCE: docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/qa-gates/msbuild-nullable-final.2026-09-12T16-09.md +AC-MAPPING: AC16 | IMPLEMENTATION: P0-T2 (Outlook closed) | TESTS: P5-T4 | EVIDENCE: docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/qa-gates/vstest-coverage-final.2026-09-12T16-09.md +AC-MAPPING: AC17 | IMPLEMENTATION: P5-T8 | TESTS: P5-T7 | EVIDENCE: docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/qa-gates/diff-scope-final.2026-09-12T16-09.md + +UNRESOLVED-GAPS: NONE + +## SELF-REVIEW: RE-DERIVED THIS PASS + +- `QuickFiler/Controllers/QfcHomeController.Metrics.cs`, line 48 — re-read in this pass; confirmed the literal `dataLineBeg = $"{now:MM/dd/yyyy},{now:HH:mm},";` and the commented-out predecessor at lines 45-47. +- `QuickFiler/Controllers/QfcHomeController.Metrics.cs`, lines 125 and 127 — re-read in this pass; confirmed `curDateText = now.ToString("MM/dd/yyyy");` and `curTimeText = now.ToString("HH:mm");` inside `WriteMetricsAsync`. +- `QuickFiler/Controllers/QfcHomeController.Metrics.cs`, lines 70, 73, 150, 153 — re-read in this pass; confirmed the four pre-existing `CultureInfo.InvariantCulture` sites used as the baseline-of-4 control. +- `QuickFiler/Controllers/EfcHomeController.Metrics.cs`, lines 95, 96, 118, 119 — re-read in this pass; confirmed all four uncultured `ToString` sites and that line 3 already carries `using System.Globalization;`. +- `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs`, lines 1-22 and line 465 — re-read in this pass; confirmed no `System.Globalization` using directive and the exact two-call literal at line 465. +- `QuickFiler/Controllers/QfcCollectionController.cs`, lines 1-22, 228-238, 1290-1299, and 2296-2305 — re-read in this pass; confirmed the `[ExcludeFromCodeCoverage]` attribute at line 21, no `System.Globalization` using directive, and the exact literals at lines 235, 1296, and 2302. +- `QuickFiler/Controllers/EfcItemController.cs`, lines 1-27 and 600-613 — re-read in this pass; confirmed the `[ExcludeFromCodeCoverage]` attribute at line 25, no `System.Globalization` using directive, and the exact literals at lines 607 and 612. +- `QuickFiler.Test/Controllers/QfcHomeControllerMetricsTests.cs`, lines 180-299 — re-read in this pass; confirmed the byte-identical expected-value construction at lines 242-243 and 277-278, that `CultureInfo.InvariantCulture` has zero occurrences in this file today, and that `using System.Globalization;` is already present at line 6, so no new using directive is needed for Phase 4. +- `QuickFiler.Test/QuickFiler.Test.csproj`, lines 50-64 and 195-234 — re-read in this pass; confirmed the `` opens at line 57 and closes at line 230, and that `QfcItemController.ViewerSetupTests.cs` sits at line 199 inside it. +- `QuickFiler.Test/Controllers/EfcItemControllerTests.cs`, lines 1-80, 195-235, and 285-320 — re-read in this pass; confirmed the `FormatterServices.GetUninitializedObject` and `SetPrivateField` reflection patterns already established for constructing an `EfcItemController` without its constructor. +- `QuickFiler/Controllers/EfcItemController.cs`, `_itemInfo` field and `PopulateControls`/`DataModel.MailInfo` seam — re-read in this pass; confirmed `_itemInfo` is a private field settable via reflection, corroborating the Phase 1 Test 5 seam. +- `QuickFiler/Controllers/QfcItemController.cs`, lines 130-141 — re-read in this pass; confirmed `ItemHelper` is a public get/set property backed by the private `_itemInfo` field, corroborating the Phase 1 Test 3 seam. +- `QuickFiler.Test/Controllers/QfcCollectionControllerDefects468MoveTests.cs`, lines 1-100 — re-read in this pass; confirmed the `QfcCollectionControllerTestSupport.CreateUninitializedController()` and `SetField`/`GetField` helpers, and the `_itemGroups` vs. `_itemGroupsToMove` field distinction between `TryGetMoveReadiness`/`ToggleExpansionStyle` and `GetMoveDiagnostics`. +- `QuickFiler.Test/Controllers/QfcCollectionControllerDefects468Tests.cs`, lines 405-421 — re-read in this pass; confirmed the `GroupWithFolder` mock-construction pattern (`Mock` plus `Mock`) reused for Phase 1 Test 4. +- `QuickFiler/Interfaces/IQfcItemController.cs`, lines 41-47 — re-read in this pass; confirmed `ItemHelper`, `Mail`, `SelectedFolder`, and `ItemNumber` members used by the Phase 1 Test 4 mock setup. +- The QuickFiler production project file (not backticked here; it is out of scope and needs no edit) and the non-Metrics partial-class file of the QfcHomeController class at QuickFiler/Controllers/QfcHomeController.cs (also not backticked here; it is the same out-of-scope file named in this plan's Scope boundary paragraph and in spec.md's Scope and Non-Goals section), plus `QuickFiler/Properties/AssemblyInfo.cs` — re-read (grep) in this pass; confirmed the non-Metrics QfcHomeController.cs file and QuickFiler/Properties/AssemblyInfo.cs both carry `[assembly: InternalsVisibleTo("QuickFiler.Test")]`, corroborating that `QuickFiler.Test` can call the `internal` `GetItemSummary()`, `TryGetMoveReadiness`, and `ToggleExpansionStyle` members exercised by Phase 1 Test 3 and Test 4. QfcHighConfidencePreFilter.cs and the Legacy folder's IAcceleratorCallbacks file were also read; both carry only `[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]`, which targets Moq's dynamic-proxy assembly and does not grant `QuickFiler.Test` access, so they are named here in plain prose without backticks rather than cited as supporting evidence; the Legacy file is additionally out of this issue's scope per the Scope and Non-Goals section. +- `scripts/vscode/Invoke-MSTestWithCoverage.ps1`, lines 1-77, lines 296-303, and line 309 — re-read in this pass; confirmed the default `-CoverageOutput` parameter, that the script's inner `dotnet-coverage collect -- ` invocation already appends `/InIsolation` and `/TestCaseFilter:TestCategory!=LiveOutlook`, that `$resolvedOutputPath = Join-Path $repoRoot $CoverageOutput` (line 309) does not special-case an already-rooted `$CoverageOutput` value, and that `Get-ChildItem -Path $resolvedSearchRoot -Recurse -Filter '*.Test.dll'` (lines 296-303) is scoped by `$resolvedSearchRoot`, so `-SearchRoot QuickFiler.Test` restricts assembly discovery to that subtree; P0-T8 and P5-T4 therefore pass a relative `coverage\issue-742-coverage\...` value and a `QuickFiler.Test` search root rather than a path outside the repository, confirmed against `.gitignore`'s `coverage/*` entry (`.gitignore` lines 143-145). +- `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/spec.md`, Acceptance Criteria section (lines 343-442) and Write Set section (lines 472-486) — re-read in this pass; confirmed all 17 acceptance-criteria items, that the residual-sweep criterion (lines 383-400) already uses the escaped `@\?` form and the orchestrator's own two-spelling measurement note, and the exact eight-path Write Set used for the AC-INVENTORY and the diff-scope tasks. +- `dotnet-tools.json`, lines 1-13 — re-read in this pass; confirmed the manifest pins only `csharpier` at version `1.2.6` and carries no `dotnet-coverage` entry, corroborating P0-T3's bootstrap of `dotnet-coverage` separately from `dotnet tool restore`. +- `.gitignore`, lines 139-145 — re-read in this pass; confirmed `coverage/*` is ignored with a single `!coverage/.gitkeep` exception, so `coverage\issue-742-coverage\` used by P0-T8 and P5-T4 is gitignored and cannot be swept into a commit even if the task's own deletion step is skipped. diff --git a/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/research/2026-09-12T16-00-quickfiler-datetime-invariant-culture-research.md b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/research/2026-09-12T16-00-quickfiler-datetime-invariant-culture-research.md new file mode 100644 index 000000000..dd0b81962 --- /dev/null +++ b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/research/2026-09-12T16-00-quickfiler-datetime-invariant-culture-research.md @@ -0,0 +1,370 @@ +# Research: Issue #742 — Date/time rendering missing `CultureInfo.InvariantCulture` + +This is a re-verification pass. Every claim below was re-derived by reading the current +worktree (`C:/Users/DanMoisan/repos/TaskMaster/.claude/worktrees/agent-a6f0fc7cce6d28aaa`) +on 2026-09-12, not carried over from the prior research artifact +(`agent-a1324bb743d9f3fbe`) or from the issue body. Where a claim from the prior artifact +was checked and found accurate, it is retained; where it could not be verified as written, +that is noted explicitly. No drift was found: every line-numbered citation re-checked +against this worktree matched the prior artifact exactly, including the prior artifact's +own "Orchestrator Correction" addendum that added the interpolated site at +`QfcHomeController.Metrics.cs:48`. + +## Q1. Exhaustive repository-wide sweep — two independent strategies + +The defect is any rendering of a `System.DateTime` value through a custom format string +whose `MM/dd/yyyy`-, `HH:mm`-, or `HH:mm:ss`-style specifiers resolve `DateSeparator`/ +`TimeSeparator` from the ambient culture, with no `IFormatProvider` supplied. That +rendering can occur through two distinct C# syntactic forms: a two-argument-eligible +`.ToString("format")` call, or a `$"...{value:format}..."` interpolation hole. A sweep +built only from the token `.ToString(` is structurally blind to the second form. The two +strategies below are anchored on different syntactic mechanisms so that this blind spot +is not shared between them. + +**Strategy 1 — call-form and interpolation-hole pattern union.** Two Grep queries, run +separately over every `*.cs` file in the entire repository source tree and then unioned: + +``` +\.ToString\(@?"[^"]*[:/][^"]*"\) +\{[A-Za-z_][A-Za-z0-9_.\[\]() ]*:[^}"]*(MM|dd|yyyy|HH|hh|mm|ss)[^}"]*\} +``` + +The first query matches any single-argument `.ToString("...")`/`.ToString(@"...")` call +whose literal format string contains a `:` or `/` character (a call passing a second +`IFormatProvider` argument would not match, because the string literal is not immediately +followed by `)`). The second matches an interpolation hole whose format specifier contains +a date/time custom-format letter. **Stated blind spot:** this strategy requires the format +text to be a literal visible at the call/hole site; it is blind to any call that builds its +format string in a variable, passes it through a helper/extension method that does not +repeat the literal at the call site, or uses `ToShortDateString`/`ToShortTimeString`/ +`ToLongDateString`/`ToLongTimeString`/`String.Format` (none of the latter four APIs contain +a `.ToString(` token or a colon-format hole in the caller's own source line). + +Run against the whole repository, this returned 58 raw lines from the first query (34 +live, non-comment) and 15 raw lines from the second (all but one either a backslash-escaped +`TimeSpan` literal or a separator-free `yyyyMMddHHmmss`/`yyyyMMddHHmmssf` stamp). Full live, +date/time-typed member set (comment lines excluded by inspecting each line's leading +characters): + +- `QuickFiler/Legacy/QuickFileController.cs:757,1010,1013` +- `QuickFiler/Legacy/QfcGroupOperationsLegacy.cs:703,732,770,1221,1307` +- `QuickFiler/Legacy/QfcController.cs:601` +- `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs:465` (2 calls) +- `QuickFiler/Controllers/QfcHomeController.Metrics.cs:48` (interpolated, 2 specifiers), `:125`, `:127` +- `QuickFiler/Controllers/QfcHomeController.cs:74` +- `QuickFiler/Controllers/QfcCollectionController.cs:235`, `:1296` (2 calls), `:2302` (2 calls) +- `QuickFiler/Controllers/EfcItemController.cs:607,612` +- `QuickFiler/Controllers/EfcHomeController.Metrics.cs:95,96,118,119` +- `QuickFiler.Test/Controllers/QfcHomeControllerMetricsTests.cs:243,278` (test-oracle construction, see Q4) +- `ToDoModel/Email Utilities/CaptureEmailDetailsModule.cs:29` +- `UtilitiesCS/OutlookObjects/MailItem/EmailDetails.cs:45` +- `UtilitiesCS/EmailIntelligence/EmailParsingSorting/MovedMailInfo.cs:156` +- `UtilitiesCS/OutlookObjects/Item/OlItemSummary.cs:82,125,138,151,164` + +**False-positive/escape caveats confirmed by reading the literals:** `TaskMaster.Test/AppGlobals/StartupTimingRecorderTests.cs:89` and `UtilitiesCS/HelperClasses/SegmentStopWatch.cs:102` use `"%m\\:ss\\.ff"` on a `TimeSpan` receiver, where every backslash forces the following character to render literally regardless of culture; these are shape-matches, not live defects. `QuickFiler/Controllers/EmailSorter.cs:72` (`dateTime.ToString("yyyyMMddHHmmss")`) contains zero separator characters, so it did not match either query and is confirmed as a true negative. + +**Strategy 2 — receiver-identifier anchor plus manual sequential read.** A Grep query +anchored on the receiver rather than the format text, run over the whole repository: + +``` +(SentDate|SentOn|GetLocalNow\(\)\.LocalDateTime|CreationTime|LastModificationTime|\.Start\.ToString|\bNow\.ToString|currentDateTime|itemInfo\.SentDate) +``` + +filtered to lines also containing `.ToString(` or an interpolation colon, corroborated by +a full sequential read of the entire body of each of the five files named in the issue +(`QfcHomeController.Metrics.cs`, `EfcHomeController.Metrics.cs`, +`QfcItemController.ViewerSetup.cs`, `QfcCollectionController.cs`, `EfcItemController.cs` +were each read start to end). **Stated blind spot:** this strategy depends on already +knowing which identifier names denote `DateTime` receivers; outside the five files it is +blind to receivers under unlisted names (e.g. `oMail_Current`, bare `Mail`, `DateTime.Now` +in the legacy files without an intervening named variable) and to verbatim-string calls +whose receiver name is not in the alternation, because the manual full-read mitigation was +applied only to the five named files, not to the whole repository. + +Live member set from this strategy: `QuickFiler/Legacy/QuickFileController.cs:757`; +`QuickFiler/Legacy/QfcGroupOperationsLegacy.cs:1221,1307`; `QuickFiler/Legacy/QfcController.cs:601`; +`QuickFiler/Controllers/QfcHomeController.Metrics.cs:48,125,127`; +`QuickFiler/Controllers/QfcItemController.ViewerSetup.cs:465` (2 calls); +`QuickFiler/Controllers/QfcHomeController.cs:74`; +`QuickFiler/Controllers/QfcCollectionController.cs:235,1296(×2),2302(×2)`; +`QuickFiler/Controllers/EfcItemController.cs:607,612`; +`QuickFiler/Controllers/EfcHomeController.Metrics.cs:95,96,118,119`; +`UtilitiesCS/OutlookObjects/AppointmentItem/MeetingItemHelper.cs:123` (`SentDate.ToString("g")`); +`UtilitiesCS/OutlookObjects/MailItem/MailItemHelper.cs:149` (`SentDate.ToString("g")`); +`UtilitiesCS/OutlookObjects/Item/OlItemSummary.cs:82,125,138,151,164`; +`UtilitiesCS/EmailIntelligence/EmailParsingSorting/MovedMailInfo.cs:156`; +`QuickFiler.Test/Controllers/QfcHomeControllerMetricsTests.cs:243,278`. + +**Member-set comparison (whole repository, both strategies unioned and normalized to +`path:line`):** the two sets are **not** equal outside the five in-scope files, and this +divergence is expected and diagnostic. Strategy 1 finds the verbatim-string sites +(`CaptureEmailDetailsModule.cs:29`, `EmailDetails.cs:45`) that Strategy 2's identifier +alternation and manual-read scope do not cover; Strategy 2 finds `MeetingItemHelper.cs:123` +and `MailItemHelper.cs:149` (the standard `"g"` format specifier, which has no `:`/`/` +character and so cannot match Strategy 1's character-class test) that Strategy 1 misses +entirely. **Intersection (the reliable core):** both strategies agree on all 13 live source +lines inside the five named controller files (17 individual rendering operations — see the +Numeric Derivation Evidence section), on the five `OlItemSummary.cs` sites, on +`MovedMailInfo.cs:156`, on the three `Legacy/` sites both regexes can reach, and on the two +test-oracle lines. Because the two strategies' disjoint members are attributable to each +strategy's own stated, distinct blind spot rather than to a shared one, the two-strategy +requirement is satisfied for this sweep. + +## Q2. Scope recommendation + +**(a) Maintainer-confirmed in-scope sites inside the five named files** — all 15 sites +from the issue's "VERIFIED CALL SITES" list, re-verified at their current line numbers by +direct reading: + +| File | Lines | Calls | +|---|---|---| +| `QfcHomeController.Metrics.cs` | 125, 127 | 2 | +| `EfcHomeController.Metrics.cs` | 95, 96, 118, 119 | 4 | +| `QfcItemController.ViewerSetup.cs` | 465 | 2 (one line) | +| `QfcCollectionController.cs` | 235, 1296, 2302 | 5 (1296 and 2302 each carry 2) | +| `EfcItemController.cs` | 607, 612 | 2 | + +**(b) Further in-scope site inside the five files not on the maintainer's list:** one. +`QuickFiler/Controllers/QfcHomeController.Metrics.cs:48`: + +``` +dataLineBeg = $"{now:MM/dd/yyyy},{now:HH:mm},"; +``` + +confirmed present in the current worktree, three lines below `var now = +TimeProvider.GetLocalNow().LocalDateTime;` (line 44) inside `QuickFileMetrics_WRITE` +(method opens line 36). Lines 45–47 immediately above are the commented-out predecessor +statement, which is why a reader scanning past comments can miss the live line. Both Q1 +strategies were deliberately built to catch this form (Strategy 1's second regex, and +Strategy 2's identifier match on `now` combined with the manual full read); each did +independently detect it, so it is not a single-strategy artifact. + +The remedy for this site differs from the call-form remedy: a C# interpolated string +cannot take an `IFormatProvider` argument. The two supported rewrites are to convert the +two specifiers to explicit two-argument `ToString` calls (recommended — it matches the +call shape already used two lines below at 125/127 in the same file and needs no new +`using` directive beyond the one the file already has), or to wrap the interpolation in +`FormattableString.Invariant(...)`. + +**(c) Sites outside the five files**, classified: + +- `QuickFiler/Controllers/QfcHomeController.cs:74` — `$"{controller.TimeProvider.GetLocalNow().LocalDateTime.ToString("mm:ss.fff")} "`, inside a `catch (OperationCanceledException)` diagnostic log line, never persisted or user-facing. Same class (`QfcHomeController`) as an in-scope file, but a different partial-class file the maintainer's list does not name. **Recommendation: follow-up issue**, kept separate so the reviewed diff for #742 stays confined to the five named files. +- `QuickFiler/Controllers/EmailSorter.cs:72` — `long.Parse(dateTime.ToString("yyyyMMddHHmmss"))`. **Not a defect of this class**: the format string has zero separator characters, confirmed by re-running both Q1 sweeps, neither of which matched this line. **Recommendation: no action.** +- `QuickFiler/Legacy/**` (`QuickFileController.cs`, `QfcGroupOperationsLegacy.cs`, `QfcController.cs`) — re-confirmed **not compiled**: a grep for `Legacy\QuickFileController`, `Legacy\QfcGroupOperationsLegacy`, `Legacy\QfcController` inside `QuickFiler/QuickFiler.csproj` returned zero matches. **Recommendation: dead code, no follow-up issue.** +- `UtilitiesCS/**` (`OlItemSummary.cs`, `MovedMailInfo.cs`, `MeetingItemHelper.cs`, `MailItemHelper.cs`, `SegmentStopWatch.cs`) and `ToDoModel/Email Utilities/CaptureEmailDetailsModule.cs` — confirmed live, compiled production code in different projects (`UtilitiesCS`/`ToDoModel`) than the five in-scope `QuickFiler` files. `SegmentStopWatch.cs:102` is a false positive (backslash-escaped `TimeSpan` literal). The remaining `SentOn`/`Start`/`CreationTime`/`LastModificationTime`/`SentDate.ToString("g")` sites are genuine instances of the same defect class. **Recommendation: separate follow-up issue** scoped to `UtilitiesCS`/`ToDoModel`, since these cross a project boundary the maintainer's issue text does not reach. + +## Q3. The fix form + +| File | `using System.Globalization;` present? | Call sites | Receiver type | Recommended rewrite | +|---|---|---|---|---| +| `QfcHomeController.Metrics.cs` | Yes — line 2 | 48 (interpolated, 2 specifiers), 125, 127 | `now` is `System.DateTime` (from `TimeProvider.GetLocalNow().LocalDateTime`, line 44; `LocalDateTime` on `DateTimeOffset` returns non-nullable `DateTime`) | Convert line 48's interpolation to two `now.ToString("MM/dd/yyyy", CultureInfo.InvariantCulture)`/`now.ToString("HH:mm", CultureInfo.InvariantCulture)` calls; append `, CultureInfo.InvariantCulture` to the calls at 125 and 127 | +| `EfcHomeController.Metrics.cs` | Yes — line 3 | 95, 96, 118, 119 | `currentDateTime` is the method's `DateTime` parameter (`BuildQuickFileMetricLines(DateTime currentDateTime, ...)`, line 84); `itemInfo.SentDate` is `MailItemHelper.SentDate`, `public virtual DateTime SentDate` (`UtilitiesCS/OutlookObjects/MailItem/MailItemHelper.Properties.cs:238`) | Append `, CultureInfo.InvariantCulture` to all four calls | +| `QfcItemController.ViewerSetup.cs` | **Absent** (verified lines 1–22) | 465 (2 calls, one line) | `ItemHelper` is `MailItemHelper` (`QfcItemController.cs:135`); `ItemHelper.SentDate` is `DateTime` | Add `using System.Globalization;`; append `, CultureInfo.InvariantCulture` to both calls | +| `QfcCollectionController.cs` | **Absent** (verified lines 1–17) | 235, 1296 (×2), 2302 (×2) | Line 235: `grp.ItemController.Mail` is `Microsoft.Office.Interop.Outlook.MailItem` (`IQfcItemController.Mail`, `QuickFiler/Interfaces/IQfcItemController.cs:42`); `.SentOn` marshals from the COM VARIANT date to plain `System.DateTime`. Lines 1296/2302: `c.ItemHelper.SentDate`/`qf.ItemHelper.SentDate`, `MailItemHelper.SentDate`, `DateTime` | Add `using System.Globalization;`; append `, CultureInfo.InvariantCulture` to all five calls | +| `EfcItemController.cs` | **Absent** (verified lines 1–21) | 607, 612 | `_itemInfo` is `MailItemHelper` (field declared line 379); `_itemInfo.SentDate` is `DateTime` | Add `using System.Globalization;`; append `, CultureInfo.InvariantCulture` to both calls | + +`DateTime.ToString(string, IFormatProvider)` is present on every receiver above (all +resolve to non-nullable `System.DateTime`). The only COM-interop-touched receiver +(`Mail.SentOn` at `QfcCollectionController.cs:235`) still returns plain `System.DateTime` +once dereferenced — the COM boundary affects how a test constructs `Mail`, not the +applicability of the two-argument overload. `QuickFiler.Test/QuickFiler.Test.csproj:18` +confirms `v4.8.1`; the two-argument +overload is unaffected by this target. + +## Q4. Existing test oracles + +`QuickFiler.Test/Controllers/QfcHomeControllerMetricsTests.cs`: + +- **Line 243** (method `WriteMetricsAsync_UsesInjectedClock_ForDateAndTimeStamps`, opens + line 231): builds `expectedDataLineBeg = expectedLocal.ToString("MM/dd/yyyy") + "," + + expectedLocal.ToString("HH:mm") + ","` from the same `FakeTimeProvider` instant given to + the controller (lines 239–241), then asserts via `groups.Verify(...)` (lines 249–260) + that `GetMoveDiagnostics` was called with that exact string. +- **Line 278** (method `QuickFileMetrics_WRITE_UsesInjectedClock_ForDataLine`, opens line + 269): identical construction and assertion shape for the synchronous overload. + +Both tests build their "expected" string with the same uncultured `.ToString(format)` call +the (unfixed) production code uses. Under the repository's default test-host culture, this +still passes after the production fix, because both sides render the same separators. Under +an `it-IT` test-host culture, the two sides would diverge post-fix (production renders +invariant separators; the test's own expected-value construction still renders culture- +driven separators), so the assertion would fail. **These two tests are not a regression +guard for the fix as written**; they would need to build the expected string with +`CultureInfo.InvariantCulture` (or assert the invariant literal directly) to remain valid +after the fix. + +**Sweep for other tests asserting a date/time literal from one of the five in-scope files:** + +- `QfcCollectionControllerTests.cs:44,99,125` — mocks `SentDate` and passes a literal + `"01/01/2026,12:00,"` into `GetMoveDiagnostics`, but assertions never inspect the + returned string's date/time substring. +- `QfcCollectionControllerDefects468MoveTests.cs:290,327,369,430` — sets `SentDate` to a + fixed `DateTime` but asserts only on line count and null-freedom. +- `QfcCollectionControllerDefects468Tests.cs:411–470` + (`TryGetMoveReadiness_WithUnassignedDestination_ReturnsFalseAndProducesNotificationText`) + — re-verified: `GroupWithFolder` (line 411) mocks `Outlook.MailItem.SentOn` at line 414 + (`mail.SetupGet(m => m.SentOn).Returns(new DateTime(2026, 1, 1));`); the test method (line + 431) asserts `notifications.Should().StartWith(...).And.ContainAll("Subject 1", "Subject + 2", "Subject 3", "Subject 4")` (line 462) — never the date substring the notification + text embeds at production line 235. +- `EfcHomeControllerMetricsTests.cs:333` — re-verified: `SentDate = new DateTime(2026, 6, + 30, 9, 45, 10)` inside the `MovedItems` helper; the tests that consume it + (`BuildQuickFileMetricLines_WithNinetySeconds_RendersUntruncatedDuration`, + `..._UnderGermanCulture_RendersInvariantDecimalSeparator`, lines 121–158, re-verified) + assert only on the numeric duration fields (`",90,1.50,"`, `",2.00,"`, field count), never + on the date/time substring. + +None of these other tests are a date/time-format oracle for the five in-scope files; only +the two lines above (243, 278) construct an expected value from a date/time `.ToString` +call. + +## Q5. Test seams and determinism + +- **`QfcHomeController.Metrics.cs`** — covered by `QfcHomeControllerMetricsTests.cs`. + `now` is obtained from the injectable `TimeProvider` property (`internal TimeProvider + TimeProvider { get; set; } = TimeProvider.System;`, line 19), which tests already replace + with `FakeTimeProvider` (`FixedClock()`, lines 221–222). +- **`EfcHomeController.Metrics.cs`** — covered by `EfcHomeControllerMetricsTests.cs`. + `currentDateTime` is a plain method parameter to `BuildQuickFileMetricLines(DateTime + currentDateTime, ...)` (line 84); the seam is already fully injectable — the test file's + `Build` helper (line 298) supplies the fixed value `MetricsNow` directly (line 301), with + no `TimeProvider` object involved for this file. +- **`QfcItemController.ViewerSetup.cs:465` (`GetItemSummary`)** — no clock is read; the + value comes from `ItemHelper.SentDate`, a plain `MailItemHelper` property. A test needs + only `controller.ItemHelper = new MailItemHelper { SentDate = ... };` — `MailItemHelper` + has a public parameterless constructor. No live Outlook COM object or WinForms handle is + required. **Re-verified: a repo-wide grep for `GetItemSummary` inside `QuickFiler.Test/` + returns zero matches** — the method is currently uncovered by any test. +- **`QfcCollectionController.cs:235` (`TryGetMoveReadiness`)** — needs + `Mock` with `.SetupGet(i => i.Mail).Returns(mockMailItem.Object)` + where `mockMailItem` is `new Mock(MockBehavior.Loose)` with + `.SetupGet(m => m.SentOn).Returns(...)`. This exact pattern already exists in + `QfcCollectionControllerDefects468Tests.cs:411–420` (`GroupWithFolder` helper, + re-verified). No live Outlook process or WinForms handle is required; the mock is a COM + interop mock, not a live COM object. +- **`QfcCollectionController.cs:1296`/`2302`** — needs a `Mock` + exposing `.ItemHelper` returning a plain `MailItemHelper` with `SentDate` set (no COM mock + needed, since `ItemHelper` is the plain helper type). The existing helpers in + `QfcCollectionControllerTests.cs` and `QfcCollectionControllerDefects468MoveTests.cs` + already build this shape. **Coverage caveat:** `QfcCollectionController` carries a + type-level `[ExcludeFromCodeCoverage]` attribute (re-verified, line 21), which hides the + entire merged partial type from the Cobertura percentage even though the methods are + demonstrably unit-tested today. +- **`EfcItemController.cs:607`/`612` (`SentDate`/`SentTime` properties)** — needs only a + plain `MailItemHelper` assigned to the internal `_itemInfo` field (reachable via + `dataModel.MailInfo`, line 273). `EfcItemController` also carries a type-level + `[ExcludeFromCodeCoverage]` attribute (re-verified, line 25). **Re-verified: a grep for + `SentDate|SentTime` inside `EfcItemControllerTests.cs` returns zero matches** — neither + property is exercised by any existing test today. + +**Deterministic culture-swap pattern already established in the repository:** +`EfcHomeControllerMetricsTests.cs:136–158` +(`BuildQuickFileMetricLines_UnderGermanCulture_RendersInvariantDecimalSeparator`) and +`QfcHomeControllerMetricsTests.cs:183–213` +(`WriteMetricsAsync_UnderGermanCulture_RendersInvariantDecimalSeparator`) both re-verified +to use: + +``` +var originalCulture = CultureInfo.CurrentCulture; +try { CultureInfo.CurrentCulture = new CultureInfo("de-DE"); /* act + assert */ } +finally { CultureInfo.CurrentCulture = originalCulture; } +``` + +A new regression test for this issue should mirror this exact try/finally shape (no temp +files, no `Thread.Sleep`) and should assert the invariant literal directly (e.g. +`.Should().Contain("06/30/2026")` under an active `it-IT` culture) rather than re-deriving +an "expected" string with another uncultured `.ToString(format)` call — the self-referential +oracle failure mode identified in Q4. + +**MSTest thread/culture leak risk:** `CultureInfo.CurrentCulture` is process/thread-wide +and MSTest does not reset it between test methods. A test that sets it without a `finally` +restore can leak the changed culture into a sibling test on the same worker thread. Both +existing de-DE tests already restore correctly; any new test for this issue must do the +same. + +## Q6. Project file mechanics + +- `QuickFiler.Test/QuickFiler.Test.csproj` is non-SDK-style: it uses an explicit + `` chain and `` list rather than implicit + globbing (re-verified). +- The single `` holding every `Compile Include` entry (covering `Controllers\` + and every other folder in this project) opens at **line 57** and closes at **line 230** + (re-verified by direct read of both boundary lines). `Controllers\QfcItemController.ViewerSetupTests.cs` + — the existing test file that already covers `QfcItemController.ViewerSetup.cs` — is + present inside that range at **line 199** (re-verified). +- A new file at `QuickFiler.Test/Controllers/.cs` requires exactly one new line + inside that ``, in the literal form used by every sibling entry: + ``. +- `QuickFiler/QuickFiler.csproj` requires **no edit** for this fix, because the fix only + changes the body of five already-`Compile`-included files (adding an argument to + existing `.ToString(...)` calls, converting one interpolation to call form, and adding a + `using System.Globalization;` directive to three of the five files); the set of compiled + files does not change. + +## Q7. Convention precedent + +`EfcHomeController.Metrics.cs:101–103`, re-verified verbatim: + +``` +// The metrics file is machine-read, so numeric fields are rendered with the invariant +// culture rather than the operator's locale, which would emit a decimal comma and +// corrupt the CSV field count. +``` + +This comment sits immediately above line 104's +`duration.ToString("##0", CultureInfo.InvariantCulture)`. The parallel numeric-field +precedent recurs at `QfcHomeController.Metrics.cs:70,73` (re-verified: +`duration.ToString("##0", CultureInfo.InvariantCulture)` and +`(duration / 60d).ToString("##0.00", CultureInfo.InvariantCulture)`) and again inside +`WriteMetricsAsync` at lines 150 and 153, and inside `EfcHomeController.Metrics.cs:104–108`. +All of these numeric fields already pass `CultureInfo.InvariantCulture` explicitly; the +issue is that the sibling **date/time** fields in the same methods do not follow the +convention the numeric fields in those same methods already established. + +**Analyzer/rule search, re-verified:** `.editorconfig:27` sets a blanket +`dotnet_analyzer_diagnostic.severity = suggestion`, the catch-all default for any analyzer +rule not given an explicit override. A repo-wide grep for `CA1304`, `CA1305`, and `CA1307` +inside `.editorconfig` returns zero hits. `BannedSymbols.txt` (repository root) contains no +entry matching `ToString`, `Culture`, or `Format` (re-verified by direct grep); its entries +target `DateTime.Now`/`UtcNow`, `Random.Shared`, `Thread.Sleep`, `Task.Delay`, +`CancellationTokenSource.CancelAfter`/constructor, and `WaitHandle.WaitOne`. **No analyzer +rule or banned-symbol entry currently targets this defect class at any severity above +`suggestion`.** + +## Q8. Coverage posture (read-only; no coverage tool run) + +- `QfcHomeController.Metrics.cs` (`QuickFileMetrics_WRITE`, `WriteMetricsAsync`) — + exercised by `QfcHomeControllerMetricsTests.cs`, including the two clock-injection tests + that call both methods end-to-end. The `dataLineBeg`/`curDateText`/`curTimeText` lines + execute on every run, but no assertion inspects their culture-dependent content. +- `EfcHomeController.Metrics.cs` (`BuildQuickFileMetricLines`) — exercised by + `EfcHomeControllerMetricsTests.cs`; the date/time lines execute on every call without a + content assertion on the date/time substring. +- `QfcItemController.ViewerSetup.cs` (`GetItemSummary`) — **not covered at all**, + re-verified (zero matches for `GetItemSummary` in `QuickFiler.Test/`). +- `QfcCollectionController.cs` (`TryGetMoveReadiness`, the `ToggleExpansionStyle` + message-builder at line 1296, the `GetMoveDiagnostics` line builder at line 2302) — all + three call sites are reached by existing tests, but none assert on the formatted + date/time substring, and the type's `[ExcludeFromCodeCoverage]` attribute (line 21) + hides this execution from the Cobertura percentage regardless. Re-verified: the only + `ToggleExpansionStyle` reference in `QuickFiler.Test/` is a mock setup in + `QfcItemController.NavigationTests.cs:416` for the distinct, `Async`-suffixed member + `ToggleExpansionStyleAsync`; no existing test reaches line 1296 specifically. +- `EfcItemController.cs` (`SentDate`, `SentTime` properties) — **not covered**, + re-verified (zero matches for `SentDate|SentTime` in `EfcItemControllerTests.cs`). Also + excluded from the Cobertura percentage via the type-level `[ExcludeFromCodeCoverage]` + attribute (line 25). + +## Numeric Derivation Evidence + +- Complete Family: MM/dd/yyyy, HH:mm, HH:mm:ss +- Exhaustive Search Scope: The entire repository source tree, covering all tracked files across every project, with the reported member set then confined to the five QuickFiler controller files named in the issue. +- Inclusion Rules: Live code only, excluding every line whose code begins with a comment marker; any rendering of a System.DateTime value through a custom format string containing MM/dd/yyyy, HH:mm, or HH:mm:ss, reached either by a two-argument-eligible ToString call or by an interpolated format specifier; with no IFormatProvider argument supplied. +- Exclusion Rules: Numeric receivers of type double or int that already supply CultureInfo.InvariantCulture at the durationText and durationMinutesText sites; TimeSpan format strings whose separators are backslash-escaped literals; separator-free stamps such as yyyyMMddHHmmss; commented-out code; files outside the five named QuickFiler controller files; the uncompiled sources under the QuickFiler Legacy folder. +- Primary Search Strategy or Query Expression: Grep tool run over the entire repository source tree unioning the call-form pattern ToString\("(MM/dd/yyyy|HH:mm|HH:mm:ss)"\) with the interpolation-hole pattern \{[A-Za-z_][^}"]*:(MM/dd/yyyy|HH:mm|HH:mm:ss)\}, then discarding every hit whose code begins with a comment marker and confining the result to the five named QuickFiler controller files. +- Primary Member Set: QuickFiler/Controllers/QfcHomeController.Metrics.cs:48, QuickFiler/Controllers/QfcHomeController.Metrics.cs:125, QuickFiler/Controllers/QfcHomeController.Metrics.cs:127, QuickFiler/Controllers/EfcHomeController.Metrics.cs:95, QuickFiler/Controllers/EfcHomeController.Metrics.cs:96, QuickFiler/Controllers/EfcHomeController.Metrics.cs:118, QuickFiler/Controllers/EfcHomeController.Metrics.cs:119, QuickFiler/Controllers/QfcItemController.ViewerSetup.cs:465, QuickFiler/Controllers/QfcCollectionController.cs:235, QuickFiler/Controllers/QfcCollectionController.cs:1296, QuickFiler/Controllers/QfcCollectionController.cs:2302, QuickFiler/Controllers/EfcItemController.cs:607, QuickFiler/Controllers/EfcItemController.cs:612 +- Primary Count: 13 +- Cross-check Search Strategy or Query Expression: A receiver-identifier-anchored alternation over (SentDate|SentOn|now|currentDateTime|itemInfo) requiring either a ToString argument or an interpolation colon carrying MM/dd/yyyy or HH:mm or HH:mm:ss, corroborated by a full sequential read of the entire body of each of the five controller files to confirm no rendering of MM/dd/yyyy, HH:mm, or HH:mm:ss was missed by the alternation. +- Cross-check Member Set: QuickFiler/Controllers/EfcItemController.cs:612, QuickFiler/Controllers/EfcItemController.cs:607, QuickFiler/Controllers/QfcCollectionController.cs:2302, QuickFiler/Controllers/QfcCollectionController.cs:1296, QuickFiler/Controllers/QfcCollectionController.cs:235, QuickFiler/Controllers/QfcItemController.ViewerSetup.cs:465, QuickFiler/Controllers/EfcHomeController.Metrics.cs:119, QuickFiler/Controllers/EfcHomeController.Metrics.cs:118, QuickFiler/Controllers/EfcHomeController.Metrics.cs:96, QuickFiler/Controllers/EfcHomeController.Metrics.cs:95, QuickFiler/Controllers/QfcHomeController.Metrics.cs:127, QuickFiler/Controllers/QfcHomeController.Metrics.cs:125, QuickFiler/Controllers/QfcHomeController.Metrics.cs:48 +- Cross-check Count: 13 +- Member-set Comparison: The primary and cross-check member sets are identical after order-insensitive, case-insensitive normalization to path:line form; every member of each set appears in the other and the two counts match at 13. diff --git a/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/spec.md b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/spec.md new file mode 100644 index 000000000..e2ef44019 --- /dev/null +++ b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/spec.md @@ -0,0 +1,486 @@ +# quickfiler-date-time-format-missing-invariant-culture (Spec) + +- **Issue:** #742 +- **Parent (optional):** none +- **Owner:** drmoisan +- **Last Updated:** 2026-09-12T17-00 +- **Status:** Draft +- **Version:** 1.0 +- **Work Mode:** full-bug — this document is the sole authoritative acceptance-criteria source for this + issue. `user-story.md` in this feature folder is a supporting narrative only and carries no acceptance + criteria. + +## Context + +Five QuickFiler controller files render `System.DateTime` values through custom format strings whose +separators are culture-dependent, while the adjacent numeric fields in the same methods already pass +`CultureInfo.InvariantCulture`. In a .NET custom date/time format string, `:` is the `TimeSeparator` +custom specifier and `/` is the `DateSeparator` custom specifier; both resolve through the ambient +`DateTimeFormatInfo` rather than rendering as literal characters. Under a culture such as `it-IT`, whose +`TimeSeparator` is `.` rather than `:`, a call such as `now.ToString("HH:mm")` renders `13.05` instead of +`13:05` whenever the host machine's regional settings differ from `en-US`. + +This is a distinct defect from issue #645 (twelve-hour time format). It was discovered while researching +that issue, and #645's own acceptance criteria do not mention culture-invariance; the two defects are +independent (12-hour ambiguity vs. culture-dependent separator) and are tracked separately. + +Environment: +- OS/version: Windows 11, Outlook VSTO add-in host +- Runtime: C# / .NET Framework 4.8.1 (`TargetFrameworkVersion v4.8.1`) +- Command/flags used: not applicable (static code review and research-verified finding) +- Data source or fixture: the session-metrics CSV emitted by the QuickFiler and EFC metrics writers, and + on-screen/exception summary strings built from `SentDate` + +Impact / Severity: +- [ ] Blocker +- [ ] High +- [x] Medium +- [ ] Low + +Medium for the two CSV-writer files (`QfcHomeController.Metrics.cs`, `EfcHomeController.Metrics.cs`): a +wrong time or date separator corrupts a machine-read, write-only artifact in the same way the numeric +fields were already protected against. Low/cosmetic for the three UI-facing sites +(`QfcItemController.ViewerSetup.cs`, `QfcCollectionController.cs`, `EfcItemController.cs`), which only +affect on-screen or exception summary text. Filed as a single Medium entry since the writer sites +dominate. + +## Repro & Evidence + +Steps to Reproduce: +1. Set the Windows regional format (or thread `CurrentCulture`) to a locale whose + `DateTimeFormatInfo.TimeSeparator` or `DateSeparator` differs from `en-US` (for example `it-IT`, whose + `TimeSeparator` is `.`). +2. Run a QuickFiler filing session or an EFC move session so the session-metrics CSV writer executes, or + trigger a code path that builds a `SentDate`/`SentTime` display string. +3. Inspect the emitted CSV date/time columns, or the on-screen/exception summary string. + +Expected: + +Every date/time field emitted by these files renders with a fixed, invariant separator regardless of the +host machine's regional settings, matching the invariant-culture handling already applied to the +adjacent numeric fields in the same methods (per the existing comment at +`EfcHomeController.Metrics.cs:101-103`: "the metrics file is machine-read, so numeric fields are rendered +with the invariant culture rather than the operator's locale"). + +Actual: + +None of the date/time rendering operations in the affected files supply `CultureInfo.InvariantCulture` +(or any other `IFormatProvider`), so the rendered separator character is culture-dependent. The +authoritative site list below was re-derived against the current worktree on 2026-09-12 and supersedes +the stale line numbers previously recorded against this issue. It covers 5 production files, 13 distinct +source lines, and 17 individual date/time rendering operations: + +| File | Lines | Operations | Notes | +|---|---|---|---| +| `QuickFiler/Controllers/QfcHomeController.Metrics.cs` | 48, 125, 127 | 4 (line 48 carries 2) | Line 48 is the interpolated form `$"{now:MM/dd/yyyy},{now:HH:mm},"`; an interpolated string cannot take an `IFormatProvider` argument, so this site is rewritten as two explicit two-argument `ToString` calls rather than an appended argument. | +| `QuickFiler/Controllers/EfcHomeController.Metrics.cs` | 95, 96, 118, 119 | 4 | Two-argument call-form rewrite; `using System.Globalization;` already present. | +| `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` | 465 | 2 | Two calls on one line (`SentDate.ToString("MM/dd/yyyy")`, `SentDate.ToString("HH:mm")`); no `using System.Globalization;` in this file today. | +| `QuickFiler/Controllers/QfcCollectionController.cs` | 235, 1296, 2302 | 5 (1296 and 2302 carry 2 each) | No `using System.Globalization;` in this file today. | +| `QuickFiler/Controllers/EfcItemController.cs` | 607, 612 | 2 | No `using System.Globalization;` in this file today. | + +The line-48 site is a further in-scope site identified during research, beyond the five-file, +fifteen-site list the maintainer originally confirmed. It is an interpolated format specifier rather than +a two-argument `ToString` call, and both of the current research pass's independently constructed search +strategies detected it, so its inclusion is not a single-strategy artifact. See the research artifact's +Q1, Q2, and Numeric Derivation Evidence sections for the full two-strategy sweep that produced the +13-line, 17-operation, 5-file total used throughout this document. + +The CSV-writer sites carry the higher severity, since a wrong separator corrupts a machine-read artifact +in the same way the numeric fields were already protected against; the UI-facing sites are lower +severity, cosmetic-only. + +Logs / Screenshots: +- [x] Attached minimal logs or snippet +- Snippet: none of the date/time rendering operations at the sites listed above pass a `CultureInfo` + argument; contrast with the `durationText`/`durationMinutesText` calls in the same two Metrics.cs + files, which already do (`.ToString("##0", CultureInfo.InvariantCulture)`). + +## Scope & Non-Goals + +- In scope: + - The 13 source lines / 17 rendering operations in the five files listed in the authoritative table + above. + - Adding the missing `using System.Globalization;` directive to the three files that lack it. + - Rewriting the two self-referential test-oracle assertions described in Test Strategy below. + - Adding one new regression-test file with named tests covering each of the five production files. + - The one line in the test project file needed to compile the new test file. + +- Out of scope / non-goals (paths below are deliberately not backticked; this change does not touch + them): + - The UtilitiesCS and ToDoModel projects contain several genuine instances of the same defect class + (date/time-typed receivers rendered through an uncultured format string with a separator character), + but they sit in a different project boundary than the five named QuickFiler controller files this + issue is scoped to, and fixing them would pull unrelated assemblies into this change. They are + recommended as a separate follow-up issue and are not touched here. + - A diagnostic log line inside a different partial-class file of the QfcHomeController class (the + partial-class file that is not the Metrics file) renders a date/time value the same uncultured way, + but the string is written only to a diagnostic log inside a caught-cancellation handler, never + persisted or user-facing. It shares the defect mechanism but is deliberately excluded from this + issue's scope; it is recommended as a separate minimal follow-up issue rather than being bundled into + this diff. + - The uncompiled sources under the QuickFiler Legacy folder contain several matches for the same + format-string shape, but that folder is confirmed dead source: the QuickFiler project file has zero + compile references into it, so the code is not part of the built assembly and fixing it would have + no runtime effect. No action and no follow-up issue. + - A sortable-key method in the EmailSorter file formats a date/time value with a format string that + contains no separator characters at all (every component is a fixed-width numeric field with no + culture-dependent punctuation), so the ambient culture cannot alter its output. It is not a defect of + this class and needs no action. + - Two other apparent matches for the culture-sensitive shape are TimeSpan format strings whose + separators are backslash-escaped literals, which render as literal characters regardless of culture + under .NET's custom-format-string escape rules; they are false positives for this defect class and + need no fix. + - Any change to the `[ExcludeFromCodeCoverage]` attributes already present on the QfcCollectionController + and EfcItemController classes is out of scope; this issue neither adds nor removes that attribute. + - The QuickFiler production project file needs no edit, because no production source file is added or + deleted by this change; the set of compiled files is unchanged. + +## Root Cause Analysis + +Discovered during research for issue #645 ("Bug: quickfiler-session-metrics-twelve-hour-time-format") +while investigating whether `CultureInfo.InvariantCulture` should be added to the `hh:mm` -> `HH:mm` +sites that issue fixes. Tracing that issue's own cited target convention (`SentDate`'s `"HH:mm:ss"`) +showed that convention is itself uncultured, which means the gap is systemic across both Metrics.cs files +rather than isolated to the three sites issue #645 touches, and, per the follow-up research for this +issue, systemic across three additional controller files as well. + +The `H`/`h`/`m`/`d`/`M`/`y` letter specifiers in these format strings render ASCII 0-9 digits regardless +of culture (they are not digit-substituting specifiers); the only culture-dependent element in +`"HH:mm"`/`"MM/dd/yyyy"`/`"HH:mm:ss"` is the separator character, which resolves via +`DateTimeFormatInfo.DateSeparator` / `DateTimeFormatInfo.TimeSeparator`. The adjacent numeric fields +(`durationText`, `durationMinutesText`) in the same methods already guard against exactly this failure +mode by passing `CultureInfo.InvariantCulture` explicitly (see the comment at +`EfcHomeController.Metrics.cs:101-103`); the date/time fields simply never received the same treatment. + +Neither the repository's editor-configuration file nor its banned-symbols list carries an entry targeting +this defect class (CA1304, CA1305, CA1307) at any severity above the blanket suggestion default, so no +existing gate would have caught it. Neither of those two files is touched by this change, and both are +named here in plain prose without backticks so that the change-footprint harvester does not read them as +part of this issue's write footprint. + +## Proposed Fix + +### Design summary (what changes where) + +For each of the 12 call-form operations (all sites except line 48 of +`QuickFiler/Controllers/QfcHomeController.Metrics.cs`), append `, CultureInfo.InvariantCulture` as the +second argument to the existing single-argument `ToString("...")` call, leaving the format string itself +unchanged. + +For the one interpolated-form operation (line 48 of +`QuickFiler/Controllers/QfcHomeController.Metrics.cs`, `$"{now:MM/dd/yyyy},{now:HH:mm},"`), replace the +two interpolated specifiers with two explicit two-argument `ToString` calls, matching the shape already +used two lines below at lines 125 and 127 of the same file (`FormattableString.Invariant(...)` was +considered and rejected in favor of matching the file's own existing convention). + +Add `using System.Globalization;` to the three files that currently lack it: +`QuickFiler/Controllers/QfcItemController.ViewerSetup.cs`, `QuickFiler/Controllers/QfcCollectionController.cs`, +and `QuickFiler/Controllers/EfcItemController.cs`. The two Metrics files already have the directive. + +### Boundaries and invariants to preserve + +- The CSV field count, field order, and delimiter character emitted by the two metrics writers must not + change; only the separator character inside the date and time fields changes. +- No format string's letter specifiers (`MM`, `dd`, `yyyy`, `HH`, `mm`, `ss`) change; only the + `IFormatProvider` argument is added or, for the interpolated site, the syntax used to supply it. +- No public method signature, property signature, or return type changes. + +### Dependencies or blocked work + +None. `DateTime.ToString(string, IFormatProvider)` has been available since .NET Framework 1.1 and is +present on every receiver in the five files; all receivers resolve to plain, non-nullable +`System.DateTime` (confirmed for the COM-interop receiver at `QfcCollectionController.cs:235` as well, +since the Outlook PIA marshals the VARIANT date to `System.DateTime` before the `.ToString` call +executes). + +### Implementation strategy (what changes, not sequencing) + +#### Files/modules to change + +- `QuickFiler/Controllers/QfcHomeController.Metrics.cs` — 4 operations across lines 48, 125, 127. +- `QuickFiler/Controllers/EfcHomeController.Metrics.cs` — 4 operations across lines 95, 96, 118, 119. +- `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` — 2 operations at line 465, plus a new + `using System.Globalization;` directive. +- `QuickFiler/Controllers/QfcCollectionController.cs` — 5 operations across lines 235, 1296, 2302, plus a + new `using System.Globalization;` directive. +- `QuickFiler/Controllers/EfcItemController.cs` — 2 operations across lines 607, 612, plus a new + `using System.Globalization;` directive. + +#### Functions/classes/CLI commands impacted + +`QuickFileMetrics_WRITE`/`WriteMetricsAsync` (`QfcHomeController.Metrics.cs`), +`BuildQuickFileMetricLines` (`EfcHomeController.Metrics.cs`), `GetItemSummary` +(`QfcItemController.ViewerSetup.cs`), `TryGetMoveReadiness` / the `ToggleExpansionStyle` message builder / +the `GetMoveDiagnostics` diagnostics-line builder (`QfcCollectionController.cs`), and the `SentDate`/ +`SentTime` properties (`EfcItemController.cs`). No CLI commands are impacted. + +#### Data flow and validation changes + +None. No new inputs, no new validation. The only observable change is the rendered separator character +in existing output strings. + +#### Error handling and logging updates + +None required; no exception path or log statement changes shape. + +#### Rollback/feature-flag considerations (if applicable) + +No feature flag. The change is a formatting-argument addition with no behavioral branch; rollback is a +plain source revert of the five files. + +### Technical specifications (interfaces/contracts) + +#### Inputs/outputs and formats + +Output format strings (`"MM/dd/yyyy"`, `"HH:mm"`, `"HH:mm:ss"`) are unchanged. Output separator +characters become fixed (`/` for date, `:` for time) regardless of the host machine's regional settings, +because every rendering operation now supplies `CultureInfo.InvariantCulture`. + +#### Required configuration keys and defaults + +None. + +#### Backward-compatibility expectations + +No public API changes. On a host whose regional settings already use `en-US`-equivalent separators +(`/` and `:`), rendered output is byte-for-byte identical before and after the fix. On a host whose +regional settings use different separators, rendered output changes from the previous, incorrect +culture-dependent separator to the fixed invariant separator; this is the intended defect fix, not a +compatibility break. + +#### Performance constraints (latency/throughput/memory) + +None; adding a second, already-allocated static `CultureInfo.InvariantCulture` argument has no measurable +performance impact. + +## Assumptions, Constraints, Dependencies + +- Assumptions (environment, data, access): + - The test-host's default culture renders `en-US`-equivalent separators (`/` for date, `:` for time), + which are identical to `CultureInfo.InvariantCulture`'s separators, so existing literal assertions + elsewhere in the test suite that were not built from an uncultured `ToString(format)` call are + unaffected by this change. + - The five files' receivers are always plain `System.DateTime` (never `DateTime?`), confirmed for every + site in this issue's research. +- Constraints (budget, performance, compatibility): + - Both the production project and the test project target `TargetFrameworkVersion v4.8.1`; the + two-argument `ToString(string, IFormatProvider)` overload is available on that target with no + compatibility concern. +- External dependencies (services, libraries, releases): none. Only the `System.Globalization` namespace, + already part of the base class library, is required. + +## Data / API / Config Impact + +- User-facing or API changes: none to method signatures or property types. The rendered text of the + session-metrics CSV date/time fields and of the three UI-facing summary strings changes only on hosts + whose regional settings differ from `en-US`-equivalent separators, and only in the direction of + becoming culture-invariant. +- Data or migration considerations: the session-metrics CSV is a write-only, machine-read artifact with + no stored history that needs migration; each run produces a fresh file. +- Logging/telemetry updates: none. +- Compatibility notes: no CLI flags, config schema, or versioning changes. + +## Test Strategy + +- Rewrite the two self-referential test oracles in + `QuickFiler.Test/Controllers/QfcHomeControllerMetricsTests.cs` + (`WriteMetricsAsync_UsesInjectedClock_ForDateAndTimeStamps` at line 243 and + `QuickFileMetrics_WRITE_UsesInjectedClock_ForDataLine` at line 278) so that the expected value they + build for comparison is constructed with `CultureInfo.InvariantCulture` rather than an uncultured + `.ToString(format)` call. Today both tests build their expected string with the same uncultured call + the unfixed production code uses, so after the production fix the two calls would diverge under any + non-invariant-equivalent ambient culture; rewriting the expected-value construction to use + `CultureInfo.InvariantCulture` restores them as a valid regression oracle. +- Add one new test file, `QuickFiler.Test/Controllers/QuickFilerInvariantCultureIssue742Tests.cs`, with + one named regression test per production file (five total), each following the deterministic + culture-swap pattern already established at lines 138-156 of the existing EfcHomeControllerMetricsTests + file (not modified by this change) and + `QfcHomeControllerMetricsTests.cs:186-211`: save `CultureInfo.CurrentCulture`, set a substitute culture + inside a `try` block, and restore the original in a `finally` block. + - The substitute culture must not be a named culture whose separator characters are asserted against + directly, because `DateTimeFormatInfo` separator values for a named culture can vary across .NET and + ICU versions, which would make the assertion environment-dependent. Instead, each new test clones a + `CultureInfo` and assigns explicit sentinel characters to `DateTimeFormat.DateSeparator` and + `DateTimeFormat.TimeSeparator` (any character other than `/` and `:`) before activating it as + `CurrentCulture`. The assertion is then that production output contains `/` and `:` — the invariant + separators — while the ambient culture, if it had been honored, would have produced the sentinel + characters instead. + - Each of the five new tests exercises its production file's documented seam: the injectable + `TimeProvider` property for `QfcHomeController.Metrics.cs`; a direct `DateTime` parameter for + `EfcHomeController.Metrics.cs`'s `BuildQuickFileMetricLines`; a directly constructed `MailItemHelper` + with `SentDate` set for `QfcItemController.ViewerSetup.cs`'s `GetItemSummary`; Moq mocks of + `IQfcItemController` (and, for the line-235 site only, a Moq mock of the Outlook interop `MailItem`) + for `QfcCollectionController.cs`; and a directly assigned `MailItemHelper` for + `EfcItemController.cs`'s `SentDate`/`SentTime` properties. None of the five requires a live Outlook + process or a WinForms handle. + - No test created for this issue uses a temporary file, `Thread.Sleep`, or `Task.Delay`, per repository + policy. +- No change to any other existing test file; the sweep in the research artifact's Q4 section confirms no + other existing test constructs an expected value from a date/time `.ToString` call on one of the five + in-scope files. +- Toolchain commands to run, in order, restarting from the top if any step fails or auto-fixes files: + 1. `dotnet tool run csharpier format .` (verify with `dotnet tool run csharpier check .`) + 2. `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true` + 3. `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true` + 4. `vstest.console.exe QuickFiler.Test.dll /EnableCodeCoverage /InIsolation`, run against the built + QuickFiler.Test test assembly. + Never add `/p:Nullable=enable` to step 2 or 3. Never substitute `/t:Build` for `/t:Rebuild` in step 2 or + 3. Close Outlook (never kill the process) before running step 2, 3, or 4, since the rebuild targets the + QuickFiler VSTO add-in output that a running Outlook process can hold locked. +- Evidence handling, per the maintainer decision on issue #671 (effective 2026-09-11): commit projections + only. Record the pre-fix discovery-count figures this document's acceptance criteria depend on (the + 14/4/1/3/2 per-file counts, the single `[{]now:` count, and the 4/2 `CultureInfo.InvariantCulture` + counts) as a Markdown baseline artifact under the feature folder's evidence directory, kind baseline, + written before any Write Set source file is modified. Record the toolchain pass result and the + named-test pass/fail outcome as a Markdown summary under the feature folder's evidence directory, kind + qa-gates. Record the fixed/regression test run outcome as a Markdown summary under the feature folder's + evidence directory, kind regression-testing. Use the kind other for any evidence artifact that does not + fit the first three kinds. Do not commit a raw test-results file carrying the trx extension, and do not + commit a raw Cobertura coverage XML file, as an evidence artifact in any of these directories; discard + the raw tool output after transcribing the pass/fail counts and coverage figures into the Markdown + artifact. + +## Acceptance Criteria + +> Verification note, applicable to every item below: any check expressed as a `git grep` search against +> a file this change newly creates (the two new-file entries in the Write Set) must use +> `git grep --untracked`, or must run only after that file has been staged with `git add`; a plain +> `git grep` does not see an untracked file and reports nothing for it. Any check expressed as a +> zero-match expectation must be read as "the command prints no line for that path and exits 1," never as +> "the command returns 0," because `git grep -c` prints no line at all, and exits 1, for a path with zero +> matches. + +- [ ] The interpolated site formerly at line 48 of `QuickFiler/Controllers/QfcHomeController.Metrics.cs` + is rewritten as two explicit two-argument `ToString` calls, each passing `CultureInfo.InvariantCulture`. + Checked two ways, because an absence claim alone is not a sufficient gate. First: a search of that file + for the interpolated-specifier token, written with a bracketed character class as `[{]now:` so the + pattern is not affected by shell metacharacter handling, prints no line for that file and exits 1 after + the fix. On the unfixed tree that same search matches exactly 1 line, which is line 48 itself; that + pre-fix figure of 1 is the discovery-count control proving the search can match, so the post-fix + zero-match result is a real observation rather than a search that cannot match anything for an + unrelated reason. Second: the count of lines in that file containing `CultureInfo.InvariantCulture` + rises from exactly 4 on the unfixed tree (the pre-existing numeric-field sites at lines 70, 73, 150 and + 153) to a strictly greater number after the fix. The exact post-fix figure is deliberately not fixed + here, because CSharpier decides whether the two replacement calls land on one line or two apiece, and a + predicted figure would be a formatter artifact rather than a property of the change. Both pre-fix + figures were measured on 2026-09-12 against the unfixed tree. +- [ ] Lines 125 and 127 of `QuickFiler/Controllers/QfcHomeController.Metrics.cs` each pass + `CultureInfo.InvariantCulture` as the second argument to their `ToString` call. +- [ ] Lines 95, 96, 118, and 119 of `QuickFiler/Controllers/EfcHomeController.Metrics.cs` each pass + `CultureInfo.InvariantCulture` as the second argument to their `ToString` call. As a discovery-count + control, the count of lines in that file containing `CultureInfo.InvariantCulture` rises from exactly 2 + on the unfixed tree (the pre-existing numeric-field sites) to a strictly greater number after the fix; + this pre-fix figure of 2 was measured on 2026-09-12 against the unfixed tree. +- [ ] Line 465 of `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` has both of its `ToString` + calls passing `CultureInfo.InvariantCulture` as the second argument, and the file gains a + `using System.Globalization;` directive (exactly one occurrence). +- [ ] Lines 235, 1296, and 2302 of `QuickFiler/Controllers/QfcCollectionController.cs` have all five of + their `ToString` calls passing `CultureInfo.InvariantCulture` as the second argument, and the file + gains a `using System.Globalization;` directive (exactly one occurrence). +- [ ] Lines 607 and 612 of `QuickFiler/Controllers/EfcItemController.cs` each pass + `CultureInfo.InvariantCulture` as the second argument to their `ToString` call, and the file gains a + `using System.Globalization;` directive (exactly one occurrence). +- [ ] A line-count search using the regular expression written with bracketed character classes as + `[.]ToString[(]@\?"[^"]*[:/.\-][^"]*"[)]`, restricted to the five production files listed above, returns + exactly 2 matching lines after the fix, both of them in + `QuickFiler/Controllers/QfcHomeController.Metrics.cs`. The residual 2 are the commented-out predecessor + statements at lines 45 and 46 of that file, which this change deliberately does not edit. The same + search on the unfixed tree returns 14 matching lines, distributed 4 / 4 / 1 / 3 / 2 across + `QuickFiler/Controllers/QfcHomeController.Metrics.cs`, + `QuickFiler/Controllers/EfcHomeController.Metrics.cs`, + `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs`, + `QuickFiler/Controllers/QfcCollectionController.cs` and + `QuickFiler/Controllers/EfcItemController.cs`; that pre-fix figure of 14 is the discovery-count control + that makes the post-fix figure of 2 a real observation rather than a search that matches nothing for an + unrelated reason. Both figures were measured on 2026-09-12 against the unfixed tree. The `@` is written + as `@\?` and not as `@?` because the search runs in basic-regular-expression mode, in which a bare + question mark is a literal character rather than a quantifier; the unescaped form matches nothing in + this repository whatever the executor does, which would make this criterion unfalsifiable. Both + spellings were run against the unfixed tree on 2026-09-12: the escaped form printed the 4/4/1/3/2 + per-file distribution and exited 0, and the unescaped form printed no line and exited 1. +- [ ] In `QuickFiler.Test/Controllers/QfcHomeControllerMetricsTests.cs`, the test method + `WriteMetricsAsync_UsesInjectedClock_ForDateAndTimeStamps` and the test method + `QuickFileMetrics_WRITE_UsesInjectedClock_ForDataLine` both build their expected date/time substring + using `CultureInfo.InvariantCulture` rather than an uncultured `.ToString(format)` call, and both tests + pass. +- [ ] The new file `QuickFiler.Test/Controllers/QuickFilerInvariantCultureIssue742Tests.cs` exists and + contains exactly five test methods, one exercising each of the five production files named in this + issue, each following the try/finally culture-swap pattern with an explicit sentinel `DateSeparator` + and `TimeSeparator` (not a named culture's separators) and asserting that production output contains + `/` and `:`; all five tests pass. +- [ ] `QuickFiler.Test/QuickFiler.Test.csproj` gains exactly one new `Compile Include` entry naming the + new test file. Checked as a pair of counts rather than by eye: a line-count search of that file for + `QuickFilerInvariantCultureIssue742Tests` returns exactly 1 after the fix and prints no line, exiting 1, + before the fix, and a line-count search for `Compile Include` in that file returns exactly one more + after the fix than it did before the fix. The new entry sits inside the single existing `ItemGroup` + element that opens at line 57 of that file, which is the element holding every existing `Compile` + entry for the Controllers folder. +- [ ] No new test-results file carrying the trx extension, and no new Cobertura coverage XML file, + appears as a tracked evidence artifact in the repository as a result of this change. Per the maintainer + decision on issue #671, effective + 2026-09-11, only Markdown evidence summaries are committed; the numeric pass/fail counts and coverage + figures produced by the toolchain run are transcribed into Markdown artifacts under the feature + folder's evidence directory, and the raw tool output is discarded rather than committed. +- [ ] A Markdown baseline evidence artifact exists under the feature folder's evidence directory, kind + baseline, recording the pre-fix discovery-count figures this document's acceptance criteria depend on + (the 14/4/1/3/2 per-file counts for the `[.]ToString[(]` sweep, the single `[{]now:` count, and the 4/2 + `CultureInfo.InvariantCulture` counts), and that artifact was written before any Write Set source file + was modified. +- [ ] `dotnet tool run csharpier format .` followed by `dotnet tool run csharpier check .` both complete + with no remaining diff. +- [ ] `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true` + completes with zero analyzer errors. +- [ ] `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true` + completes with zero errors. +- [ ] `vstest.console.exe QuickFiler.Test.dll /EnableCodeCoverage /InIsolation`, run against the built + QuickFiler.Test test assembly, reports zero failures for QuickFiler.Test, including the two rewritten tests and the + five new tests. Outlook was closed, not killed, before the preceding rebuild steps. +- [ ] Within the QuickFiler and QuickFiler.Test source trees, the change touches exactly the eight paths + listed in the Write Set section below and no other file. This is scoped to those two trees on purpose: + the same change also adds Markdown evidence artifacts and updates planning documents inside this + feature folder, and those additions are expected rather than a scope violation, so a repository-wide + phrasing of this criterion would be unsatisfiable. + +## Risks & Mitigations + +- Risk: MSTest does not reset `CultureInfo.CurrentCulture` between test methods, and a new test that sets + it without restoring it in a `finally` block can leak the changed culture into a sibling test running + on the same worker thread afterward. Mitigation: every new test mirrors the exact try/finally restore + shape already used by the two existing de-DE tests in this test suite. +- Risk: `QfcCollectionController` and `EfcItemController` both carry a type-level + `[ExcludeFromCodeCoverage]` attribute, so the new assertions on lines 235/1296/2302/607/612 will not + raise the Cobertura coverage percentage even though they are genuinely tested. Mitigation: rely on the + named test pass/fail result, not the coverage percentage, as the quality signal for these two files; + this issue does not change the attribute. +- Risk: an incorrect rewrite of the interpolated line 48 could shift the CSV field delimiter or field + count. Mitigation: match the exact literal shape already used at lines 125 and 127 of the same file, + which are of a known-correct construction (`ToString(format, CultureInfo.InvariantCulture)` on the same + `now` value, comma-appended in the same position). + +## Rollout & Follow-up + +- Release/rollout steps: no feature flag or staged rollout; the fix ships in the same build as any other + source change to these five files. +- Post-fix monitoring or clean-up tasks: none required; the change has no telemetry or config surface to + monitor. +- Follow-up issues recommended by research (not part of this issue's scope): a separate issue for the + UtilitiesCS and ToDoModel projects' equivalent sites, and a separate minimal follow-up issue for the + diagnostic-only log line in the non-Metrics partial-class file of QfcHomeController. +- Links: issue #742; prior related issue #645 (twelve-hour time format, distinct defect); research + artifact dated 2026-09-12T16-00 in this feature folder's research directory. + +## Write Set + +> Do not add or remove backticks in this section. This is the canonical list of paths this change will +> create, modify, or delete. Every backticked path elsewhere in this document repeats one of these eight +> entries or is an out-of-scope path deliberately left unbackticked in prose; do not backtick any path +> outside this list. + +- `QuickFiler/Controllers/QfcHomeController.Metrics.cs` +- `QuickFiler/Controllers/EfcHomeController.Metrics.cs` +- `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` +- `QuickFiler/Controllers/QfcCollectionController.cs` +- `QuickFiler/Controllers/EfcItemController.cs` +- `QuickFiler.Test/Controllers/QfcHomeControllerMetricsTests.cs` +- `QuickFiler.Test/Controllers/QuickFilerInvariantCultureIssue742Tests.cs` +- `QuickFiler.Test/QuickFiler.Test.csproj` diff --git a/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/user-story.md b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/user-story.md new file mode 100644 index 000000000..5befa5a71 --- /dev/null +++ b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/user-story.md @@ -0,0 +1,37 @@ +# User Story — quickfiler-date-time-format-missing-invariant-culture (Issue #742) + +- Work Mode: full-bug +- Note: this document is a supporting narrative only. It does not carry acceptance criteria. The + acceptance criteria for this issue live exclusively in spec.md, per this repository's full-bug work + mode convention. + +## Who is affected + +An operator running the QuickFiler or EFC add-in on a Windows machine whose regional date or time format +uses a separator character other than the ones the original code assumed. This includes any locale whose +date separator is not a forward slash or whose time separator is not a colon. + +## What breaks + +When the operator's regional settings use a different separator, two kinds of output are affected: + +- The session-metrics CSV file that QuickFiler and the EFC writer produce after a filing or move session + is a machine-read artifact. Its date and time columns pick up the operator's regional separator instead + of the fixed separator the file format expects, so a downstream process reading that CSV encounters an + unexpected character in a field it treats as fixed-format. +- On-screen summary text and exception messages that include a sent date or sent time show the same + locale-dependent separator instead of a consistent one, which is a cosmetic inconsistency rather than a + data-corruption risk. + +The numeric fields recorded alongside these date and time fields in the same output already avoid this +problem: they are rendered with a fixed, locale-independent format. The date and time fields were never +given the same protection, which is the gap this issue closes. + +## What done looks like + +After this fix, every date and time value written to the session-metrics CSV, and every date and time +value shown in the affected on-screen or exception summary text, renders with the same fixed separator +characters no matter what regional format the operator's machine is set to. An operator running on any +Windows regional setting sees and produces the same date and time formatting as an operator running on +the setting the code was originally written against. No behavior other than the separator character +changes: the same fields appear in the same order, and no fields are added or removed. From cfc3780bf6b03a14921c051fa0b38c973f7e4c79 Mon Sep 17 00:00:00 2001 From: Dan Moisan Date: Mon, 14 Sep 2026 01:48:37 -0400 Subject: [PATCH 2/4] docs(742): coordinator amendment re-deriving two baselines and the raw coverage read location --- .../plan.2026-09-12T16-09.md | 65 +++++++++++++++++-- 1 file changed, 58 insertions(+), 7 deletions(-) diff --git a/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/plan.2026-09-12T16-09.md b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/plan.2026-09-12T16-09.md index 1839f6490..d2c98ba3f 100644 --- a/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/plan.2026-09-12T16-09.md +++ b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/plan.2026-09-12T16-09.md @@ -20,7 +20,7 @@ **Evidence location:** every evidence artifact below resolves under `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence//` using kinds `baseline`, `regression-testing`, `qa-gates`, `other`. No other evidence location is used. -**Evidence-artifact discipline (per the maintainer decision on issue #671, effective 2026-09-11):** no task commits a raw test-results file carrying the `.trx` extension, and no task commits a raw Cobertura coverage XML file, as an evidence artifact anywhere in the repository. Where a coverage-collection step needs a raw output file, it is written under the repository's gitignored `coverage\` directory (this plan uses `coverage\issue-742-coverage\`, matched by the `coverage/*` entry in `.gitignore`), the needed numeric figures are transcribed into the Markdown evidence artifact, and the raw output directory is deleted by the same task before the task is considered complete. +**Evidence-artifact discipline (per the maintainer decision on issue #671, effective 2026-09-11):** no task commits a raw test-results file carrying the `.trx` extension, and no task commits a raw Cobertura coverage XML file, as an evidence artifact anywhere in the repository. Where a coverage-collection step needs a raw output file, it is written under the repository's gitignored `coverage\` directory (this plan uses the coverage runner's default output path `coverage\coverage.cobertura.xml`, matched by the `coverage/*` entry in `.gitignore`), the needed numeric figures are transcribed into the Markdown evidence artifact, and the raw output directory is deleted by the same task before the task is considered complete. **Grep-visibility convention (untracked file):** `QuickFiler.Test/Controllers/QuickFilerInvariantCultureIssue742Tests.cs` is created by this plan and remains untracked until the Phase 5 staging task. Every `git grep` command in this plan that targets that file therefore carries the `--untracked` flag. Every other file this plan edits is already tracked, so `git grep` against those files needs no `--untracked` flag. @@ -48,14 +48,14 @@ - [ ] [P0-T7] Build the current (unfixed) `QuickFiler.Test` assembly so a baseline test run is possible. Run `pwsh -NoProfile -Command 'msbuild TaskMaster.sln /t:Build /m /p:Configuration=Debug "/p:Platform=Any CPU"'`. Acceptance: `EXIT_CODE` is 0. Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/quickfiler-test-build.2026-09-12T16-09.md`. -- [ ] [P0-T8] Baseline `QuickFiler.Test` pass/fail state and coverage figure on the unfixed tree, using the repository's coverage runner, scoped to the `QuickFiler.Test` assembly only, with the raw Cobertura output written under the repository's gitignored `coverage\` directory and deleted by this task before completion (the script's `-CoverageOutput` parameter is passed through `Join-Path $repoRoot $CoverageOutput`, which does not special-case an already-rooted path, so an absolute value produces a malformed path; a relative value avoids this). Run `pwsh -NoProfile -Command './scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot QuickFiler.Test -Configuration Debug -CoverageOutput "coverage\issue-742-coverage\baseline.cobertura.xml"; Write-Output "EXITCODE=$LASTEXITCODE"'`. This inner invocation already carries `/InIsolation` and `/TestCaseFilter:TestCategory!=LiveOutlook` (see `scripts/vscode/Invoke-MSTestWithCoverage.ps1`'s own argument construction), satisfying the plan's vstest-invocation requirements; scoping `-SearchRoot` to `QuickFiler.Test` restricts assembly discovery to `QuickFiler.Test.dll` and avoids running the rest of the repository's test suite, including the UtilitiesCS.Test shell-icon classes known to stall vstest on this machine. Acceptance: none (baseline only). Open the produced Cobertura XML at `coverage\issue-742-coverage\baseline.cobertura.xml` and transcribe the `line-rate` figure for the classes covering `QfcHomeController.Metrics.cs`, `EfcHomeController.Metrics.cs`, and the `QfcItemController` `ViewerSetup` partial, together with the overall pass/fail/skip counts vstest reports; explicitly note that `QfcCollectionController` and `EfcItemController` carry a type-level `[ExcludeFromCodeCoverage]` attribute and are absent from the report, so their baseline quality signal is the named-test pass/fail state captured here, not a coverage percentage. Delete the `coverage\issue-742-coverage\` directory after transcribing. Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` (pass/fail/skip counts plus the three transcribed line-rate figures) in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/vstest-coverage-baseline.2026-09-12T16-09.md`. +- [ ] [P0-T8] Baseline `QuickFiler.Test` pass/fail state and coverage figure on the unfixed tree, using the repository's coverage runner, scoped to the `QuickFiler.Test` assembly only, with the raw Cobertura output written under the repository's gitignored `coverage\` directory and deleted by this task before completion (the script's `-CoverageOutput` parameter is passed through `Join-Path $repoRoot $CoverageOutput`, which does not special-case an already-rooted path, so an absolute value produces a malformed path; a relative value avoids this). Run `pwsh -NoProfile -Command './scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot QuickFiler.Test -Configuration Debug -CoverageOutput "coverage\coverage.cobertura.xml"; Write-Output "EXITCODE=$LASTEXITCODE"'`. This inner invocation already carries `/InIsolation` and `/TestCaseFilter:TestCategory!=LiveOutlook` (see `scripts/vscode/Invoke-MSTestWithCoverage.ps1`'s own argument construction), satisfying the plan's vstest-invocation requirements; scoping `-SearchRoot` to `QuickFiler.Test` restricts assembly discovery to `QuickFiler.Test.dll` and avoids running the rest of the repository's test suite, including the UtilitiesCS.Test shell-icon classes known to stall vstest on this machine. Acceptance: none (baseline only). Open the produced Cobertura XML at `coverage\coverage.cobertura.xml` and transcribe the `line-rate` figure for the classes covering `QfcHomeController.Metrics.cs`, `EfcHomeController.Metrics.cs`, and the `QfcItemController` `ViewerSetup` partial, together with the overall pass/fail/skip counts vstest reports; explicitly note that `QfcCollectionController` and `EfcItemController` carry a type-level `[ExcludeFromCodeCoverage]` attribute and are absent from the report, so their baseline quality signal is the named-test pass/fail state captured here, not a coverage percentage. Delete `coverage\coverage.cobertura.xml` after transcribing. Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` (pass/fail/skip counts plus the three transcribed line-rate figures) in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/vstest-coverage-baseline.2026-09-12T16-09.md`. - [ ] [P0-T9] Baseline discovery-count controls, re-derived against the current tree in this pass (values confirmed 2026-09-12), written BEFORE any Write Set source file is modified. Run each of these eleven `git grep` commands and transcribe the exact printed output and exit code of each: 1. `git grep -c '[.]ToString[(]@\?"[^"]*[:/.\-][^"]*"[)]' -- QuickFiler/Controllers/QfcHomeController.Metrics.cs QuickFiler/Controllers/EfcHomeController.Metrics.cs QuickFiler/Controllers/QfcItemController.ViewerSetup.cs QuickFiler/Controllers/QfcCollectionController.cs QuickFiler/Controllers/EfcItemController.cs` — expected per-file counts 4, 4, 1, 3, 2 (14 total). The `@` is escaped as `@\?` rather than `@?` because `git grep`'s default basic-regex mode treats a bare `?` as a literal character, not a quantifier; `\?` is the documented GNU basic-regex extension for "optional preceding atom." An unescaped `@?` never matches anything in this repository (no site uses a verbatim string literal), so every acceptance condition built on this pattern would read as a permanent zero-match regardless of whether the fix was applied. 2. `git grep -c '[{]now:' -- QuickFiler/Controllers/QfcHomeController.Metrics.cs` — expected 1. 3. `git grep -c 'CultureInfo[.]InvariantCulture' -- QuickFiler/Controllers/QfcHomeController.Metrics.cs` — expected 4. 4. `git grep -c 'CultureInfo[.]InvariantCulture' -- QuickFiler/Controllers/EfcHomeController.Metrics.cs` — expected 2. - 5. `git grep -c -F 'Compile Include' -- QuickFiler.Test/QuickFiler.Test.csproj` — expected 172. + 5. `git grep -c -F 'Compile Include' -- QuickFiler.Test/QuickFiler.Test.csproj` — expected 176. 6. `git grep -c -F 'QuickFilerInvariantCultureIssue742Tests' -- QuickFiler.Test/QuickFiler.Test.csproj` — expected: prints no line, exits 1. 7. `git grep -c -F 'using System.Globalization;' -- QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` — expected: prints no line, exits 1. 8. `git grep -c -F 'using System.Globalization;' -- QuickFiler/Controllers/QfcCollectionController.cs` — expected: prints no line, exits 1. @@ -76,7 +76,7 @@ - Test 5, for `QuickFiler/Controllers/EfcItemController.cs`: construct an `EfcItemController` via `FormatterServices.GetUninitializedObject(typeof(EfcItemController))`, assign a directly constructed `MailItemHelper` with `SentDate` set to the private `_itemInfo` field via the established `SetPrivateField` reflection helper already present in `QuickFiler.Test/Controllers/EfcItemControllerTests.cs`, read the `SentDate` and `SentTime` properties, and assert both returned strings contain `/` and `:` respectively. No test in this file uses a temporary file, `Thread.Sleep`, or `Task.Delay`. Acceptance: `git grep --untracked -c -F '[TestMethod]' -- QuickFiler.Test/Controllers/QuickFilerInvariantCultureIssue742Tests.cs` prints the count `5` for that path and `EXIT_CODE` is 0. -- [ ] [P1-T2] Add exactly one new line, ``, inside the single `` element that opens at line 57 of `QuickFiler.Test/QuickFiler.Test.csproj` (the element closes at line 230 and holds every other `Controllers\` compile entry, including `` at line 199; anchor the insertion adjacent to that literal line rather than by line number). Acceptance: `git grep -c -F 'QuickFilerInvariantCultureIssue742Tests' -- QuickFiler.Test/QuickFiler.Test.csproj` prints the count `1` (baseline was: prints no line, exits 1, per P0-T9 control 6), and `git grep -c -F 'Compile Include' -- QuickFiler.Test/QuickFiler.Test.csproj` prints the count `173` (baseline was 172, per P0-T9 control 5). +- [ ] [P1-T2] Add exactly one new line, ``, inside the single `` element that opens at line 57 of `QuickFiler.Test/QuickFiler.Test.csproj` (the element closes at line 230 and holds every other `Controllers\` compile entry, including `` at line 199; anchor the insertion adjacent to that literal line rather than by line number). Acceptance: `git grep -c -F 'QuickFilerInvariantCultureIssue742Tests' -- QuickFiler.Test/QuickFiler.Test.csproj` prints the count `1` (baseline was: prints no line, exits 1, per P0-T9 control 6), and `git grep -c -F 'Compile Include' -- QuickFiler.Test/QuickFiler.Test.csproj` prints the count `177` (baseline was 176, per P0-T9 control 5). - [ ] [P1-T3] Build the test project with the new file included, against the still-unfixed production tree. Run `pwsh -NoProfile -Command 'msbuild TaskMaster.sln /t:Build /m /p:Configuration=Debug "/p:Platform=Any CPU"'`. Acceptance: `EXIT_CODE` is 0 (the new test file compiles even though production is not yet fixed, because the test only reads production output rather than requiring production behavior to already be correct in order to compile). @@ -122,7 +122,7 @@ - [ ] [P5-T3] Run `pwsh -NoProfile -Command '$out = msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true 2>&1; $exit = $LASTEXITCODE; $out | Select-String -Pattern "^\s*\d+ Error\(s\)\s*$" | ForEach-Object { $_.Line.Trim() }; Write-Output "EXITCODE=$exit"'`. Do not add `/p:Nullable=enable`. Acceptance: the transcribed summary line, once trimmed, equals exactly the literal `0 Error(s)` and `EXITCODE` is 0. If this fails, restart from P5-T1. Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/qa-gates/msbuild-nullable-final.2026-09-12T16-09.md`. -- [ ] [P5-T4] Run the `QuickFiler.Test` suite with coverage enabled via the repository's coverage runner, scoped to the `QuickFiler.Test` assembly only, again writing the raw Cobertura output under the repository's gitignored `coverage\` directory and deleting it before completion (same `Join-Path`/rooted-path reasoning as P0-T8). Run `pwsh -NoProfile -Command './scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot QuickFiler.Test -Configuration Debug -CoverageOutput "coverage\issue-742-coverage\final.cobertura.xml"; Write-Output "EXITCODE=$LASTEXITCODE"'`. Acceptance: `EXITCODE` is 0 (zero failures for `QuickFiler.Test`, including the two rewritten oracle tests and the five new tests; this inner invocation already carries `/InIsolation` and `/TestCaseFilter:TestCategory!=LiveOutlook`). If this fails or a prior step in this phase rewrote a tracked file since P5-T1, restart from P5-T1. Open the produced Cobertura XML at `coverage\issue-742-coverage\final.cobertura.xml`, transcribe the post-change `line-rate` figures for the same three non-excluded classes captured in P0-T8, state the baseline-vs-final comparison for each, and confirm no regression on the lines this change touched; also transcribe the overall pass/fail/skip counts. Delete the `coverage\issue-742-coverage\` directory after transcribing. Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/qa-gates/vstest-coverage-final.2026-09-12T16-09.md`. +- [ ] [P5-T4] Run the `QuickFiler.Test` suite with coverage enabled via the repository's coverage runner, scoped to the `QuickFiler.Test` assembly only, again writing the raw Cobertura output under the repository's gitignored `coverage\` directory and deleting it before completion (same `Join-Path`/rooted-path reasoning as P0-T8). Run `pwsh -NoProfile -Command './scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot QuickFiler.Test -Configuration Debug -CoverageOutput "coverage\coverage.cobertura.xml"; Write-Output "EXITCODE=$LASTEXITCODE"'`. Acceptance: `EXITCODE` is 0 (zero failures for `QuickFiler.Test`, including the two rewritten oracle tests and the five new tests; this inner invocation already carries `/InIsolation` and `/TestCaseFilter:TestCategory!=LiveOutlook`). If this fails or a prior step in this phase rewrote a tracked file since P5-T1, restart from P5-T1. Open the produced Cobertura XML at `coverage\coverage.cobertura.xml`, transcribe the post-change `line-rate` figures for the same three non-excluded classes captured in P0-T8, state the baseline-vs-final comparison for each, and confirm no regression on the lines this change touched; also transcribe the overall pass/fail/skip counts. Delete `coverage\coverage.cobertura.xml` after transcribing. Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/qa-gates/vstest-coverage-final.2026-09-12T16-09.md`. - [ ] [P5-T5] Confirm the `[TestMethod]` count in the new test file remains correct after formatting. Run `git grep --untracked -c -F '[TestMethod]' -- QuickFiler.Test/Controllers/QuickFilerInvariantCultureIssue742Tests.cs`. Acceptance: the command prints the count `5` (unchanged from P1-T1; CSharpier does not add or remove attributes) and `EXIT_CODE` is 0. Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/qa-gates/test-method-count-final.2026-09-12T16-09.md`. @@ -198,7 +198,58 @@ UNRESOLVED-GAPS: NONE - `QuickFiler.Test/Controllers/QfcCollectionControllerDefects468Tests.cs`, lines 405-421 — re-read in this pass; confirmed the `GroupWithFolder` mock-construction pattern (`Mock` plus `Mock`) reused for Phase 1 Test 4. - `QuickFiler/Interfaces/IQfcItemController.cs`, lines 41-47 — re-read in this pass; confirmed `ItemHelper`, `Mail`, `SelectedFolder`, and `ItemNumber` members used by the Phase 1 Test 4 mock setup. - The QuickFiler production project file (not backticked here; it is out of scope and needs no edit) and the non-Metrics partial-class file of the QfcHomeController class at QuickFiler/Controllers/QfcHomeController.cs (also not backticked here; it is the same out-of-scope file named in this plan's Scope boundary paragraph and in spec.md's Scope and Non-Goals section), plus `QuickFiler/Properties/AssemblyInfo.cs` — re-read (grep) in this pass; confirmed the non-Metrics QfcHomeController.cs file and QuickFiler/Properties/AssemblyInfo.cs both carry `[assembly: InternalsVisibleTo("QuickFiler.Test")]`, corroborating that `QuickFiler.Test` can call the `internal` `GetItemSummary()`, `TryGetMoveReadiness`, and `ToggleExpansionStyle` members exercised by Phase 1 Test 3 and Test 4. QfcHighConfidencePreFilter.cs and the Legacy folder's IAcceleratorCallbacks file were also read; both carry only `[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]`, which targets Moq's dynamic-proxy assembly and does not grant `QuickFiler.Test` access, so they are named here in plain prose without backticks rather than cited as supporting evidence; the Legacy file is additionally out of this issue's scope per the Scope and Non-Goals section. -- `scripts/vscode/Invoke-MSTestWithCoverage.ps1`, lines 1-77, lines 296-303, and line 309 — re-read in this pass; confirmed the default `-CoverageOutput` parameter, that the script's inner `dotnet-coverage collect -- ` invocation already appends `/InIsolation` and `/TestCaseFilter:TestCategory!=LiveOutlook`, that `$resolvedOutputPath = Join-Path $repoRoot $CoverageOutput` (line 309) does not special-case an already-rooted `$CoverageOutput` value, and that `Get-ChildItem -Path $resolvedSearchRoot -Recurse -Filter '*.Test.dll'` (lines 296-303) is scoped by `$resolvedSearchRoot`, so `-SearchRoot QuickFiler.Test` restricts assembly discovery to that subtree; P0-T8 and P5-T4 therefore pass a relative `coverage\issue-742-coverage\...` value and a `QuickFiler.Test` search root rather than a path outside the repository, confirmed against `.gitignore`'s `coverage/*` entry (`.gitignore` lines 143-145). +- `scripts/vscode/Invoke-MSTestWithCoverage.ps1`, lines 1-77, lines 296-303, and line 309 — re-read in this pass; confirmed the default `-CoverageOutput` parameter, that the script's inner `dotnet-coverage collect -- ` invocation already appends `/InIsolation` and `/TestCaseFilter:TestCategory!=LiveOutlook`, that `$resolvedOutputPath = Join-Path $repoRoot $CoverageOutput` (line 309) does not special-case an already-rooted `$CoverageOutput` value, and that `Get-ChildItem -Path $resolvedSearchRoot -Recurse -Filter '*.Test.dll'` (lines 296-303) is scoped by `$resolvedSearchRoot`, so `-SearchRoot QuickFiler.Test` restricts assembly discovery to that subtree; P0-T8 and P5-T4 therefore pass a relative `coverage\coverage.cobertura.xml` value and a `QuickFiler.Test` search root rather than a path outside the repository, confirmed against `.gitignore`'s `coverage/*` entry (`.gitignore` lines 143-145). - `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/spec.md`, Acceptance Criteria section (lines 343-442) and Write Set section (lines 472-486) — re-read in this pass; confirmed all 17 acceptance-criteria items, that the residual-sweep criterion (lines 383-400) already uses the escaped `@\?` form and the orchestrator's own two-spelling measurement note, and the exact eight-path Write Set used for the AC-INVENTORY and the diff-scope tasks. - `dotnet-tools.json`, lines 1-13 — re-read in this pass; confirmed the manifest pins only `csharpier` at version `1.2.6` and carries no `dotnet-coverage` entry, corroborating P0-T3's bootstrap of `dotnet-coverage` separately from `dotnet tool restore`. -- `.gitignore`, lines 139-145 — re-read in this pass; confirmed `coverage/*` is ignored with a single `!coverage/.gitkeep` exception, so `coverage\issue-742-coverage\` used by P0-T8 and P5-T4 is gitignored and cannot be swept into a commit even if the task's own deletion step is skipped. +- `.gitignore`, lines 139-145 — re-read in this pass; confirmed `coverage/*` is ignored with a single `!coverage/.gitkeep` exception, so `coverage\coverage.cobertura.xml` used by P0-T8 and P5-T4 is gitignored and cannot be swept into a commit even if the task's own deletion step is skipped. + +## Coordinator amendment 2026-09-14 (issue 742) + +This plan was authored 2026-09-12T16-09 and cleared preflight before item 873 merged. Two of its +premises were falsified by that merge and were re-derived against `main` at +`1e32500de4cb54558da934c69dd46405f62e155f` before this amendment was applied. The amendment is +bounded to re-derived measurements and one evidence read-location; it changes no acceptance +criterion, no test, and nothing this item delivers. + +1. **Baseline `Compile Include` count, P0-T9 control 5 and P1-T2.** Items 871, 873 and 877 added + four unrelated `` entries to `QuickFiler.Test/QuickFiler.Test.csproj`. The + count was re-derived three times independently and is **176**, so the post-insertion + expectation becomes **177**. Previously 172 and 173. + +2. **Raw Cobertura read location, P0-T8 and P5-T4.** Item 873 added + `Test-RawCoverageDocumentRetained` to `scripts/vscode/Invoke-MSTestWithCoverage.Projection.ps1`, + which retains the raw collector document only when its parent directory is exactly + `\coverage`. The function's own documentation states that equality rather than + containment is deliberate and that a subdirectory is discarded. This plan targeted + `coverage\issue-742-coverage\`, a subdirectory, so the runner deleted the document before + P0-T8 and P5-T4 could transcribe from it. Both spans now use the runner's default + `coverage\coverage.cobertura.xml`, which is retained, and the deletion instruction targets that + file rather than the former directory. + + This follows the precedent already set inside this run: item 879's plan, authored after 873 + merged, leaves `-CoverageOutput` at its default for exactly this reason. + + The figures transcribed, the evidence artifacts written, and AC11 are all unchanged. AC11 maps + to P0-T8 and P5-T4 for hygiene and is tested by P5-T6, which asserts that no raw `.trx` or + Cobertura file is introduced as a **tracked** evidence artifact; `.gitignore` line 144 + (`coverage/*`) covers the new path, so that test is unaffected. + +Applied by `parallel-orchestrator` under an explicit coordinator authorization bounded to +mechanical re-derivation, with the standing instruction to stop and report rather than exercise +judgement if the change proved to alter the deliverable. It did not. + +### Amendment note: a stale citation deliberately left standing + +The planner's own self-review record cites `scripts/vscode/Invoke-MSTestWithCoverage.ps1` at +"lines 1-77, lines 296-303, and line 309". Item 873 added roughly 97 lines to that file, so those +line numbers have moved. Those citations are the planner's attestation about what it read in its +own authoring pass, not statements about what this plan does, so they have NOT been rewritten here: +rewriting another agent's attestation would misrepresent what that agent actually verified and +when. + +The path references in the same record WERE corrected, because those describe what P0-T8 and P5-T4 +pass at execution time and this amendment changed that value. + +The executor should re-locate the three cited regions by text rather than by line number. This is +the same rot class that halted item 870, where item 873 rewrote content at unchanged line numbers +and a line-number check passed while the text underneath had changed. From 96ea9631869ff0ab2be98ac5a02eb823730e2b7c Mon Sep 17 00:00:00 2001 From: Dan Moisan Date: Mon, 14 Sep 2026 02:27:10 -0400 Subject: [PATCH 3/4] Fix issue 742: append CultureInfo.InvariantCulture to QuickFiler date/time rendering Co-Authored-By: Claude Opus 5 (1M context) --- .../QfcHomeControllerMetricsTests.cs | 16 +- ...QuickFilerInvariantCultureIssue742Tests.cs | 461 ++++++++++++++++++ QuickFiler.Test/QuickFiler.Test.csproj | 1 + .../Controllers/EfcHomeController.Metrics.cs | 8 +- QuickFiler/Controllers/EfcItemController.cs | 5 +- .../Controllers/QfcCollectionController.cs | 10 +- .../Controllers/QfcHomeController.Metrics.cs | 10 +- .../QfcItemController.ViewerSetup.cs | 3 +- .../csharpier-check.2026-09-12T16-09.md | 14 + ...scovery-count-controls.2026-09-12T16-09.md | 129 +++++ ...ecoverage-confirmation.2026-09-12T16-09.md | 24 + .../msbuild-analyzer.2026-09-12T16-09.md | 49 ++ .../msbuild-nullable.2026-09-12T16-09.md | 18 + ...ok-closed-confirmation.2026-09-12T16-09.md | 12 + ...ase0-instructions-read.2026-09-12T16-09.md | 44 ++ .../quickfiler-test-build.2026-09-12T16-09.md | 17 + .../toolchain-bootstrap.2026-09-12T16-09.md | 46 ++ ...test-coverage-baseline.2026-09-12T16-09.md | 52 ++ .../csharpier-final.2026-09-12T16-09.md | 35 ++ .../diff-scope-final.2026-09-12T16-09.md | 51 ++ ...msbuild-analyzer-final.2026-09-12T16-09.md | 24 + ...msbuild-nullable-final.2026-09-12T16-09.md | 24 + ...trx-cobertura-evidence.2026-09-12T16-09.md | 41 ++ ...est-method-count-final.2026-09-12T16-09.md | 22 + .../vstest-coverage-final.2026-09-12T16-09.md | 83 ++++ .../expect-fail-new-tests.2026-09-12T16-09.md | 50 ++ .../phase2-verification.2026-09-12T16-09.md | 45 ++ .../phase3-verification.2026-09-12T16-09.md | 35 ++ .../phase4-confirmation.2026-09-12T16-09.md | 42 ++ .../phase4-residual-count.2026-09-12T16-09.md | 43 ++ .../plan.2026-09-12T16-09.md | 70 +-- .../spec.md | 32 +- 32 files changed, 1450 insertions(+), 66 deletions(-) create mode 100644 QuickFiler.Test/Controllers/QuickFilerInvariantCultureIssue742Tests.cs create mode 100644 docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/csharpier-check.2026-09-12T16-09.md create mode 100644 docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/discovery-count-controls.2026-09-12T16-09.md create mode 100644 docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/excludefromcodecoverage-confirmation.2026-09-12T16-09.md create mode 100644 docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/msbuild-analyzer.2026-09-12T16-09.md create mode 100644 docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/msbuild-nullable.2026-09-12T16-09.md create mode 100644 docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/outlook-closed-confirmation.2026-09-12T16-09.md create mode 100644 docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/phase0-instructions-read.2026-09-12T16-09.md create mode 100644 docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/quickfiler-test-build.2026-09-12T16-09.md create mode 100644 docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/toolchain-bootstrap.2026-09-12T16-09.md create mode 100644 docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/vstest-coverage-baseline.2026-09-12T16-09.md create mode 100644 docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/qa-gates/csharpier-final.2026-09-12T16-09.md create mode 100644 docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/qa-gates/diff-scope-final.2026-09-12T16-09.md create mode 100644 docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/qa-gates/msbuild-analyzer-final.2026-09-12T16-09.md create mode 100644 docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/qa-gates/msbuild-nullable-final.2026-09-12T16-09.md create mode 100644 docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/qa-gates/no-trx-cobertura-evidence.2026-09-12T16-09.md create mode 100644 docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/qa-gates/test-method-count-final.2026-09-12T16-09.md create mode 100644 docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/qa-gates/vstest-coverage-final.2026-09-12T16-09.md create mode 100644 docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/regression-testing/expect-fail-new-tests.2026-09-12T16-09.md create mode 100644 docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/regression-testing/phase2-verification.2026-09-12T16-09.md create mode 100644 docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/regression-testing/phase3-verification.2026-09-12T16-09.md create mode 100644 docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/regression-testing/phase4-confirmation.2026-09-12T16-09.md create mode 100644 docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/regression-testing/phase4-residual-count.2026-09-12T16-09.md diff --git a/QuickFiler.Test/Controllers/QfcHomeControllerMetricsTests.cs b/QuickFiler.Test/Controllers/QfcHomeControllerMetricsTests.cs index 7cb459263..103879a52 100644 --- a/QuickFiler.Test/Controllers/QfcHomeControllerMetricsTests.cs +++ b/QuickFiler.Test/Controllers/QfcHomeControllerMetricsTests.cs @@ -239,8 +239,15 @@ public async Task WriteMetricsAsync_UsesInjectedClock_ForDateAndTimeStamps() var fake = FixedClock(); controller.TimeProvider = fake; var expectedLocal = fake.GetLocalNow().LocalDateTime; + // Issue #742: the oracle must name the invariant culture explicitly. An uncultured + // ToString(format) here resolves the "/" and ":" placeholders against the operator's + // locale, exactly as the pre-fix production code did, so the two would agree under any + // culture and the assertion could not detect the defect it is meant to pin. var expectedDataLineBeg = - expectedLocal.ToString("MM/dd/yyyy") + "," + expectedLocal.ToString("HH:mm") + ","; + expectedLocal.ToString("MM/dd/yyyy", CultureInfo.InvariantCulture) + + "," + + expectedLocal.ToString("HH:mm", CultureInfo.InvariantCulture) + + ","; // Act await controller.WriteMetricsAsync("metrics.csv"); @@ -274,8 +281,13 @@ public void QuickFileMetrics_WRITE_UsesInjectedClock_ForDataLine() var fake = FixedClock(); controller.TimeProvider = fake; var expectedLocal = fake.GetLocalNow().LocalDateTime; + // Issue #742: see the note in the sibling test above. The oracle names the invariant + // culture so it cannot silently track a culture-dependent production rendering. var expectedDataLineBeg = - expectedLocal.ToString("MM/dd/yyyy") + "," + expectedLocal.ToString("HH:mm") + ","; + expectedLocal.ToString("MM/dd/yyyy", CultureInfo.InvariantCulture) + + "," + + expectedLocal.ToString("HH:mm", CultureInfo.InvariantCulture) + + ","; // Act controller.QuickFileMetrics_WRITE("metrics.csv"); diff --git a/QuickFiler.Test/Controllers/QuickFilerInvariantCultureIssue742Tests.cs b/QuickFiler.Test/Controllers/QuickFilerInvariantCultureIssue742Tests.cs new file mode 100644 index 000000000..c422aa02f --- /dev/null +++ b/QuickFiler.Test/Controllers/QuickFilerInvariantCultureIssue742Tests.cs @@ -0,0 +1,461 @@ +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Reflection; +using System.Runtime.Serialization; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Extensions.Time.Testing; +using Microsoft.Office.Interop.Outlook; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using QuickFiler.Interfaces; +using UtilitiesCS; +using Outlook = Microsoft.Office.Interop.Outlook; + +namespace QuickFiler.Controllers.Tests +{ + /// + /// Issue #742 regression tests. Five QuickFiler production files render dates and times with an + /// uncultured ToString(format) call or an interpolated format specifier, both of which + /// resolve the / and : placeholders against + /// . Under an operator locale whose date or time + /// separator is not / or :, the rendered output changes shape: the session-metrics + /// CSV gains or loses field boundaries, and the user-facing summaries and diagnostics no longer + /// match the format the rest of the system assumes. + /// + /// Each test activates a culture whose DateSeparator and TimeSeparator are + /// explicit sentinel characters that are neither / nor :, exercises one production + /// file, and asserts that the rendered output still carries the invariant separators. A named + /// culture is deliberately not used: a sentinel makes the assertion independent of any + /// particular locale's separator choices, and of any future ICU data change. + /// + /// + /// Every test restores the original culture in a finally block, mirroring the two + /// existing de-DE tests in QfcHomeControllerMetricsTests and + /// EfcHomeControllerMetricsTests. MSTest does not reset + /// between test methods, so an unrestored culture would + /// leak into whichever sibling test next runs on the same worker thread. + /// + /// + /// No test here touches a temporary file, sleeps, or delays. + /// + /// + [TestClass] + public class QuickFilerInvariantCultureIssue742Tests + { + private const string DateSeparatorSentinel = "#"; + private const string TimeSeparatorSentinel = "~"; + private const string FakeDocumentsRoot = @"C:\FakeDocs"; + + /// A fixed instant whose date and time parts both render with a separator. + private static readonly DateTime SentInstant = new DateTime(2024, 3, 4, 14, 30, 45); + + /// + /// Builds a culture whose date and time separators are sentinel characters. Cloning the + /// invariant culture keeps every other formatting rule invariant, so a failure is + /// attributable to the separator alone. + /// + private static CultureInfo SentinelSeparatorCulture() + { + var culture = (CultureInfo)CultureInfo.InvariantCulture.Clone(); + culture.DateTimeFormat.DateSeparator = DateSeparatorSentinel; + culture.DateTimeFormat.TimeSeparator = TimeSeparatorSentinel; + return culture; + } + + private static string ExpectedDate(DateTime value) => + value.ToString("MM/dd/yyyy", CultureInfo.InvariantCulture); + + private static string ExpectedTime(DateTime value) => + value.ToString("HH:mm", CultureInfo.InvariantCulture); + + /// + /// Sets a non-public instance field, asserting first that the field exists so a renamed + /// member fails loudly instead of leaving the field at its default. + /// + private static void SetPrivateField(object target, string fieldName, object value) + { + FieldInfo field = target + .GetType() + .GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); + field.Should().NotBeNull($"{fieldName} must remain available for this headless seam"); + field.SetValue(target, value); + } + + /// + /// Builds a directly, with only the fields the rendering paths + /// read. None of the assigned text contains / or :, so either separator + /// appearing in a rendered result is attributable to the date/time formatting under test. + /// + private static MailItemHelper BuildItemHelper() => + new MailItemHelper + { + SentDate = SentInstant, + Subject = "Subject text", + SenderName = "Sender name", + ToRecipientsName = "Recipient name", + }; + + /// + /// Builds a with loose mocks wired so the synchronous + /// metrics path runs to its GetMoveDiagnostics call without touching live COM: the + /// special-folders map carries MyDocuments, the calendar root enumerates no subfolders so + /// the calendar resolves to null, and the diagnostics call returns an empty array so the + /// downstream write iterates nothing. This mirrors BuildLooseMetricsController in + /// QfcHomeControllerMetricsTests, with the diagnostics setup changed to capture the + /// dataLineBeg argument this test asserts on. + /// + private static QfcHomeController BuildMetricsController( + List capturedDataLineBeginnings + ) + { + var mockGlobals = new Mock(MockBehavior.Loose); + + var specialFolders = new ConcurrentDictionary(); + specialFolders["MyDocuments"] = FakeDocumentsRoot; + var mockFs = new Mock(MockBehavior.Loose); + mockFs.SetupGet(x => x.SpecialFolders).Returns(specialFolders); + mockGlobals.SetupGet(x => x.FS).Returns(mockFs.Object); + + var mockFolders = new Mock(MockBehavior.Loose); + mockFolders + .Setup(x => x.GetEnumerator()) + .Returns(() => new ArrayList().GetEnumerator()); + var mockCalendarRoot = new Mock(MockBehavior.Loose); + mockCalendarRoot.SetupGet(x => x.Folders).Returns(mockFolders.Object); + var mockSession = new Mock(MockBehavior.Loose); + mockSession + .Setup(x => x.GetDefaultFolder(OlDefaultFolders.olFolderCalendar)) + .Returns(mockCalendarRoot.Object); + var mockOlApp = new Mock(MockBehavior.Loose); + mockOlApp.SetupGet(x => x.Session).Returns(mockSession.Object); + var mockOl = new Mock(MockBehavior.Loose); + mockOl.SetupGet(x => x.App).Returns(mockOlApp.Object); + mockGlobals.SetupGet(x => x.Ol).Returns(mockOl.Object); + + var mockGroups = new Mock(MockBehavior.Loose); + mockGroups.SetupGet(x => x.EmailsToMove).Returns(1); + mockGroups + .Setup(x => + x.GetMoveDiagnostics( + It.IsAny(), + It.IsAny(), + It.IsAny(), + Capture.In(capturedDataLineBeginnings), + It.IsAny(), + ref It.Ref.IsAny + ) + ) + .Returns(Array.Empty()); + var mockFormController = new Mock(MockBehavior.Loose); + mockFormController.SetupGet(x => x.Groups).Returns(mockGroups.Object); + + mockGlobals.SetupGet(x => x.AF.CancelToken).Returns(CancellationToken.None); + + var controller = new QfcHomeController(mockGlobals.Object, () => { }); + controller.CreateCancellationToken(); + controller.MetricsFileWriter = (filename, lines, folderRoot, token) => + Task.FromResult(true); + SetPrivateField(controller, "_formController", mockFormController.Object); + SetPrivateField(controller, "_stopWatchMoved", new Stopwatch()); + return controller; + } + + /// + /// Test 1 of 5, for QuickFiler/Controllers/QfcHomeController.Metrics.cs. + /// + /// Scenario: run QuickFileMetrics_WRITE under a sentinel-separator culture with an + /// injected fixed clock. Expected outcome: the dataLineBeg argument handed to + /// GetMoveDiagnostics is the invariant rendering of the injected instant, so it + /// carries / and : rather than the sentinel characters. + /// + /// + /// Before the fix the beginning of the data line is built from an interpolated format + /// specifier, which resolves both separators against the current culture, so the + /// machine-read metrics CSV changes shape with the operator's locale. + /// + /// + [TestMethod] + public void QuickFileMetricsWrite_UnderSentinelSeparatorCulture_RendersInvariantDataLineBeginning() + { + // Arrange + var captured = new List(); + var originalCulture = CultureInfo.CurrentCulture; + try + { + CultureInfo.CurrentCulture = SentinelSeparatorCulture(); + var controller = BuildMetricsController(captured); + var fake = new FakeTimeProvider( + new DateTimeOffset(2024, 1, 15, 14, 30, 45, TimeSpan.Zero) + ); + controller.TimeProvider = fake; + var expectedLocal = fake.GetLocalNow().LocalDateTime; + var expected = + ExpectedDate(expectedLocal) + "," + ExpectedTime(expectedLocal) + ","; + + // Act + controller.QuickFileMetrics_WRITE("issue742-metrics.csv"); + + // Assert + captured + .Should() + .ContainSingle( + because: "the metrics path calls GetMoveDiagnostics exactly once per write" + ); + captured[0] + .Should() + .Be( + expected, + because: "issue #742 requires the data line to be rendered with the invariant culture" + ) + .And.Contain("/") + .And.Contain(":"); + } + finally + { + CultureInfo.CurrentCulture = originalCulture; + } + } + + /// + /// Test 2 of 5, for QuickFiler/Controllers/EfcHomeController.Metrics.cs. + /// + /// Scenario: call BuildQuickFileMetricLines directly under a sentinel-separator + /// culture with a literal instant and a one-item moved list. Expected outcome: the returned + /// line carries the invariant rendering of both the leading date/time fields and the + /// trailing sent-date fields. + /// + /// + [TestMethod] + public void BuildQuickFileMetricLines_UnderSentinelSeparatorCulture_RendersInvariantDateAndTime() + { + // Arrange + var moved = new List { BuildItemHelper() }; + var currentDateTime = new DateTime(2024, 1, 15, 14, 30, 45); + var originalCulture = CultureInfo.CurrentCulture; + try + { + CultureInfo.CurrentCulture = SentinelSeparatorCulture(); + + // Act + string[] lines = EfcHomeController.BuildQuickFileMetricLines( + currentDateTime, + 60d, + "Destination folder", + moved + ); + + // Assert + lines.Should().ContainSingle(because: "the moved list carries exactly one item"); + lines[0] + .Should() + .Contain("/") + .And.Contain(":") + .And.Contain( + ExpectedDate(currentDateTime), + because: "the leading date field must be rendered with the invariant culture" + ) + .And.Contain( + ExpectedDate(SentInstant), + because: "the trailing sent-date field must be rendered with the invariant culture" + ) + .And.NotContain(DateSeparatorSentinel) + .And.NotContain(TimeSeparatorSentinel); + } + finally + { + CultureInfo.CurrentCulture = originalCulture; + } + } + + /// + /// Test 3 of 5, for QuickFiler/Controllers/QfcItemController.ViewerSetup.cs. + /// + /// Scenario: read GetItemSummary() from a controller allocated without its + /// WinForms-dependent constructor under a sentinel-separator culture. Expected outcome: the + /// summary carries the invariant rendering of the sent date and time. + /// + /// + /// The assertion names the expected rendered substrings rather than only the separator + /// characters, because the summary's own literal prefix already contains a colon and a bare + /// colon assertion would therefore pass on the unfixed code. + /// + /// + [TestMethod] + public void GetItemSummary_UnderSentinelSeparatorCulture_RendersInvariantDateAndTime() + { + // Arrange + var controller = (QfcItemController) + FormatterServices.GetUninitializedObject(typeof(QfcItemController)); + controller.ItemHelper = BuildItemHelper(); + var originalCulture = CultureInfo.CurrentCulture; + try + { + CultureInfo.CurrentCulture = SentinelSeparatorCulture(); + + // Act + string summary = controller.GetItemSummary(); + + // Assert + summary + .Should() + .Contain("/") + .And.Contain(":") + .And.Contain( + ExpectedDate(SentInstant), + because: "issue #742 requires the sent date to be rendered with the invariant culture" + ) + .And.Contain( + ExpectedTime(SentInstant), + because: "issue #742 requires the sent time to be rendered with the invariant culture" + ); + } + finally + { + CultureInfo.CurrentCulture = originalCulture; + } + } + + /// + /// Test 4 of 5, for QuickFiler/Controllers/QfcCollectionController.cs. + /// + /// Scenario: drive the file's three rendering sites from one controller allocated without + /// its WinForms-dependent constructor under a sentinel-separator culture: the move-readiness + /// notification, the expansion-guard exception message, and the move-diagnostics data line. + /// Expected outcome: each output carries the invariant rendering. + /// + /// + /// The move-readiness notification is asserted on the date only. That site renders a date + /// and no time, so a colon assertion against it could not pass after the fix either. + /// + /// + [TestMethod] + public void QfcCollectionControllerRenderingSites_UnderSentinelSeparatorCulture_RenderInvariantDateAndTime() + { + // Arrange + var helper = BuildItemHelper(); + var mockMail = new Mock(MockBehavior.Loose); + mockMail.SetupGet(x => x.SentOn).Returns(SentInstant); + mockMail.SetupGet(x => x.Subject).Returns("Subject text"); + var mockItemController = new Mock(MockBehavior.Loose); + mockItemController.SetupGet(x => x.Mail).Returns(mockMail.Object); + mockItemController.SetupGet(x => x.ItemHelper).Returns(helper); + mockItemController.SetupGet(x => x.ItemNumber).Returns(1); + mockItemController.SetupGet(x => x.IsActiveUI).Returns(false); + var group = new QfcItemGroup { ItemController = mockItemController.Object }; + + QfcCollectionController controller = + QfcCollectionControllerTestSupport.CreateUninitializedController(); + QfcCollectionControllerTestSupport.SetField( + controller, + "_itemGroups", + new List { group } + ); + QfcCollectionControllerTestSupport.SetField( + controller, + "_itemGroupsToMove", + new List { group } + ); + + var originalCulture = CultureInfo.CurrentCulture; + try + { + CultureInfo.CurrentCulture = SentinelSeparatorCulture(); + + // Act + controller.TryGetMoveReadiness(out string notifications); + System.Action expandInactiveItem = () => + controller.ToggleExpansionStyle(0, Enums.ToggleState.On); + AppointmentItem appointment = null; + string[] diagnostics = controller.GetMoveDiagnostics( + "0", + "0.00", + 0d, + "beg,", + SentInstant, + ref appointment + ); + + // Assert + notifications + .Should() + .Contain("/") + .And.Contain( + ExpectedDate(SentInstant), + because: "the move-readiness notification must render its date with the invariant culture" + ); + expandInactiveItem + .Should() + .Throw() + .Which.Message.Should() + .Contain("/") + .And.Contain(":") + .And.Contain(ExpectedDate(SentInstant)) + .And.Contain(ExpectedTime(SentInstant)); + diagnostics.Should().ContainSingle(because: "exactly one group is cached for move"); + diagnostics[0] + .Should() + .Contain("/") + .And.Contain(":") + .And.Contain(ExpectedDate(SentInstant)) + .And.Contain(ExpectedTime(SentInstant)); + } + finally + { + CultureInfo.CurrentCulture = originalCulture; + } + } + + /// + /// Test 5 of 5, for QuickFiler/Controllers/EfcItemController.cs. + /// + /// Scenario: read the SentDate and SentTime properties from a controller + /// allocated without its constructor, with the cached mail-item model injected by + /// reflection, under a sentinel-separator culture. Expected outcome: SentDate carries + /// the invariant date separator and SentTime the invariant time separator. + /// + /// + [TestMethod] + public void EfcItemControllerSentDateAndSentTime_UnderSentinelSeparatorCulture_RenderInvariantSeparators() + { + // Arrange + var controller = (EfcItemController) + FormatterServices.GetUninitializedObject(typeof(EfcItemController)); + SetPrivateField(controller, "_itemInfo", BuildItemHelper()); + var originalCulture = CultureInfo.CurrentCulture; + try + { + CultureInfo.CurrentCulture = SentinelSeparatorCulture(); + + // Act + string sentDate = controller.SentDate; + string sentTime = controller.SentTime; + + // Assert + sentDate + .Should() + .Contain("/") + .And.Be( + ExpectedDate(SentInstant), + because: "issue #742 requires SentDate to be rendered with the invariant culture" + ); + sentTime + .Should() + .Contain(":") + .And.Be( + ExpectedTime(SentInstant), + because: "issue #742 requires SentTime to be rendered with the invariant culture" + ); + } + finally + { + CultureInfo.CurrentCulture = originalCulture; + } + } + } +} diff --git a/QuickFiler.Test/QuickFiler.Test.csproj b/QuickFiler.Test/QuickFiler.Test.csproj index cf1e03d02..be567fa20 100644 --- a/QuickFiler.Test/QuickFiler.Test.csproj +++ b/QuickFiler.Test/QuickFiler.Test.csproj @@ -200,6 +200,7 @@ + diff --git a/QuickFiler/Controllers/EfcHomeController.Metrics.cs b/QuickFiler/Controllers/EfcHomeController.Metrics.cs index 55d51de35..f3c9405fc 100644 --- a/QuickFiler/Controllers/EfcHomeController.Metrics.cs +++ b/QuickFiler/Controllers/EfcHomeController.Metrics.cs @@ -92,8 +92,8 @@ List moved return Array.Empty(); } - var curDateText = currentDateTime.ToString("MM/dd/yyyy"); - var curTimeText = currentDateTime.ToString("HH:mm"); + var curDateText = currentDateTime.ToString("MM/dd/yyyy", CultureInfo.InvariantCulture); + var curTimeText = currentDateTime.ToString("HH:mm", CultureInfo.InvariantCulture); var dataLineBeg = curDateText + "," + curTimeText + ","; var duration = elapsedSeconds; @@ -115,8 +115,8 @@ List moved + $",SingleSorted,{durationText},{durationMinutesText}," + $"{QfcCollectionController.xComma(itemInfo.ToRecipientsName)}," + $"{QfcCollectionController.xComma(itemInfo.SenderName)},Email,{folderText}," - + $"{itemInfo.SentDate.ToString("MM/dd/yyyy")}," - + $"{itemInfo.SentDate.ToString("HH:mm:ss")}" + + $"{itemInfo.SentDate.ToString("MM/dd/yyyy", CultureInfo.InvariantCulture)}," + + $"{itemInfo.SentDate.ToString("HH:mm:ss", CultureInfo.InvariantCulture)}" ) .ToArray(); } diff --git a/QuickFiler/Controllers/EfcItemController.cs b/QuickFiler/Controllers/EfcItemController.cs index 37b14c1e5..020bfdf93 100644 --- a/QuickFiler/Controllers/EfcItemController.cs +++ b/QuickFiler/Controllers/EfcItemController.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Drawing; +using System.Globalization; using System.IO; using System.Linq; using System.Threading; @@ -604,12 +605,12 @@ public string Sender public string SentDate { - get => _itemInfo.SentDate.ToString("MM/dd/yyyy"); + get => _itemInfo.SentDate.ToString("MM/dd/yyyy", CultureInfo.InvariantCulture); } public string SentTime { - get => _itemInfo.SentDate.ToString("HH:mm"); + get => _itemInfo.SentDate.ToString("HH:mm", CultureInfo.InvariantCulture); } public string Subject diff --git a/QuickFiler/Controllers/QfcCollectionController.cs b/QuickFiler/Controllers/QfcCollectionController.cs index da5156b4b..4d6f7e35e 100644 --- a/QuickFiler/Controllers/QfcCollectionController.cs +++ b/QuickFiler/Controllers/QfcCollectionController.cs @@ -2,6 +2,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Linq; using System.Net.NetworkInformation; using System.Runtime.CompilerServices; @@ -232,7 +233,10 @@ internal bool TryGetMoveReadiness(out string notifications) strNotifications + grp.ItemController.ItemNumber + " " - + grp.ItemController.Mail.SentOn.ToString("MM/dd/yyyy") + + grp.ItemController.Mail.SentOn.ToString( + "MM/dd/yyyy", + CultureInfo.InvariantCulture + ) + " " + grp.ItemController.Mail.Subject + Environment.NewLine; @@ -1293,7 +1297,7 @@ public void ToggleExpansionStyle(int itemIndex, Enums.ToggleState desiredState) var c = _itemGroups[itemIndex].ItemController; var msg = $"Cannot expand item with index {itemIndex} because UI is not active.\n" - + $"Controller for message \"{c.ItemHelper.Subject} sent on {c.ItemHelper.SentDate.ToString("MM/dd/yyyy")} at {c.ItemHelper.SentDate.ToString("HH:mm")} " + + $"Controller for message \"{c.ItemHelper.Subject} sent on {c.ItemHelper.SentDate.ToString("MM/dd/yyyy", CultureInfo.InvariantCulture)} at {c.ItemHelper.SentDate.ToString("HH:mm", CultureInfo.InvariantCulture)} " + $"by {c.ItemHelper.SenderName} has a value of {c.IsActiveUI} for {nameof(c.IsActiveUI)}"; throw new InvalidOperationException(msg); } @@ -2299,7 +2303,7 @@ ref AppointmentItem olAppointment var dataLine = $"{dataLineBeg} {xComma(qf.ItemHelper.Subject)},QuickFiled,{durationText},{durationMinutesText},"; dataLine += - $"{xComma(qf.ItemHelper.ToRecipientsName)},{xComma(qf.ItemHelper.SenderName)},Email,{xComma(qf.SelectedFolder)},{qf.ItemHelper.SentDate.ToString("MM/dd/yyyy")},{qf.ItemHelper.SentDate.ToString("HH:mm")}"; + $"{xComma(qf.ItemHelper.ToRecipientsName)},{xComma(qf.ItemHelper.SenderName)},Email,{xComma(qf.SelectedFolder)},{qf.ItemHelper.SentDate.ToString("MM/dd/yyyy", CultureInfo.InvariantCulture)},{qf.ItemHelper.SentDate.ToString("HH:mm", CultureInfo.InvariantCulture)}"; strOutput[k] = dataLine; } diff --git a/QuickFiler/Controllers/QfcHomeController.Metrics.cs b/QuickFiler/Controllers/QfcHomeController.Metrics.cs index 3d2d4b109..37dc53446 100644 --- a/QuickFiler/Controllers/QfcHomeController.Metrics.cs +++ b/QuickFiler/Controllers/QfcHomeController.Metrics.cs @@ -45,7 +45,11 @@ public void QuickFileMetrics_WRITE(string filename) //var curDateText = DateTime.Now.ToString("MM/dd/yyyy"); //var curTimeText = DateTime.Now.ToString("hh:mm"); //dataLineBeg = curDateText + "," + curTimeText + ","; - dataLineBeg = $"{now:MM/dd/yyyy},{now:HH:mm},"; + dataLineBeg = + now.ToString("MM/dd/yyyy", CultureInfo.InvariantCulture) + + "," + + now.ToString("HH:mm", CultureInfo.InvariantCulture) + + ","; if (!Globals.FS.SpecialFolders.TryGetValue("MyDocuments", out var folderRoot)) { @@ -122,9 +126,9 @@ public async Task WriteMetricsAsync(string filename) // Create a line of comma seperated valued to store data var now = TimeProvider.GetLocalNow().LocalDateTime; - curDateText = now.ToString("MM/dd/yyyy"); + curDateText = now.ToString("MM/dd/yyyy", CultureInfo.InvariantCulture); - curTimeText = now.ToString("HH:mm"); + curTimeText = now.ToString("HH:mm", CultureInfo.InvariantCulture); dataLineBeg = curDateText + "," + curTimeText + ","; diff --git a/QuickFiler/Controllers/QfcItemController.ViewerSetup.cs b/QuickFiler/Controllers/QfcItemController.ViewerSetup.cs index f0b11aa77..742aab12f 100644 --- a/QuickFiler/Controllers/QfcItemController.ViewerSetup.cs +++ b/QuickFiler/Controllers/QfcItemController.ViewerSetup.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Drawing; +using System.Globalization; using System.IO; using System.Linq; using System.Net.NetworkInformation; @@ -473,6 +474,6 @@ private void DetachWebResourceRequestedHandler() } internal string GetItemSummary() => - $"Subject: {ItemHelper.Subject} sent on {ItemHelper.SentDate.ToString("MM/dd/yyyy")} at {ItemHelper.SentDate.ToString("HH:mm")} by {ItemHelper.SenderName}"; + $"Subject: {ItemHelper.Subject} sent on {ItemHelper.SentDate.ToString("MM/dd/yyyy", CultureInfo.InvariantCulture)} at {ItemHelper.SentDate.ToString("HH:mm", CultureInfo.InvariantCulture)} by {ItemHelper.SenderName}"; } } diff --git a/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/csharpier-check.2026-09-12T16-09.md b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/csharpier-check.2026-09-12T16-09.md new file mode 100644 index 000000000..9414b02ba --- /dev/null +++ b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/csharpier-check.2026-09-12T16-09.md @@ -0,0 +1,14 @@ +# Baseline CSharpier Check (issue #742, [P0-T4]) + +Timestamp: 2026-09-14T01-58 + +Command: `pwsh -NoProfile -Command 'dotnet tool run csharpier check .'` + +EXIT_CODE: 0 + +Output Summary: `Checked 1634 files in 4554ms.` CSharpier reported no formatting diff anywhere in the +tree. This is the read-only baseline captured before any write-mode formatter runs in this plan, so +the Phase 5 `csharpier format` / `csharpier check` gate ([P5-T1]) measures only drift introduced by +this change rather than pre-existing drift. + +Acceptance: none stated by the task; this is a baseline capture only. diff --git a/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/discovery-count-controls.2026-09-12T16-09.md b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/discovery-count-controls.2026-09-12T16-09.md new file mode 100644 index 000000000..1163b03ce --- /dev/null +++ b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/discovery-count-controls.2026-09-12T16-09.md @@ -0,0 +1,129 @@ +# Baseline Discovery-Count Controls (issue #742, [P0-T9]) + +Timestamp: 2026-09-14T02-05 + +All eleven commands below were run against the unmodified tree, before [P1-T1] created or modified +any Write Set file. Branch HEAD at capture time: `6d6da116f9084db5da0b2f25d80941c41c5fbf61`. + +Zero-match reading convention: `git grep -c PATTERN -- path` prints no line at all, and exits `1`, +when `path` has zero matching lines; it never prints a `0`. + +Every command below was executed with `git -C ` from the item worktree +`C:/Users/DanMoisan/repos/TaskMaster-wt/bugs-2026-09-11-item-742`. The `-C` form is an invocation +detail of this execution environment and does not change any pattern or pathspec. + +--- + +## Control 1 — uncultured `ToString` sweep across the five production files + +Command: `git grep -c '[.]ToString[(]@\?"[^"]*[:/.\-][^"]*"[)]' -- QuickFiler/Controllers/QfcHomeController.Metrics.cs QuickFiler/Controllers/EfcHomeController.Metrics.cs QuickFiler/Controllers/QfcItemController.ViewerSetup.cs QuickFiler/Controllers/QfcCollectionController.cs QuickFiler/Controllers/EfcItemController.cs` + +EXIT_CODE: 0 + +Output Summary: + +``` +QuickFiler/Controllers/EfcHomeController.Metrics.cs:4 +QuickFiler/Controllers/EfcItemController.cs:2 +QuickFiler/Controllers/QfcCollectionController.cs:3 +QuickFiler/Controllers/QfcHomeController.Metrics.cs:4 +QuickFiler/Controllers/QfcItemController.ViewerSetup.cs:1 +``` + +Per-file distribution 4 / 4 / 1 / 3 / 2 (14 matching lines total), exactly as the plan and +`spec.md` state. The `@\?` escape is load-bearing: `git grep` runs in basic-regular-expression mode, +in which a bare `?` is a literal character rather than an optional-atom quantifier. + +## Control 2 — interpolated specifier token + +Command: `git grep -c '[{]now:' -- QuickFiler/Controllers/QfcHomeController.Metrics.cs` + +EXIT_CODE: 0 + +Output Summary: `QuickFiler/Controllers/QfcHomeController.Metrics.cs:1` — expected 1. + +## Control 3 — pre-existing InvariantCulture sites, QfcHomeController.Metrics.cs + +Command: `git grep -c 'CultureInfo[.]InvariantCulture' -- QuickFiler/Controllers/QfcHomeController.Metrics.cs` + +EXIT_CODE: 0 + +Output Summary: `QuickFiler/Controllers/QfcHomeController.Metrics.cs:4` — expected 4. + +## Control 4 — pre-existing InvariantCulture sites, EfcHomeController.Metrics.cs + +Command: `git grep -c 'CultureInfo[.]InvariantCulture' -- QuickFiler/Controllers/EfcHomeController.Metrics.cs` + +EXIT_CODE: 0 + +Output Summary: `QuickFiler/Controllers/EfcHomeController.Metrics.cs:2` — expected 2. + +## Control 5 — `Compile Include` count in the test project file + +Command: `git grep -c -F 'Compile Include' -- QuickFiler.Test/QuickFiler.Test.csproj` + +EXIT_CODE: 0 + +Output Summary: `QuickFiler.Test/QuickFiler.Test.csproj:176` — expected 176 per the coordinator +amendment dated 2026-09-14, which re-derived this figure after items 871, 873 and 877 each added an +unrelated `` entry to this file. The plan body's original figure of 172 is +superseded. + +## Control 6 — new test file not yet referenced by the project file + +Command: `git grep -c -F 'QuickFilerInvariantCultureIssue742Tests' -- QuickFiler.Test/QuickFiler.Test.csproj` + +EXIT_CODE: 1 + +Output Summary: prints no line — expected: prints no line, exits 1. + +## Control 7 — no `System.Globalization` using directive, QfcItemController.ViewerSetup.cs + +Command: `git grep -c -F 'using System.Globalization;' -- QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` + +EXIT_CODE: 1 + +Output Summary: prints no line — expected: prints no line, exits 1. + +## Control 8 — no `System.Globalization` using directive, QfcCollectionController.cs + +Command: `git grep -c -F 'using System.Globalization;' -- QuickFiler/Controllers/QfcCollectionController.cs` + +EXIT_CODE: 1 + +Output Summary: prints no line — expected: prints no line, exits 1. + +## Control 9 — no `System.Globalization` using directive, EfcItemController.cs + +Command: `git grep -c -F 'using System.Globalization;' -- QuickFiler/Controllers/EfcItemController.cs` + +EXIT_CODE: 1 + +Output Summary: prints no line — expected: prints no line, exits 1. + +## Control 10 — self-referential uncultured test oracle, both occurrences + +Command: `git grep -c -F 'expectedLocal.ToString("MM/dd/yyyy") + "," + expectedLocal.ToString("HH:mm") + ",";' -- QuickFiler.Test/Controllers/QfcHomeControllerMetricsTests.cs` + +EXIT_CODE: 0 + +Output Summary: `QuickFiler.Test/Controllers/QfcHomeControllerMetricsTests.cs:2` — expected 2, the +identical expected-value construction in the two sibling test methods. + +## Control 11 — no InvariantCulture in the metrics test file + +Command: `git grep -c -F 'CultureInfo.InvariantCulture' -- QuickFiler.Test/Controllers/QfcHomeControllerMetricsTests.cs` + +EXIT_CODE: 1 + +Output Summary: prints no line — expected: prints no line, exits 1. + +--- + +Acceptance: this artifact exists, records all eleven commands with `Timestamp:`, `Command:`, +`EXIT_CODE:` and `Output Summary:`, every observed figure equals the figure stated by the task +(with control 5 read against the coordinator's amended value of 176), and it was written before +[P1-T1], which is the first task in this plan that creates or modifies any Write Set file. + +This artifact is the baseline evidence artifact required by the Acceptance Criteria item covering +the pre-fix discovery-count figures. diff --git a/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/excludefromcodecoverage-confirmation.2026-09-12T16-09.md b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/excludefromcodecoverage-confirmation.2026-09-12T16-09.md new file mode 100644 index 000000000..308c9b91e --- /dev/null +++ b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/excludefromcodecoverage-confirmation.2026-09-12T16-09.md @@ -0,0 +1,24 @@ +# ExcludeFromCodeCoverage Confirmation (issue #742, [P0-T10]) + +Timestamp: 2026-09-14T02-05 + +Command: `git grep -c -F '[ExcludeFromCodeCoverage]' -- QuickFiler/Controllers/QfcCollectionController.cs QuickFiler/Controllers/EfcItemController.cs` + +EXIT_CODE: 0 + +Output Summary: + +``` +QuickFiler/Controllers/EfcItemController.cs:1 +QuickFiler/Controllers/QfcCollectionController.cs:1 +``` + +Acceptance: the command prints exactly one matching line for each of the two files and `EXIT_CODE` +is 0 — satisfied. `git grep` orders its output by pathspec resolution rather than by argument +order, so `EfcItemController.cs` precedes `QfcCollectionController.cs` here; both required lines are +present with a count of 1 each. + +This confirms the coverage-signal framing used by [P0-T8] and [P5-T4]: both classes carry a +type-level `[ExcludeFromCodeCoverage]` attribute, are therefore absent from the Cobertura report, +and their quality signal is the named-test pass/fail state rather than a coverage percentage. This +change does not add, remove, or modify either attribute. diff --git a/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/msbuild-analyzer.2026-09-12T16-09.md b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/msbuild-analyzer.2026-09-12T16-09.md new file mode 100644 index 000000000..1f5abbb7e --- /dev/null +++ b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/msbuild-analyzer.2026-09-12T16-09.md @@ -0,0 +1,49 @@ +# Baseline MSBuild Analyzer State (issue #742, [P0-T5]) + +Timestamp: 2026-09-14T02-00 + +Command: `pwsh -NoProfile -Command '$out = msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true 2>&1; $exit = $LASTEXITCODE; $out | Select-String -Pattern "^\s*\d+ Error\(s\)\s*$" | ForEach-Object { $_.Line.Trim() }; Write-Output "EXITCODE=$exit"'` + +EXIT_CODE: 0 + +Output Summary: transcribed summary line `0 Error(s)`, exit code `0`. The same command additionally +printed `0 Warning(s)`, recorded here for completeness rather than as a baseline floor. + +Acceptance: none stated by the task; this is a baseline capture only. + +## Pre-existing cold-worktree blocker encountered and resolved before this measurement + +The first run of this exact command in this worktree printed `2 Error(s)` and exited `1`. The two +errors were: + +``` +CSC : error CS0006: Metadata file '..\packages\Meziantou.Analyzer.3.0.203\analyzers\dotnet\roslyn5.0\cs\Meziantou.Analyzer.dll' could not be found [...\VBFunctions\VBFunctions.csproj] +CSC : error CS0006: Metadata file '..\packages\Meziantou.Analyzer.3.0.203\analyzers\dotnet\roslyn5.0\cs\Meziantou.Analyzer.dll' could not be found [...\UtilitiesCS\UtilitiesCS.csproj] +``` + +Diagnosis: an `` HintPath skew that predates this branch. Fifteen of sixteen +first-party project files name `Meziantou.Analyzer.3.0.203` in their `` item while +`packages.config`, the `` and the `` guard in the same files all name +`3.0.235`, which is the version `nuget restore` installs. `TaskMaster.csproj` alone already names +`3.0.235`. Only two errors surface because the two failing projects are upstream of every other +project in the graph, so the rest are skipped rather than compiled. + +Verification that this is pre-existing and not introduced by this change: + +- `git grep -c -F 'Meziantou.Analyzer.3.0.203' origin/main -- UtilitiesCS/UtilitiesCS.csproj VBFunctions/VBFunctions.csproj QuickFiler/QuickFiler.csproj QuickFiler.Test/QuickFiler.Test.csproj` printed `1` for each of the four paths, so `origin/main` carries the identical skew. +- `git diff --name-only origin/main...HEAD -- "*.csproj" "*.config" "*.props" "*.targets"` printed no line, so this branch has modified no project or package file. + +Remedy applied (provisioning only, no tracked file edited): + +`pwsh -NoProfile -Command 'nuget install Meziantou.Analyzer -Version 3.0.203 -OutputDirectory packages -DependencyVersion Ignore'` — exit code `0`. + +This provisions the HintPath-named analyzer version into the repository's `packages/` tree, which +`git check-ignore -v` confirms is ignored at `.gitignore:191` (`**/[Pp]ackages/*`). After the +install, `git status --porcelain --untracked-files=all -- "*.csproj" "*.config" "*.props" "*.targets" "packages"` +printed no line, confirming zero tracked files changed. The mandated msbuild command was then re-run +unmodified and produced the `0 Error(s)` result recorded above. + +This is the same class of cold-worktree provisioning as the repo-local .NET SDK install recorded in +`toolchain-bootstrap.2026-09-12T16-09.md`. It edits no source file, no project file, and nothing +belonging to any sibling work item, and it is outside this change's Write Set because it changes no +tracked file at all. diff --git a/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/msbuild-nullable.2026-09-12T16-09.md b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/msbuild-nullable.2026-09-12T16-09.md new file mode 100644 index 000000000..a8cb21a7c --- /dev/null +++ b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/msbuild-nullable.2026-09-12T16-09.md @@ -0,0 +1,18 @@ +# Baseline MSBuild Nullable / Warnings-As-Errors State (issue #742, [P0-T6]) + +Timestamp: 2026-09-14T02-01 + +Command: `pwsh -NoProfile -Command '$out = msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true 2>&1; $exit = $LASTEXITCODE; $out | Select-String -Pattern "^\s*\d+ Error\(s\)\s*$" | ForEach-Object { $_.Line.Trim() }; Write-Output "EXITCODE=$exit"'` + +EXIT_CODE: 0 + +Output Summary: transcribed summary line `0 Error(s)`, exit code `0`. `/p:Nullable=enable` was not +added, per `CLAUDE.md` and `.claude/rules/csharp.md`; nullable participation in this repository is +per-file opt-in via `#nullable enable`. + +Acceptance: none stated by the task; this is a baseline capture only. + +Note: this run followed the `packages/Meziantou.Analyzer.3.0.203` provisioning recorded in +`msbuild-analyzer.2026-09-12T16-09.md`. Without that provisioning this gate fails from the same +pre-existing CS0006 HintPath skew rather than from any nullable diagnostic, because both gates use +`/t:Rebuild` and compile the same projects. diff --git a/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/outlook-closed-confirmation.2026-09-12T16-09.md b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/outlook-closed-confirmation.2026-09-12T16-09.md new file mode 100644 index 000000000..373b56ecb --- /dev/null +++ b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/outlook-closed-confirmation.2026-09-12T16-09.md @@ -0,0 +1,12 @@ +# Outlook Closed Confirmation (issue #742, [P0-T2]) + +Timestamp: 2026-09-14T01-56 + +Command: `pwsh -NoProfile -Command 'if (Get-Process OUTLOOK -ErrorAction SilentlyContinue) { Write-Output "OUTLOOK_RUNNING" } else { Write-Output "OUTLOOK_CLOSED" }'` + +EXIT_CODE: 0 + +Output Summary: the command printed exactly `OUTLOOK_CLOSED`. No Outlook process was present at the +time of this check, so no rebuild step in this plan can be blocked by MSB3021 from a locked +QuickFiler VSTO build output. No process-termination command was issued by this task or by any other +task in this plan; the precondition was already satisfied and required no manual intervention. diff --git a/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/phase0-instructions-read.2026-09-12T16-09.md b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/phase0-instructions-read.2026-09-12T16-09.md new file mode 100644 index 000000000..ddb4a129d --- /dev/null +++ b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/phase0-instructions-read.2026-09-12T16-09.md @@ -0,0 +1,44 @@ +# Phase 0 — Policy Instructions Read (issue #742) + +Timestamp: 2026-09-14T01-55 + +Policy Order: the five policy files below were read in this exact order, per [P0-T1] of +`docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/plan.2026-09-12T16-09.md` +and per the repository's `policy-compliance-order` skill. + +1. `CLAUDE.md` +2. `.claude/rules/general-code-change.md` +3. `.claude/rules/general-unit-test.md` +4. `.claude/rules/csharp.md` +5. `.claude/rules/tonality.md` + +## Files Read (explicit list) + +- `CLAUDE.md` +- `.claude/rules/general-code-change.md` +- `.claude/rules/general-unit-test.md` +- `.claude/rules/csharp.md` +- `.claude/rules/tonality.md` + +## Output Summary + +All five policy files were read in full from the item worktree +`C:/Users/DanMoisan/repos/TaskMaster-wt/bugs-2026-09-11-item-742`. + +Constraints carried into execution: + +- C# toolchain order is format (`dotnet tool run csharpier format .` then `check .`), analyzers + (`msbuild ... /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true`), nullable + (`msbuild ... /p:TreatWarningsAsErrors=true`, without `/p:Nullable=enable`), then tests. Restart + from step 1 whenever a step fails or rewrites a tracked file. +- `/t:Rebuild` is required for the analyzer and nullable gates; a warm `/t:Build` skips + `CoreCompile` and cannot fail. +- Tests use MSTest, Moq, and FluentAssertions; no temporary files; no `Thread.Sleep` or + `Task.Delay`; deterministic time via `TimeProvider` / `FakeTimeProvider`. +- `CLAUDE.md` carries a `## Committed Test Evidence Format` section stating that raw coverage + collector documents and raw test-platform (`.trx`) documents must not be added to git in any + form, including under a feature folder's evidence tree. This matches this plan's + evidence-artifact discipline: raw output is written under the gitignored `coverage\` directory, + transcribed into Markdown, and deleted. +- Tonality policy: professional, factual, neutral; no humor, hyperbole, or decorative metaphor in + any artifact written by this execution. diff --git a/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/quickfiler-test-build.2026-09-12T16-09.md b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/quickfiler-test-build.2026-09-12T16-09.md new file mode 100644 index 000000000..790227f5c --- /dev/null +++ b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/quickfiler-test-build.2026-09-12T16-09.md @@ -0,0 +1,17 @@ +# Baseline QuickFiler.Test Build (issue #742, [P0-T7]) + +Timestamp: 2026-09-14T02-01 + +Command: `pwsh -NoProfile -Command 'msbuild TaskMaster.sln /t:Build /m /p:Configuration=Debug "/p:Platform=Any CPU"'` + +EXIT_CODE: 0 + +Output Summary: `0 Warning(s)`, `0 Error(s)`, exit code `0`. The solution, including the unfixed +`QuickFiler.Test` assembly, builds successfully, so a baseline test-and-coverage run against the +unfixed production tree is possible in [P0-T8]. + +Acceptance: `EXIT_CODE` is 0 — satisfied. + +Note: the build was incremental relative to the `/t:Rebuild` runs in [P0-T5] and [P0-T6], which is +what the task intends; its purpose is to produce the `QuickFiler.Test.dll` output rather than to +gate analyzer or nullable diagnostics. diff --git a/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/toolchain-bootstrap.2026-09-12T16-09.md b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/toolchain-bootstrap.2026-09-12T16-09.md new file mode 100644 index 000000000..11037018d --- /dev/null +++ b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/toolchain-bootstrap.2026-09-12T16-09.md @@ -0,0 +1,46 @@ +# Toolchain Bootstrap (issue #742, [P0-T3]) + +Timestamp: 2026-09-14T01-57 + +Command: + +1. `pwsh -NoProfile -Command 'nuget restore TaskMaster.sln'` +2. `pwsh -NoProfile -Command 'dotnet tool restore'` +3. `pwsh -NoProfile -Command 'if (Get-Command dotnet-coverage -ErrorAction SilentlyContinue) { Write-Output "DOTNET_COVERAGE_PRESENT" } else { dotnet tool install --global dotnet-coverage; Write-Output "DOTNET_COVERAGE_INSTALLED" }'` + +EXIT_CODE: + +1. `nuget restore TaskMaster.sln` — 0 +2. `dotnet tool restore` — 0 (second attempt; see the prerequisite note below) +3. `dotnet-coverage` availability probe — 0 + +Output Summary: + +1. `nuget restore` reported `Installed: 172 package(s) to packages.config projects` using + MSBuild 18.10.1.42706 from the VS 18 Community install. Packages were written to + `\packages`. +2. `dotnet tool restore` reported `Tool 'csharpier' (version '1.2.6') was restored.` followed by + `Restore was successful.` +3. The probe printed exactly `DOTNET_COVERAGE_PRESENT`, so `dotnet-coverage` was already on PATH + and no global tool install was performed. + +## Prerequisite note (recorded for audit fidelity) + +The first `dotnet tool restore` attempt in this fresh worktree failed with +`The repo-local .NET SDK is missing. Run ./scripts/vscode/Install-RepoDotNetSdk.ps1 from the +repository root`, exit code `-2147450725`. The remedy named by the tool's own error message was +executed as a mechanically necessary micro-action: + +`pwsh -NoProfile -Command './scripts/vscode/Install-RepoDotNetSdk.ps1'` + +which reported `Installed repo-local .NET SDK 8.0.205 to \.dotnet-sdk.` and exited 0. +`dotnet tool restore` was then re-run and exited 0, as recorded above. `.dotnet-sdk` and `packages` +are build inputs restored per worktree and are not tracked files; neither appears in this change's +Write Set. + +## Build-lock discipline + +`nuget restore`, `dotnet tool restore` and the `dotnet-coverage` probe were run with the shared +parallel build lock held for the build-affecting command and released immediately after. The SDK +install ran with the lock released, because it writes only to this worktree's private +`.dotnet-sdk` directory and does not contend on shared build outputs. diff --git a/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/vstest-coverage-baseline.2026-09-12T16-09.md b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/vstest-coverage-baseline.2026-09-12T16-09.md new file mode 100644 index 000000000..faac17dc0 --- /dev/null +++ b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/vstest-coverage-baseline.2026-09-12T16-09.md @@ -0,0 +1,52 @@ +# Baseline QuickFiler.Test Run and Coverage (issue #742, [P0-T8]) + +Timestamp: 2026-09-14T02-04 + +Command: `pwsh -NoProfile -Command './scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot QuickFiler.Test -Configuration Debug -CoverageOutput "coverage\coverage.cobertura.xml"; Write-Output "EXITCODE=$LASTEXITCODE"'` + +EXIT_CODE: 1 + +Output Summary: + +- Test outcome: `Completed`. Counters transcribed from the runner's own trx document + (`coverage\test-results\mstest-coverage-run.trx`, gitignored and not committed): + total 1429, executed 1429, **passed 1429, failed 0**, error 0, timeout 0, aborted 0, + inconclusive 0, notExecuted 0 (skipped 0). +- Coverage document totals: `line-rate` 0.242996, `lines-covered` 15040, `lines-valid` 61894. +- Transcribed class-level `line-rate` figures for the three non-excluded classes named by the task: + - `QuickFiler.Controllers.QfcHomeController` — `QuickFiler\Controllers\QfcHomeController.Metrics.cs` — line-rate 0.75969, branch-rate 0.791667 + - `QuickFiler.EfcHomeController` — `QuickFiler\Controllers\EfcHomeController.Metrics.cs` — line-rate 1, branch-rate 1 + - `QuickFiler.Controllers.QfcItemController` — `QuickFiler\Controllers\QfcItemController.ViewerSetup.cs` — line-rate 0.906103, branch-rate 0.829268 +- `QfcCollectionController` and `EfcItemController` carry a type-level `[ExcludeFromCodeCoverage]` + attribute (confirmed separately in [P0-T10]) and are **absent** from the report: a query for class + nodes whose `filename` matches either name returned 0 nodes. Their baseline quality signal is + therefore the named-test pass/fail state captured above, not a coverage percentage. +- Raw output deleted: `coverage\coverage.cobertura.xml` was removed after transcription + (`[System.IO.File]::Exists(...)` returned `False` afterwards). No `.trx` and no Cobertura XML was + written into the feature evidence tree. + +Acceptance: none stated by the task; this is a baseline capture only. + +## Why EXIT_CODE is 1 despite zero test failures (recorded for [P5-T4]) + +The non-zero exit code is not a test failure. The runner's own post-run gate threw: + +``` +Exception: ...\scripts\vscode\Invoke-MSTestWithCoverage.Threshold.ps1:54 +Cobertura line coverage 24.3028% is below the required 80% threshold. +``` + +`Assert-CoberturaLineCoverageThreshold` in `scripts/vscode/Invoke-MSTestWithCoverage.Threshold.ps1` +reads the document-level `line-rate` of the `/coverage` element and throws when it is below 80 +percent. The threshold is a hard-coded literal `80` in that function; `Invoke-MSTestWithCoverage.ps1` +exposes only `-SearchRoot`, `-Configuration`, `-CoverageOutput` and `-NoExecute`, so no parameter +lowers or disables it. + +The document-level denominator covers every instrumented assembly (61894 valid lines), while +`-SearchRoot QuickFiler.Test` executes only the QuickFiler test assembly. A scoped single-assembly +run therefore cannot reach the document-level 80 percent threshold regardless of how many of its own +tests pass. This is a property of the runner on the current tree, independent of this change. + +This observation is recorded here because [P5-T4] states its acceptance as `EXITCODE is 0 (zero +failures for QuickFiler.Test ...)`, equating the runner's exit code with the test-failure count. On +this tree those two are not equivalent. diff --git a/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/qa-gates/csharpier-final.2026-09-12T16-09.md b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/qa-gates/csharpier-final.2026-09-12T16-09.md new file mode 100644 index 000000000..e2ae1da6c --- /dev/null +++ b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/qa-gates/csharpier-final.2026-09-12T16-09.md @@ -0,0 +1,35 @@ +# Final QA Gate 1 — CSharpier (issue #742, [P5-T1]) + +Timestamp: 2026-09-14T02-21 + +Command: + +1. `pwsh -NoProfile -Command 'dotnet tool run csharpier format .'` +2. `pwsh -NoProfile -Command 'dotnet tool run csharpier check .'` + +EXIT_CODE: 0 (of the read-only `check` command; the `format` command also exited 0) + +Output Summary: + +- `format` printed `Formatted 1635 files in 5451ms.` +- `check` printed `Checked 1635 files in 5291ms.` and exited 0, so no formatting diff remains + anywhere in the tree. + +The file count rose from the 1634 recorded in the [P0-T4] baseline to 1635, which is the one new +test file this change adds. + +Acceptance: the `check` command's `EXIT_CODE` is 0 — satisfied. + +## Observation beyond the exit code + +`format` is a write-mode command and exits 0 whether or not it rewrote a file, so its exit code +alone is not evidence. Two independent observations are recorded instead: + +1. The read-only `check` run above reports no remaining diff over all 1635 files. +2. `git status --porcelain --untracked-files=all`, taken between the `format` and `check` runs, + listed only this change's own Write Set paths, this feature folder's evidence artifacts, the plan + file and `spec.md`. No unrelated tracked file was rewritten by the formatter, so the [P0-T4] + baseline of a clean tree held and this gate is measuring only this change. + +CSharpier was invoked through `dotnet tool run` so the manifest-pinned version 1.2.6 was used, per +`CLAUDE.md` and `.claude/rules/csharp.md`. No global installation was used. diff --git a/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/qa-gates/diff-scope-final.2026-09-12T16-09.md b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/qa-gates/diff-scope-final.2026-09-12T16-09.md new file mode 100644 index 000000000..21b938b13 --- /dev/null +++ b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/qa-gates/diff-scope-final.2026-09-12T16-09.md @@ -0,0 +1,51 @@ +# Final QA Gate 7 — Diff Scope (issue #742, [P5-T7]) + +Timestamp: 2026-09-14T02-26 + +Command: `git status --porcelain --untracked-files=all -- QuickFiler QuickFiler.Test` + +EXIT_CODE: 0 + +Output Summary (the full porcelain listing): + +``` + M QuickFiler.Test/Controllers/QfcHomeControllerMetricsTests.cs + M QuickFiler.Test/QuickFiler.Test.csproj + M QuickFiler/Controllers/EfcHomeController.Metrics.cs + M QuickFiler/Controllers/EfcItemController.cs + M QuickFiler/Controllers/QfcCollectionController.cs + M QuickFiler/Controllers/QfcHomeController.Metrics.cs + M QuickFiler/Controllers/QfcItemController.ViewerSetup.cs +?? QuickFiler.Test/Controllers/QuickFilerInvariantCultureIssue742Tests.cs +``` + +Acceptance: satisfied. The output lists exactly the eight Write Set paths and no others: + +1. `QuickFiler/Controllers/QfcHomeController.Metrics.cs` — modified +2. `QuickFiler/Controllers/EfcHomeController.Metrics.cs` — modified +3. `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` — modified +4. `QuickFiler/Controllers/QfcCollectionController.cs` — modified +5. `QuickFiler/Controllers/EfcItemController.cs` — modified +6. `QuickFiler.Test/Controllers/QfcHomeControllerMetricsTests.cs` — modified +7. `QuickFiler.Test/Controllers/QuickFilerInvariantCultureIssue742Tests.cs` — untracked (`??`), as + expected until [P5-T8] stages it +8. `QuickFiler.Test/QuickFiler.Test.csproj` — modified + +The check is scoped to the `QuickFiler` and `QuickFiler.Test` source trees on purpose. The same +change also adds Markdown evidence artifacts and updates `plan.2026-09-12T16-09.md` and `spec.md` +inside this feature folder, which is expected rather than a scope violation, so a repository-wide +phrasing of this criterion would be unsatisfiable. + +`--untracked-files=all` is required because a name-listing diff enumerates tracked changes only and +would be blind to the one file this change creates. + +## Scope-boundary confirmation + +No file was touched in the UtilitiesCS project, the ToDoModel project, the uncompiled sources under +the QuickFiler Legacy folder, the non-Metrics partial-class file of the QfcHomeController class, the +sortable-key method in the EmailSorter file, the repository editor-configuration file, the +banned-symbols list, or the QuickFiler production project file. A separate check, +`git diff --name-only origin/main...HEAD -- "*.csproj" "*.config" "*.props" "*.targets"`, printed no +line before the Phase 1 project-file edit, confirming this branch carried no prior project-file +change; the only project-file change this branch makes is the single `Compile Include` line in +`QuickFiler.Test/QuickFiler.Test.csproj` added by [P1-T2]. diff --git a/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/qa-gates/msbuild-analyzer-final.2026-09-12T16-09.md b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/qa-gates/msbuild-analyzer-final.2026-09-12T16-09.md new file mode 100644 index 000000000..a671434a8 --- /dev/null +++ b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/qa-gates/msbuild-analyzer-final.2026-09-12T16-09.md @@ -0,0 +1,24 @@ +# Final QA Gate 2 — .NET Analyzers (issue #742, [P5-T2]) + +Timestamp: 2026-09-14T02-22 + +Command: `pwsh -NoProfile -Command '$out = msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true 2>&1; $exit = $LASTEXITCODE; $out | Select-String -Pattern "^\s*\d+ Error\(s\)\s*$" | ForEach-Object { $_.Line.Trim() }; Write-Output "EXITCODE=$exit"'` + +EXIT_CODE: 0 + +Output Summary: the transcribed summary line, trimmed, is exactly `0 Error(s)`, and `EXITCODE` is 0. +The `Select-String` pattern is anchored to the whole trimmed line (`^\s*\d+ Error\(s\)\s*$`), so a +`10 Error(s)` line could not satisfy this condition. + +Acceptance: satisfied. + +`/t:Rebuild` was used rather than `/t:Build`, per `CLAUDE.md` and `.claude/rules/csharp.md`: MSBuild's +incremental up-to-date check does not invalidate on a command-line `/p:` change, so a warm +`/t:Build` returns exit 0 with `CoreCompile` skipped and runs no analyzers. + +This run followed [P5-T1]. CSharpier rewrote no tracked file between the two, so no restart of the +Phase 5 sequence was required. + +The `packages/Meziantou.Analyzer.3.0.203` provisioning recorded in +`../baseline/msbuild-analyzer.2026-09-12T16-09.md` remains in place for this run. It is a gitignored +build input and changes no tracked file. diff --git a/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/qa-gates/msbuild-nullable-final.2026-09-12T16-09.md b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/qa-gates/msbuild-nullable-final.2026-09-12T16-09.md new file mode 100644 index 000000000..8a8f23883 --- /dev/null +++ b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/qa-gates/msbuild-nullable-final.2026-09-12T16-09.md @@ -0,0 +1,24 @@ +# Final QA Gate 3 — Nullable / Warnings As Errors (issue #742, [P5-T3]) + +Timestamp: 2026-09-14T02-23 + +Command: `pwsh -NoProfile -Command '$out = msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true 2>&1; $exit = $LASTEXITCODE; $out | Select-String -Pattern "^\s*\d+ Error\(s\)\s*$" | ForEach-Object { $_.Line.Trim() }; Write-Output "EXITCODE=$exit"'` + +EXIT_CODE: 0 + +Output Summary: the transcribed summary line, trimmed, is exactly `0 Error(s)`, and `EXITCODE` is 0. +The `Select-String` pattern is anchored to the whole trimmed line, so a two-digit error count could +not satisfy this condition. + +Acceptance: satisfied. + +`/p:Nullable=enable` was not added, per `CLAUDE.md` and `.claude/rules/csharp.md`. Nullable +enforcement in this repository is per-file opt-in via `#nullable enable`, and the solution-wide +property would conscript every file that has never adopted the pragma. `/t:Rebuild` was used rather +than `/t:Build` for the same incrementality reason recorded for [P5-T2]. + +This command is character-for-character the one in `.github/workflows/_build-nullable.yml`, aside +from the PowerShell wrapper that captures the summary line and the exit code. + +This run followed [P5-T2] with no intervening file rewrite, so no restart of the Phase 5 sequence +was required. diff --git a/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/qa-gates/no-trx-cobertura-evidence.2026-09-12T16-09.md b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/qa-gates/no-trx-cobertura-evidence.2026-09-12T16-09.md new file mode 100644 index 000000000..d55d8958a --- /dev/null +++ b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/qa-gates/no-trx-cobertura-evidence.2026-09-12T16-09.md @@ -0,0 +1,41 @@ +# Final QA Gate 6 — No Raw Test-Result or Coverage Document in Evidence (issue #742, [P5-T6]) + +Timestamp: 2026-09-14T02-25 + +Command: `pwsh -NoProfile -Command '$m = git status --porcelain --untracked-files=all -- docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence | Select-String -Pattern "\.trx$|\.cobertura\.xml$"; $m; if ($m) { exit 0 } else { exit 1 }'` + +EXIT_CODE: 1 + +ExpectedExitCode: 1 + +Output Summary: the command printed no line and exited 1. No file carrying the `.trx` extension and +no Cobertura coverage XML file appears, tracked or untracked, anywhere under this feature's evidence +directory. + +Acceptance: satisfied, read per this plan's zero-match reading convention. The command is written so +that exit 0 means a prohibited file was found and exit 1 means none was, which is why the expected +exit code here is 1 rather than 0. + +## Where the raw documents went instead + +Both coverage-collecting tasks wrote their raw output to `coverage\coverage.cobertura.xml` under the +repository's `coverage\` directory, transcribed the needed figures into a Markdown artifact, and +deleted the raw file: + +- [P0-T8] — figures in `../baseline/vstest-coverage-baseline.2026-09-12T16-09.md` +- [P5-T4] — figures in `vstest-coverage-final.2026-09-12T16-09.md` + +`coverage\` is ignored by `.gitignore`'s `coverage/*` entry, so neither the Cobertura document nor +the runner's own `coverage\test-results\mstest-coverage-run.trx` could be committed even if a +deletion step were missed. The two direct `vstest.console.exe` invocations in [P1-T4] and [P4-T4] +used the console logger and produced no trx at all. + +This satisfies the maintainer decision on issue #671 and the `## Committed Test Evidence Format` +section of `CLAUDE.md`: committed test evidence is a projection of a tool's output, never the tool's +raw document. + +## Search scope for this negative claim + +- SearchScope: `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence`, recursively, including untracked files (`--untracked-files=all`) +- SearchPatterns: `\.trx$` and `\.cobertura\.xml$` +- SearchResult: none diff --git a/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/qa-gates/test-method-count-final.2026-09-12T16-09.md b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/qa-gates/test-method-count-final.2026-09-12T16-09.md new file mode 100644 index 000000000..92a33ea00 --- /dev/null +++ b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/qa-gates/test-method-count-final.2026-09-12T16-09.md @@ -0,0 +1,22 @@ +# Final QA Gate 5 — TestMethod Count After Formatting (issue #742, [P5-T5]) + +Timestamp: 2026-09-14T02-25 + +Command: `git grep --untracked -c -F '[TestMethod]' -- QuickFiler.Test/Controllers/QuickFilerInvariantCultureIssue742Tests.cs` + +EXIT_CODE: 0 + +Output Summary: + +``` +QuickFiler.Test/Controllers/QuickFilerInvariantCultureIssue742Tests.cs:5 +``` + +Acceptance: the command prints the count `5`, unchanged from [P1-T1], and `EXIT_CODE` is 0 — +satisfied. CSharpier reformatted the tree in [P5-T1] and did not add or remove an attribute. + +The `--untracked` flag is required here: the file is created by this plan and remains untracked +until [P5-T8] stages it, and a plain `git grep` does not see an untracked file. + +The count of 5 also matches the seven-test run in [P4-T4], which executed the five tests from this +file plus the two rewritten oracle tests. diff --git a/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/qa-gates/vstest-coverage-final.2026-09-12T16-09.md b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/qa-gates/vstest-coverage-final.2026-09-12T16-09.md new file mode 100644 index 000000000..7cfc487d3 --- /dev/null +++ b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/qa-gates/vstest-coverage-final.2026-09-12T16-09.md @@ -0,0 +1,83 @@ +# Final QA Gate 4 — QuickFiler.Test with Coverage (issue #742, [P5-T4]) + +Timestamp: 2026-09-14T02-24 + +Command: `pwsh -NoProfile -Command './scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot QuickFiler.Test -Configuration Debug -CoverageOutput "coverage\coverage.cobertura.xml"; Write-Output "EXITCODE=$LASTEXITCODE"'` + +EXIT_CODE: 1 + +**Task acceptance NOT met.** [P5-T4] states its acceptance as `EXITCODE is 0`. The observed exit +code is 1. The task is therefore left unchecked in the plan, and the acceptance criterion it carries +is left unchecked in `spec.md`. The cause is recorded in full below; it is a property of the +coverage runner rather than a failure of this change. + +## Test result + +Transcribed from the runner's own trx document (`coverage\test-results\mstest-coverage-run.trx`, +written under the gitignored `coverage\` directory and not committed): + +- outcome: `Completed` +- total 1434, executed 1434, **passed 1434, failed 0** +- error 0, timeout 0, aborted 0, inconclusive 0, notExecuted 0 (skipped 0) + +The baseline run in [P0-T8] reported 1429 total / 1429 passed / 0 failed. The increase of exactly 5 +is the five new tests this change adds. **Zero failures for `QuickFiler.Test`**, including the two +rewritten oracle tests and the five new tests, all seven of which are also confirmed individually +green in `../regression-testing/phase4-confirmation.2026-09-12T16-09.md`. + +## Coverage figures, baseline versus final + +Document totals: `line-rate` 0.243093, `lines-covered` 15047, `lines-valid` 61898 +(baseline: 0.242996, 15040, 61894). + +| Class | File | Baseline line-rate | Final line-rate | Change | +|---|---|---|---|---| +| `QuickFiler.Controllers.QfcHomeController` | `QuickFiler\Controllers\QfcHomeController.Metrics.cs` | 0.75969 | 0.766917 | +0.007227 | +| `QuickFiler.EfcHomeController` | `QuickFiler\Controllers\EfcHomeController.Metrics.cs` | 1 | 1 | unchanged | +| `QuickFiler.Controllers.QfcItemController` | `QuickFiler\Controllers\QfcItemController.ViewerSetup.cs` | 0.906103 | 0.910798 | +0.004695 | + +**No regression on the lines this change touched.** Two of the three non-excluded classes rose and +the third was already at 1. Every line edited by this change is inside a method exercised by the new +tests, so each edited line is covered. + +`QfcCollectionController` and `EfcItemController` carry a type-level `[ExcludeFromCodeCoverage]` +attribute ([P0-T10]) and remain absent from the report in both runs. Their quality signal is the +named-test pass/fail state, which is green: the two tests exercising their five edited call sites +pass in [P4-T4] and are part of the 1434 above. + +Raw output deleted: `coverage\coverage.cobertura.xml` was removed after transcription +(`[System.IO.File]::Exists(...)` returned `False` afterwards). No `.trx` and no Cobertura XML was +written into the feature evidence tree. + +## Why EXIT_CODE is 1 + +The runner's own post-run gate threw: + +``` +Exception: ...\scripts\vscode\Invoke-MSTestWithCoverage.Threshold.ps1:54 +Cobertura line coverage 24.3093% is below the required 80% threshold. +``` + +`Assert-CoberturaLineCoverageThreshold` in `scripts/vscode/Invoke-MSTestWithCoverage.Threshold.ps1` +reads the document-level `line-rate` of the `/coverage` element and throws when it is below 80 +percent. The threshold is a hard-coded literal `80` in that function, and +`Invoke-MSTestWithCoverage.ps1` exposes only `-SearchRoot`, `-Configuration`, `-CoverageOutput` and +`-NoExecute`, so no parameter lowers or disables it. + +The document-level denominator spans every instrumented assembly (61898 valid lines) while +`-SearchRoot QuickFiler.Test` executes only the QuickFiler test assembly, covering 15047 of them. A +single-assembly scoped run therefore cannot reach the document-level 80 percent threshold no matter +how many of its own tests pass. + +The same exit code and the same 24.3 percent figure were observed on the **unfixed** tree in +[P0-T8], before any production edit, and are recorded in +`../baseline/vstest-coverage-baseline.2026-09-12T16-09.md`. The gate is therefore red identically +before and after this change, which is the evidence that this change did not cause it. + +The scoping to `QuickFiler.Test` is not optional on this host: an unscoped run executes the +`UtilitiesCS.Test` shell-icon classes known to stall vstest here. The plan selected the scoping for +that reason and equated the runner's exit code with the test-failure count; on this tree those two +are not equivalent. + +This is reported to the coordinator as a falsified plan premise rather than adapted. The command was +run exactly as the plan writes it, with no parameter added, removed, or altered. diff --git a/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/regression-testing/expect-fail-new-tests.2026-09-12T16-09.md b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/regression-testing/expect-fail-new-tests.2026-09-12T16-09.md new file mode 100644 index 000000000..cfecb907b --- /dev/null +++ b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/regression-testing/expect-fail-new-tests.2026-09-12T16-09.md @@ -0,0 +1,50 @@ +# Expect-Fail Run of the New Regression Tests (issue #742, [P1-T4]) + +Timestamp: 2026-09-14T02-14 + +Command: `pwsh -NoProfile -Command '$vswherePath = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"; $vstestPath = (& $vswherePath -latest -products * -find "Common7\IDE\Extensions\TestPlatform\vstest.console.exe") | Select-Object -First 1; & $vstestPath "QuickFiler.Test\bin\Debug\QuickFiler.Test.dll" /TestCaseFilter:"FullyQualifiedName~QuickFilerInvariantCultureIssue742Tests" /InIsolation /Logger:"console;verbosity=normal"; Write-Output "EXITCODE=$LASTEXITCODE"'` + +EXIT_CODE: 1 + +ExpectedExitCode: 1 + +Output Summary: `Test Run Failed. Total tests: 5, Failed: 5, Total time: 1.4974 Seconds.` VSTest +version 18.10.0 (x64). All five new tests failed against the unfixed production tree, which is the +expected outcome for this `[expect-fail]` task. + +Acceptance: `EXITCODE` is non-zero — satisfied. + +## Per-test failure, each attributable to the defect under test + +Every failure is a separator mismatch under the sentinel-separator culture +(`DateSeparator` = `#`, `TimeSeparator` = `~`), not a fixture or wiring error. The observed strings +below are quoted from the run's own failure messages. + +1. `QuickFileMetricsWrite_UnderSentinelSeparatorCulture_RendersInvariantDataLineBeginning` — + the captured `dataLineBeg` was `01#15#2024,09~30,` where `01/15/2024,09:30,` was expected. + Source: the interpolated specifier in `QfcHomeController.Metrics.cs`. +2. `BuildQuickFileMetricLines_UnderSentinelSeparatorCulture_RendersInvariantDateAndTime` — + the produced line was + `01#15#2024,14~30,Subject text,SingleSorted,60,1.00,Recipient name,Sender name,Email,Destination folder,03#04#2024,14~30~45`, + which contains no `/`. Both the leading date/time fields and the trailing sent-date fields are + affected. Source: `EfcHomeController.Metrics.cs`. +3. `GetItemSummary_UnderSentinelSeparatorCulture_RendersInvariantDateAndTime` — + the summary was `Subject: Subject text sent on 03#04#2024 at 14~30 by Sender name`, which + contains no `/`. Source: `QfcItemController.ViewerSetup.cs`. +4. `QfcCollectionControllerRenderingSites_UnderSentinelSeparatorCulture_RenderInvariantDateAndTime` — + the move-readiness notification was + `Can't complete actions! Not all emails assigned to folder` / `1 03#04#2024 Subject text`, + which contains no `/`. Source: `QfcCollectionController.cs`. The test fails at the first of its + three assertions; the expansion-guard and move-diagnostics sites are asserted in the same test + and are confirmed green in [P4-T4]. +5. `EfcItemControllerSentDateAndSentTime_UnderSentinelSeparatorCulture_RenderInvariantSeparators` — + `SentDate` was `03#04#2024`, which contains no `/`. Source: `EfcItemController.cs`. + +## Notes + +- The run used `/InIsolation` and was scoped by `/TestCaseFilter` to the new test class only, so it + did not execute the `UtilitiesCS.Test` shell-icon classes known to stall vstest on this machine. +- The FluentAssertions licence banner printed on standard output is emitted by the assertion library + on every run in this repository and is unrelated to this change. +- No raw `.trx` and no Cobertura XML was produced or committed by this task; the console logger was + used and the summary transcribed above. diff --git a/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/regression-testing/phase2-verification.2026-09-12T16-09.md b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/regression-testing/phase2-verification.2026-09-12T16-09.md new file mode 100644 index 000000000..97bfd47f1 --- /dev/null +++ b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/regression-testing/phase2-verification.2026-09-12T16-09.md @@ -0,0 +1,45 @@ +# Phase 2 Verification — Session-Metrics Writer Files (issue #742, [P2-T4]) + +Timestamp: 2026-09-14T02-16 + +Command: `git grep -c '[.]ToString[(]@\?"[^"]*[:/.\-][^"]*"[)]' -- QuickFiler/Controllers/QfcHomeController.Metrics.cs QuickFiler/Controllers/EfcHomeController.Metrics.cs` + +EXIT_CODE: 0 + +Output Summary: + +``` +QuickFiler/Controllers/QfcHomeController.Metrics.cs:2 +``` + +The command printed exactly one line, for `QfcHomeController.Metrics.cs` with a count of 2, and +printed no line for `EfcHomeController.Metrics.cs`. The baseline combined total for these two files +was 4 + 4 = 8 ([P0-T9] control 1). + +Acceptance: satisfied. The residual 2 are the two commented-out predecessor statements at the top of +`QuickFileMetrics_WRITE`: + +``` +//var curDateText = DateTime.Now.ToString("MM/dd/yyyy"); +//var curTimeText = DateTime.Now.ToString("hh:mm"); +``` + +This change deliberately does not edit them. They are inert comments and carry no runtime behaviour. + +## Discovery-control note (guard against a vacuous zero) + +The same pattern was run in the same pass against the three UI-facing files, which Phase 2 does not +touch, and printed `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs:1`, +`QuickFiler/Controllers/QfcCollectionController.cs:3` and +`QuickFiler/Controllers/EfcItemController.cs:2`, exit code 0. The pattern therefore still matches on +this tree, so the zero-match result for `EfcHomeController.Metrics.cs` is a real observation rather +than a search that cannot match. + +## Invocation note + +The pattern contains both double quotes and a backslash escape, which neither a `pwsh -Command` +string nor a Bash-quoted argument reproduces reliably on this host. It was therefore placed verbatim +in a single-quoted PowerShell variable inside a throwaway scratchpad script run with +`pwsh -NoProfile -File`, which passes the pattern to `git grep` as one unmodified argument. The +script lives outside the repository, is a temporary throwaway created and deleted within this +session, and contains no repository logic beyond the literal pattern and pathspec quoted above. diff --git a/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/regression-testing/phase3-verification.2026-09-12T16-09.md b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/regression-testing/phase3-verification.2026-09-12T16-09.md new file mode 100644 index 000000000..4e840c371 --- /dev/null +++ b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/regression-testing/phase3-verification.2026-09-12T16-09.md @@ -0,0 +1,35 @@ +# Phase 3 Verification — UI-Facing Files (issue #742, [P3-T4]) + +Timestamp: 2026-09-14T02-18 + +Command: `git grep -c '[.]ToString[(]@\?"[^"]*[:/.\-][^"]*"[)]' -- QuickFiler/Controllers/QfcItemController.ViewerSetup.cs QuickFiler/Controllers/QfcCollectionController.cs QuickFiler/Controllers/EfcItemController.cs` + +EXIT_CODE: 1 + +Output Summary: the command printed no line for any of the three paths and exited 1. The baseline +combined total for these three files was 1 + 3 + 2 = 6 ([P0-T9] control 1), distributed as +`QfcItemController.ViewerSetup.cs:1`, `QfcCollectionController.cs:3` and `EfcItemController.cs:2`. + +Acceptance: satisfied, read per this plan's zero-match convention — `git grep -c` prints no line at +all, and exits 1, for a path with zero matching lines, and never prints a `0`. + +## Discovery-control note (guard against a vacuous zero) + +The identical pattern, in the same script and the same invocation form, printed +`QuickFiler/Controllers/QfcHomeController.Metrics.cs:2` in [P2-T4], so the pattern still matches on +this tree and this zero is a real observation rather than a search that cannot match. The same three +paths returned 1, 3 and 2 under this pattern immediately before the Phase 3 edits were applied. + +## Sites changed in Phase 3 + +- `QfcItemController.ViewerSetup.cs` — both `ToString` calls in `GetItemSummary`, plus a new + `using System.Globalization;` directive (exactly one occurrence, confirmed in [P3-T1]). +- `QfcCollectionController.cs` — the `TryGetMoveReadiness` call, the two calls in the + `ToggleExpansionStyle` exception-message builder, and the two calls in the `GetMoveDiagnostics` + line builder, plus a new `using System.Globalization;` directive (exactly one occurrence, + confirmed in [P3-T2]). +- `EfcItemController.cs` — the `SentDate` and `SentTime` property getters, plus a new + `using System.Globalization;` directive (exactly one occurrence, confirmed in [P3-T3]). + +Only argument lists and the three using-directive insertions were changed. No interpolation, field +order, or CSV column count was altered. diff --git a/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/regression-testing/phase4-confirmation.2026-09-12T16-09.md b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/regression-testing/phase4-confirmation.2026-09-12T16-09.md new file mode 100644 index 000000000..a297a19f1 --- /dev/null +++ b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/regression-testing/phase4-confirmation.2026-09-12T16-09.md @@ -0,0 +1,42 @@ +# Phase 4 Confirmation — Seven Named Tests Pass (issue #742, [P4-T4]) + +Timestamp: 2026-09-14T02-19 + +Command: `pwsh -NoProfile -Command '$vswherePath = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"; $vstestPath = (& $vswherePath -latest -products * -find "Common7\IDE\Extensions\TestPlatform\vstest.console.exe") | Select-Object -First 1; & $vstestPath "QuickFiler.Test\bin\Debug\QuickFiler.Test.dll" /TestCaseFilter:"FullyQualifiedName~QuickFilerInvariantCultureIssue742Tests|FullyQualifiedName~WriteMetricsAsync_UsesInjectedClock_ForDateAndTimeStamps|FullyQualifiedName~QuickFileMetrics_WRITE_UsesInjectedClock_ForDataLine" /InIsolation /Logger:"console;verbosity=normal"; Write-Output "EXITCODE=$LASTEXITCODE"'` + +EXIT_CODE: 0 + +Output Summary: `Test Run Successful. Total tests: 7.` All seven named tests passed: + +``` +Passed WriteMetricsAsync_UsesInjectedClock_ForDateAndTimeStamps [226 ms] +Passed QuickFileMetrics_WRITE_UsesInjectedClock_ForDataLine [3 ms] +Passed QuickFileMetricsWrite_UnderSentinelSeparatorCulture_RendersInvariantDataLineBeginning [28 ms] +Passed BuildQuickFileMetricLines_UnderSentinelSeparatorCulture_RendersInvariantDateAndTime [29 ms] +Passed GetItemSummary_UnderSentinelSeparatorCulture_RendersInvariantDateAndTime [< 1 ms] +Passed QfcCollectionControllerRenderingSites_UnderSentinelSeparatorCulture_RenderInvariantDateAndTime [46 ms] +Passed EfcItemControllerSentDateAndSentTime_UnderSentinelSeparatorCulture_RenderInvariantSeparators [< 1 ms] +``` + +Acceptance: `EXITCODE` is 0, covering all seven named tests — the five new tests from [P1-T1] plus +the two rewritten oracle tests from [P4-T1] and [P4-T2] — satisfied. + +## Fail-before / pass-after pairing + +The same five new tests failed 5 of 5 against the unfixed production tree in [P1-T4], recorded in +`expect-fail-new-tests.2026-09-12T16-09.md`, each on the date or time separator. The only change +between the two runs is the Phase 2 and Phase 3 production edits plus the Phase 4 oracle repair, so +the transition from 5 failed to 7 passed is attributable to the fix. + +The two rewritten oracle tests were passing before this change as well. That is expected and is the +reason they needed repair: their expected values were built with the same uncultured +`ToString(format)` call the production code used, so the oracle tracked the defect instead of +detecting it. After [P4-T1] and [P4-T2] they name `CultureInfo.InvariantCulture` explicitly and can +no longer agree with a culture-dependent production rendering. + +## Environment notes + +- The run used `/InIsolation` and was scoped by `/TestCaseFilter` to the seven named tests, so it + did not execute the `UtilitiesCS.Test` shell-icon classes known to stall vstest on this machine. +- The console logger was used; no `.trx` and no Cobertura XML was produced or committed by this + task. The figures above are transcribed from the console output. diff --git a/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/regression-testing/phase4-residual-count.2026-09-12T16-09.md b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/regression-testing/phase4-residual-count.2026-09-12T16-09.md new file mode 100644 index 000000000..a8c6dfa6d --- /dev/null +++ b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/regression-testing/phase4-residual-count.2026-09-12T16-09.md @@ -0,0 +1,43 @@ +# Phase 4 Residual Count — All Five Production Files (issue #742, [P4-T5]) + +Timestamp: 2026-09-14T02-20 + +Command: `git grep -c '[.]ToString[(]@\?"[^"]*[:/.\-][^"]*"[)]' -- QuickFiler/Controllers/QfcHomeController.Metrics.cs QuickFiler/Controllers/EfcHomeController.Metrics.cs QuickFiler/Controllers/QfcItemController.ViewerSetup.cs QuickFiler/Controllers/QfcCollectionController.cs QuickFiler/Controllers/EfcItemController.cs` + +EXIT_CODE: 0 + +Output Summary: + +``` +QuickFiler/Controllers/QfcHomeController.Metrics.cs:2 +``` + +The command printed exactly one line, and printed no line for any of the other four paths. + +Acceptance: satisfied. The baseline total across all five files was 14 matching lines, distributed +4 / 4 / 1 / 3 / 2, recorded in +`../baseline/discovery-count-controls.2026-09-12T16-09.md` ([P0-T9] control 1). The post-fix total is +2, both in `QfcHomeController.Metrics.cs`. + +## What the residual 2 are + +The two remaining matches are the commented-out predecessor statements at the top of +`QuickFileMetrics_WRITE`: + +``` +//var curDateText = DateTime.Now.ToString("MM/dd/yyyy"); +//var curTimeText = DateTime.Now.ToString("hh:mm"); +``` + +They are inert comments, carry no runtime behaviour, and are deliberately not edited by this change. +`spec.md`'s residual-sweep acceptance criterion names this figure of 2 and identifies these two +lines as the expected residual. + +## Discovery-control note (guard against a vacuous zero) + +The pattern used here is the same one that returned the 4 / 4 / 1 / 3 / 2 baseline distribution +against the unfixed tree, and it still returns a non-empty result on this tree (the two lines +counted above). The four zero results are therefore real observations rather than a search that +cannot match. The `@` is written `@\?` because the search runs in basic-regular-expression mode, in +which a bare `?` is a literal character; the unescaped spelling matches nothing in this repository +whatever the executor does. diff --git a/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/plan.2026-09-12T16-09.md b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/plan.2026-09-12T16-09.md index d2c98ba3f..b520d5f3f 100644 --- a/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/plan.2026-09-12T16-09.md +++ b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/plan.2026-09-12T16-09.md @@ -34,23 +34,23 @@ ### Phase 0 — Policy Reads and Toolchain Baseline -- [ ] [P0-T1] Read, in this order, `CLAUDE.md`, `.claude/rules/general-code-change.md`, `.claude/rules/general-unit-test.md`, `.claude/rules/csharp.md`, and `.claude/rules/tonality.md`. Write `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/phase0-instructions-read.2026-09-12T16-09.md` containing `Timestamp:`, `Policy Order:` (the five files in the order read), and an explicit list of the five file paths. Acceptance: the artifact file exists and contains all five listed paths and both required fields. +- [x] [P0-T1] Read, in this order, `CLAUDE.md`, `.claude/rules/general-code-change.md`, `.claude/rules/general-unit-test.md`, `.claude/rules/csharp.md`, and `.claude/rules/tonality.md`. Write `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/phase0-instructions-read.2026-09-12T16-09.md` containing `Timestamp:`, `Policy Order:` (the five files in the order read), and an explicit list of the five file paths. Acceptance: the artifact file exists and contains all five listed paths and both required fields. -- [ ] [P0-T2] Confirm Outlook is closed before any rebuild in this plan. Run `pwsh -NoProfile -Command 'if (Get-Process OUTLOOK -ErrorAction SilentlyContinue) { Write-Output "OUTLOOK_RUNNING" } else { Write-Output "OUTLOOK_CLOSED" }'`. Acceptance: the printed line is exactly `OUTLOOK_CLOSED`. If `OUTLOOK_RUNNING` is printed, close Outlook through its own UI (never a process-termination command) and re-run this task before proceeding. Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/outlook-closed-confirmation.2026-09-12T16-09.md`. +- [x] [P0-T2] Confirm Outlook is closed before any rebuild in this plan. Run `pwsh -NoProfile -Command 'if (Get-Process OUTLOOK -ErrorAction SilentlyContinue) { Write-Output "OUTLOOK_RUNNING" } else { Write-Output "OUTLOOK_CLOSED" }'`. Acceptance: the printed line is exactly `OUTLOOK_CLOSED`. If `OUTLOOK_RUNNING` is printed, close Outlook through its own UI (never a process-termination command) and re-run this task before proceeding. Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/outlook-closed-confirmation.2026-09-12T16-09.md`. -- [ ] [P0-T3] Bootstrap the toolchain. Run `pwsh -NoProfile -Command 'nuget restore TaskMaster.sln'` followed by `pwsh -NoProfile -Command 'dotnet tool restore'`, followed by `pwsh -NoProfile -Command 'if (Get-Command dotnet-coverage -ErrorAction SilentlyContinue) { Write-Output "DOTNET_COVERAGE_PRESENT" } else { dotnet tool install --global dotnet-coverage; Write-Output "DOTNET_COVERAGE_INSTALLED" }'` (P0-T8 and P5-T4 both require `dotnet-coverage` on PATH; `dotnet-tools.json` pins only `csharpier`, so `dotnet tool restore` alone does not provide it). Acceptance: all three `EXIT_CODE` values are 0, and the third command prints exactly one of `DOTNET_COVERAGE_PRESENT` or `DOTNET_COVERAGE_INSTALLED`. Record `Timestamp:`, `Command:` (all three), `EXIT_CODE:` (all three), `Output Summary:` in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/toolchain-bootstrap.2026-09-12T16-09.md`. +- [x] [P0-T3] Bootstrap the toolchain. Run `pwsh -NoProfile -Command 'nuget restore TaskMaster.sln'` followed by `pwsh -NoProfile -Command 'dotnet tool restore'`, followed by `pwsh -NoProfile -Command 'if (Get-Command dotnet-coverage -ErrorAction SilentlyContinue) { Write-Output "DOTNET_COVERAGE_PRESENT" } else { dotnet tool install --global dotnet-coverage; Write-Output "DOTNET_COVERAGE_INSTALLED" }'` (P0-T8 and P5-T4 both require `dotnet-coverage` on PATH; `dotnet-tools.json` pins only `csharpier`, so `dotnet tool restore` alone does not provide it). Acceptance: all three `EXIT_CODE` values are 0, and the third command prints exactly one of `DOTNET_COVERAGE_PRESENT` or `DOTNET_COVERAGE_INSTALLED`. Record `Timestamp:`, `Command:` (all three), `EXIT_CODE:` (all three), `Output Summary:` in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/toolchain-bootstrap.2026-09-12T16-09.md`. -- [ ] [P0-T4] Baseline formatting state, captured BEFORE any write-mode formatter task runs anywhere in this plan (a baseline taken after `csharpier format` has already repaired drift would make the Phase 5 clean-pass gate either a blanket waiver or unsatisfiable). Run the read-only `pwsh -NoProfile -Command 'dotnet tool run csharpier check .'`. Acceptance: none (baseline only). Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` (the literal exit code and whatever diff/no-diff summary the command prints) in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/csharpier-check.2026-09-12T16-09.md`. +- [x] [P0-T4] Baseline formatting state, captured BEFORE any write-mode formatter task runs anywhere in this plan (a baseline taken after `csharpier format` has already repaired drift would make the Phase 5 clean-pass gate either a blanket waiver or unsatisfiable). Run the read-only `pwsh -NoProfile -Command 'dotnet tool run csharpier check .'`. Acceptance: none (baseline only). Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` (the literal exit code and whatever diff/no-diff summary the command prints) in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/csharpier-check.2026-09-12T16-09.md`. -- [ ] [P0-T5] Baseline analyzer state. Run `pwsh -NoProfile -Command '$out = msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true 2>&1; $exit = $LASTEXITCODE; $out | Select-String -Pattern "^\s*\d+ Error\(s\)\s*$" | ForEach-Object { $_.Line.Trim() }; Write-Output "EXITCODE=$exit"'`. Acceptance: none (baseline only). Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` (the transcribed `N Error(s)` line and the exit code) in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/msbuild-analyzer.2026-09-12T16-09.md`. +- [x] [P0-T5] Baseline analyzer state. Run `pwsh -NoProfile -Command '$out = msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true 2>&1; $exit = $LASTEXITCODE; $out | Select-String -Pattern "^\s*\d+ Error\(s\)\s*$" | ForEach-Object { $_.Line.Trim() }; Write-Output "EXITCODE=$exit"'`. Acceptance: none (baseline only). Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` (the transcribed `N Error(s)` line and the exit code) in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/msbuild-analyzer.2026-09-12T16-09.md`. -- [ ] [P0-T6] Baseline nullable/warnings-as-errors state. Run `pwsh -NoProfile -Command '$out = msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true 2>&1; $exit = $LASTEXITCODE; $out | Select-String -Pattern "^\s*\d+ Error\(s\)\s*$" | ForEach-Object { $_.Line.Trim() }; Write-Output "EXITCODE=$exit"'`. Do not add `/p:Nullable=enable`. Acceptance: none (baseline only). Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/msbuild-nullable.2026-09-12T16-09.md`. +- [x] [P0-T6] Baseline nullable/warnings-as-errors state. Run `pwsh -NoProfile -Command '$out = msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true 2>&1; $exit = $LASTEXITCODE; $out | Select-String -Pattern "^\s*\d+ Error\(s\)\s*$" | ForEach-Object { $_.Line.Trim() }; Write-Output "EXITCODE=$exit"'`. Do not add `/p:Nullable=enable`. Acceptance: none (baseline only). Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/msbuild-nullable.2026-09-12T16-09.md`. -- [ ] [P0-T7] Build the current (unfixed) `QuickFiler.Test` assembly so a baseline test run is possible. Run `pwsh -NoProfile -Command 'msbuild TaskMaster.sln /t:Build /m /p:Configuration=Debug "/p:Platform=Any CPU"'`. Acceptance: `EXIT_CODE` is 0. Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/quickfiler-test-build.2026-09-12T16-09.md`. +- [x] [P0-T7] Build the current (unfixed) `QuickFiler.Test` assembly so a baseline test run is possible. Run `pwsh -NoProfile -Command 'msbuild TaskMaster.sln /t:Build /m /p:Configuration=Debug "/p:Platform=Any CPU"'`. Acceptance: `EXIT_CODE` is 0. Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/quickfiler-test-build.2026-09-12T16-09.md`. -- [ ] [P0-T8] Baseline `QuickFiler.Test` pass/fail state and coverage figure on the unfixed tree, using the repository's coverage runner, scoped to the `QuickFiler.Test` assembly only, with the raw Cobertura output written under the repository's gitignored `coverage\` directory and deleted by this task before completion (the script's `-CoverageOutput` parameter is passed through `Join-Path $repoRoot $CoverageOutput`, which does not special-case an already-rooted path, so an absolute value produces a malformed path; a relative value avoids this). Run `pwsh -NoProfile -Command './scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot QuickFiler.Test -Configuration Debug -CoverageOutput "coverage\coverage.cobertura.xml"; Write-Output "EXITCODE=$LASTEXITCODE"'`. This inner invocation already carries `/InIsolation` and `/TestCaseFilter:TestCategory!=LiveOutlook` (see `scripts/vscode/Invoke-MSTestWithCoverage.ps1`'s own argument construction), satisfying the plan's vstest-invocation requirements; scoping `-SearchRoot` to `QuickFiler.Test` restricts assembly discovery to `QuickFiler.Test.dll` and avoids running the rest of the repository's test suite, including the UtilitiesCS.Test shell-icon classes known to stall vstest on this machine. Acceptance: none (baseline only). Open the produced Cobertura XML at `coverage\coverage.cobertura.xml` and transcribe the `line-rate` figure for the classes covering `QfcHomeController.Metrics.cs`, `EfcHomeController.Metrics.cs`, and the `QfcItemController` `ViewerSetup` partial, together with the overall pass/fail/skip counts vstest reports; explicitly note that `QfcCollectionController` and `EfcItemController` carry a type-level `[ExcludeFromCodeCoverage]` attribute and are absent from the report, so their baseline quality signal is the named-test pass/fail state captured here, not a coverage percentage. Delete `coverage\coverage.cobertura.xml` after transcribing. Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` (pass/fail/skip counts plus the three transcribed line-rate figures) in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/vstest-coverage-baseline.2026-09-12T16-09.md`. +- [x] [P0-T8] Baseline `QuickFiler.Test` pass/fail state and coverage figure on the unfixed tree, using the repository's coverage runner, scoped to the `QuickFiler.Test` assembly only, with the raw Cobertura output written under the repository's gitignored `coverage\` directory and deleted by this task before completion (the script's `-CoverageOutput` parameter is passed through `Join-Path $repoRoot $CoverageOutput`, which does not special-case an already-rooted path, so an absolute value produces a malformed path; a relative value avoids this). Run `pwsh -NoProfile -Command './scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot QuickFiler.Test -Configuration Debug -CoverageOutput "coverage\coverage.cobertura.xml"; Write-Output "EXITCODE=$LASTEXITCODE"'`. This inner invocation already carries `/InIsolation` and `/TestCaseFilter:TestCategory!=LiveOutlook` (see `scripts/vscode/Invoke-MSTestWithCoverage.ps1`'s own argument construction), satisfying the plan's vstest-invocation requirements; scoping `-SearchRoot` to `QuickFiler.Test` restricts assembly discovery to `QuickFiler.Test.dll` and avoids running the rest of the repository's test suite, including the UtilitiesCS.Test shell-icon classes known to stall vstest on this machine. Acceptance: none (baseline only). Open the produced Cobertura XML at `coverage\coverage.cobertura.xml` and transcribe the `line-rate` figure for the classes covering `QfcHomeController.Metrics.cs`, `EfcHomeController.Metrics.cs`, and the `QfcItemController` `ViewerSetup` partial, together with the overall pass/fail/skip counts vstest reports; explicitly note that `QfcCollectionController` and `EfcItemController` carry a type-level `[ExcludeFromCodeCoverage]` attribute and are absent from the report, so their baseline quality signal is the named-test pass/fail state captured here, not a coverage percentage. Delete `coverage\coverage.cobertura.xml` after transcribing. Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` (pass/fail/skip counts plus the three transcribed line-rate figures) in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/vstest-coverage-baseline.2026-09-12T16-09.md`. -- [ ] [P0-T9] Baseline discovery-count controls, re-derived against the current tree in this pass (values confirmed 2026-09-12), written BEFORE any Write Set source file is modified. Run each of these eleven `git grep` commands and transcribe the exact printed output and exit code of each: +- [x] [P0-T9] Baseline discovery-count controls, re-derived against the current tree in this pass (values confirmed 2026-09-12), written BEFORE any Write Set source file is modified. Run each of these eleven `git grep` commands and transcribe the exact printed output and exit code of each: 1. `git grep -c '[.]ToString[(]@\?"[^"]*[:/.\-][^"]*"[)]' -- QuickFiler/Controllers/QfcHomeController.Metrics.cs QuickFiler/Controllers/EfcHomeController.Metrics.cs QuickFiler/Controllers/QfcItemController.ViewerSetup.cs QuickFiler/Controllers/QfcCollectionController.cs QuickFiler/Controllers/EfcItemController.cs` — expected per-file counts 4, 4, 1, 3, 2 (14 total). The `@` is escaped as `@\?` rather than `@?` because `git grep`'s default basic-regex mode treats a bare `?` as a literal character, not a quantifier; `\?` is the documented GNU basic-regex extension for "optional preceding atom." An unescaped `@?` never matches anything in this repository (no site uses a verbatim string literal), so every acceptance condition built on this pattern would read as a permanent zero-match regardless of whether the fix was applied. 2. `git grep -c '[{]now:' -- QuickFiler/Controllers/QfcHomeController.Metrics.cs` — expected 1. 3. `git grep -c 'CultureInfo[.]InvariantCulture' -- QuickFiler/Controllers/QfcHomeController.Metrics.cs` — expected 4. @@ -64,11 +64,11 @@ 11. `git grep -c -F 'CultureInfo.InvariantCulture' -- QuickFiler.Test/Controllers/QfcHomeControllerMetricsTests.cs` — expected: prints no line, exits 1. Acceptance: `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/discovery-count-controls.2026-09-12T16-09.md` exists and records all eleven commands with `Timestamp:`, `Command:` (all eleven), `EXIT_CODE:` (each), and `Output Summary:` (each figure exactly as stated above). This artifact is the baseline evidence artifact required by the Acceptance Criteria's discovery-count-figures item, and it is written before P1-T1 (the first task that creates or modifies any Write Set file). -- [ ] [P0-T10] Confirm the two `[ExcludeFromCodeCoverage]` attributes this plan relies on for its coverage-signal framing are present as expected. Run `git grep -c -F '[ExcludeFromCodeCoverage]' -- QuickFiler/Controllers/QfcCollectionController.cs QuickFiler/Controllers/EfcItemController.cs`. Acceptance: the command prints exactly one matching line for each of the two files (`QuickFiler/Controllers/QfcCollectionController.cs:1` and `QuickFiler/Controllers/EfcItemController.cs:1`) and `EXIT_CODE` is 0. Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/excludefromcodecoverage-confirmation.2026-09-12T16-09.md`. +- [x] [P0-T10] Confirm the two `[ExcludeFromCodeCoverage]` attributes this plan relies on for its coverage-signal framing are present as expected. Run `git grep -c -F '[ExcludeFromCodeCoverage]' -- QuickFiler/Controllers/QfcCollectionController.cs QuickFiler/Controllers/EfcItemController.cs`. Acceptance: the command prints exactly one matching line for each of the two files (`QuickFiler/Controllers/QfcCollectionController.cs:1` and `QuickFiler/Controllers/EfcItemController.cs:1`) and `EXIT_CODE` is 0. Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/excludefromcodecoverage-confirmation.2026-09-12T16-09.md`. ### Phase 1 — Regression Tests Demonstrating the Defect -- [ ] [P1-T1] [expect-fail] Create `QuickFiler.Test/Controllers/QuickFilerInvariantCultureIssue742Tests.cs` containing exactly five `[TestMethod]` tests inside one `[TestClass]`, one test per production file, each following the try/finally culture-swap pattern already established at `QuickFiler.Test/Controllers/QfcHomeControllerMetricsTests.cs:183-213` and `QuickFiler.Test/Controllers/EfcHomeControllerMetricsTests.cs:136-158` (clone a `CultureInfo`, assign explicit sentinel characters — neither `/` nor `:` — to `DateTimeFormat.DateSeparator` and `DateTimeFormat.TimeSeparator`, activate it as `CultureInfo.CurrentCulture` inside a `try` block, restore the original culture in a `finally` block), asserting with FluentAssertions that production output contains `/` and/or `:` rather than the sentinel character: +- [x] [P1-T1] [expect-fail] Create `QuickFiler.Test/Controllers/QuickFilerInvariantCultureIssue742Tests.cs` containing exactly five `[TestMethod]` tests inside one `[TestClass]`, one test per production file, each following the try/finally culture-swap pattern already established at `QuickFiler.Test/Controllers/QfcHomeControllerMetricsTests.cs:183-213` and `QuickFiler.Test/Controllers/EfcHomeControllerMetricsTests.cs:136-158` (clone a `CultureInfo`, assign explicit sentinel characters — neither `/` nor `:` — to `DateTimeFormat.DateSeparator` and `DateTimeFormat.TimeSeparator`, activate it as `CultureInfo.CurrentCulture` inside a `try` block, restore the original culture in a `finally` block), asserting with FluentAssertions that production output contains `/` and/or `:` rather than the sentinel character: - Test 1, for `QuickFiler/Controllers/QfcHomeController.Metrics.cs`: build a `QfcHomeController` with a loose `Mock` for `_formController.Groups` (mirroring the existing `BuildLooseMetricsController` helper pattern in `QfcHomeControllerMetricsTests.cs`), assign a `FakeTimeProvider` to the injectable `TimeProvider` property, call `QuickFileMetrics_WRITE` or `WriteMetricsAsync`, and assert the captured `dataLineBeg` argument contains `/` and `:`. - Test 2, for `QuickFiler/Controllers/EfcHomeController.Metrics.cs`: call the internal static `BuildQuickFileMetricLines` directly with a literal `DateTime` argument and a one-item `List`, and assert the returned line contains `/` and `:` (both the leading date/time fields and the trailing `itemInfo.SentDate` fields). - Test 3, for `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs`: construct a `QfcItemController` via `FormatterServices.GetUninitializedObject(typeof(QfcItemController))` (mirroring the established pattern in `QuickFiler.Test/Controllers/EfcItemControllerTests.cs:200,315`), assign a directly constructed `MailItemHelper` with `SentDate` set to the public `ItemHelper` property, call the internal `GetItemSummary()`, and assert the returned string contains `/` and `:`. @@ -76,63 +76,63 @@ - Test 5, for `QuickFiler/Controllers/EfcItemController.cs`: construct an `EfcItemController` via `FormatterServices.GetUninitializedObject(typeof(EfcItemController))`, assign a directly constructed `MailItemHelper` with `SentDate` set to the private `_itemInfo` field via the established `SetPrivateField` reflection helper already present in `QuickFiler.Test/Controllers/EfcItemControllerTests.cs`, read the `SentDate` and `SentTime` properties, and assert both returned strings contain `/` and `:` respectively. No test in this file uses a temporary file, `Thread.Sleep`, or `Task.Delay`. Acceptance: `git grep --untracked -c -F '[TestMethod]' -- QuickFiler.Test/Controllers/QuickFilerInvariantCultureIssue742Tests.cs` prints the count `5` for that path and `EXIT_CODE` is 0. -- [ ] [P1-T2] Add exactly one new line, ``, inside the single `` element that opens at line 57 of `QuickFiler.Test/QuickFiler.Test.csproj` (the element closes at line 230 and holds every other `Controllers\` compile entry, including `` at line 199; anchor the insertion adjacent to that literal line rather than by line number). Acceptance: `git grep -c -F 'QuickFilerInvariantCultureIssue742Tests' -- QuickFiler.Test/QuickFiler.Test.csproj` prints the count `1` (baseline was: prints no line, exits 1, per P0-T9 control 6), and `git grep -c -F 'Compile Include' -- QuickFiler.Test/QuickFiler.Test.csproj` prints the count `177` (baseline was 176, per P0-T9 control 5). +- [x] [P1-T2] Add exactly one new line, ``, inside the single `` element that opens at line 57 of `QuickFiler.Test/QuickFiler.Test.csproj` (the element closes at line 230 and holds every other `Controllers\` compile entry, including `` at line 199; anchor the insertion adjacent to that literal line rather than by line number). Acceptance: `git grep -c -F 'QuickFilerInvariantCultureIssue742Tests' -- QuickFiler.Test/QuickFiler.Test.csproj` prints the count `1` (baseline was: prints no line, exits 1, per P0-T9 control 6), and `git grep -c -F 'Compile Include' -- QuickFiler.Test/QuickFiler.Test.csproj` prints the count `177` (baseline was 176, per P0-T9 control 5). -- [ ] [P1-T3] Build the test project with the new file included, against the still-unfixed production tree. Run `pwsh -NoProfile -Command 'msbuild TaskMaster.sln /t:Build /m /p:Configuration=Debug "/p:Platform=Any CPU"'`. Acceptance: `EXIT_CODE` is 0 (the new test file compiles even though production is not yet fixed, because the test only reads production output rather than requiring production behavior to already be correct in order to compile). +- [x] [P1-T3] Build the test project with the new file included, against the still-unfixed production tree. Run `pwsh -NoProfile -Command 'msbuild TaskMaster.sln /t:Build /m /p:Configuration=Debug "/p:Platform=Any CPU"'`. Acceptance: `EXIT_CODE` is 0 (the new test file compiles even though production is not yet fixed, because the test only reads production output rather than requiring production behavior to already be correct in order to compile). -- [ ] [P1-T4] [expect-fail] Run `pwsh -NoProfile -Command '$vswherePath = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"; $vstestPath = (& $vswherePath -latest -products * -find "Common7\IDE\Extensions\TestPlatform\vstest.console.exe") | Select-Object -First 1; & $vstestPath "QuickFiler.Test\bin\Debug\QuickFiler.Test.dll" /TestCaseFilter:"FullyQualifiedName~QuickFilerInvariantCultureIssue742Tests" /InIsolation /Logger:"console;verbosity=normal"; Write-Output "EXITCODE=$LASTEXITCODE"'`. Acceptance: `EXITCODE` is non-zero, demonstrating the defect against unfixed production code. Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` (the printed test-run summary and the non-zero exit code observed) in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/regression-testing/expect-fail-new-tests.2026-09-12T16-09.md`. +- [x] [P1-T4] [expect-fail] Run `pwsh -NoProfile -Command '$vswherePath = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"; $vstestPath = (& $vswherePath -latest -products * -find "Common7\IDE\Extensions\TestPlatform\vstest.console.exe") | Select-Object -First 1; & $vstestPath "QuickFiler.Test\bin\Debug\QuickFiler.Test.dll" /TestCaseFilter:"FullyQualifiedName~QuickFilerInvariantCultureIssue742Tests" /InIsolation /Logger:"console;verbosity=normal"; Write-Output "EXITCODE=$LASTEXITCODE"'`. Acceptance: `EXITCODE` is non-zero, demonstrating the defect against unfixed production code. Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` (the printed test-run summary and the non-zero exit code observed) in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/regression-testing/expect-fail-new-tests.2026-09-12T16-09.md`. ### Phase 2 — Fix: Session-Metrics Writer Files -- [ ] [P2-T1] In `QuickFiler/Controllers/QfcHomeController.Metrics.cs`, replace the literal statement `dataLineBeg = $"{now:MM/dd/yyyy},{now:HH:mm},";` with two explicit two-argument `ToString` calls passing `CultureInfo.InvariantCulture`, matching the shape already used two lines below at (currently) lines 125 and 127 of the same file, concatenated with the identical comma placement so the CSV field count and order do not change: `dataLineBeg = now.ToString("MM/dd/yyyy", CultureInfo.InvariantCulture) + "," + now.ToString("HH:mm", CultureInfo.InvariantCulture) + ",";`. Do not edit the commented-out predecessor lines immediately above it (`//var curDateText = ...`, `//var curTimeText = ...`, `//dataLineBeg = ...`). Acceptance: `git grep -c '[{]now:' -- QuickFiler/Controllers/QfcHomeController.Metrics.cs` prints no line for that path and exits 1 (baseline was 1, per P0-T9 control 2). +- [x] [P2-T1] In `QuickFiler/Controllers/QfcHomeController.Metrics.cs`, replace the literal statement `dataLineBeg = $"{now:MM/dd/yyyy},{now:HH:mm},";` with two explicit two-argument `ToString` calls passing `CultureInfo.InvariantCulture`, matching the shape already used two lines below at (currently) lines 125 and 127 of the same file, concatenated with the identical comma placement so the CSV field count and order do not change: `dataLineBeg = now.ToString("MM/dd/yyyy", CultureInfo.InvariantCulture) + "," + now.ToString("HH:mm", CultureInfo.InvariantCulture) + ",";`. Do not edit the commented-out predecessor lines immediately above it (`//var curDateText = ...`, `//var curTimeText = ...`, `//dataLineBeg = ...`). Acceptance: `git grep -c '[{]now:' -- QuickFiler/Controllers/QfcHomeController.Metrics.cs` prints no line for that path and exits 1 (baseline was 1, per P0-T9 control 2). -- [ ] [P2-T2] In `QuickFiler/Controllers/QfcHomeController.Metrics.cs`, append `, CultureInfo.InvariantCulture` as the second argument to the two literal statements `curDateText = now.ToString("MM/dd/yyyy");` and `curTimeText = now.ToString("HH:mm");` inside `WriteMetricsAsync` (located immediately below the statement P2-T1 replaced; anchor on this literal text, not on a line number, since P2-T1's edit shifts every subsequent line number in this file), changing only the argument list at each site. Acceptance: `git grep -c 'CultureInfo[.]InvariantCulture' -- QuickFiler/Controllers/QfcHomeController.Metrics.cs` prints a count strictly greater than `4` (baseline was 4, per P0-T9 control 3; the exact post-fix figure is not fixed here because CSharpier decides whether the two edited statements land on one line or wrap across two apiece in Phase 5, and a predicted figure would be a formatter artifact rather than a property of the change). +- [x] [P2-T2] In `QuickFiler/Controllers/QfcHomeController.Metrics.cs`, append `, CultureInfo.InvariantCulture` as the second argument to the two literal statements `curDateText = now.ToString("MM/dd/yyyy");` and `curTimeText = now.ToString("HH:mm");` inside `WriteMetricsAsync` (located immediately below the statement P2-T1 replaced; anchor on this literal text, not on a line number, since P2-T1's edit shifts every subsequent line number in this file), changing only the argument list at each site. Acceptance: `git grep -c 'CultureInfo[.]InvariantCulture' -- QuickFiler/Controllers/QfcHomeController.Metrics.cs` prints a count strictly greater than `4` (baseline was 4, per P0-T9 control 3; the exact post-fix figure is not fixed here because CSharpier decides whether the two edited statements land on one line or wrap across two apiece in Phase 5, and a predicted figure would be a formatter artifact rather than a property of the change). -- [ ] [P2-T3] In `QuickFiler/Controllers/EfcHomeController.Metrics.cs`, append `, CultureInfo.InvariantCulture` as the second argument to the four literal statements `currentDateTime.ToString("MM/dd/yyyy")`, `currentDateTime.ToString("HH:mm")`, `itemInfo.SentDate.ToString("MM/dd/yyyy")`, and `itemInfo.SentDate.ToString("HH:mm:ss")` (currently at lines 95, 96, 118, and 119 respectively), changing only the argument list at each site (`using System.Globalization;` is already present at line 3 of this file; no new `using` directive is added here). Acceptance: `git grep -c '[.]ToString[(]@\?"[^"]*[:/.\-][^"]*"[)]' -- QuickFiler/Controllers/EfcHomeController.Metrics.cs` prints no line for that path and exits 1 (baseline was 4, per P0-T9 control 1). +- [x] [P2-T3] In `QuickFiler/Controllers/EfcHomeController.Metrics.cs`, append `, CultureInfo.InvariantCulture` as the second argument to the four literal statements `currentDateTime.ToString("MM/dd/yyyy")`, `currentDateTime.ToString("HH:mm")`, `itemInfo.SentDate.ToString("MM/dd/yyyy")`, and `itemInfo.SentDate.ToString("HH:mm:ss")` (currently at lines 95, 96, 118, and 119 respectively), changing only the argument list at each site (`using System.Globalization;` is already present at line 3 of this file; no new `using` directive is added here). Acceptance: `git grep -c '[.]ToString[(]@\?"[^"]*[:/.\-][^"]*"[)]' -- QuickFiler/Controllers/EfcHomeController.Metrics.cs` prints no line for that path and exits 1 (baseline was 4, per P0-T9 control 1). -- [ ] [P2-T4] Verify the combined result of P2-T1 through P2-T3, scoped to only the two Metrics files (the three UI-facing files are fixed in Phase 3 and are deliberately excluded from this check). Run `git grep -c '[.]ToString[(]@\?"[^"]*[:/.\-][^"]*"[)]' -- QuickFiler/Controllers/QfcHomeController.Metrics.cs QuickFiler/Controllers/EfcHomeController.Metrics.cs`. Acceptance: the command prints exactly one line, `QuickFiler/Controllers/QfcHomeController.Metrics.cs:2` (the two commented-out predecessor statements, unedited by this change), prints no line for `QuickFiler/Controllers/EfcHomeController.Metrics.cs`, and `EXIT_CODE` is 0 (baseline combined total for these two files was 4 + 4 = 8, per P0-T9 control 1). Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/regression-testing/phase2-verification.2026-09-12T16-09.md`. +- [x] [P2-T4] Verify the combined result of P2-T1 through P2-T3, scoped to only the two Metrics files (the three UI-facing files are fixed in Phase 3 and are deliberately excluded from this check). Run `git grep -c '[.]ToString[(]@\?"[^"]*[:/.\-][^"]*"[)]' -- QuickFiler/Controllers/QfcHomeController.Metrics.cs QuickFiler/Controllers/EfcHomeController.Metrics.cs`. Acceptance: the command prints exactly one line, `QuickFiler/Controllers/QfcHomeController.Metrics.cs:2` (the two commented-out predecessor statements, unedited by this change), prints no line for `QuickFiler/Controllers/EfcHomeController.Metrics.cs`, and `EXIT_CODE` is 0 (baseline combined total for these two files was 4 + 4 = 8, per P0-T9 control 1). Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/regression-testing/phase2-verification.2026-09-12T16-09.md`. ### Phase 3 — Fix: UI-Facing Files -- [ ] [P3-T1] Add `using System.Globalization;` to `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` (placed alongside the file's existing `using System.*` directives at the top of the file, which currently has no `System.Globalization` entry), and append `, CultureInfo.InvariantCulture` as the second argument to both `ToString` calls inside the literal statement `$"Subject: {ItemHelper.Subject} sent on {ItemHelper.SentDate.ToString("MM/dd/yyyy")} at {ItemHelper.SentDate.ToString("HH:mm")} by {ItemHelper.SenderName}"` (currently at line 465, inside `GetItemSummary`), changing only the two `ToString` argument lists; the interpolation itself is unaffected because these are ordinary method calls evaluated inside an interpolation hole, not interpolated format specifiers. Acceptance: `git grep -c -F 'using System.Globalization;' -- QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` prints the count `1` (baseline was: prints no line, exits 1, per P0-T9 control 7). +- [x] [P3-T1] Add `using System.Globalization;` to `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` (placed alongside the file's existing `using System.*` directives at the top of the file, which currently has no `System.Globalization` entry), and append `, CultureInfo.InvariantCulture` as the second argument to both `ToString` calls inside the literal statement `$"Subject: {ItemHelper.Subject} sent on {ItemHelper.SentDate.ToString("MM/dd/yyyy")} at {ItemHelper.SentDate.ToString("HH:mm")} by {ItemHelper.SenderName}"` (currently at line 465, inside `GetItemSummary`), changing only the two `ToString` argument lists; the interpolation itself is unaffected because these are ordinary method calls evaluated inside an interpolation hole, not interpolated format specifiers. Acceptance: `git grep -c -F 'using System.Globalization;' -- QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` prints the count `1` (baseline was: prints no line, exits 1, per P0-T9 control 7). -- [ ] [P3-T2] Add `using System.Globalization;` to `QuickFiler/Controllers/QfcCollectionController.cs` (placed alongside the file's existing `using System.*` directives, which currently has no `System.Globalization` entry), and append `, CultureInfo.InvariantCulture` as the second argument to all five `ToString` calls: the literal `grp.ItemController.Mail.SentOn.ToString("MM/dd/yyyy")` (currently line 235, inside `TryGetMoveReadiness`), the two calls in the literal `c.ItemHelper.SentDate.ToString("MM/dd/yyyy")` / `c.ItemHelper.SentDate.ToString("HH:mm")` (currently line 1296, inside `ToggleExpansionStyle`'s exception-message builder), and the two calls in the literal `qf.ItemHelper.SentDate.ToString("MM/dd/yyyy")` / `qf.ItemHelper.SentDate.ToString("HH:mm")` (currently line 2302, inside the `GetMoveDiagnostics` line builder), changing only the argument lists. Acceptance: `git grep -c -F 'using System.Globalization;' -- QuickFiler/Controllers/QfcCollectionController.cs` prints the count `1` (baseline was: prints no line, exits 1, per P0-T9 control 8). +- [x] [P3-T2] Add `using System.Globalization;` to `QuickFiler/Controllers/QfcCollectionController.cs` (placed alongside the file's existing `using System.*` directives, which currently has no `System.Globalization` entry), and append `, CultureInfo.InvariantCulture` as the second argument to all five `ToString` calls: the literal `grp.ItemController.Mail.SentOn.ToString("MM/dd/yyyy")` (currently line 235, inside `TryGetMoveReadiness`), the two calls in the literal `c.ItemHelper.SentDate.ToString("MM/dd/yyyy")` / `c.ItemHelper.SentDate.ToString("HH:mm")` (currently line 1296, inside `ToggleExpansionStyle`'s exception-message builder), and the two calls in the literal `qf.ItemHelper.SentDate.ToString("MM/dd/yyyy")` / `qf.ItemHelper.SentDate.ToString("HH:mm")` (currently line 2302, inside the `GetMoveDiagnostics` line builder), changing only the argument lists. Acceptance: `git grep -c -F 'using System.Globalization;' -- QuickFiler/Controllers/QfcCollectionController.cs` prints the count `1` (baseline was: prints no line, exits 1, per P0-T9 control 8). -- [ ] [P3-T3] Add `using System.Globalization;` to `QuickFiler/Controllers/EfcItemController.cs` (placed alongside the file's existing `using System.*` directives, which currently has no `System.Globalization` entry), and append `, CultureInfo.InvariantCulture` as the second argument to the two literal statements `_itemInfo.SentDate.ToString("MM/dd/yyyy")` (currently line 607, the `SentDate` property getter) and `_itemInfo.SentDate.ToString("HH:mm")` (currently line 612, the `SentTime` property getter), changing only the argument lists. Acceptance: `git grep -c -F 'using System.Globalization;' -- QuickFiler/Controllers/EfcItemController.cs` prints the count `1` (baseline was: prints no line, exits 1, per P0-T9 control 9). +- [x] [P3-T3] Add `using System.Globalization;` to `QuickFiler/Controllers/EfcItemController.cs` (placed alongside the file's existing `using System.*` directives, which currently has no `System.Globalization` entry), and append `, CultureInfo.InvariantCulture` as the second argument to the two literal statements `_itemInfo.SentDate.ToString("MM/dd/yyyy")` (currently line 607, the `SentDate` property getter) and `_itemInfo.SentDate.ToString("HH:mm")` (currently line 612, the `SentTime` property getter), changing only the argument lists. Acceptance: `git grep -c -F 'using System.Globalization;' -- QuickFiler/Controllers/EfcItemController.cs` prints the count `1` (baseline was: prints no line, exits 1, per P0-T9 control 9). -- [ ] [P3-T4] Verify the combined result of P3-T1 through P3-T3, scoped to only the three UI-facing files. Run `git grep -c '[.]ToString[(]@\?"[^"]*[:/.\-][^"]*"[)]' -- QuickFiler/Controllers/QfcItemController.ViewerSetup.cs QuickFiler/Controllers/QfcCollectionController.cs QuickFiler/Controllers/EfcItemController.cs`. Acceptance: the command prints no line for any of the three paths and `EXIT_CODE` is 1 (baseline combined total for these three files was 1 + 3 + 2 = 6, per P0-T9 control 1). Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/regression-testing/phase3-verification.2026-09-12T16-09.md`. +- [x] [P3-T4] Verify the combined result of P3-T1 through P3-T3, scoped to only the three UI-facing files. Run `git grep -c '[.]ToString[(]@\?"[^"]*[:/.\-][^"]*"[)]' -- QuickFiler/Controllers/QfcItemController.ViewerSetup.cs QuickFiler/Controllers/QfcCollectionController.cs QuickFiler/Controllers/EfcItemController.cs`. Acceptance: the command prints no line for any of the three paths and `EXIT_CODE` is 1 (baseline combined total for these three files was 1 + 3 + 2 = 6, per P0-T9 control 1). Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/regression-testing/phase3-verification.2026-09-12T16-09.md`. ### Phase 4 — Test Oracle Repair and Regression Confirmation -- [ ] [P4-T1] In `QuickFiler.Test/Controllers/QfcHomeControllerMetricsTests.cs`, rewrite the expected-value construction inside `WriteMetricsAsync_UsesInjectedClock_ForDateAndTimeStamps` (currently at lines 242-243: `var expectedDataLineBeg = expectedLocal.ToString("MM/dd/yyyy") + "," + expectedLocal.ToString("HH:mm") + ",";`, disambiguated from the identical statement in the sibling method below by the enclosing method's preceding `SetPrivateField(controller, "_stopWatch", new Stopwatch());` line, which the sibling method does not have) so that both `ToString` calls pass `CultureInfo.InvariantCulture` as their second argument, removing the self-referential uncultured oracle for this test. Acceptance: `git grep -c -F 'expectedLocal.ToString("MM/dd/yyyy") + "," + expectedLocal.ToString("HH:mm") + ",";' -- QuickFiler.Test/Controllers/QfcHomeControllerMetricsTests.cs` prints the count `1` (baseline was 2, per P0-T9 control 10; the one remaining occurrence is the untouched sibling statement inside `QuickFileMetrics_WRITE_UsesInjectedClock_ForDataLine`, fixed in P4-T2). +- [x] [P4-T1] In `QuickFiler.Test/Controllers/QfcHomeControllerMetricsTests.cs`, rewrite the expected-value construction inside `WriteMetricsAsync_UsesInjectedClock_ForDateAndTimeStamps` (currently at lines 242-243: `var expectedDataLineBeg = expectedLocal.ToString("MM/dd/yyyy") + "," + expectedLocal.ToString("HH:mm") + ",";`, disambiguated from the identical statement in the sibling method below by the enclosing method's preceding `SetPrivateField(controller, "_stopWatch", new Stopwatch());` line, which the sibling method does not have) so that both `ToString` calls pass `CultureInfo.InvariantCulture` as their second argument, removing the self-referential uncultured oracle for this test. Acceptance: `git grep -c -F 'expectedLocal.ToString("MM/dd/yyyy") + "," + expectedLocal.ToString("HH:mm") + ",";' -- QuickFiler.Test/Controllers/QfcHomeControllerMetricsTests.cs` prints the count `1` (baseline was 2, per P0-T9 control 10; the one remaining occurrence is the untouched sibling statement inside `QuickFileMetrics_WRITE_UsesInjectedClock_ForDataLine`, fixed in P4-T2). -- [ ] [P4-T2] In `QuickFiler.Test/Controllers/QfcHomeControllerMetricsTests.cs`, rewrite the expected-value construction inside `QuickFileMetrics_WRITE_UsesInjectedClock_ForDataLine` (the remaining occurrence of the literal statement located by P4-T1's acceptance check) so that both `ToString` calls pass `CultureInfo.InvariantCulture` as their second argument. Acceptance: `git grep -c -F 'expectedLocal.ToString("MM/dd/yyyy") + "," + expectedLocal.ToString("HH:mm") + ",";' -- QuickFiler.Test/Controllers/QfcHomeControllerMetricsTests.cs` prints no line for that path and exits 1 (baseline was 2, covering both line 243 and line 278, per P0-T9 control 10; P4-T1 already reduced it to 1). +- [x] [P4-T2] In `QuickFiler.Test/Controllers/QfcHomeControllerMetricsTests.cs`, rewrite the expected-value construction inside `QuickFileMetrics_WRITE_UsesInjectedClock_ForDataLine` (the remaining occurrence of the literal statement located by P4-T1's acceptance check) so that both `ToString` calls pass `CultureInfo.InvariantCulture` as their second argument. Acceptance: `git grep -c -F 'expectedLocal.ToString("MM/dd/yyyy") + "," + expectedLocal.ToString("HH:mm") + ",";' -- QuickFiler.Test/Controllers/QfcHomeControllerMetricsTests.cs` prints no line for that path and exits 1 (baseline was 2, covering both line 243 and line 278, per P0-T9 control 10; P4-T1 already reduced it to 1). -- [ ] [P4-T3] Build the now-fixed tree. Run `pwsh -NoProfile -Command 'msbuild TaskMaster.sln /t:Build /m /p:Configuration=Debug "/p:Platform=Any CPU"'`. Acceptance: `EXIT_CODE` is 0. +- [x] [P4-T3] Build the now-fixed tree. Run `pwsh -NoProfile -Command 'msbuild TaskMaster.sln /t:Build /m /p:Configuration=Debug "/p:Platform=Any CPU"'`. Acceptance: `EXIT_CODE` is 0. -- [ ] [P4-T4] Run `pwsh -NoProfile -Command '$vswherePath = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"; $vstestPath = (& $vswherePath -latest -products * -find "Common7\IDE\Extensions\TestPlatform\vstest.console.exe") | Select-Object -First 1; & $vstestPath "QuickFiler.Test\bin\Debug\QuickFiler.Test.dll" /TestCaseFilter:"FullyQualifiedName~QuickFilerInvariantCultureIssue742Tests|FullyQualifiedName~WriteMetricsAsync_UsesInjectedClock_ForDateAndTimeStamps|FullyQualifiedName~QuickFileMetrics_WRITE_UsesInjectedClock_ForDataLine" /InIsolation /Logger:"console;verbosity=normal"; Write-Output "EXITCODE=$LASTEXITCODE"'`. Acceptance: `EXITCODE` is 0, covering all seven named tests (the five new tests from P1-T1 plus the two rewritten oracle tests from P4-T1/P4-T2). Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` (the printed test-run summary) in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/regression-testing/phase4-confirmation.2026-09-12T16-09.md`. +- [x] [P4-T4] Run `pwsh -NoProfile -Command '$vswherePath = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"; $vstestPath = (& $vswherePath -latest -products * -find "Common7\IDE\Extensions\TestPlatform\vstest.console.exe") | Select-Object -First 1; & $vstestPath "QuickFiler.Test\bin\Debug\QuickFiler.Test.dll" /TestCaseFilter:"FullyQualifiedName~QuickFilerInvariantCultureIssue742Tests|FullyQualifiedName~WriteMetricsAsync_UsesInjectedClock_ForDateAndTimeStamps|FullyQualifiedName~QuickFileMetrics_WRITE_UsesInjectedClock_ForDataLine" /InIsolation /Logger:"console;verbosity=normal"; Write-Output "EXITCODE=$LASTEXITCODE"'`. Acceptance: `EXITCODE` is 0, covering all seven named tests (the five new tests from P1-T1 plus the two rewritten oracle tests from P4-T1/P4-T2). Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` (the printed test-run summary) in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/regression-testing/phase4-confirmation.2026-09-12T16-09.md`. -- [ ] [P4-T5] Verify the repository-wide residual count across all five production files. Run `git grep -c '[.]ToString[(]@\?"[^"]*[:/.\-][^"]*"[)]' -- QuickFiler/Controllers/QfcHomeController.Metrics.cs QuickFiler/Controllers/EfcHomeController.Metrics.cs QuickFiler/Controllers/QfcItemController.ViewerSetup.cs QuickFiler/Controllers/QfcCollectionController.cs QuickFiler/Controllers/EfcItemController.cs`. Acceptance: the command prints exactly one line, `QuickFiler/Controllers/QfcHomeController.Metrics.cs:2`, prints no line for any of the other four paths, and `EXIT_CODE` is 0 (baseline total across all five files was 14, per P0-T9 control 1). Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/regression-testing/phase4-residual-count.2026-09-12T16-09.md`. +- [x] [P4-T5] Verify the repository-wide residual count across all five production files. Run `git grep -c '[.]ToString[(]@\?"[^"]*[:/.\-][^"]*"[)]' -- QuickFiler/Controllers/QfcHomeController.Metrics.cs QuickFiler/Controllers/EfcHomeController.Metrics.cs QuickFiler/Controllers/QfcItemController.ViewerSetup.cs QuickFiler/Controllers/QfcCollectionController.cs QuickFiler/Controllers/EfcItemController.cs`. Acceptance: the command prints exactly one line, `QuickFiler/Controllers/QfcHomeController.Metrics.cs:2`, prints no line for any of the other four paths, and `EXIT_CODE` is 0 (baseline total across all five files was 14, per P0-T9 control 1). Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/regression-testing/phase4-residual-count.2026-09-12T16-09.md`. ### Phase 5 — Final QA Loop -- [ ] [P5-T1] Run `pwsh -NoProfile -Command 'dotnet tool run csharpier format .'` and then, as the required observation beyond the format command's own exit code, the read-only `pwsh -NoProfile -Command 'dotnet tool run csharpier check .'`. Acceptance: the `check` command's `EXIT_CODE` is 0 (no remaining diff). If `format` rewrote any tracked file, restart this task from `format` before re-running `check`. Record `Timestamp:`, `Command:` (both commands), `EXIT_CODE:` (of `check`), `Output Summary:` in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/qa-gates/csharpier-final.2026-09-12T16-09.md`. +- [x] [P5-T1] Run `pwsh -NoProfile -Command 'dotnet tool run csharpier format .'` and then, as the required observation beyond the format command's own exit code, the read-only `pwsh -NoProfile -Command 'dotnet tool run csharpier check .'`. Acceptance: the `check` command's `EXIT_CODE` is 0 (no remaining diff). If `format` rewrote any tracked file, restart this task from `format` before re-running `check`. Record `Timestamp:`, `Command:` (both commands), `EXIT_CODE:` (of `check`), `Output Summary:` in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/qa-gates/csharpier-final.2026-09-12T16-09.md`. -- [ ] [P5-T2] Run `pwsh -NoProfile -Command '$out = msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true 2>&1; $exit = $LASTEXITCODE; $out | Select-String -Pattern "^\s*\d+ Error\(s\)\s*$" | ForEach-Object { $_.Line.Trim() }; Write-Output "EXITCODE=$exit"'`. Acceptance: the transcribed summary line, once trimmed, equals exactly the literal `0 Error(s)` (anchored to the whole trimmed line, so a `10 Error(s)` line cannot satisfy this) and `EXITCODE` is 0. If the run fails or csharpier rewrote any file since P5-T1, restart from P5-T1. Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/qa-gates/msbuild-analyzer-final.2026-09-12T16-09.md`. +- [x] [P5-T2] Run `pwsh -NoProfile -Command '$out = msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true 2>&1; $exit = $LASTEXITCODE; $out | Select-String -Pattern "^\s*\d+ Error\(s\)\s*$" | ForEach-Object { $_.Line.Trim() }; Write-Output "EXITCODE=$exit"'`. Acceptance: the transcribed summary line, once trimmed, equals exactly the literal `0 Error(s)` (anchored to the whole trimmed line, so a `10 Error(s)` line cannot satisfy this) and `EXITCODE` is 0. If the run fails or csharpier rewrote any file since P5-T1, restart from P5-T1. Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/qa-gates/msbuild-analyzer-final.2026-09-12T16-09.md`. -- [ ] [P5-T3] Run `pwsh -NoProfile -Command '$out = msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true 2>&1; $exit = $LASTEXITCODE; $out | Select-String -Pattern "^\s*\d+ Error\(s\)\s*$" | ForEach-Object { $_.Line.Trim() }; Write-Output "EXITCODE=$exit"'`. Do not add `/p:Nullable=enable`. Acceptance: the transcribed summary line, once trimmed, equals exactly the literal `0 Error(s)` and `EXITCODE` is 0. If this fails, restart from P5-T1. Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/qa-gates/msbuild-nullable-final.2026-09-12T16-09.md`. +- [x] [P5-T3] Run `pwsh -NoProfile -Command '$out = msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true 2>&1; $exit = $LASTEXITCODE; $out | Select-String -Pattern "^\s*\d+ Error\(s\)\s*$" | ForEach-Object { $_.Line.Trim() }; Write-Output "EXITCODE=$exit"'`. Do not add `/p:Nullable=enable`. Acceptance: the transcribed summary line, once trimmed, equals exactly the literal `0 Error(s)` and `EXITCODE` is 0. If this fails, restart from P5-T1. Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/qa-gates/msbuild-nullable-final.2026-09-12T16-09.md`. - [ ] [P5-T4] Run the `QuickFiler.Test` suite with coverage enabled via the repository's coverage runner, scoped to the `QuickFiler.Test` assembly only, again writing the raw Cobertura output under the repository's gitignored `coverage\` directory and deleting it before completion (same `Join-Path`/rooted-path reasoning as P0-T8). Run `pwsh -NoProfile -Command './scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot QuickFiler.Test -Configuration Debug -CoverageOutput "coverage\coverage.cobertura.xml"; Write-Output "EXITCODE=$LASTEXITCODE"'`. Acceptance: `EXITCODE` is 0 (zero failures for `QuickFiler.Test`, including the two rewritten oracle tests and the five new tests; this inner invocation already carries `/InIsolation` and `/TestCaseFilter:TestCategory!=LiveOutlook`). If this fails or a prior step in this phase rewrote a tracked file since P5-T1, restart from P5-T1. Open the produced Cobertura XML at `coverage\coverage.cobertura.xml`, transcribe the post-change `line-rate` figures for the same three non-excluded classes captured in P0-T8, state the baseline-vs-final comparison for each, and confirm no regression on the lines this change touched; also transcribe the overall pass/fail/skip counts. Delete `coverage\coverage.cobertura.xml` after transcribing. Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/qa-gates/vstest-coverage-final.2026-09-12T16-09.md`. -- [ ] [P5-T5] Confirm the `[TestMethod]` count in the new test file remains correct after formatting. Run `git grep --untracked -c -F '[TestMethod]' -- QuickFiler.Test/Controllers/QuickFilerInvariantCultureIssue742Tests.cs`. Acceptance: the command prints the count `5` (unchanged from P1-T1; CSharpier does not add or remove attributes) and `EXIT_CODE` is 0. Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/qa-gates/test-method-count-final.2026-09-12T16-09.md`. +- [x] [P5-T5] Confirm the `[TestMethod]` count in the new test file remains correct after formatting. Run `git grep --untracked -c -F '[TestMethod]' -- QuickFiler.Test/Controllers/QuickFilerInvariantCultureIssue742Tests.cs`. Acceptance: the command prints the count `5` (unchanged from P1-T1; CSharpier does not add or remove attributes) and `EXIT_CODE` is 0. Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/qa-gates/test-method-count-final.2026-09-12T16-09.md`. -- [ ] [P5-T6] Confirm no `.trx` or Cobertura coverage XML file has been introduced as a tracked evidence artifact. Run `pwsh -NoProfile -Command '$m = git status --porcelain --untracked-files=all -- docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence | Select-String -Pattern "\.trx$|\.cobertura\.xml$"; $m; if ($m) { exit 0 } else { exit 1 }'`. Acceptance: the command prints no line and exits 1, matching this plan's zero-match reading convention. Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/qa-gates/no-trx-cobertura-evidence.2026-09-12T16-09.md`. +- [x] [P5-T6] Confirm no `.trx` or Cobertura coverage XML file has been introduced as a tracked evidence artifact. Run `pwsh -NoProfile -Command '$m = git status --porcelain --untracked-files=all -- docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence | Select-String -Pattern "\.trx$|\.cobertura\.xml$"; $m; if ($m) { exit 0 } else { exit 1 }'`. Acceptance: the command prints no line and exits 1, matching this plan's zero-match reading convention. Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/qa-gates/no-trx-cobertura-evidence.2026-09-12T16-09.md`. -- [ ] [P5-T7] Verify the diff scope, scoped to the QuickFiler and QuickFiler.Test source trees only (the same change also adds Markdown evidence artifacts and updates planning documents inside this feature folder, which is expected rather than a scope violation, so a repository-wide scope would be unsatisfiable). Run `git status --porcelain --untracked-files=all -- QuickFiler QuickFiler.Test`. Acceptance: the output lists exactly these eight paths and no others: `QuickFiler/Controllers/QfcHomeController.Metrics.cs`, `QuickFiler/Controllers/EfcHomeController.Metrics.cs`, `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs`, `QuickFiler/Controllers/QfcCollectionController.cs`, `QuickFiler/Controllers/EfcItemController.cs`, `QuickFiler.Test/Controllers/QfcHomeControllerMetricsTests.cs`, `QuickFiler.Test/Controllers/QuickFilerInvariantCultureIssue742Tests.cs` (as an untracked `??` entry until P5-T8 stages it), and `QuickFiler.Test/QuickFiler.Test.csproj`. Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` (the full porcelain listing) in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/qa-gates/diff-scope-final.2026-09-12T16-09.md`. +- [x] [P5-T7] Verify the diff scope, scoped to the QuickFiler and QuickFiler.Test source trees only (the same change also adds Markdown evidence artifacts and updates planning documents inside this feature folder, which is expected rather than a scope violation, so a repository-wide scope would be unsatisfiable). Run `git status --porcelain --untracked-files=all -- QuickFiler QuickFiler.Test`. Acceptance: the output lists exactly these eight paths and no others: `QuickFiler/Controllers/QfcHomeController.Metrics.cs`, `QuickFiler/Controllers/EfcHomeController.Metrics.cs`, `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs`, `QuickFiler/Controllers/QfcCollectionController.cs`, `QuickFiler/Controllers/EfcItemController.cs`, `QuickFiler.Test/Controllers/QfcHomeControllerMetricsTests.cs`, `QuickFiler.Test/Controllers/QuickFilerInvariantCultureIssue742Tests.cs` (as an untracked `??` entry until P5-T8 stages it), and `QuickFiler.Test/QuickFiler.Test.csproj`. Record `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:` (the full porcelain listing) in `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/qa-gates/diff-scope-final.2026-09-12T16-09.md`. -- [ ] [P5-T8] Stage exactly the eight Write Set paths plus this feature's own evidence folder, each named as an explicit path argument (never a bare directory-root pathspec such as `git add .` or `git add QuickFiler`, because a sibling item's queued promotion file sits under a different, unrelated tree and a broad staging span could sweep it onto this branch): `git add QuickFiler/Controllers/QfcHomeController.Metrics.cs QuickFiler/Controllers/EfcHomeController.Metrics.cs QuickFiler/Controllers/QfcItemController.ViewerSetup.cs QuickFiler/Controllers/QfcCollectionController.cs QuickFiler/Controllers/EfcItemController.cs QuickFiler.Test/Controllers/QfcHomeControllerMetricsTests.cs QuickFiler.Test/Controllers/QuickFilerInvariantCultureIssue742Tests.cs QuickFiler.Test/QuickFiler.Test.csproj docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/plan.2026-09-12T16-09.md`. Acceptance: `git status --porcelain --untracked-files=all -- QuickFiler QuickFiler.Test` now shows every one of the eight Write Set paths with a staged status character and no `??` entries remain among them. +- [x] [P5-T8] Stage exactly the eight Write Set paths plus this feature's own evidence folder, each named as an explicit path argument (never a bare directory-root pathspec such as `git add .` or `git add QuickFiler`, because a sibling item's queued promotion file sits under a different, unrelated tree and a broad staging span could sweep it onto this branch): `git add QuickFiler/Controllers/QfcHomeController.Metrics.cs QuickFiler/Controllers/EfcHomeController.Metrics.cs QuickFiler/Controllers/QfcItemController.ViewerSetup.cs QuickFiler/Controllers/QfcCollectionController.cs QuickFiler/Controllers/EfcItemController.cs QuickFiler.Test/Controllers/QfcHomeControllerMetricsTests.cs QuickFiler.Test/Controllers/QuickFilerInvariantCultureIssue742Tests.cs QuickFiler.Test/QuickFiler.Test.csproj docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/plan.2026-09-12T16-09.md`. Acceptance: `git status --porcelain --untracked-files=all -- QuickFiler QuickFiler.Test` now shows every one of the eight Write Set paths with a staged status character and no `??` entries remain among them. -- [ ] [P5-T9] Commit the staged change. Run `git commit -m "Fix issue 742: append CultureInfo.InvariantCulture to QuickFiler date/time rendering"`. Acceptance: `EXIT_CODE` is 0 and `git status --porcelain --untracked-files=all -- QuickFiler QuickFiler.Test` prints no line and exits 1. +- [x] [P5-T9] Commit the staged change. Run `git commit -m "Fix issue 742: append CultureInfo.InvariantCulture to QuickFiler date/time rendering"`. Acceptance: `EXIT_CODE` is 0 and `git status --porcelain --untracked-files=all -- QuickFiler QuickFiler.Test` prints no line and exits 1. --- diff --git a/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/spec.md b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/spec.md index e2ef44019..a0b855b59 100644 --- a/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/spec.md +++ b/docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/spec.md @@ -350,7 +350,7 @@ performance impact. > "the command returns 0," because `git grep -c` prints no line at all, and exits 1, for a path with zero > matches. -- [ ] The interpolated site formerly at line 48 of `QuickFiler/Controllers/QfcHomeController.Metrics.cs` +- [x] The interpolated site formerly at line 48 of `QuickFiler/Controllers/QfcHomeController.Metrics.cs` is rewritten as two explicit two-argument `ToString` calls, each passing `CultureInfo.InvariantCulture`. Checked two ways, because an absence claim alone is not a sufficient gate. First: a search of that file for the interpolated-specifier token, written with a bracketed character class as `[{]now:` so the @@ -364,23 +364,23 @@ performance impact. here, because CSharpier decides whether the two replacement calls land on one line or two apiece, and a predicted figure would be a formatter artifact rather than a property of the change. Both pre-fix figures were measured on 2026-09-12 against the unfixed tree. -- [ ] Lines 125 and 127 of `QuickFiler/Controllers/QfcHomeController.Metrics.cs` each pass +- [x] Lines 125 and 127 of `QuickFiler/Controllers/QfcHomeController.Metrics.cs` each pass `CultureInfo.InvariantCulture` as the second argument to their `ToString` call. -- [ ] Lines 95, 96, 118, and 119 of `QuickFiler/Controllers/EfcHomeController.Metrics.cs` each pass +- [x] Lines 95, 96, 118, and 119 of `QuickFiler/Controllers/EfcHomeController.Metrics.cs` each pass `CultureInfo.InvariantCulture` as the second argument to their `ToString` call. As a discovery-count control, the count of lines in that file containing `CultureInfo.InvariantCulture` rises from exactly 2 on the unfixed tree (the pre-existing numeric-field sites) to a strictly greater number after the fix; this pre-fix figure of 2 was measured on 2026-09-12 against the unfixed tree. -- [ ] Line 465 of `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` has both of its `ToString` +- [x] Line 465 of `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` has both of its `ToString` calls passing `CultureInfo.InvariantCulture` as the second argument, and the file gains a `using System.Globalization;` directive (exactly one occurrence). -- [ ] Lines 235, 1296, and 2302 of `QuickFiler/Controllers/QfcCollectionController.cs` have all five of +- [x] Lines 235, 1296, and 2302 of `QuickFiler/Controllers/QfcCollectionController.cs` have all five of their `ToString` calls passing `CultureInfo.InvariantCulture` as the second argument, and the file gains a `using System.Globalization;` directive (exactly one occurrence). -- [ ] Lines 607 and 612 of `QuickFiler/Controllers/EfcItemController.cs` each pass +- [x] Lines 607 and 612 of `QuickFiler/Controllers/EfcItemController.cs` each pass `CultureInfo.InvariantCulture` as the second argument to their `ToString` call, and the file gains a `using System.Globalization;` directive (exactly one occurrence). -- [ ] A line-count search using the regular expression written with bracketed character classes as +- [x] A line-count search using the regular expression written with bracketed character classes as `[.]ToString[(]@\?"[^"]*[:/.\-][^"]*"[)]`, restricted to the five production files listed above, returns exactly 2 matching lines after the fix, both of them in `QuickFiler/Controllers/QfcHomeController.Metrics.cs`. The residual 2 are the commented-out predecessor @@ -398,44 +398,44 @@ performance impact. this repository whatever the executor does, which would make this criterion unfalsifiable. Both spellings were run against the unfixed tree on 2026-09-12: the escaped form printed the 4/4/1/3/2 per-file distribution and exited 0, and the unescaped form printed no line and exited 1. -- [ ] In `QuickFiler.Test/Controllers/QfcHomeControllerMetricsTests.cs`, the test method +- [x] In `QuickFiler.Test/Controllers/QfcHomeControllerMetricsTests.cs`, the test method `WriteMetricsAsync_UsesInjectedClock_ForDateAndTimeStamps` and the test method `QuickFileMetrics_WRITE_UsesInjectedClock_ForDataLine` both build their expected date/time substring using `CultureInfo.InvariantCulture` rather than an uncultured `.ToString(format)` call, and both tests pass. -- [ ] The new file `QuickFiler.Test/Controllers/QuickFilerInvariantCultureIssue742Tests.cs` exists and +- [x] The new file `QuickFiler.Test/Controllers/QuickFilerInvariantCultureIssue742Tests.cs` exists and contains exactly five test methods, one exercising each of the five production files named in this issue, each following the try/finally culture-swap pattern with an explicit sentinel `DateSeparator` and `TimeSeparator` (not a named culture's separators) and asserting that production output contains `/` and `:`; all five tests pass. -- [ ] `QuickFiler.Test/QuickFiler.Test.csproj` gains exactly one new `Compile Include` entry naming the +- [x] `QuickFiler.Test/QuickFiler.Test.csproj` gains exactly one new `Compile Include` entry naming the new test file. Checked as a pair of counts rather than by eye: a line-count search of that file for `QuickFilerInvariantCultureIssue742Tests` returns exactly 1 after the fix and prints no line, exiting 1, before the fix, and a line-count search for `Compile Include` in that file returns exactly one more after the fix than it did before the fix. The new entry sits inside the single existing `ItemGroup` element that opens at line 57 of that file, which is the element holding every existing `Compile` entry for the Controllers folder. -- [ ] No new test-results file carrying the trx extension, and no new Cobertura coverage XML file, +- [x] No new test-results file carrying the trx extension, and no new Cobertura coverage XML file, appears as a tracked evidence artifact in the repository as a result of this change. Per the maintainer decision on issue #671, effective 2026-09-11, only Markdown evidence summaries are committed; the numeric pass/fail counts and coverage figures produced by the toolchain run are transcribed into Markdown artifacts under the feature folder's evidence directory, and the raw tool output is discarded rather than committed. -- [ ] A Markdown baseline evidence artifact exists under the feature folder's evidence directory, kind +- [x] A Markdown baseline evidence artifact exists under the feature folder's evidence directory, kind baseline, recording the pre-fix discovery-count figures this document's acceptance criteria depend on (the 14/4/1/3/2 per-file counts for the `[.]ToString[(]` sweep, the single `[{]now:` count, and the 4/2 `CultureInfo.InvariantCulture` counts), and that artifact was written before any Write Set source file was modified. -- [ ] `dotnet tool run csharpier format .` followed by `dotnet tool run csharpier check .` both complete +- [x] `dotnet tool run csharpier format .` followed by `dotnet tool run csharpier check .` both complete with no remaining diff. -- [ ] `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true` +- [x] `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true` completes with zero analyzer errors. -- [ ] `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true` +- [x] `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true` completes with zero errors. - [ ] `vstest.console.exe QuickFiler.Test.dll /EnableCodeCoverage /InIsolation`, run against the built QuickFiler.Test test assembly, reports zero failures for QuickFiler.Test, including the two rewritten tests and the five new tests. Outlook was closed, not killed, before the preceding rebuild steps. -- [ ] Within the QuickFiler and QuickFiler.Test source trees, the change touches exactly the eight paths +- [x] Within the QuickFiler and QuickFiler.Test source trees, the change touches exactly the eight paths listed in the Write Set section below and no other file. This is scoped to those two trees on purpose: the same change also adds Markdown evidence artifacts and updates planning documents inside this feature folder, and those additions are expected rather than a scope violation, so a repository-wide From e3be9df481d30588699f1b727a3ac7616ebf4bfb Mon Sep 17 00:00:00 2001 From: Dan Moisan Date: Mon, 14 Sep 2026 02:34:02 -0400 Subject: [PATCH 4/4] docs: promote coverage-runner -SearchRoot threshold false-fail to issue 891 --- ...-runner-searchroot-threshold-false-fail.md | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 docs/features/potential/promoted/2026-09-14-coverage-runner-searchroot-threshold-false-fail.md diff --git a/docs/features/potential/promoted/2026-09-14-coverage-runner-searchroot-threshold-false-fail.md b/docs/features/potential/promoted/2026-09-14-coverage-runner-searchroot-threshold-false-fail.md new file mode 100644 index 000000000..32428a884 --- /dev/null +++ b/docs/features/potential/promoted/2026-09-14-coverage-runner-searchroot-threshold-false-fail.md @@ -0,0 +1,74 @@ +# coverage-runner-searchroot-threshold-false-fail (Issue #891) + +- Date captured: 2026-09-14 +- Author: Dan Moisan +- Status: Promoted -> docs/features/active/coverage-runner-searchroot-threshold-false-fail/ (Issue #891) + +> Automation note: Keep the section headings below unchanged; the promotion tooling maps each of them into the GitHub bug issue template. + +- Issue: #891 +- Issue URL: https://github.com/drmoisan/TaskMaster/issues/891 +- Last Updated: 2026-09-14 +## Summary + +`scripts/vscode/Invoke-MSTestWithCoverage.ps1` exits non-zero for any `-SearchRoot`-scoped +invocation, even when every test in the scoped assembly passes, because its post-run gate +compares a repository-wide document-level Cobertura `line-rate` against a hard-coded 80% +threshold that a single-assembly run can never reach. + +## Environment + +- OS/version: Windows 11 Pro 10.0.26200 +- Command/flags used: `pwsh -NoProfile -Command './scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot QuickFiler.Test -Configuration Debug -CoverageOutput "coverage\coverage.cobertura.xml"; Write-Output "EXITCODE=$LASTEXITCODE"'` +- Data source or fixture: repository `TaskMaster`, branch `bug/quickfiler-date-time-format-missing-invariant-culture-742` (also reproduces on the unfixed base tree, i.e. it is not caused by any in-flight change) + +## Steps to Reproduce + +1. From the repo root, run `./scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot QuickFiler.Test -Configuration Debug -CoverageOutput "coverage\coverage.cobertura.xml"`. +2. Observe the vstest run itself completes with all `QuickFiler.Test` tests passing (confirmed via the runner's own trx: 1429/1429 passed on the unfixed tree, 1434/1434 passed after an unrelated fix landed). +3. Observe `$LASTEXITCODE` is `1` regardless, and the console shows an exception thrown from `Assert-CoberturaLineCoverageThreshold` in `scripts/vscode/Invoke-MSTestWithCoverage.Threshold.ps1` line 54: `Cobertura line coverage 24.3028% is below the required 80% threshold.` + +## Expected Behavior + +A `-SearchRoot`-scoped coverage run's exit code should reflect the pass/fail state of the tests it actually executed (or the runner should measure coverage over the scoped assembly's own instrumented lines, or expose a way to skip/adjust the threshold for a deliberately narrow run). Callers that need "did my scoped test run pass" cannot get that signal from the exit code today. + +## Actual Behavior + +`Assert-CoberturaLineCoverageThreshold` reads the `line-rate` attribute of the Cobertura document's root `/coverage` element, which spans every instrumented assembly in the solution regardless of `-SearchRoot` (61,894–61,898 valid lines observed), not just the assembly the scoped run executed. A `-SearchRoot`-scoped run only exercises and covers a fraction of that repository-wide denominator (15,040–15,047 lines observed, ~24.3%), so the hard-coded `if ($percentage -lt 80)` check at line 52 of `Invoke-MSTestWithCoverage.Threshold.ps1` always throws for a scoped run, independent of test outcome. `Invoke-MSTestWithCoverage.ps1` exposes only `-SearchRoot`, `-Configuration`, `-CoverageOutput`, and `-NoExecute` — no parameter lowers, disables, or rescales the threshold for scoped invocations. + +Discovered while executing the approved atomic plan for issue #742: the plan's task `[P5-T4]` (and, before it, baseline task `[P0-T8]`) both intentionally scope `-SearchRoot QuickFiler.Test` to avoid a known unrelated hang in `UtilitiesCS.Test`'s shell-icon test classes on this machine, and both runs hit this false-fail exit code even though the scoped test suite was 100% green in both cases. The task's literal acceptance criterion ("EXITCODE is 0") could not be satisfied, and the corresponding item in `spec.md`'s Acceptance Criteria list (the criterion mapped to `vstest.console.exe ... reports zero failures`) was left unchecked as a result, even though that criterion is substantively true (1434/1434 passed). + +## Logs / Screenshots + +- [x] Attached minimal logs or screenshot +- Snippet: + ``` + Exception: ...\scripts\vscode\Invoke-MSTestWithCoverage.Threshold.ps1:54 + Cobertura line coverage 24.3093% is below the required 80% threshold. + ``` + Full evidence transcripts: `docs/features/active/2026-09-02-quickfiler-date-time-format-missing-invariant-culture-742/evidence/baseline/vstest-coverage-baseline.2026-09-12T16-09.md` and `.../evidence/qa-gates/vstest-coverage-final.2026-09-12T16-09.md`. + +## Impact / Severity + +- [ ] Blocker +- [x] Medium +- [ ] Low + +Not a blocker because the unscoped, whole-repository invocation (without `-SearchRoot`) is unaffected — the document-level denominator then matches what is actually executed. It becomes a real obstacle any time a plan or workflow deliberately scopes a run (as this issue's plan did, to route around the separate `UtilitiesCS.Test` shell-icon hang), because the scoped run's exit code is then unusable as a pass/fail signal and every such plan must fall back to reading the trx directly. + +## Suspected Cause / Notes + +- `scripts/vscode/Invoke-MSTestWithCoverage.Threshold.ps1`, function `Assert-CoberturaLineCoverageThreshold` (line 3; threshold check at line 52; hard-coded literal `80`). +- `scripts/vscode/Invoke-MSTestWithCoverage.ps1` — the `-SearchRoot` parameter scopes test *execution* (via `Get-ChildItem -Recurse -Filter '*.Test.dll'` under the resolved search root) but does not scope, or inform, the coverage *threshold* comparison, which is computed from the full Cobertura document produced by the coverage collector. +- Reproduces identically on the unfixed base tree (pre-existing, not introduced by issue #742's change). + +## Proposed Fix / Validation Ideas + +- [x] Unit coverage areas: add a Pester/unit test for `Assert-CoberturaLineCoverageThreshold` (or its caller) covering a `-SearchRoot`-scoped invocation against a synthetic multi-assembly Cobertura document, asserting the threshold comparison uses a denominator scoped to the executed assembly rather than the whole document. +- [x] Integration scenario to retest: re-run `Invoke-MSTestWithCoverage.ps1 -SearchRoot QuickFiler.Test` (or any other single-project scope) after the fix and confirm exit code 0 when all scoped tests pass, and a genuine scoped-coverage-regression still throws. +- Candidate approaches: (a) filter the Cobertura document to only the packages/classes under the scoped search root before evaluating the threshold; (b) skip the threshold check entirely when `-SearchRoot` narrows execution below the full test suite and rely on the trx pass/fail signal instead; (c) add an explicit opt-out parameter (e.g. `-SkipCoverageThreshold`) for scoped diagnostic runs, documented as such. + +## Next Step + +- [x] Promote to GitHub issue (bug-report template) +- [ ] Move to active fix folder / branch