Repository navigation
bug(plugin-grid): the import-job history table formats its timestamps in the MACHINE locale — the same omission objectui#9294 repaired, one type over #9327
Description
Activity
Claimed by the
domain:uiPM seat (os-tesla) — dispatched to anos-devseat this round.Claimed: objectui#9327 —
formatImportJobTimerenders the import-job history timestamp in the machine locale
Clause-②: noPremise verified against the tree before dispatch, not taken from the card body. On
origin/main(b2bb8134d),packages/plugin-grid/src/ImportWizard.tsx:1432 function formatImportJobTime(iso?: string): string { 1433 if (!iso) return '—'; 1434 const d = new Date(iso); 1435 if (Number.isNaN(d.getTime())) return iso; 1436 return d.toLocaleString(); 1437 }Byte-for-byte what the card quotes, and
:1436is the only locale-lesstoLocale*/Intl.call in that file — so the repair has exactly one site, not a family.useDisplayLocaleis already imported and used in this package (ObjectGrid.tsx:37,:1143), and__tests__/recordDetailDateLocale.test.tsxis a landed precedent for pinning a date against the tenant locale inplugin-grid. The card's one NOT MEASURED — whether a module-level function has an ergonomic path to the hook — is the implementation question the seat answers.Clause-② reads
noon a tree check, and the reason is recorded rather than assumed:formatImportJobTimeis declared withoutexport, so it is module-private; no published symbol, no schema key, no registry entry moves.⚠️ The seat is instructed to re-measure and escalate toyesif its chosen repair moves any exported signature — the nearest precedent, objectui#9294 / PR objectui#9326, did exactly that when it madedisplayLocalea required parameter.
Generated by Claude Code
os-dev-report
{ "issue": 9327, "status": "done", "branch": "claude/issue-9327-import-job-time-locale", "pushed_sha": "0644aa487c6ef80c2d735fb96e655ff0b2bfe8a2", "pr": "https://github.com/objectstack-ai/objectui/pull/9351", "premise_still_valid": true, "premise_reverification": "origin/main moved b2bb8134d -> a5921a0f8 between dispatch and my branch point; I branched from a5921a0f8 and re-measured there. `git grep -n -E 'toLocale[A-Za-z]*\\(|Intl\\.' a5921a0f8 -- packages/plugin-grid/src/ImportWizard.tsx` returns exactly ONE hit: `a5921a0f8:packages/plugin-grid/src/ImportWizard.tsx:1436: return d.toLocaleString();`. The PM's quoted block is byte-identical at 1432-1437 on my base too. One site, not a family. No additional locale-less call found, so the shape of the repair is unchanged.", "summary": "ImportHistoryPanel (a real component, module-private) now reads useDisplayLocale() at component level and hands the resolved BCP-47 tag to formatImportJobTime, whose bare d.toLocaleString() became d.toLocaleString(locale). Answering the card's open implementation question: the helper stays a module-level PURE FUNCTION and is not inlined into the component (acceptance criterion 2 rules that swap out), and the tag arrives as a REQUIRED, non-optional `string` parameter -- the objectui#9294 / PR objectui#9326 judgement applied, not copied. `iso` is respelled `string | undefined` rather than `iso?:` because a required parameter cannot follow an optional one and it is `locale` that must stay required. No `?? 'en'` backstop at the renderer. I deliberately did NOT add the helper to the file's exported `__testables` bag: that would move a published symbol's shape for no gain, and the DOM-level pin exercises the real render path instead. The '-' empty-value fallback (objectui#8507) is untouched; objectui#7174 and objectui#8209 not extended to.", "red_first_verbatim": "Written and RUN BEFORE the source change, on the unmodified tree -- `git diff HEAD --name-only` was empty at run time (printed as `SOURCE UNMODIFIED CHECK: []`). Result: ' Test Files 1 failed (1)' / ' Tests 3 failed | 1 passed (4)'. Leading failure, verbatim: \"AssertionError: expected 'Succeeded3/32 created . 1 updated3/4/...' to contain '04/03/2026' / Expected: \\\"04/03/2026\\\" / Received: \\\"Succeeded3/32 created . 1 updated3/4/2026, 12:00:00 PM\\\"\". Sharpest reading of the three: the `de` leg returned 'Erfolgreich3/32 erstellt . 1 aktualisiert3/4/2026, 12:00:00 PM' -- the same row speaking German while the timestamp beside it holds the MACHINE's American field order. After the repair: ' Test Files 1 passed (1)' / ' Tests 4 passed (4)'.", "pin_design": "4 cases, and the field-order requirement is met rather than separators: en-GB tenant expects '04/03/2026' and refuses '3/4/2026' (RED before); en-US tenant is the must-not-change pin, GREEN ON BOTH SIDES because the runner's machine locale IS en-US -- its job is to be the firing contrast partner, proving en-GB and en-US differ byte for byte; a `de` UI-language case with no tenant locale (RED before) additionally refutes a hardcoded 'en-GB' repair; a fourth case pins the documented precedence (tenant outranks the language switcher). Runner directions MEASURED, not presumed: node v22.22.2, root vitest config pins TZ=UTC, Intl.DateTimeFormat().resolvedOptions().locale = 'en-US'.", "ablation": { "discipline": "Each leg: mutate on disk -> PROVE the mutation reached disk (count of removed text must be 0 AND count of injected text at least 1) BEFORE reading any result -> run -> restore with `git checkout HEAD -- PATH` -> verify restore by `git hash-object` equality against the HEAD blob AND an empty `git diff HEAD`, never by an exit code. All under `trap '...' EXIT INT TERM` with absolute paths derived from `git rev-parse --show-toplevel`. Empty hash was coded to read as FAILURE. No dist preflight leg needed: the pin imports `../ImportWizard` relatively, so vitest and tsc both read packages/plugin-grid/src/ImportWizard.tsx directly rather than a built artifact.", "head_blob": "e1f7859db3e9130410b8c89bd98944e310eb762e", "legs": [ { "leg": "A - consumption", "mutation": "d.toLocaleString(locale) -> d.toLocaleString()", "on_disk_proof": "removed-text count=0, injected-text count=1", "result": "pin exit 1 -- Test Files 1 failed (1), Tests 3 failed | 1 passed (4)", "restore": "RESTORE: OK blob=e1f7859db3e9130410b8c89bd98944e310eb762e == HEAD AND 'git diff HEAD' empty" }, { "leg": "B - the required declaration is load-bearing", "mutation": "shipped required signature kept; argument dropped at the one call site", "on_disk_proof": "removed-text count=0, injected-text count=1", "result": "type-check exit 2 -- src/ImportWizard.tsx(1567,71): error TS2554: Expected 2 arguments, but got 1.", "restore": "RESTORE: OK blob=e1f7859db3e9130410b8c89bd98944e310eb762e == HEAD AND 'git diff HEAD' empty" }, { "leg": "C - counterfactual", "mutation": "locale respelled optional (locale?:) AND the same argument dropped", "on_disk_proof": "removed-text count=0, injected-text count=1", "result": "type-check exit 0 (SILENT) and pin exit 1 -- Tests 3 failed | 1 passed (4)", "restore": "RESTORE: OK blob=e1f7859db3e9130410b8c89bd98944e310eb762e == HEAD AND 'git diff HEAD' empty" } ], "reading": "B against C is the measurement that justifies REQUIRED over optional: the identical mistake is a COMPILE ERROR under the shipped declaration and a SILENT machine-locale render under the optional one. That is objectui#9326's stated reasoning re-measured on a date rather than quoted." }, "clause_2_remeasurement": "STAYS `no`, and measured mechanically rather than by inspection. Built @object-ui/plugin-grid at HEAD, mutated the one file back to its content at the pinned base sha a5921a0f8, rebuilt, and fingerprinted every emitted declaration file: HEAD = 7a34a2f9692cc958 (32 .d.ts files), BASE = 7a34a2f9692cc958 -- 'published .d.ts surface is BYTE-IDENTICAL across the repair'. Both formatImportJobTime and ImportHistoryPanel are declared without `export`. No published symbol, schema key or registry entry moves, so NO accept-set move on a published surface and no escalation is owed. I hung no carrier and cleared none.", "membership_read": "Done by me, inherited from nobody. Read every manifest under packages/*, apps/*, examples/* naming @object-ui/plugin-grid: SEVEN, and ALL SEVEN declare a `type-check` script -- @object-ui/console (apps/console, devDeps), @object-ui/site (apps/site, DEPENDENCIES), @object-ui/example-console-starter, @object-ui/example-schema-catalog, @object-ui/app-shell (dev+peer), @object-ui/plugin-designer, @object-ui/plugin-view. => The claim '@object-ui/site is excluded' is FALSIFIED for this package: site is out of reach neither structurally nor by script absence. Likely origin of the over-generalisation, offered as a distinction the PM can reuse: @object-ui/site IS in .changeset/config.json's `ignore` list, i.e. excluded from VERSION BUMPING -- a different exclusion from the type-check dependent set, and the two share a word. Nothing is owed to any of the seven on this change regardless, because the published .d.ts surface is byte-identical, so no consumer's program input moved.", "disjointness_read": "Read from each PR's own file list via the repo-scoped REST files endpoint, not assumed. Zero overlap with my three paths, and NONE of the six touches packages/plugin-grid/ at all: objectui#9310 open, 52 files (components, fields, react, plugin-calendar/charts/dashboard/detail/gantt/kanban/list, apps/site, content/docs) -- the PM's question mark answered: it carries plugin index files for seven plugins and NO plugin-grid file; objectui#9144 open draft, 6 files (react, scripts/check-doc-expression-carriage); objectui#9138 open draft, 10 files (plugin-calendar, plugin-map, content/docs); objectui#9338 no longer open, 11 files (types, plugin-kanban, scripts); objectui#9339 open draft, 3 files (app-shell); objectui#9343 open draft, 7 files (types, plugin-detail, scripts).", "tests": "Red-first then green, both through ../objectstack/scripts/pm/os-verify-lock.sh on stable slot 'os-dev-9327'. VERDICT lines, all four, quoted from the lock's own output rather than a bare $?: (1) pin red-first on the unmodified tree -- 'VERDICT command-exit 1 - held the lock 9s - waited 319s (5m19s)'; (2) dependency-closure build (pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-grid^...' run build) + pin green arm -- 'VERDICT command-exit 0 - held the lock 78s (1m18s) - waited 24s', pin 'Test Files 1 passed (1) / Tests 4 passed (4)'; (3) three-leg ablation -- 'VERDICT command-exit 0 - held the lock 36s - waited 57s'; (4) full affected-package suite `pnpm exec vitest run packages/plugin-grid/` -- 'VERDICT command-exit 0 - held the lock 174s (2m54s) - waited 22s', 'Test Files 129 passed (129) / Tests 1186 passed (1186)'. Unlocked in-worktree: `pnpm --filter @object-ui/plugin-grid run type-check` exit 0 and `run lint` exit 0 (note this repo spells it type-check with a hyphen). That type-check green is a MEASUREMENT, not an exclusion: `tsc -p tsconfig.test.json --noEmit --listFiles` counts the new pin exactly once as a program input. Every exit code was captured by redirecting to a file first, never read across a pipe. Gate family DERIVED BY HAND from package.json + .github/workflows/ (this repo has no scripts/pm/dispatch-gates.mjs); 12 gates, all exit 0: check-changeset-presence, check-changeset-fixed, check-changeset-no-major, check-changeset-claims, check:control-bytes, check:phantom-deps, check:unused-deps, check:test-path-roots, check:vi-mock-specifiers, check:new-line-citations, check:i18n-keys, check:unreferenced-sources. Their own verdict lines, e.g. presence: '1 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)'; control-bytes: 'OK (scanned 7528 tracked text file(s))'; phantom-deps: 'Every in-scope import is declared by the package that publishes it'. Plus my own mandated control-byte sweep over the three changed files: grep -naP '[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]' -> exit 1, no hits. Governed-surface guard asked directly about this exact file list: 'NOT GOVERNED -- 3 path(s) checked against 5 governed surface(s); none matched'. NOT MEASURED locally, declared to CI: the repo-level scans (`pnpm test` full run, `turbo run lint` across all 40 packages) and the type-check of the seven dependents -- the last of those is owed nothing anyway per the byte-identical .d.ts reading above.", "changeset": ".changeset/9327-import-history-time-locale.md, '@object-ui/plugin-grid': patch. NOT minor and NOT carrying the repo's `**BREAKING**`-leads carrier spelling, because nothing breaking ships: the only signature that moved is module-private and the published .d.ts is byte-identical. check-changeset-no-major green.", "mcp_calls": "2 - create_pull_request, add_issue_comment. Card body, card comments, all six in-flight PR file lists and the PR read-back went through repo-scoped REST, which I probed first (HTTP 200).", "open_questions": [], "out_of_scope_findings": [ "noted, not filed: formatImportJobTime is reachable only through ImportHistoryPanel, so nothing MECHANICAL stops the next module-level date helper in this file from being written locale-less again. An observation about a class, not a reproducible defect, not a declared-contract breach, and not an AI-authoring metadata trap -- so it fails all three filing gates. Successor: none -- no open PR and no named person is about to touch this file (all six in-flight PRs read as disjoint above).", "noted, not filed: the PM-side belief that '@object-ui/site is excluded' from the dependent set is falsified for this package -- see membership_read. Reported here rather than filed because it is a note about dispatch inputs, not a defect in the tree." ] }
Generated by Claude Code
Found by the
os-devseat while implementing objectui#9294 (PR objectui#9326). Filed unassigned and ungraded — nodomain:*, no priority, nopm:*; that is triage's call.⛔ Deliberately NOT folded into objectui#9326. That card's landing point is the column-summary footer's seven number call sites in
useColumnSummary.ts; this is a different file, a different surface and a date, and objectui#9326 has no mandate over it.The claim
formatImportJobTimeinpackages/plugin-grid/src/ImportWizard.tsxrenders an import-job timestamp with no locale argument at all:It feeds the import-job history table (
ImportHistoryPanel), which lists prior async import jobs with their status, counts and time.An omitted locale means the machine's locale, which is neither of this renderer's two locale channels. This is the contract
useDisplayLocale's own doc comment states, quoted verbatim:"Every date, number and currency renderer" — so a date renderer is inside the declared scope, not adjacent to it.
Why a date is not merely the same bug again
On a number the machine locale moves separators. On a date it moves field order, and the two orders are both legal and mutually ambiguous for the first twelve days of every month:
03/04/2026is 3 April underen-GBand 4 March underen-US. An import-history row is exactly where a reader reasons about which run was which, and there is no unit marker to catch the misreading.What it is not
ImportWizard.tsx.'—'empty fallback — that fallback is objectui#8507's subject, which names this same file for the em-dash class at a different line and says nothing about locale.ImportWizardhas an ergonomic path touseDisplayLocale()at this call site.formatImportJobTimeis a module-level function, not a hook, so threading the tag may need a parameter or a move — that is an implementation question this note does not answer.Dedup
Channel: repo-scoped REST issues list,
state=open,per_page=100, paged to exhaustion (pages 1-4 returned 100, page 5 returned 64, page 6 returned 0). Total enumerated 464 rows = 449 issues + 15 pull requests, cross-checked against this repository'sopen_issues_countof 464 — the enumeration is complete for the open set. Then a local title-and-body regex scan.Controls fired both ways in the same scan:
useColumnSummaryreturned 4 (objectui#9295, objectui#9294, objectui#8203, objectui#2890 — all readable directly), and an impossible token returned 0.Scans:
formatImportJobTimereturned 0; an import-history scan returned 0;ImportWizardreturned 1 (objectui#8507, the bare em-dash taxonomy card — it names this file, but for the'—'fallback at a different line, and its body contains no occurrence oflocale);toLocaleStringreturned 3 (objectui#9294, objectui#8209 on readonlydatetimeWIDGET faces in@object-ui/fields, objectui#7174 on fiveformatTimestampnear-copies inplugin-detail+plugin-chatbot);useDisplayLocalereturned 2 (objectui#9294, objectui#4738 on address part order). The bodies of objectui#7174, objectui#8209 and objectui#8507 were each read directly: none containsformatImportJobTime, and none of the first two containsplugin-gridat all.⛔ Boundary, stated rather than papered over: open issues only. No text query over CLOSED cards was run, so a closed card covering this would not have been seen.
Written by an agent seat during objectui#9294; attribution is prose here deliberately, because a footer block is stripped when an issue is created.