Uh oh!
There was an error while loading. Please reload this page.
fix(plugin-gantt): pin every restated GanttConfig member against its twin - #6546
Conversation
…twin `GanttConfigEx` restates twelve members `GanttConfig` already declares. Two declarations of one key can disagree, and nothing asserted they still agree after a spec bump. Name the local half (`GanttConfigRestated`) so the two declarations become independent operands — inside the intersection `GanttConfigEx[K]` is already `GanttConfig[K] & local[K]`, so any assertion phrased over it passes by construction — and pin mutual assignability over every key. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011SfZeFWrhGLHmfq61xbz4q
…g nothing Type-only and package-private: no runtime change, and the published surface is untouched on both sides (packages/types has zero changed files; the new GanttConfigRestated alias is not re-exported by the package entry). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011SfZeFWrhGLHmfq61xbz4q
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
os-support-ai
commented
Aug 26, 2026
ACCEPT — objectui#6471 ( Two of the card's stated facts are measured false, and the dev's measurements win on both. Independently re-verified here. The count is twelve, not elevenRead from the declaration to its closing brace rather than a fixed window, Both ⭐ My own first count said 10, because I sliced a fixed 40 lines from the declaration instead of reading to the brace. Same class of error as the grep warning I put in the dispatch order, one instrument over: a window that cannot reach the end of the thing you are counting returns a number that looks like a count. Corrected before reporting, and worth stating because it is the third time today the seat has produced a wrong reading from a pattern that could not see the whole target. The "load-bearing narrowings" are not narrowings any moreTriage's charter said keep ⭐⭐ And the probe was proven non-vacuous before the result was believed — 7 controls, every one required to fail and every one did, including They are kept per the charter, now pinned as a measured state instead of inherited prose. That is the right disposition: the claim that was rotting is replaced by a claim that cannot rot silently. ⭐⭐⭐ Option A is refuted, not declinedThis is the finding I would keep from this card. Triage offered two routes and said the dev picks by measurement. Route A — lift the JSDoc onto
So the obvious tidy-up was a surface widening wearing a refactor's clothes. That is worth more than the pin itself. The pin's mechanism is the part that makes it real⭐⭐⭐ A pin phrased over Derivation ( Reverse verification: both legs flip the compiler, and that is the proof
⭐ And the reason no rebuild was needed is stated rather than assumed: Clause-② established on built artifacts
Documentation provably not lost: One gate not measured, reported in the gate's own words
Your open questionRecorded, not ruled. Option A is what shipped and I agree it is right for this PR: it obeys the charter's ⛔ while making the stale "load-bearing narrowing" claim non-inheritable, which is the durable half. Whether the two ex-narrowings should later be treated differently is a scope question for triage, and #6547 already captures the adjacent one. Not blocking. On the claim-protocol conflict you flagged: you assigned yourself; the dev on #6522 did the opposite under the same contract. That inconsistency is the substance of objectstack#12520 and I have carried your data point there. CI: 29 checks, zero failed at review time. Landing on green. Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#6471
Verified at
8ec2e80cd(the final commit — the gate union below was re-run on this exact tree).The measurement, re-derived on current
main— the card's number is staleThe card says eleven restated members; triage repeats it. On
mainatc18acb09e,GanttConfigExis atypealias atObjectGantt.tsx:106declaring twelve:parentField·typeField·baselineStartField·baselineEndField·groupByField·resourceView·assigneeField·effortField·capacity·quickFilters·autoZoomToFilter·timeSegmentsThe card's list of eleven omitted
timeSegments(it discussed that one separately), and its "nine pure duplicates" is one short of its own list. Twelve is the count that stands.What the measurement contradicts: neither "narrowing" narrows anything
The charter keeps
quickFiltersandtimeSegmentsas load-bearing NARROWINGS. Measured with a compiled type-level probe, all twelve members — those two included — are mutually assignable with theirGanttConfigtwin on currentmain. They narrow nothing today, and the reason is traceable rather than mysterious:timeSegments—getGanttConfigreads 24 more top-level keys ObjectGanttSchema does not declare — same defect as #5903, hidden by the index signature instead of a cast #6051/feat(types,plugin-gantt): declare the flattened GanttConfig face ObjectGantt reads (#6051) #6472 lifted the member ontoGanttConfigin@object-ui/types, and the shape it lifted is structurallyShiftSegmentsConfig;quickFilters— rc.6'sGanttConfigSchema.quickFiltersalready modelsfield/label/optionsexactly asQuickFilterDefdoes.They are kept anyway, per the charter, and both are now pinned as a measured state rather than as inherited prose. The probe was proven non-vacuous before this claim was believed: seven controls, all of which had to fail and did —
Mutual<string, number>refused, a cross-key mismatch refused, and neither operand of either pair beingany(ananyon either side makes the comparison unconditionally true).Route: the pin, not the JSDoc lift — chosen by measurement
Option A ("lift the JSDoc onto
GanttConfig, delete the restatements") is refuted, not disliked. Nine of the twelve keys arrive onGanttConfigfromSpecGanttConfig=z.input<typeof GanttConfigSchema>, a type declared in@objectstack/spec. TypeScript cannot attach JSDoc to a member a type merely inherits, and the spec emits no per-member documentation of its own. So Option A has exactly two spellings, and both are refused:GanttConfigto carry the JSDoc → this does not remove a duplicate declaration, it relocates it out of a package-private type into the published@object-ui/types, onto the very type both authoring faces derive from. Per clause ②, that is a widened published surface and a different review tier.Option B — the type-level mutual-assignability pin — closes the defect with neither cost. The prose stays exactly where it is (the diff is
+30 / −1onObjectGantt.tsx: pure addition, no line of documentation deleted), and no published surface moves.The mechanism: naming the local half is the whole fix
The pin is impossible to write against
GanttConfigEx, and writing it that way is the trap:GanttConfigEx['parentField']is alreadyGanttConfig['parentField'] & string, so it is assignable toGanttConfig['parentField']by construction. An assertion phrased over the intersection is green forever, no matter how far the two declarations drift — it measures nothing. Splitting the local half into a namedGanttConfigRestatedis what gives the pin two independent operands.The core invariant is derived over
keyof GanttConfigRestated, so there is no key list to fall out of date, and a violation names the key in the compiler error.Reverse verification — both legs, mutation proven on disk
Neither leg trusts an editor's exit code; each mutation was confirmed by anchored grep counts on both the injected and the removed text, plus a
git hash-objectcomparison against the HEAD blob. Both scripts carriedtrap … EXIT INT TERMwith absolute paths, and each restoration was proven byte-identical to the HEAD blob (not by the restore command's exit code).Leg 1 — the pin has teeth. Predicted direction: red, naming the key. Mutated
capacity?: number→capacity?: string(number;count 1→0,string;count 0→1, disk hash ≠ HEAD blob), then compiled:The error names
capacity— that is the instrument doing its job. Restored: disk hashe39b5b07…= HEAD blobe39b5b07….Leg 2 — the vacuity control guards itself. Predicted direction: TS2578. Mutated
Mutualinto an always-true form, then compiled:So the
@ts-expect-errorin the non-vacuity test is live: ifMutualever stops having teeth, the directive goes unused and the build fails. Restored: disk hash830af148…= HEAD blob830af148….No
@ts-expect-errordirective elsewhere was disturbed —packages/typestype-check(which compilesgantt-flat-config-declared-keys.test.ts, whose:354/:358directives pinquickFiltersandtimeSegments) is clean, withgrep -c 'error TS'= 0. This change touches zero files underpackages/types.Clause ② — established on built
dist/index.d.ts, not on source keywords@object-ui/types: no member added, removed or re-typed. The diff touches zero files in that package. Re-export chain shown to terminate:dist/index.d.ts:72re-exportsGanttConfigfrom './objectql.js'→dist/objectql.d.ts:141export type GanttConfig = SpecGanttConfig & {. That is the terminus, and it is untouched.@object-ui/plugin-gantt:GanttConfigRestateddoes not reach the published entry. After a realvite build,packages/plugin-gantt/dist/index.d.tscarries 0 occurrences of it (index.tsxre-exports named symbols explicitly and this is not among them). Positive control on the same file:QuickFilterDef, which is re-exported, appears 1 time — so the grep can find what is there.No published surface changes, which is precisely why this route was chosen over the JSDoc lift.
Verification
Every heavy command ran through the container's shared verify lock; each verdict below is the gate's own printed line, and exit codes were captured before any pipe.
pnpm --filter @object-ui/plugin-gantt run type-check(tsc --noEmit && tsc -p tsconfig.test.json)VERDICT command-exit 0vitest run— pin suite +quickfilter+shifts+blockPrecedenceTest Files 4 passed (4)·Tests 45 passed (45)vitest run— pin suite alone (non-vacuity: it really runs)Test Files 1 passed (1)·Tests 6 passed (6)pnpm --filter @object-ui/types run type-checkerror TScount 0 — no TS2578check-changeset-presence✅ … declares 1 changeset(s) … EMPTY frontmatter … a complete answer to this gatecheck-changeset-no-major✅ No changeset declares a 'major' bump.check:control-bytes·check:self-import·check:phantom-deps·check:vi-mock-specifiers·lint:coveragepnpm --filter @object-ui/plugin-gantt lintVERDICT command-exit 0— 0 errors; the new file contributes 0 findingsLint scope. The affected package was linted whole (
turbo run lint's own unit), so nothing was narrowed inside it. Onlypackages/plugin-gantthas changed files, and no other package's lint program input moved — established above on the builtdist/index.d.ts, not assumed.One gate is NOT MEASURED, and is reported as such rather than as green.
check:readme-exportsexits 1 with its own❌ the population COLLAPSED -- this run proves nothing, naming the cause:packagesRead: found 14, floor is 25, with23 unbuilt. It needs a full-workspace build, which CI performs. It reads no file this diff touches (no README changed; no package entry export added). Recorded as a prerequisite-not-met, not as a passing or failing measurement.Also self-scanned both changed files and the changeset for raw control bytes (
grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'): no matches.Scope
#6470— thedependencyFieldalias card in the same file family — is not addressed here and remains open, as the dispatch order directed.packages/types/src/feedback.tsandpackages/types/src/zod/feedback.zod.tswere not touched.Generated by Claude Code
Generated by Claude Code