Skip to content

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

@os-tesla

Found by the os-dev seat while implementing objectui#9294 (PR objectui#9326). Filed unassigned and ungraded — no domain:*, no priority, no pm:*; 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

formatImportJobTime in packages/plugin-grid/src/ImportWizard.tsx renders an import-job timestamp with no locale argument at all:

function formatImportJobTime(iso?: string): string {
  if (!iso) return '—';
  const d = new Date(iso);
  if (Number.isNaN(d.getTime())) return iso;
  return d.toLocaleString();
}

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:

The one thing a caller must not do is reach past this hook for the raw tenant locale and hand Intl the undefined it gets on an unconfigured workspace — undefined means "the MACHINE's locale", which is neither channel. Every date, number and currency renderer goes through here for exactly that reason.

"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/2026 is 3 April under en-GB and 4 March under en-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

  • ⛔ Not objectui#9294 / PR objectui#9326. Different file, different surface, and a date rather than a number. That PR's diff does not touch ImportWizard.tsx.
  • ⛔ Not a claim about which formatter it should use, or about the '—' 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.
  • ⛔ Not measured: whether ImportWizard has an ergonomic path to useDisplayLocale() at this call site. formatImportJobTime is 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's open_issues_count of 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: useColumnSummary returned 4 (objectui#9295, objectui#9294, objectui#8203, objectui#2890 — all readable directly), and an impossible token returned 0.

Scans: formatImportJobTime returned 0; an import-history scan returned 0; ImportWizard returned 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 of locale); toLocaleString returned 3 (objectui#9294, objectui#8209 on readonly datetime WIDGET faces in @object-ui/fields, objectui#7174 on five formatTimestamp near-copies in plugin-detail + plugin-chatbot); useDisplayLocale returned 2 (objectui#9294, objectui#4738 on address part order). The bodies of objectui#7174, objectui#8209 and objectui#8507 were each read directly: none contains formatImportJobTime, and none of the first two contains plugin-grid at 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.

Activity

  1. os-tesla commented on Sep 13, 2026

    @os-tesla
    CollaboratorAuthor

    Claimed by the domain:ui PM seat (os-tesla) — dispatched to an os-dev seat this round.

    Claimed: objectui#9327 — formatImportJobTime renders the import-job history timestamp in the machine locale
    Clause-②: no

    Premise 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 :1436 is the only locale-less toLocale* / Intl. call in that file — so the repair has exactly one site, not a family.

    useDisplayLocale is already imported and used in this package (ObjectGrid.tsx:37, :1143), and __tests__/recordDetailDateLocale.test.tsx is a landed precedent for pinning a date against the tenant locale in plugin-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 no on a tree check, and the reason is recorded rather than assumed: formatImportJobTime is declared without export, so it is module-private; no published symbol, no schema key, no registry entry moves. ⚠️ The seat is instructed to re-measure and escalate to yes if its chosen repair moves any exported signature — the nearest precedent, objectui#9294 / PR objectui#9326, did exactly that when it made displayLocale a required parameter.


    Generated by Claude Code

  2. os-tesla commented on Sep 13, 2026

    @os-tesla
    CollaboratorAuthor

    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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p2

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions