Repository navigation
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
Activity
objectstack-fleet commented
on Sep 29, 2026 ContributorAuthorMore actionsPath: 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) | P2Triage: first grade —
bug·priority:p2·domain:ui·area:records·pm:queue. Direction (triage's call): one continuous axis for both the headers and the barsTriage: 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.jsonships 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
dayandhourcolumns come out equal, and months come out proportional. - Each bar's
left/widthis measured on the same axis.
- Each header's width is its unit's real length inside that axis. So
- ⛔ 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 (
hourtoyear): 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 onemonthcase across a 28/31-day boundary.
- Define the axis as
- addedarea:recordsBusiness objects, records, the views that show data, usable forms, searchBusiness objects, records, the views that show data, usable forms, searchbugSomething isn't workingSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatobjectui ui stream: fix lands on the published library or apps — objectui execution seatand removed
on Sep 29, 2026 objectstack-fleet commented
on Sep 29, 2026 ContributorAuthorMore actionsClaim: PM loop round 2 —
domain:uiexecution seat 2
Session:session_011p7ikEivgXefNDaE5S5Uec
Account:os-support-ai(the seat's linked user asGET /useranswers 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 (triage5888963850: 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/widthis measured on the same axis. - One axis function feeds
generateTimeScaleHeaders(:175) andcalculateBarDimensions(:1196).
- Pins for each of the six spec scales (
hourtoyear): 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 onemonthcase 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/objectuiover this surface: no path-derived mandate, at objectstack288611e3)
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 objectuiorigin/main88fbd793d. By their file lists, no open PR touchespackages/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, isneeds-user-decisionon a gantt question; it is not in flight here and does not touch this file.
Generated by Claude Code
objectstack-fleet commented
on Sep 29, 2026 ContributorAuthorMore actionsos-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
objectstack-fleet commented
on Sep 29, 2026 ContributorAuthorMore actions✅ ACCEPT: PR objectui#11109 at
efee2d5bd; ready and auto-merge follow this ACCEPTdomain:uiseat 2,session_011p7ikEivgXefNDaE5S5Uec. Checked against the diff and the head's check-runs, not against the report's own account.- Claim
5890776280, on triage5888963850. - Dev report
5891523619. - Contract review
5891777944, atCONTRACT_REVIEW_TIER: PASS on this head.
Implemented-by: claude/issue-11079-timeline-one-axis Reviewed-by: session_011p7ikEivgXefNDaE5S5Uecitem 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-privatetimelineAxisdefines the units, and oneplaceOnAxisplaces 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 byreadGanttDate(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 === 0guard existed because a zero-wide min-to-max span divided0 / 0and 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 drawswidth: 0%, where it drew full width. It is declared in the changeset, and the review judged it inside the directiontests 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.generateTimeScaleHeadersis a public export: its signature and documented meaning are unchanged, and only its value moves for ranges the base mis-bucketed.Clause-②: nodocs one plugin-timeline.mdxsentence on the shared axis (objectuiAGENTS.mdrule 2), outside the claimed surface and acceptedCI head efee2d5bd: 40 success, 3 expected skips, 0 failuresize 9 files, +791 / −236 closing keywords Fixes #11079onlygoverned 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.mdleaves 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:uiseat 2 · ACCEPT · 2026-09-29T14:04Z
Generated by Claude Code
- Claim
- added a commit that references this issue
on Oct 7, 2026
Filing-gate category: ① a product defect with a named site, class (a).
reach:a public door, measured. The objectui#10866 slice-3 dev rendered atimelinenode withvariant: 'gantt': three day headers, Oct 5 to Oct 7, with the second bar atleft: 50%. Named producer:examples/schema-catalog/src/schemas/plugin-timeline/gantt-style-timeline.json(registered inexamples/schema-catalog/src/index.ts), avariant: 'gantt'timeline on themonthscale.Reader: triage first (grade and route), then the seat that dispatches it. Filed by
domain:uiseat 2 (session_011p7ikEivgXefNDaE5S5Uec) at objectui#10866 slice 6's landing, as the card's releases5873480966and5886049068direct. 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(objectuiorigin/main24a0f146e) 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
5868685874,out_of_scope_findings). The header row reads Oct 5, Oct 6, Oct 7, three equal columns. The bar starting Oct 6 renders atleft: 50%, where its column starts at one third. The column widths are inferred fromflex-1, not measured.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:
[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,
hourthroughyear.Dedupe
The 1100 most recently updated objectui issues and PRs, open and closed (down to #2231), were listed via REST and grepped locally:
calculateBarDimensionsandgenerateTimeScaleHeaders: 3 hits each, all objectui#10866 slice PRs (objectui#10901, fix(plugin-timeline,core): the gantt axis draws a stored date-only day on that day in every zone, and the formula date functions do a calendar day's arithmetic on the UTC calendar (objectui#10866, slice 3) #10957, fix(plugin-calendar): a day event moved across a DST change writes the days it was dropped on; timeZone prose and zone pins (objectui#10866, slice 4) #10994), which carry it as a note;misalign: PR objectui#10994 and PR objectui#11055, the same notes;gantt-style-timeline: PR objectui#10901 and PR objectui#10957;timeline gantt: 3 hits; absent controlzzqq_never_written_token_4242: 0.No card covers it.
Generated by Claude Code