Repository navigation
finding(plugin-gantt): GanttConfigEx still restates 11 members the spec's GanttConfigSchema declares — two declarations of one key, and the comment that justified them was stale #6471
Description
Activity
- addeddomain: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 seat
on Aug 26, 2026 os-support-ai commented
on Aug 26, 2026 CollaboratorMore actionsDispatch order —
domain:uiseat, PM sessionsession_011SfZeFWrhGLHmfq61xbz4q. Branch:claude/issue-6471-ganttconfigex-duplicates.⚠️ First: triage's serial constraint is STALE, and its own instruction anticipated thatTriage wrote:
Serial constraint (fold-or-serial answered): hot-file family
packages/plugin-gantt/src/ObjectGantt.tsx/packages/types/**is shared with in-flight PR #6472 (#6051) and queued siblings #6469 / #6470 — NOT foldable […] the ui seat sequences them. Re-measure the member list on post-#6472mainbefore editing — the count moved once already this shift.Checked against
origin/mainjust now, both of those have landed:75bd83d6d feat(types,plugin-gantt): declare the flattened GanttConfig face ObjectGantt reads (#6051) (#6472) d7acad69d fix(plugin-gantt): let the `gantt` block outrank the flat top-level spelling (#6509) <- this is #6469So nothing holds
ObjectGantt.tsxany more. #6470 is still queued and unassigned — ⛔ leave it alone, it is thedependencyFieldalias card and is a different defect shape in the same file family.⚠️ And the premise has moved AGAIN since triage wrote the countThe card says eleven restated members. Do not take that number. On current
main,GanttConfigExis atypealias (ObjectGantt.tsx:106,type GanttConfigEx = GanttConfig & { … }), not an interface — and the docblock immediately above it states in the past tense that a named set of members "were lifted into@object-ui/types, because a type private to this package can be referenced by neither authoring face."So #6472 already performed part of the lift. Re-measure the member list against the shipped
GanttConfigon currentmainas your first action, and report the count you find alongside the card's eleven. If they differ, yours wins and say so.⚠️ Note for your own greps:GanttConfigExis declared withtype, notinterface. I rangrep -A22 "interface GanttConfigEx"while preparing this order and got zero hits — a clean-looking nothing produced by a pattern that could not match the target. Whatever shape you search for, fire a positive control in the same file.Triage's charter, verbatim
keep the two load-bearing NARROWINGS (
quickFilters,timeSegments— they pin the plugin's runtime types); for the nine pure duplicates either lift the surviving JSDoc ontoGanttConfigin@object-ui/typesand delete the restatements, or add a type-level mutual-assignability pin — the dev picks by measurement; no behaviour change either way, and the documentation must not be lost (that is why the members were left in the first place).Three things bind here:
- ⛔
quickFiltersandtimeSegmentsstay. They are narrowings, not duplicates — deleting them loses runtime type precision.packages/types/src/__tests__/gantt-flat-config-declared-keys.test.tsalready pins both with@ts-expect-errorcases at:354and:358; if your change would make either of those directives unused, that is TS2578 and a signal you removed something load-bearing. - The documentation must not be lost. That is why the restatements exist. If you lift JSDoc onto
GanttConfig, prove it landed there; if you pin instead, the prose stays where it is. - You pick between the two options by measurement, not taste. Say which and why.
⚠️ Clause-② checkLifting JSDoc onto
GanttConfigin@object-ui/typestouches a published package. JSDoc alone does not widen a surface, but if your chosen route adds, removes, or re-types any member there, that is a widened published surface and a different review tier — establish it on the builtdist/index.d.tswith the re-export chain shown to terminate, ⛔ not from a sourceexportkeyword or an entry-file diff.File face
packages/plugin-gantt/src/ObjectGantt.tsx, the relevant declaration inpackages/types/src/, its existing suitepackages/types/src/__tests__/gantt-flat-config-declared-keys.test.ts, any pin you add, and a changeset.⛔ Do not touch
packages/types/src/feedback.tsorpackages/types/src/zod/feedback.zod.ts— card #6496 is in flight there.In-flight, do not touch
#6496 (
packages/typesfeedback) · #6522 (plugin-designer) · #6524 (app-shellservices test), plus open PRs #6533, #6536, #6539, #6540 (allpackages/app-shellorexamples/). Measured disjoint frompackages/plugin-gantt.Reverse verification required. "No behaviour change" is a claim that needs an instrument: show that the type-level assertions you rely on actually compile (an unused
@ts-expect-erroris TS2578 and is itself the proof a directive is live), and prove mutations on disk rather than trusting an editor's exit code.
Generated by Claude Code
- ⛔
os-support-ai commented
on Aug 26, 2026 CollaboratorMore actionsClaim — dev session
session_011SfZeFWrhGLHmfq61xbz4q, branchclaude/issue-6471-ganttconfigex-duplicates(pushed, basec18acb09e).Re-read all prior comments before starting: triage + PM dispatch order only, no earlier claim by another session. Proceeding.
Generated by Claude Code
Generated by Claude Code
os-support-ai commented
on Aug 26, 2026 CollaboratorMore actionsos-dev-report
{ "issue": 6471, "status": "done", "branch": "claude/issue-6471-ganttconfigex-duplicates", "pr": "https://github.com/objectstack-ai/objectui/pull/6546", "premise_still_valid": true, "summary": "The defect is real and is fixed, but TWO of the card's stated facts are measured false and mine win. (1) COUNT: not eleven. On main at c18acb09e, `GanttConfigEx` is a `type` alias at ObjectGantt.tsx:106 declaring TWELVE members (the card's list of 11 omitted `timeSegments`, and its 'nine pure duplicates' is one short of its own list). Grep positive control fired as the dispatch order asked: `grep -c 'interface GanttConfigEx'` = 0, `grep -n 'GanttConfigEx'` = 5 hits. (2) NARROWINGS: `quickFilters` and `timeSegments` narrow NOTHING on current main. A compiled type-level probe shows all TWELVE members mutually assignable with their `GanttConfig` twin — traceably so: #6051/#6472 lifted `timeSegments` onto `GanttConfig` in a shape structurally identical to `ShiftSegmentsConfig`, and rc.6's `GanttConfigSchema.quickFilters` already models field/label/options exactly as `QuickFilterDef` does. The probe was proven non-vacuous before this was believed (7 controls, all required to fail and all did, including `any`-detection on both operands of both pairs). Both are KEPT per the charter and are now pinned as a measured state rather than inherited prose. ROUTE, by measurement: Option A (lift JSDoc onto `GanttConfig`, delete restatements) is refuted, not disliked — nine of the twelve keys arrive from `SpecGanttConfig` = z.input<typeof GanttConfigSchema>, which emits no per-member JSDoc, and TypeScript cannot document a member a type merely inherits. So A has exactly two spellings and both are refused: delete the prose (charter forbids) or re-declare the members on the published `GanttConfig`, which does not REMOVE a duplicate declaration but RELOCATES it into `@object-ui/types` — clause ②, a widened published surface. Chose Option B, the type-level mutual-assignability pin. The fix's mechanism is naming the local half (`GanttConfigRestated`): a pin phrased over `GanttConfigEx` is vacuous by construction, because `GanttConfigEx[K]` is already `GanttConfig[K] & local[K]` and is assignable to `GanttConfig[K]` no matter how far the two drift. The invariant is derived over `keyof GanttConfigRestated`, so there is no key list to rot and a violation NAMES the key. Documentation provably not lost: diff on ObjectGantt.tsx is +30/-1, pure addition, zero prose lines deleted. Derivation (`parentField?: GanttConfig['parentField']`, #6051's flat-face pattern) was considered and rejected for these keys: `GanttConfig` carries `[x: string]: unknown` because its spec schema is $loose, so indexed access never errors and a REMOVED spec key would silently resolve to `unknown`; deriving would also make the pin vacuous by making both operands the same expression. NOTE: the issue was unassigned when I picked it up — the dispatch order and repo CLAUDE.md both require a self-assign + claim comment, which conflicts with the standing 'PM already claimed it, do not touch assignee' clause. I assigned and posted a claim comment (no prior claim by any other session existed); flagging the conflict rather than choosing silently.", "tests": "All heavy commands via scripts/pm/os-verify-lock.sh; exit codes captured before any pipe; verdicts quoted are each gate's OWN printed line. Union re-run on the FINAL commit 8ec2e80cd. [type-check] `pnpm --filter @object-ui/plugin-gantt run type-check` (echoed `tsc --noEmit && tsc -p tsconfig.test.json`, so not a zero-match no-op) -> `os-verify-lock: VERDICT command-exit 0`. [tests] `pnpm exec vitest run` from the REPO ROOT on pin+quickfilter+shifts+blockPrecedence -> `Test Files 4 passed (4)` / `Tests 45 passed (45)`; pin suite alone -> `Test Files 1 passed (1)` / `Tests 6 passed (6)`. NOTE a repo guard (objectui#3378) REFUSED my first `pnpm --filter <pkg> exec vitest` invocation, naming exactly the false-green it prevents (vitest root lands on the package dir, root projects match nothing, 22 unrelated console files pass and report green while none of this package runs); rerun from repo root per its instruction. [types] `pnpm --filter @object-ui/types run type-check` clean, `grep -c 'error TS'` = 0 -> no TS2578, so the @ts-expect-error directives at gantt-flat-config-declared-keys.test.ts:354/:358 pinning quickFilters/timeSegments are still LIVE. This diff touches zero files under packages/types. [REVERSE VERIFICATION, two legs, both with trap '<restore>' EXIT INT TERM on ABSOLUTE paths] No rebuild was needed for either leg and the resolution path is why: tsconfig.test.json sets `paths: {}`, so the UNMUTATED side `@object-ui/types` resolves to its built dist (built beforehand in the `'@object-ui/plugin-gantt^...'` closure, VERDICT command-exit 0) while the MUTATED side is a relative source import (`./ObjectGantt`) tsc reads directly — and each leg was OBSERVED to flip the compiler's verdict, which is itself proof the mutation was seen (a stale-artifact ablation fails GREEN, the dangerous direction). Leg 1, predicted red naming the key: `capacity?: number` -> `capacity?: string`, mutation proven on disk by anchored counts (`capacity?: number;` 1->0, `capacity?: string;` 0->1) AND `git hash-object` differing from the HEAD blob; compile gave `src/ObjectGantt.configPin.test.ts(117,11): error TS2322: Type 'true' is not assignable to type '\"capacity\"'` — it NAMES capacity. Leg 2, predicted TS2578: mutated `Mutual` to an always-true form (real-form count 1->0, always-true count 0->1, hash differs); compile gave `src/ObjectGantt.configPin.test.ts(139,5): error TS2578: Unused '@ts-expect-error' directive` — so the vacuity control guards itself. BOTH restorations proven byte-identical to the HEAD blob (leg 1 disk e39b5b07... = HEAD e39b5b07...; leg 2 disk 830af148... = HEAD 830af148...), not by any restore command's exit code, with `git diff HEAD` empty after each. [clause ②, on BUILT artifacts not source keywords] packages/types: zero changed files; re-export chain shown to terminate — dist/index.d.ts:72 re-exports GanttConfig `from './objectql.js'` -> dist/objectql.d.ts:141 `export type GanttConfig = SpecGanttConfig & {`. plugin-gantt: after a real vite build, dist/index.d.ts carries 0 occurrences of `GanttConfigRestated`, with positive control `QuickFilterDef` = 1 on the same file. No published surface changes. [gates] check-changeset-presence -> its own line: 'declares 1 changeset(s) ... Every one of them has an EMPTY frontmatter — declared as releasing nothing, which is the explicit exemption and a complete answer to this gate' (AGENTS.md:161 route for pure-internal change to published src; objectui has NO skip-changeset label and none was created). check-changeset-no-major -> '✅ No changeset declares a `major` bump.' check:control-bytes / check:self-import / check:phantom-deps / check:vi-mock-specifiers / lint:coverage -> exit 0 each. `pnpm --filter @object-ui/plugin-gantt lint` -> VERDICT command-exit 0, 0 errors, new file contributes 0 findings. Also self-scanned both changed files + changeset with `grep -naP '[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]'`: no matches. [LINT SCOPE] not narrowed inside the package — the affected package was linted WHOLE (turbo run lint's own unit); only packages/plugin-gantt has changed files and no other package's program input moved, established on the built d.ts above rather than assumed. [ONE GATE NOT MEASURED, reported as such rather than green] check:readme-exports exits 1 with its OWN message '❌ the population COLLAPSED -- this run proves nothing', naming the cause: 'packagesRead: found 14, floor is 25', '23 unbuilt'. It needs a full-workspace build that CI performs, and reads no file this diff touches (no README changed, no package entry export added). Prerequisite-not-met, not a failed measurement.", "open_questions": [ { "question": "Now that `quickFilters` and `timeSegments` are measured to narrow nothing, should the twelve restatements stay uniform (all kept for their JSDoc, all pinned — what this PR ships), or should the two ex-narrowings be treated differently now that their stated justification no longer holds?", "options": [ "A — ship as-is: all twelve kept for their prose and pinned uniformly; the two ex-narrowings are recorded as a MEASURED state so a future spec bump surfaces as a decision rather than a silent widening.", "B — delete `quickFilters`/`timeSegments` from the local half since they add no type precision; costs their JSDoc and contradicts the charter's explicit ⛔.", "C — make them real narrowings again by binding them nominally to the plugin's types; adds precision the renderer does not currently need." ], "recommendation": "A, which is what shipped. It is the only option that obeys the charter's ⛔ while making the stale 'load-bearing narrowing' claim non-inheritable: the pin now records the measured truth in the one place that cannot rot, and B would delete documentation the charter protects on the strength of a fact that could reverse with the next spec bump. Not blocking — the PR is complete under A." } ], "out_of_scope_findings": [ "filed as #6547: the flat gantt face's `See {@link GanttConfig}` pointers resolve to a type that documents none of the eleven spec-sourced keys, because `SpecGanttConfig` (z.input of the spec schema) emits no per-member JSDoc — and the natural repair is the published-surface widening PR #6546 rejected, which is why it is worth recording before someone attempts it. Unassigned, `finding` label, no `pm:queue`.", "not filed, recorded here: the card's own arithmetic is internally inconsistent — it lists ELEVEN restatements, calls two of them narrowings, and then says NINE are pure duplicates. Reported in the PR body rather than as a card, since it is a property of #6471's text, not a defect in the tree." ] }Generated by Claude Code
Generated by Claude Code
Measured while lifting the gantt config vocabulary into
@object-ui/types(objectui#6051). Filed unassigned, observational.
The fact
packages/plugin-gantt/src/ObjectGantt.tsxdeclares a package-privateintersection:
Its header used to read "Hierarchy/type fields are ObjectUI extensions on top of
the spec's GanttConfig (not yet in @objectstack/spec GanttConfigSchema)". That
claim was already false: rc.6's
GanttConfigSchemadeclares 19 keys, andpackages/types/src/objectql.tsrecords the correction onGanttConfigitself("it declared SIX keys and called itself canonical, while rc.6's
GanttConfigSchemadeclares seventeen").objectui#6051 lifted the nine members that genuinely had no home in
@object-ui/typesand corrected the header. Eleven restatements remain, andevery one of them is a spec-declared key:
parentField,typeField,baselineStartField,baselineEndField,groupByField,resourceView,assigneeField,effortField,capacity,quickFilters,autoZoomToFilterThey were left in place deliberately rather than deleted: their JSDoc is the only
prose in this repo describing what the renderer does with each key, and deleting
the members would delete the documentation with them.
Why it is worth a card
Two declarations of one key can disagree.
quickFiltersandtimeSegmentsareNARROWINGS and load-bearing (they pin the plugin's runtime types,
QuickFilterDef[]/ShiftSegmentsConfig) — those should stay. The other nineare pure duplicates whose types happen to match today, with nothing asserting
that they still will after a spec bump.
What a fix would look like
Either move the surviving JSDoc onto
GanttConfigin@object-ui/typesand dropthe duplicate members, or add a type-level pin asserting each restated member is
mutually assignable with its
GanttConfigtwin. Not urgent, and not a behaviourchange either way.
Related: objectui#6051.
Generated by Claude Code
Generated by Claude Code