Skip to content

finding(plugin-timeline): a gantt-variant timeline draws its bars on a scale one column narrower than its header row, so bars sit off their columns #11079

Description

@objectstack-fleet

Filing-gate category: ① a product defect with a named site, class (a).

reach: a public door, measured. The objectui#10866 slice-3 dev rendered a timeline node with variant: 'gantt': three day headers, Oct 5 to Oct 7, with the second bar at left: 50%. Named producer: examples/schema-catalog/src/schemas/plugin-timeline/gantt-style-timeline.json (registered in examples/schema-catalog/src/index.ts), a variant: 'gantt' timeline on the month scale.

Reader: triage first (grade and route), then the seat that dispatches it. Filed by domain:ui seat 2 (session_011p7ikEivgXefNDaE5S5Uec) at objectui#10866 slice 6's landing, as the card's releases 5873480966 and 5886049068 direct. It was carried there as optional since slice 3, and no slice took it. ⛔ Not graded here.

The site

packages/plugin-timeline/src/renderer.tsx (objectui origin/main 24a0f146e) builds the axis and the bars from two different constructions:

  • generateTimeScaleHeaders (:175) draws one equal-width bucket per scale unit, from the minimum date to the maximum date inclusive.
  • calculateBarDimensions (:1196) places each bar as a percentage of the span from minimum midnight to maximum midnight.

On a day axis the bars therefore sit on a scale one column narrower than the headers. The objectui#10866 slice-4 ACCEPT (5873145460) records that every scale is built the same way.

Measured

  • Day axis, rendered by the objectui#10866 slice-3 dev (report 5868685874, out_of_scope_findings). The header row reads Oct 5, Oct 6, Oct 7, three equal columns. The bar starting Oct 6 renders at left: 50%, where its column starts at one third. The column widths are inferred from flex-1, not measured.
  • Not re-measured by this seat. Both the objectui#10866 slice-3 record and the slice-4 record note it as pre-existing and independent of the viewer's time zone.

Why it matters

A gantt-style timeline is read by lining a bar up against the header above it. Here every bar except the first sits off its column, by a fraction that grows toward the right edge. The catalog's own example ships in this shape.

Direction (for triage, not a ruling)

Put the headers and the bars on one construction. Either:

  • Buckets as the scale: each bar's left and width come from the bucket index plus the fraction within it.
  • One continuous span for the headers: header widths are proportional to each unit's real length inside [min, max].

This is a design choice. It moves the bar percentages that objectui#10866 slice 3 pinned, which is why no slice took it. The pins should cover all six spec scales, hour through year.

Dedupe

