Skip to content

feat(spec): declare viewMode on GanttConfigSchema (#9463) - #9489

Merged
os-steve merged 2 commits into
mainfrom
claude/issue-9463-gantt-viewmode
Aug 18, 2026
Merged

os-steve merged 2 commits into
mainfrom
claude/issue-9463-gantt-viewmode

Conversation

@os-steve

Copy link
Copy Markdown
Collaborator

Fixes #9463

Spec half of the objectui#5074 maintainer ruling (2026-08-18, verbatim 「同意」: declare viewMode and wire BOTH branches). This PR declares the key on GanttConfigSchema; the objectui half (objectui#5074, pm:blocked on this card) declares it on ObjectGanttSchema and wires the timeline branch after this lands. objectui#5074 is not addressed here and remains open; the PM posts the unlock there after landing.

Measurement receipts (enum from the renderer, not invented)

Measured on objectui origin/main @ 68d9e282c83af26f639180fc22dacba4c26dc201 (fetched 2026-08-18):

  • packages/plugin-gantt/src/GanttView.tsx:197 — export type GanttViewMode = 'day' | 'week' | 'month' | 'quarter' | 'year'
  • packages/plugin-gantt/src/GanttView.tsx:199 — const VIEW_MODES: GanttViewMode[] = ['day', 'week', 'month', 'quarter', 'year'] (the runtime membership guard actually applied to the prop at :645, :803, :808)
  • packages/plugin-gantt/src/GanttView.tsx:207-213 — NOMINAL_DAYS maps all five members (each granularity really renders; year included, despite the stale JSDoc at :195 that names only four — filed as objectui#5132)
  • Default: packages/plugin-gantt/src/GanttView.tsx:802-805 — viewModeProp && VIEW_MODES.includes(viewModeProp) ? viewModeProp : restoredLayout?.viewMode ?? 'day'; same 'day' default in the resource branch (ResourceWorkload.tsx:63) and at the existing cast read (ObjectGantt.tsx:1445, (schema as any).viewMode … || 'day')

So the declared enum is exactly the measured set: day | week | month | quarter | year.

Why optional with no spec-side .default('day'): the renderer's absence behaviour is not a constant — an omitted viewMode lets a persisted layout (persistLayoutKey, GanttView.tsx:784-805) seed the granularity before falling back to 'day'. A materialized default would arrive at the renderer as an explicit author choice and defeat that seeding — the same reasoning the map block records for its defaultless zoom/center. This also mirrors the gantt block's own sibling convention (autoZoomToFilter, effortField: optional + default documented in prose).

What landed

  • packages/spec/src/ui/view.zod.ts — viewMode: z.enum(['day','week','month','quarter','year']).optional().describe(…) on GanttConfigSchema. The block's deliberate .passthrough() posture is unchanged (renderer-ahead knobs still flow); declaring the key means an out-of-vocabulary value is now refused at authoring instead of silently falling back to 'day'.
  • packages/spec/src/ui/view.test.ts — accept pin per measured member (it.each), an absence pin (omitted viewMode stays absent — no materialized default), and a refusal pin (viewMode: 'hour' — a real granularity on the timeline block's scale, deliberately not in the gantt vocabulary — rejected with invalid_value naming the full member list).
  • packages/spec/liveness/view.json — the view/list/gantt blanket note now records viewMode at PLANNED strength (mirroring the spec: ListViewSchema(strictObject)没有 map 可视化配置块 —— map 列表视图的 titleField/locationField/相机整体不可声明,showcase 全部 marker 标题为 undefined #9340 map precedent one level down): today only the resourceView branch reads it (objectui ObjectGantt.tsx:1445, an as any cast); flips to plain live when objectui#5074 wires both branches.
  • packages/spec/authorable-surface/ui.json + content/docs/references/ui/view.mdx — regenerated (check:generated --fix; only the artifacts it proved stale).
  • .changeset/gantt-viewmode-declared.md — @objectstack/spec minor (widens the accept set; not breaking, no ADR-0087 disposition required).

Out of scope, untouched: the component's other undeclared as any reads (readOnly, markers, holidays, …) — objectui#5043's family track.

Verification

All at head 84a5f2185, run after the final commit (the working tree was byte-identical to HEAD for every run; git status --porcelain empty):

  • pnpm --filter @objectstack/spec test — 409 files / 10922 tests passed; pnpm --filter @objectstack/spec typecheck — green (incl. check:test-typecheck: spec test layer compiles, debt ledger untouched)
  • pnpm --filter @objectstack/spec build + check:generated — 1 artifact proved stale (content/docs/references/**), regenerated with --fix; authorable-surface/ui.json picked up exactly ui/GanttConfig:viewMode
  • Full derived gate union (node scripts/pm/dispatch-gates.mjs over the changed paths, 24 families + check:nul-bytes): all green — incl. check:liveness, check:strictness-ledger, check:empty-state, check:variant-docs, check:authorable-surface (via check:generated), check:merge-driver, check:adr-0087-registration, check:changeset-no-major, check:engine-double-contract, check:where-matcher, check:type-check-coverage, and — after building the workspace closure exactly as lint.yml does — check:dev-prereqs, lint check:doc-formula-expressions, and check:type-check-debt --re-measure (33 entries re-measured, none above its recorded number)

Reverse verification

From the committed state, git restore --source=origin/main -- packages/spec/src/ui/view.zod.ts (schema alone reverted, tests kept). Direction predicted before running: exactly 1 of the 7 new pins red — the refusal pin — because the deliberately-passthrough parent already tolerated the key's presence, so declaring it changes value judgment only. Observed exactly that: 1 failed | 266 passed, the failure being the refusal pin (view.test.ts:304 — viewMode: 'hour' parses successfully once the key is undeclared). Restored with git checkout HEAD -- ..., re-run: 267 passed (267), tree clean at 84a5f2185.


Generated by Claude Code

Enum measured from objectui plugin-gantt GanttView.tsx (GanttViewMode /
VIEW_MODES): day | week | month | quarter | year, renderer default 'day'.
Optional with no spec-side default — absence lets the renderer's
persisted-layout seeding run. Passthrough posture of the gantt block
unchanged. Spec half of the objectui ruling; the consumer wiring follows
in the objectui repo once this lands.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fs18A2DdXLVN2h8PaaFBcP
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ✅

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/spec/authorable-surface/ui.json, packages/spec/liveness/view.json) — pages documenting those are invisible to this run

Coarse fallback — 113 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json origin/main → packageMentionDocs.

Copy link
Copy Markdown
Collaborator Author

PM — holding this PR for a manual landing: three SQUASH auto-merge arms all echoed method: MERGE and the queue never admitted it. Receipts below.

domain:spec seat (#6017, session session_01Fs18A2DdXLVN2h8PaaFBcP).

State: head 84a5f218, 28/28 check runs green (last: Test Core 3/3 success 04:43:27Z), mergeable_state: blocked, review of record ACCEPTED on #9463. Not dirty, not lacking checks — and never admitted to the merge queue.

Receipts — three enablePullRequestAutoMerge calls, all explicitly SQUASH, all echoed MERGE:

arm reply queue admission
04:42:07Z (Test Core 3/3 still running) method: MERGE, enabled at 2026-08-18T04:42:07Z none
04:58:17Z (all 28 green) method: MERGE, enabled at 2026-08-18T04:58:17Z none (probed 04:59Z and 05:07Z — gh-readonly-queue/main/* empty of pr-9489)
05:11:56Z (disable→re-enable, controlled third try) method: MERGE, enabled at 2026-08-18T05:11:56Z none

Contrast, same session, same tool, same repo: four arms tonight echoed EMPTY method/enabledAt fields and every one admitted to the queue within ~2 minutes and squashed (PRs #9370, #9393, #9405, #9411). The correlation "empty echo → admitted; MERGE echo → never admitted" now has three failures and four successes across two sessions (the third failure being PR #9355's 23:2xZ episode, which was confounded by a dirty state this PR does not have).

Possible confound, flagged not asserted: main's tip is now 81316864b — #9421's rename of the ESLint required context to Lint & Repo Gates, whose own subject says the branch-protection Settings swap is still pending and needs the maintainer present. During the same window #9440/#9455/#9421 landed through the queue while this PR was never admitted, so a total queue freeze is ruled out — but a half-renamed required-context configuration interacting with NEW auto-merge arms is exactly the kind of state I cannot inspect from here.

Held: auto-merge is OFF (a MERGE-pinned trigger in a squash repo is not something to leave armed). The PR is green, reviewed, and ready — it needs either a manual queue entry / merge by the maintainer, or a retry once the #9421 Settings swap is done. Wave-8 siblings (#9493 next) will attempt their own arms when their CI is green; their receipts will tell us whether this is PR-specific or systemic.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Root cause found — my hold comment above is superseded; no maintainer action needed for THIS PR.

The discriminating experiment answered it: PR #9493 (same wave, CI started 04:44Z) carries the renamed required context Lint & Repo Gates — no ESLint job at all — and its auto-merge arm echoed the healthy EMPTY fields and took immediately. So the #9421 Settings swap has already happened. This PR's CI ran 04:23Z, before the rename reached it: its head carries ESLint (old name) and simply does not have the now-required Lint & Repo Gates context — an unsatisfiable required check, which explains mergeable_state: blocked, the queue refusing admission, and (evidently) the method: MERGE echo on every arm.

Refined reading of the receipt shapes for the ops ledger: enable_pr_auto_merge echoing method: MERGE ≈ the arm cannot take effect on this head (unsatisfiable/incomplete required contexts — here a pre-rename CI run; in the #9355 episode, a dirty state). The empty echo is the arm taking effect. Still a correlation, but now with a mechanism and 5-for-5 / 3-for-3 on both sides.

Fix applied: branch update triggered (merge main into the head) — the fresh CI runs the renamed workflow and produces the required context. Once green, this seat re-arms auto-merge (SQUASH) and expects the empty echo + queue admission.


Generated by Claude Code

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

Labels

documentation Improvements or additions to documentation size/s tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

spec: declare viewMode on GanttConfigSchema — the contract half of objectui#5074's ruled both-branches wiring

2 participants