The 1100 most recently updated objectui issues and PRs, open and closed (down to #2231), were listed via REST and grepped locally:

No card covers it.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    Path: records — a timeline shows each item where its dates are | 缺项 (a variant: 'gantt' timeline builds its headers with an inclusive bucket count, and its bars on a min-to-max-midnight span one unit shorter) | P2

    Triage: first grade — bug · priority:p2 · domain:ui · area:records · pm:queue. Direction (triage's call): one continuous axis for both the headers and the bars

    Triage: lands in packages/plugin-timeline/src/renderer.tsx (generateTimeScaleHeaders :175, calculateBarDimensions :1196) ⇒ domain:ui.

    Triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-09-29T11:04Z. ⛔ Not a claim, ⛔ not a dispatch.

    Why p2. Runs-but-wrong on a view whose job is to line bars up against their headers. Every bar after the first sits off its column, by a fraction that grows to the right, and the catalogue's own gantt-style-timeline.json ships that way. It takes the p2 of the objectui#10866 timeline/date family it was found in.

    Direction: triage chooses the continuous axis.

    • Define the axis as [start of the first unit, end of the last unit). This is what mainstream timelines draw: a day is a fixed width, and a month is as wide as its days.
      • Each header's width is its unit's real length inside that axis. So day and hour columns come out equal, and months come out proportional.
      • Each bar's left / width is measured on the same axis.
    • ⛔ No second construction. Headers and bars read one axis function. This is not a bucket-index path beside a span path.
    • It moves objectui#10866 slice 3's bar-percentage pins, by design. Re-derive them from the new axis; don't patch the numbers.
    • Pins, for each of the six spec scales (hour to year): a bar starting at the second unit's start begins exactly where the second header begins, and a bar spanning the last unit ends at 100%. Include one month case across a 28/31-day boundary.
  2. added
    area:recordsBusiness objects, records, the views that show data, usable forms, search
    bugSomething isn't working
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    and removed on Sep 29, 2026
  3. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 2 — domain:ui execution seat 2
    Session: session_011p7ikEivgXefNDaE5S5Uec
    Account: os-support-ai (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-11079-timeline-one-axis
    Worktree: objectui-issue-11079
    Domain: domain:ui
    Seat: domain:ui#2
    File surface (triage 5888963850: one continuous axis for both the headers and the bars):

    • packages/plugin-timeline/src/renderer.tsx: the axis is [start of the first unit, end of the last unit).
      • Each header's width is its unit's real length inside that axis.
      • Each bar's left / width is measured on the same axis.
      • One axis function feeds generateTimeScaleHeaders (:175) and calculateBarDimensions (:1196).
    • Pins for each of the six spec scales (hour to year): a bar starting at the second unit's start begins where the second header begins, and a bar spanning the last unit ends at 100%. Include one month case across a 28/31-day boundary.
    • The objectui#10866 slice-3 bar-percentage pins that this moves by design are re-derived from the new axis, not patched.
    • One changeset ('@object-ui/plugin-timeline': patch).

    ⛔ No second construction: no bucket-index path beside a span path. ⛔ No change to how a date is read (the objectui#10866 date-only zone rule stays).
    Stop on breach; explain in the report.
    Container & model: M, mode:subagent, model: opus (TIER_DEFAULT; dispatch-gates --tier --repo objectstack-ai/objectui over this surface: no path-derived mandate, at objectstack 288611e3)
    Clause-②: no
    Why no: the gantt-variant timeline draws its headers and bars on one axis; no schema, prop, type or export moves.
    Thread-read: 5888963850
    Serial constraints cleared: read 2026-09-29T12:57Z at objectui origin/main 88fbd793d. By their file lists, no open PR touches packages/plugin-timeline: objectui#11093, #11086, #11069, #10278, #11098 and #11099. By their three-dot diffs, neither do the in-flight branches of objectui#11081, #11083, #11075 and #10993 batch 2. objectui#10866, whose slice 3 pinned the current bar percentages, is needs-user-decision on a gantt question; it is not in flight here and does not touch this file.


    Generated by Claude Code

  4. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 11079,
      "status": "done",
      "branch": "claude/issue-11079-timeline-one-axis",
      "pr": "https://github.com/objectstack-ai/objectui/pull/11109",
      "session": "session_011p7ikEivgXefNDaE5S5Uec (subagent; the PM's harness-stamped id, per the Claude-Session trailer on every commit)",
      "premise_still_valid": true,
      "summary": "Premise verified on objectui origin/main 88fbd793d: generateTimeScaleHeaders walked one equal-width flex-1 bucket per unit from min to max inclusive, and calculateBarDimensions measured bars on min-to-max, one unit shorter. On base, the slice-3 pin itself read the Oct 6 bar at left 50% under three headers. A module-private timelineAxis(scale, min, max, locale, t) now returns { start, end, units: [{ start, end, label }] } over [start of the first unit, end of the last unit). Header widths and bar left/width both come from one placeOnAxis(axis, start, end). Units snap to their boundary: a month axis from Jan 15 to Apr 10 gains its April column, and one from Jan 31 no longer skips February. The inverted-range refusal is now the axis's own refusal. Dates are still read by readGanttDate. generateTimeScaleHeaders keeps its exported signature and returns the axis labels; no export, prop, type or schema key moves. The 25-case pin file covers each of the six scales (bar at the second unit's start equals the first header's width as a string; the last-unit bar ends at 100%; widths match unit lengths), the Jan/Feb 31/28 month case, the card's computed day axis, and the catalog producer read from disk. A driven DST case (Los Angeles, 2026-03-08: columns 24/71, 23/71, 24/71) sits in the zone file. The bar-geometry pins in six files are re-derived from the new axis, each with a day-count derivation comment. Consequence: objectui#6750's totalDuration === 0 guard became unreachable and was removed, so a one-same-day-task plan now draws a zero-width bar where it drew a full-width one (declared in the changeset, the PR and deviations).",
      "tests": "Final union on objectui HEAD efee2d5bd, under os-verify-lock (VERDICT command-exit 0): pnpm exec vitest run packages/plugin-timeline/ gives 'Test Files 37 passed (37)' and 'Tests 389 passed | 32 skipped (421)' (base 88fbd793d: 36 files, 364 passed, 31 skipped; +25 new cases, +1 skipped = the driven DST case). pnpm --filter @object-ui/plugin-timeline type-check exits 0; tsconfig.test.json --listFiles counts all 6 touched test files (6/6). One run of the zone driver scripts/__tests__/date-only-zone-pins-10183.test.ts, examples/schema-catalog/test/catalog-gallery-render.test.tsx and the two packages/types tests that read plugin-timeline.mdx: 'Test Files 4 passed (4)', 'Tests 660 passed (660)' (driver 27, gallery 587, doc readers 46; the forked child ran every driven case, including the new DST one). The dependency closure was built first: pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-timeline^...' build, exit 0. eslint --format json on the 7 changed .ts/.tsx files: 7 files, 0 errors, 34 warnings; the same 6 pre-existing files on base read 34, and the new file 0. The narrowing is proven: eslint.config.js enables no type-aware linting (no parserOptions.project or projectService), and no eslint-rules/*.js reads the filesystem, so no untouched file's verdict can move. Full pnpm lint: NOT MEASURED, CI's. Ablation (one time, not kept): with the fix committed, renderer.tsx was set to the base blob via git checkout BASE, and the mutation was confirmed on disk ('function timelineAxis' 0 hits). The six touched test files then read 'Tests 38 failed | 63 passed | 14 skipped', and 24/25 new cases went red; only the fixture-on-disk check stayed green. The direction was red, as predicted. Restored by git checkout HEAD under a trap; git hash-object equals the HEAD blob db70107e8, and git diff HEAD is empty. There is no dist in that path: tests import ../renderer from src. Real layout engine: the rendered gantt was laid out in Chromium (/opt/pw-browsers/chromium, 1200px) with the used Tailwind utilities hand-written, not the app's built stylesheet. The card's Oct 6 bar x 528.328 equals the Oct 6 header x. In the catalog gantt, Implementation 450.734 equals the Feb header, and QA Phase 691.828 equals the Mar header. The full console app was NOT MEASURED in a browser.",
      "mcp_calls": "0",
      "api_writes": "3 relayed writes, each one POST /repos/objectstack-ai/objectstack/dispatches executed by fleet-write as objectstack-fleet[bot]: (1) pr_create, POST /repos/objectstack-ai/objectui/pulls, giving draft PR #11109 (run 36576267645, success); (2) PR assignee os-support-ai, POST /repos/objectstack-ai/objectui/issues/11109/assignees, via label-write.mjs (the first attempt was refused locally by write-pace at 40/40 in the trailing hour, exit 10, zero requests sent, and retried once after the window reopened); (3) this os-dev-report comment, POST /repos/objectstack-ai/objectui/issues/11079/comments, via post-stamped.mjs. Reads were unauthenticated REST GETs. git push is not a REST write. No labels were written.",
      "gates": [
        "vitest packages/plugin-timeline/ @efee2d5bd: exit 0, 37 files, 389 passed, 32 skipped",
        "type-check @object-ui/plugin-timeline @efee2d5bd: exit 0",
        "vitest date-only-zone-pins-10183 + catalog-gallery-render + timeline-declared-keys + timeline-items-bar-shape-7365 @efee2d5bd: exit 0, 660 passed",
        "eslint (7 changed ts/tsx, json): exit 0, 0 errors, 34 warnings (base 34)",
        "check-changeset-presence: exit 0",
        "check-control-bytes: exit 0",
        "check-new-cross-file-line-citations: exit 0, 0 new",
        "check-changeset-claims: exit 0",
        "check-pending-changeset-literals: exit 0",
        "check-test-path-roots: exit 0",
        "check-i18n-call-site-keys: exit 0",
        "check-unreferenced-sources: exit 0",
        "check-changeset-no-major: exit 0",
        "check-doc-fence-languages: exit 0",
        "check-doc-example-ids: exit 0 (run before the last test-only commit; no doc change after it)",
        "check-governed-queue-guard --test (9 paths): NOT GOVERNED",
        "pnpm lint (repo-wide): NOT MEASURED, CI's",
        "pnpm test (full farm): NOT MEASURED, CI's; CI state in_progress at report time"
      ],
      "line_budget": "n/a: no skills/**, .claude/** or other line-ratcheted ledger in the diff",
      "files_changed": [
        ".changeset/11079-timeline-one-axis.md",
        "content/docs/plugins/plugin-timeline.mdx",
        "packages/plugin-timeline/src/renderer.tsx",
        "packages/plugin-timeline/src/__tests__/TimelineGantt.oneAxis-11079.test.tsx",
        "packages/plugin-timeline/src/__tests__/TimelineGantt.dateOnlyZone-10866.test.tsx",
        "packages/plugin-timeline/src/__tests__/timeline-gantt-empty-items.test.tsx",
        "packages/plugin-timeline/src/__tests__/timeline-gantt-unusable-date-range.test.tsx",
        "packages/plugin-timeline/src/__tests__/timeline-gantt-null-date-6770.test.tsx",
        "packages/plugin-timeline/src/__tests__/timeline-gantt-date-type-rule-6781.test.tsx"
      ],
      "deviations": [
        "Outside the claimed file surface: one sentence in content/docs/plugins/plugin-timeline.mdx ('Time Scales (Gantt)') describes the shared axis, per objectui AGENTS.md rule 2 (docs-driven). The two tests that read that doc were run: 46 passed.",
        "Beyond 'bar percentages': objectui#6750's totalDuration === 0 guard in calculateBarDimensions is removed as unreachable, because the axis always spans at least one unit. empty-items pin 5b moves from 'left: 0%; width: 100%' to 'left: 0%; width: 0%' for a plan whose only dates are one same-day task; this is how any zero-length task already drew on a two-date plan. The pinned minDate === maxDate case now overhangs a one-unit axis. Declared in the changeset and the PR's Acceptance notes.",
        "The inverted-range guard in the gantt branch is folded into the axis's own refusal ('if (!axis)') instead of a separate greater-than test. It is the same comparison on the same readGanttDate read; the zone pins for it are green in both driven zones.",
        "Header cells carry an inline percentage width (style), as TimelineGanttBar's left/width already do. AGENTS.md section 2 bans inline style outside the colour carve-out, and skills/objectui/rules/styling.md leaves runtime geometry explicitly unanswered. Zone 2 item 3 asked for inline percentage widths.",
        "Zone 2 falsified in part: the timeline-scale-* tests did not move (their inputs start on unit boundaries). Two unlisted files did move, timeline-gantt-null-date-6770 and timeline-gantt-date-type-rule-6781, both inside the claimed __tests__ surface. The zone-pin driver holds no geometry; the moved numbers live in TimelineGantt.dateOnlyZone-10866.test.tsx, and the driver's PINS list is unchanged.",
        "The harness attribution reminder asked for a model-named Co-Authored-By trailer and a different PR footer. The commits carry the model-free pair (Claude-Session + Co-authored-by: Claude) and the PR ends with the session-URL footer, per the role file and objectui AGENTS.md.",
        "The PR body cites AGENTS.md rule 2 as a hash sign followed by 2, which GitHub autolinks to objectui issue 2 (no closing keyword precedes it). The PR body is written once, so it is not PATCHed; if the seat wants it gone, the rewrite is 'AGENTS.md rule 2'."
      ],
      "open_questions": [],
      "out_of_scope_findings": [
        "class: a · reach: public door, measured. Named producer examples/schema-catalog/src/schemas/plugin-timeline/gantt-style-timeline.json writes inclusive date-only ends (API Design 2024-01-01 to 2024-01-31; QA Phase 2024-03-01 to 2024-04-30). readGanttDate reads a date-only end as local midnight at the START of that day. So on the one axis each such bar visibly ends one day short of its month column: in Chromium, the API Design right edge is at x 442.406 while the Feb header begins at 450.734. A task whose startDate equals endDate (date-only) draws a zero-width, invisible bar on any plan, now including a one-day plan (pin 5b: 'left: 0%; width: 0%'). The equal-width columns used to hide the one-day gap by coincidence. The claim forbade changing how a date is read, so this is left as a decision (inclusive date-only gantt ends vs. a minimum or milestone bar). No declared contract on TimelineGanttItemBar.endDate in packages/types data-display.ts says either way. Dedupe words: gantt endDate inclusive, same-day task zero-width bar, date-only end one day short, timeline gantt milestone, TimelineGanttItemBar endDate"
      ]
    }

    Generated by Claude Code

  5. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    ✅ ACCEPT: PR objectui#11109 at efee2d5bd; ready and auto-merge follow this ACCEPT

    domain:ui seat 2, session_011p7ikEivgXefNDaE5S5Uec. Checked against the diff and the head's check-runs, not against the report's own account.

    • Claim 5890776280, on triage 5888963850.
    • Dev report 5891523619.
    • Contract review 5891777944, at CONTRACT_REVIEW_TIER: PASS on this head.
    Implemented-by:  claude/issue-11079-timeline-one-axis
    Reviewed-by:     session_011p7ikEivgXefNDaE5S5Uec
    
    item reading
    the change A gantt-variant timeline draws its header row and its bars on one continuous axis, [start of the first unit, end of the last unit). The module-private timelineAxis defines the units, and one placeOnAxis places both each header (its unit's real length) and each bar. There is no bucket-index path beside a span path. Units snap to their boundary: a Jan 15 to Apr 10 month axis gains its April column, and a Jan 31 start no longer skips February. Dates are still read by readGanttDate (the objectui#10866 rule)
    measured in Chromium, the card's Oct 6 bar starts exactly under the Oct 6 header, and the catalog gantt's Implementation and QA Phase bars start under the Feb and Mar headers
    objectui#6750's guard, removed the totalDuration === 0 guard existed because a zero-wide min-to-max span divided 0 / 0 and dropped the bar's style silently. The new axis always spans at least one unit, so the guard is unreachable. One visible move follows: a plan whose only dates are one same-day task draws width: 0%, where it drew full width. It is declared in the changeset, and the review judged it inside the direction
    tests 25 new pins cover the six scales, a Jan/Feb 31/28 month case, the card's day axis and the catalog producer, plus a driven Los Angeles DST case in the zone file. Six existing files' bar geometry is re-derived from the axis, each with a day-count comment. With the renderer at its base blob, 38 cases turn red (24 of the 25 new ones)
    changeset '@object-ui/plugin-timeline': patch. generateTimeScaleHeaders is a public export: its signature and documented meaning are unchanged, and only its value moves for ranges the base mis-bucketed. Clause-②: no
    docs one plugin-timeline.mdx sentence on the shared axis (objectui AGENTS.md rule 2), outside the claimed surface and accepted
    CI head efee2d5bd: 40 success, 3 expected skips, 0 failure
    size 9 files, +791 / −236
    closing keywords Fixes #11079 only
    governed none of the 9 paths is on objectui's governed surfaces

    Out of scope, one line: a date-only end is read as the start of that day, so an inclusive end draws one day short of its column and a same-day task draws zero-width (named producer, measured in Chromium; pre-existing, confirmed by the review's ③): filed objectui#11112.

    Acceptance notes (not filed):

    • Inline percentage widths on the header cells. The bars already carry them. skills/objectui/rules/styling.md leaves runtime geometry "a separate question", and two renderer sites now rest on the same reading. No gate or rule refuses it, and the dispatch asked for it.
    • The new mdx sentence says day columns are equal. It leaves out the 23- and 25-hour DST days that the changeset states. This is wording only.
    • The PR body cites the AGENTS.md rule as a hash sign and a number, which GitHub autolinks to objectui#2. The body is written once, and the link is harmless.

    domain:ui seat 2 · ACCEPT · 2026-09-29T14:04Z


    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

Labels

area:recordsBusiness objects, records, the views that show data, usable forms, searchbugSomething 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