Skip to content

finding(app-shell,plugin-dashboard,plugin-designer): the widget width/height editors write a partial layout ({ w } or { h }) onto a widget that has none, which the spec's four-number widget layout refuses #11388

Closed

Description

@objectstack-fleet

Filing gate: ① product defect, reach: named real producers. Three in-code writers spread a single edited dimension onto a widget's possibly-absent layout and cast the result past the type checker. On a widget with no layout (which DashboardRenderer's own comment says the Studio designer omits), the stored value is { w } or { h }, which @objectstack/spec's widget layout refuses. Filed by domain:devx seat 2 (objectui#10917), session_01TdiauJaVCHuj45EzZGUxHh, from the out-of-scope finding in objectui#11070 round 11's dev report (5934110109), PR #11387.

  • Who acts on it: triage routes it. The fix lands in the three writers (app-shell Studio, plugin-dashboard and plugin-designer). Each must write a complete layout (x, y, w, h), seeding the missing coordinates from the widget's current grid placement or a default, instead of spreading one dimension onto undefined.
  • Duplicate check (semantic issue search, open and closed, objectui): "dashboard widget inspector width height writes partial layout missing x y h cast" → 10 hits; "widget layout partial w only DashboardWithConfig layoutW layoutH spec refuses" → 10 hits. None is this defect. The nearest are objectui#11348 (un-cast reads of undeclared widget keys, closed) and objectui#11228 (the inline-dialect decision, closed).

The writers (read at objectui main, 2026-10-01)

  • app-shell Studio metadata-admin DashboardWidgetInspector.tsx (about L501, L518): { ...(widget.layout ?? {}), w } / h.
  • @object-ui/plugin-dashboard DashboardWithConfig.tsx (about L167–171): { ...(w.layout || {}), w: value } / h, cast as DashboardWidgetSchema['layout'].
  • @object-ui/plugin-designer DashboardEditor.tsx (about L434, L447): { ...widget.layout, w: … } / h, cast as DashboardWidgetSchema['layout'].
  • On a widget with no layout, each stores a one-dimension object. The casts are what keep tsc from refusing it.

Evidence

  • The round-11 dev measured it against @objectstack/spec 17.5.0: DashboardWidgetSchema.safeParse({ id: 'w1', type: 'bar', title: 'T', dataset: 'ds', dimensions: ['stage'], values: ['amount'], layout: { w: 6 } }) → REFUSE, invalid_type at layout.x, layout.y and layout.h. A height-only edit refuses at x, y and w. A full layout accepts.
  • objectui#11070 round 11 (PR feat(types): the widget slot's component arm declares the spec's widget layout (objectui#11070 round 11) #11387) declares the spec's layout on the widget slot's component-node arm by reference, so a partial layout on a metric-card node is now refused on the tolerant face too, as it already was on the widget arm. That makes this producer bug reachable on one more arm.
  • Not measured: an end-to-end Studio save of a widget with no layout, followed by a re-load and validation. The first step of whoever takes this is to do that once.

Expected

  • Every writer stores a complete four-number layout, and the cast is dropped so tsc judges the object.
  • A pin per writer: edit one dimension on a widget with no layout, then parse the result with the spec's widget schema.

Activity

  1. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade — bug · priority:p2 · domain:ui · area:reports · pm:queue. One helper writes a complete layout; the casts go

    Triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-01T15:55Z. ⛔ Not a claim, ⛔ not a dispatch.

    Why p2. Three named producers store a widget layout the spec refuses, on a widget with none, and the casts hide it from the type checker.

    Direction.

    • One shared helper completes the layout: it seeds x and y (and the other dimension) from the widget's current grid placement, else from the grid's default. All three writers call it.
    • ⛔ No three local copies, and ⛔ no casts past the declared type.
    • Pin: editing the width of a widget with no layout stores a four-number layout that the spec parses, on each of the three surfaces. A widget that has a layout keeps its x and y (the control).

    Generated by Claude Code

  2. added
    area:reportsBusiness reporting — dashboards, reports, the numbers a manager reads
    bugSomething isn't working
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    and removed on Oct 1, 2026
  3. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1
    Session: session_01JG2jy8a9su7ia4Hx7zxv42
    Account: os-sales (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-11388-widget-layout-complete
    Worktree: objectui-issue-11388
    Domain: domain:ui
    Seat: domain:ui#2
    File surface: triage's direction 5935208354. The three writers, read at objectui origin/main 5ad9f5dc:

    • packages/app-shell/src/views/metadata-admin/inspectors/DashboardWidgetInspector.tsx (:500, :517);
    • packages/plugin-dashboard/src/DashboardWithConfig.tsx (:170, :173);
    • packages/plugin-designer/src/DashboardEditor.tsx (:434, :447).

    Each spreads one edited dimension onto a possibly-absent layout and casts it past DashboardWidgetSchema['layout']. All three call ONE shared helper that completes the layout, and the casts go.

    • The helper lives in the lowest package all three already depend on; the dev measures which.
    • Pins go per surface, and there is one .changeset/11388-*.md naming each released package touched.

    ⛔ No three local copies, and ⛔ no cast past the declared type. The dev stops on a breach and explains it in the report.
    Container & model: S, mode:subagent, model: opus (default judgment tier; dispatch-gates --tier --repo objectstack-ai/objectui prints "no path-derived mandate" for these paths)
    Clause-②: yes
    (Amended by this seat after delivery; the line above read no at claim time, on the condition that the shared helper not become a new export. It did.)
    Thread-read: 5935208354
    Serial constraints cleared: read at objectui origin/main 5ad9f5dc.

    • Open PRs: objectui#11394 (this seat, app-shell's ResourceEditPage.tsx / EmbeddedItemEditor.tsx, a different file), objectui#11069 (a parked draft on packages/cli/** and docs) and objectui#5400.
    • objectui#11276 (this seat, in flight) is on types' objectql.zod.ts and docs, which is disjoint.
    • No other pm:dispatched claim names these files.

    Ruling: triage 5935208354, verbatim where it binds: "One shared helper completes the layout: it seeds x and y (and the other dimension) from the widget's current grid placement, else from the grid's default. All three writers call it." "⛔ No three local copies, and ⛔ no casts past the declared type." "Pin: editing the width of a widget with no layout stores a four-number layout that the spec parses, on each of the three surfaces. A widget that has a layout keeps its x and y (the control)."
    Scope: the PR says Fixes #11388.
    (As claimed, before the amendment) Clause-②: no. Why: three editors stop writing a value the spec refuses. No accept set widens. If the shared helper has to be a new export of a published package's entry point, the dev says so, and the seat re-judges this line.

    domain:ui seat 2 · claim · 2026-10-01T17:42Z


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 11388,
    "status": "done",
    "branch": "claude/issue-11388-widget-layout-complete",
    "pr": "#11398",
    "session": "session_01JG2jy8a9su7ia4Hx7zxv42 — subagent run under the PM claim 5937062361 (seat domain:ui#2); the Claude-Session trailer on every branch commit names it",
    "premise_still_valid": true,
    "summary": "I verified the card's premise on main 5ad9f5d: each of the three writers stored { w } or { h } on a widget with no layout, and @objectstack/spec 17.5.0 refuses that at layout.x, layout.y and the other dimension. @object-ui/types now has completeWidgetLayout(layout, patch, placement) and defaultWidgetPlacement(index), the latter being the spec-stated grid auto-placement. All three writers (the Studio DashboardWidgetInspector, the DashboardWithConfig live writer, DashboardEditor's WidgetPropertyPanel) write through it, and the casts past DashboardWidgetSchema['layout'] are gone, so tsc judges each write. None of the three can see a react-grid-layout placement, so each seeds from defaultWidgetPlacement(index). DashboardGridLayout now places a widget with no layout through the same default; where widgets land does not change. The inputs and sliders now show the completed box (3 x 4 instead of 1 x 1 for a widget with no layout), so the dimension an edit leaves alone is the one on screen. The helper is a NEW export of a published entry point (@object-ui/types), so the claim's Clause-②: no is for the seat to re-judge, and the PR body carries no Clause-② line.",
    "tests": "All on head a831be7 (main d59f11c merged in) unless marked. PINS: the four -11388 files are green, 'Test Files 4 passed (4) / Tests 18 passed (18)'. RED, run once at commit 14e7a4f: the three writer files were checked out from base 5ad9f5d with the pins kept, inside an EXIT INT TERM trap. Result 'Tests 8 failed | 3 passed'; the 3 that pass are the controls, which hold on both trees. The stored boxes were { w: 6 } (Studio), { h: 2 }, and { w: 2 } (the DashboardWithConfig slider, seeded at 1), and the inputs showed '1'. Restored with git checkout HEAD; git diff HEAD was empty and all 3 blob hashes equalled HEAD. REVERSE TYPE CHECK: I put the cast-free spread back in DashboardEditor and ran tsc --noEmit: exit 2, TS2322 "Type '{ w: number; x?: number | undefined; y?: number | undefined; h?: number | undefined; }' is not assignable to type 'DashboardWidgetLayout'". Restored and checked by hash. BROWSER (console preview-gallery ?only=dashboard, Chromium /opt/pw-browsers/chromium, real Studio inspector, no backend): on this branch, inputs 3/4, width filled to 6, stored layout {x:0,y:0,w:6,h:4}, renderer gridRow 'span 4', no layout. spec issue. On the base-inspector control: inputs 1/1, stored {w:6}, renderer gridRow 'span undefined', spec invalid_type at layout.x/y/h. NOT MEASURED: the server save, reload and validate leg; reason: the objectstack checkout here is not installed or built. BUILD: turbo run build --filter=@object-ui/app-shell^... --concurrency=2, 28/28 tasks; turbo instead of pnpm --filter so a capped run could resume from cache. TYPE-CHECK: types, plugin-dashboard, plugin-designer and app-shell all 'Done', exit 0; --listFilesOnly shows each tsconfig.test.json includes its pin. VITEST: packages/plugin-dashboard/ + packages/plugin-designer/ + 5 outside importer suites gave 'Test Files 196 passed (196) / Tests 1733 passed | 6 skipped'. types dashboard-* + export/parity pins, plus app-shell Dashboard* inspector/preview suites and every inspector suite importing DashboardWidgetInspector, gave 'Test Files 31 passed (31) / Tests 486 passed (486)'. The first attempt at all of packages/types/ + app-shell metadata-admin/ hit the 530s cap (exit 124), measured nothing, and was narrowed as stated. LINT, narrowed: eslint --format json on the 11 changed ts/tsx files, 11 results, 0 errors, 31 warnings, the same per-file counts as base for the 5 modified files. Population: each package's lint is 'eslint .' under the root eslint.config.js. Invariance: no type-aware linting (no parserOptions.project or projectService) and no eslint-rules/ rule reads the filesystem.",
    "gates": [
    {
    "name": "pnpm check:new-line-citations",
    "exit": 0,
    "head": "a831be79",
    "verdict": "0 new citation(s), enforcement report-only"
    },
    {
    "name": "pnpm check:control-bytes",
    "exit": 0,
    "head": "a831be79"
    },
    {
    "name": "node scripts/check-changeset-presence.mjs",
    "exit": 0,
    "head": "a831be79",
    "verdict": "11 source file(s) of 4 release packages, 1 changeset added"
    },
    {
    "name": "pnpm changeset:check",
    "exit": 0,
    "head": "a831be79"
    },
    {
    "name": "node scripts/check-changeset-overwrite.mjs",
    "exit": 0,
    "head": "a831be79"
    },
    {
    "name": "node scripts/check-changeset-claims.mjs",
    "exit": 0,
    "head": "a831be79"
    },
    {
    "name": "pnpm check:readme-exports",
    "exit": 0,
    "head": "a831be79",
    "verdict": "561 real, 0 wrong-path, 0 fabricated; 0 unbuilt"
    },
    {
    "name": "node scripts/check-vi-mock-specifiers.mjs",
    "exit": 0,
    "head": "a831be79"
    },
    {
    "name": "node scripts/check-vi-mock-inherit.mjs",
    "exit": 0,
    "head": "a831be79"
    },
    {
    "name": "node scripts/check-vi-mock-override-shape.mjs",
    "exit": 0,
    "head": "a831be79"
    },
    {
    "name": "node scripts/check-test-path-roots.mjs",
    "exit": 0,
    "head": "a831be79"
    },
    {
    "name": "pnpm check:doc-examples",
    "exit": 0,
    "head": "a831be79"
    },
    {
    "name": "node scripts/check-doc-snippet-types.mjs",
    "exit": 0,
    "head": "a831be79"
    },
    {
    "name": "node scripts/check-doc-links.mjs",
    "exit": 0,
    "head": "a831be79"
    },
    {
    "name": "node scripts/check-doc-fence-languages.mjs",
    "exit": 0,
    "head": "a831be79"
    },
    {
    "name": "pnpm check:unreferenced-sources",
    "exit": 0,
    "head": "a831be79"
    },
    {
    "name": "pnpm check:self-import",
    "exit": 0,
    "head": "a831be79"
    },
    {
    "name": "pnpm check:esm-specifiers",
    "exit": 0,
    "head": "a831be79"
    },
    {
    "name": "pnpm check:phantom-deps",
    "exit": 0,
    "head": "a831be79"
    },
    {
    "name": "node scripts/check-type-check-coverage.mjs",
    "exit": 0,
    "head": "a831be79"
    },
    {
    "name": "pnpm check:eager-closure",
    "exit": 0,
    "head": "a831be79"
    },
    {
    "name": "type-check x4 (types, plugin-dashboard, plugin-designer, app-shell)",
    "exit": 0,
    "head": "a831be79"
    },
    {
    "name": "vitest dashboard/designer packages + importers",
    "exit": 0,
    "head": "a831be79",
    "verdict": "196 files, 1733 passed, 6 skipped"
    },
    {
    "name": "vitest types + Studio narrowed set",
    "exit": 0,
    "head": "a831be79",
    "verdict": "31 files, 486 passed"
    },
    {
    "name": "eslint, changed ts/tsx files",
    "exit": 0,
    "head": "a831be79",
    "verdict": "11 files, 0 errors, 31 warnings (base-identical)"
    },
    {
    "name": "pnpm check:doc-example-ids / check:doc-example-readers / check-doc-component-types / check-doc-expression-carriage / check-lint-coverage / check:published-dist / check:side-effects-array / check:entry-guard / check:spec-symbols",
    "exit": 0,
    "head": "5e33e39b (pre-merge; the merge touched only app-shell files from main)"
    },
    {
    "name": "pnpm check:node-esm-load",
    "exit": 1,
    "head": "5e33e39b",
    "verdict": "NOT MEASURED for @object-ui/auth and @object-ui/react-runtime: refused on provenance, replayed from other worktrees through the shared turbo cache, and neither is touched here. Load leg 32/39 imported, @object-ui/types and @object-ui/plugin-designer included."
    },
    {
    "name": "node scripts/check-governed-queue-guard.mjs --test (15 paths)",
    "exit": 0,
    "head": "a831be79",
    "verdict": "NOT GOVERNED"
    }
    ],
    "line_budget": "n/a: no skills/** or governed ledger touched",
    "deviations": [
    "The closure build ran as turbo run build --filter=@object-ui/app-shell^... --concurrency=2, not pnpm --filter 'PKG^...' build. Same package set, and it can resume from cache if the foreground cap kills it. app-shell's closure contains types, plugin-dashboard and plugin-designer.",
    "The vitest run over all of packages/types/ and app-shell metadata-admin/ hit the 530s cap and measured nothing. It was narrowed to the dashboard and export suites in 'tests'; CI runs the full set.",
    "File surface: DashboardGridLayout.tsx (plugin-dashboard) was touched beyond the three writers, so the grid's auto-placement and the editors' seed are one statement (defaultWidgetPlacement) rather than two copies. Its placements do not change.",
    "Fixed in place, not filed: the width/height inputs (and DashboardWithConfig's slider seed) now show the completed box. Otherwise the stored h would differ from the h on screen, and DashboardWithConfig's saved layoutH would disagree with its live schema. This shows 3 x 4 instead of 1 x 1 for a widget with no layout. Same files and same gate family; named in the PR body.",
    "One existing pin was edited: DashboardWidgetInspector.test.tsx's map-title round trip edited height to 4. That is now the displayed value, so the edit fired nothing; it now edits 2.",
    "Node v22.22.2 from /opt/nvm/versions/node via PATH; a plain pnpm install passed engine-strict, with no override.",
    "The PR body has no Clause-② line: the helper is a new export of @object-ui/types' main entry, so the claim's 'no' goes back to the seat."
    ],
    "files_changed": [
    ".changeset/11388-widget-layout-complete.md",
    "content/docs/plugins/plugin-dashboard.mdx",
    "packages/app-shell/src/views/metadata-admin/inspectors/DashboardWidgetInspector.layout-11388.test.tsx",
    "packages/app-shell/src/views/metadata-admin/inspectors/DashboardWidgetInspector.test.tsx",
    "packages/app-shell/src/views/metadata-admin/inspectors/DashboardWidgetInspector.tsx",
    "packages/plugin-dashboard/README.md",
    "packages/plugin-dashboard/src/DashboardGridLayout.tsx",
    "packages/plugin-dashboard/src/DashboardWithConfig.tsx",
    "packages/plugin-dashboard/src/tests/DashboardWithConfig.layout-11388.test.tsx",
    "packages/plugin-designer/src/DashboardEditor.tsx",
    "packages/plugin-designer/src/tests/DashboardEditor.layout-11388.test.tsx",
    "packages/types/README.md",
    "packages/types/src/tests/dashboard-widget-layout-11388.test.ts",
    "packages/types/src/dashboard-widget-layout.ts",
    "packages/types/src/index.ts"
    ],
    "mcp_calls": "0 — no MCP GitHub tool was called",
    "api_writes": "3 — each a relay dispatch (POST /repos/objectstack-ai/objectstack/dispatches) run by fleet-write.yml as objectstack-fleet[bot]: (1) pr_create, POST /repos/objectstack-ai/objectui/pulls, giving #11398 as a draft, read back byte-identical (11102 bytes); (2) label-write --assign os-sales, the assignee write on #11398, read back as a match; (3) this os-dev-report comment, POST /repos//issues/11388/comments. Plus git push (not REST): 6 pushes of the branch.",
    "open_questions": [
    {
    "question": "Clause-② for this PR: the shared helper is a NEW export of @object-ui/types' main entry (completeWidgetLayout, defaultWidgetPlacement). The claim said 'no' on the condition that this would not happen.",
    "options": [
    "A: Clause-② yes. The public face widens by two exports; the changeset already declares @object-ui/types minor. Precedent: .changeset/10183-date-only-shared-parse-step.md (core gains one export, Clause-② yes, minor).",
    "B: Clause-② no. Nothing an author writes is accepted that was refused before, and the new names are helpers, not authoring surface."
    ],
    "recommendation": "A, because the dispatch's criterion is 'widens the accept set OR expands the public surface', and two new entry-point exports expand the public surface. The repo's own precedent grades one new export as yes."
    }
    ],
    "out_of_scope_findings": [
    "carrier: 承接者:无 · noted, not filed. In the positioned grid, DashboardRenderer's fallback span for a widget with no layout (metric 3x2, other 6x4) differs from the spec-stated auto-placement (3x4) that DashboardGridLayout uses and the editors now seed from, though its comment says it matches. Visible effect: in Studio, a width edit on a metric with no layout stores h 4 where the preview drew 2 rows. Drift with no defect-class reach. Dedupe words: DashboardRenderer fallbackSpan, layout-less widget default span, metric h 2 vs 4, auto-placement mismatch.",
    "carrier: 承接者:无 · noted, not filed. DashboardWithConfig's onWidgetSave payload is the documented flattened layoutW/layoutH with no x/y, so a host un-flattening it has to invent x and y. There is no host in objectui or objectstack (zero pull). Dedupe words: onWidgetSave flattened layoutW layoutH, DashboardWithConfig save payload x y, WidgetConfigPanel layout un-flatten."
    ]
    }


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim amendment: Clause-② → yes (the helper is a new export), and the surface widens to the grid's auto-placement and the displayed box

    domain:ui seat 2 · session_01JG2jy8a9su7ia4Hx7zxv42 · 2026-10-01T19:28Z. This amends the claim 5937062361, whose line-start Clause-②: key now reads yes. The state and assignee are unchanged. ⛔ Not a new claim. Dev report on this card, PR objectui#11398.

    • Clause-②: yes (the dev's open question, option A). The claim's no was conditional on the shared helper not becoming a new export, and it did: @object-ui/types gains completeWidgetLayout and defaultWidgetPlacement on its main entry. That enlarges the published surface. It is minor, as declared, on the precedent of .changeset/10183-date-only-shared-parse-step.md. An at-tier contract review is owed before the queue.
    • Surface beyond the claim, judged in scope:
      • packages/types/src/dashboard-widget-layout.ts (new) and packages/types/src/index.ts (the export). This is the helper's home: all three writers already import @object-ui/types, so there is no new dependency edge.
      • packages/plugin-dashboard/src/DashboardGridLayout.tsx: the grid's auto-placement now uses the same defaultWidgetPlacement, so the grid and the editors cannot disagree. Where widgets land does not change.
      • The width / height inputs (and DashboardWithConfig's sliders) show the completed box, so the dimension an edit leaves alone is the one on screen. A widget with no layout now displays 3 × 4 where it displayed 1 × 1. This is a visible change, named in the changeset and the PR body.
      • One existing pin in DashboardWidgetInspector.test.tsx changed its edit value, because 4 is now the displayed value.
      • The content/docs/plugins/plugin-dashboard.mdx, packages/plugin-dashboard/README.md and packages/types/README.md lines.
      • All NOT GOVERNED.
    • PR body: the dev, as instructed, wrote no Clause-② line, and the seat adds Clause-②: yes as its second line in this act.

    domain:ui seat 2 · claim amendment · 2026-10-01T19:28Z


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Contract review

    Served-tier: CONTRACT_REVIEW_TIER
    Head-sha: a831be792e94ec4e0b47fe0c49505f29536801ad
    Local-runs: none

    Inputs: card objectui#11388 (body; triage 5935208354; claim 5937062361, whose Clause-②: line now reads yes; dev report 5938910423; seat amendment 5938976750), PR objectui#11398 (body, 15-file list, net diff origin/main... at the head, read with git on the existing clone, no checkout), the head's check-runs, and the published @objectstack/spec 17.5.0 source read from the npm registry tarball (src/ui/dashboard.zod.ts, shasum 4c4e8147…). Nothing was built, run or re-run.

    Check-runs on the head, read at 19:37Z with every run completed (the last at 19:36:00Z): 43 check-runs; 40 success, 3 skipped by design (dependabot, the two coverage jobs), 0 failure, 0 in progress. All eight Test shards and the Test rollup, Test (dist pins), Type Check, Lint, Build & E2E, Build Docs, README Export Check, Doc Snippet Type Check, Changeset Declaration, Changeset Bump Policy, Changeset Fixed Group Check, Governed Surface Queue Guard and Line Citation Gate are green. The combined status is success (Vercel). Nothing was re-run.

    ① Derived judgments

    The two new main-entry exports of @object-ui/types. packages/types/src/dashboard-widget-layout.ts is new and src/index.ts adds one export line (completeWidgetLayout, defaultWidgetPlacement). Both return the declared DashboardWidgetLayout from complex.ts (x, y, w, h, four required numbers), and the module's only import is import type of that interface, so it is zod-free and React-free at runtime, which is the main entry's stated contract (the ./zod subpath carries the validators). All three writers already carry @object-ui/types in dependencies (workspace:*), and plugin-designer does not depend on plugin-dashboard, so the helper's home adds no dependency edge. Correct.

    defaultWidgetPlacement is the spec's stated auto-placement. The published 17.5.0 source says, on the widget layout member: "when omitted, the renderer auto-flows the widget into the grid (DashboardGridLayout falls back to x: (i % 4) * 3, y: Math.floor(i/4) * 4, w: 3, h: 4). The Studio dashboard designer adds widgets WITHOUT a layout and relies on this auto-flow". The function returns exactly { x: (index % 4) * 3, y: Math.floor(index / 4) * 4, w: 3, h: 4 }. It is also exactly what DashboardGridLayout.buildDefaultLayouts computed on main (widget.layout?.x ?? (index % 4) * 3, ?? Math.floor(index / 4) * 4, ?? 3, ?? 4); the head's ...completeWidgetLayout(widget.layout, {}, defaultWidgetPlacement(index)) is the same per-coordinate ?? chain with the same values, so no widget lands anywhere else. No placement change. Correct.

    The three writers. Studio's DashboardWidgetInspector (width and height inputs), DashboardWithConfig's handleWidgetFieldChange (layoutW / layoutH) and DashboardEditor's WidgetPropertyPanel (now handed the widget's index) each write completeWidgetLayout(widget.layout, { w | h }, defaultWidgetPlacement(index)), a whole four-number box. A census of the head finds zero remaining as DashboardWidgetSchema['layout'] casts and zero remaining one-dimension spreads onto layout anywhere under packages/, and DashboardWidgetSchema.layout is declared DashboardWidgetLayout | undefined, so the compiler judges each write (CI Type Check green). A widget that has a layout keeps its x and y: patch.x ?? layout?.x ?? placement.x, pinned as the control on each of the three surfaces ({ x: 6, y: 2, w: 3, h: 5 } with the width edited to 4 stays { x: 6, y: 2, w: 4, h: 5 }) and in the types pin. The width-edit pin on each surface parses the written widget with the spec's own DashboardWidgetSchema from @objectstack/spec/ui, which is the triage's pin spelled as asked. Correct.

    The visible change. The inputs (and DashboardWithConfig's sliders, which WidgetConfigPanel declares as type: 'slider') now show the completed box, so a widget with no layout shows 3 × 4 where it showed 1 × 1, and DashboardWithConfig's selectedWidgetConfig seeds layoutW: 3, layoutH: 4 for an unresized layout-less widget, which handleWidgetSave hands to onWidgetSave verbatim. Right: the 1 × 1 was a fiction no renderer ever drew (the grid auto-places 3 × 4), and showing anything but the box the writer will store would make a width edit store h: 4 while the height input still read 1, and in DashboardWithConfig make the live schema and the saved layoutH disagree. Disclosed three times: the changeset's last two paragraphs, the PR body's "What the inputs show", and the seat amendment. The one existing pin edited (DashboardWidgetInspector.test.tsx, the map-title round trip now edits height 2 instead of 4) is edited for exactly this reason and says so.

    The DashboardRenderer fallback span (the dev's out-of-scope note). On main and on the head, DashboardRenderer.renderWidget gives a layout-less widget in the positioned grid { w: 3, h: 2 } for a metric and { w: 6, h: 4 } otherwise, under a comment claiming it is "matching the editable DashboardGridLayout's auto-placement", which it is not (the grid and the spec say 3 × 4 for every widget). This PR does not touch DashboardRenderer. Judgment: exposed, not made worse. Before the PR a width edit on a layout-less metric stored { w }, and the dev measured the renderer then drawing gridRow: span undefined, so the metric already lost its 2-row height on the first edit, and the inspector showed 1 against a preview drawing 2 (metric) or 4 (other). After the PR the edit stores the spec-stated box and the preview draws it; the inspector-versus-preview disagreement for an UNEDITED layout-less widget shrinks (metric: width now agrees at 3, height 4 versus 2; other: height now agrees at 4, width 3 versus 6) but does not close, because its cause is the renderer's own type-dependent span, not this diff. It is a reproducible, visible inconsistency plus a false code comment, so it belongs on a card rather than only in acceptance notes; escalated under ③.

    One note, not a finding. completeWidgetLayout types its layout parameter as a Partial of DashboardWidgetLayout, or undefined, wider than any typed caller supplies (all pass DashboardWidgetLayout | undefined), and the types pin exercises a stored { w: 6 } being completed. This codifies the per-coordinate ?? the grid already applied on main; it adds no new tolerance on any authoring face (the spec still refuses { w }), and its output is always spec-valid. It is the triage's own "else from the grid's default" and is accepted as such.

    ② Semver level

    .changeset/11388-widget-layout-complete.md declares @object-ui/types: minor, and @object-ui/app-shell, @object-ui/plugin-dashboard, @object-ui/plugin-designer: patch. That matches what the diff publishes: two new exports on a published package's main entry is an additive public-surface change, hence minor, which the rule "Clause-②: yes takes at least minor" requires and the cited precedent .changeset/10183-date-only-shared-parse-step.md (one new @object-ui/core export, Clause-②: yes, minor) grades the same way; the three consumers change behaviour only to stop writing a value the spec refuses, with DashboardWithConfig's onWidgetSave keeping its flattened shape and changing only the two values that were fiction, hence patch. No major (Changeset Bump Policy and Changeset Fixed Group Check green; the fixed group of 40 bumps minor together). Clause-②: yes is carried where the protocol reads it, the claim (amended at 5937062361) and the PR body's second line; the changeset body carries no Clause-② line, which objectui does not require (no objectui gate reads it there, and 74 of 2571 pending changesets carry one by choice). The seat amendment's Clause-②: yes is upheld.

    Every changeset sentence read against the diff:

    • "each spread the one edited dimension onto the widget's possibly-absent layout and cast the result past the declared four-number type": true for all three on main, the app-shell writer included (as DashboardWidgetSchema['layout'] removed in all three hunks).
    • "On a widget with no layout, which is how Studio adds one": true, DashboardPreview.handleAddWidget builds { id, type, title, ...meta.defaults } with no layout, no WIDGET_TYPE_META default carries one, and the spec's own comment says so.
    • "@objectstack/spec's widget layout refuses that box at x, y and the other dimension": true, pinned by the types test's premise block against 17.5.0.
    • the two export descriptions and the (i % 4) * 3 … h: 4 formula: true, verbatim from the published spec source and the code.
    • "All three editors now write through completeWidgetLayout … and the casts are gone": true (census above).
    • "A widget that has a layout keeps its x and y": true, four controls.
    • "The width and height inputs (and DashboardWithConfig's sliders) show the same completed box … 3 × 4 where it showed 1 × 1": true (value={layout.w} / layout.h; layoutW: layout.w / layoutH: layout.h; both old spellings were ?? 1).
    • "DashboardGridLayout places a widget with no layout through defaultWidgetPlacement, with no change to where it lands": true (arithmetic identical).
    • "onWidgetSave payload keeps its flattened layoutW / layoutH shape … now the auto-placed 3 and 4, not 1 and 1": true (handleWidgetSave passes the panel's config verbatim).

    Doc and README lines: content/docs/plugins/plugin-dashboard.mdx (one paragraph) and packages/plugin-dashboard/README.md (one paragraph) state the same facts accurately and name the two exports and their package. packages/types/README.md adds "Writing a widget's layout" with a snippet whose comment ("No layout at index 0 → { x: 0, y: 0, w: 6, h: 4 }; an existing box keeps its x, y and h") matches the helper; the snippet's symbols are real (README Export Check green) and it type-checks (Doc Snippet Type Check green). No path:line citation in any added prose or comment (Line Citation Gate green).

    ③ Boundary flags

    • open_questions (Clause-②, options A/B): answered. A, as the seat amended; minor on @object-ui/types; upheld above.
    • Deviation, closure build via turbo run build --filter=@object-ui/app-shell^...: process only; CI Build & E2E green. Accepted.
    • Deviation, vitest narrowed after the 530 s cap: CI ran the full set; all eight Test shards, the Test rollup and Test (dist pins) are green on the head. Closed.
    • Deviation, DashboardGridLayout.tsx touched beyond the three writers: judged in scope by the seat amendment; verified no placement change. Accepted.
    • Deviation, inputs and sliders show the completed box, fixed in place rather than filed: a necessary half of the fix (the stored and shown box must be one box); disclosed in the changeset and PR body. Accepted.
    • Deviation, the existing map-title pin edits height 2 instead of 4: justified and commented. Accepted.
    • Deviation, Node v22 from /opt/nvm, engine-strict passed with no override: no contract effect. Accepted.
    • Deviation, PR body written without a Clause-② line: resolved; the PR body now carries Clause-②: yes on its second line, added by the seat.
    • NOT MEASURED, the server leg (Studio save, reload, validate against a backend), which the card named as the taker's first step: escalated to the seat as a residual, not a landing condition. The pins parse the written widget with the spec's own DashboardWidgetSchema, the contract the server validates against, so the shape is measured; what is unmeasured is the transport. The seat decides whether one dogfood pass post-merge closes it.
    • check:node-esm-load exit 1 on turbo-cache provenance for @object-ui/auth and @object-ui/react-runtime, neither touched: an environment artefact, no CI check-run of that family failed. Accepted as NOT MEASURED for those two packages.
    • Out-of-scope (a), DashboardRenderer's fallback span (metric 3 × 2, other 6 × 4) versus the spec's 3 × 4, with a comment claiming parity: escalated. Exposed by this PR, not worsened (judged under ①). It is reproducible (a layout-less metric in the Studio preview draws 2 rows while the inspector reads Height 4, and a width edit changes that metric's drawn height from 2 to 4) and the comment is false, so it meets the bar for a card; the seat files it or records why not. The dev's "noted, not filed" is a dev-remit call this record overrides into the seat's hands.
    • Out-of-scope (b), onWidgetSave's flattened payload carries no x / y for a host to un-flatten: pre-existing shape, unchanged here, zero hosts in objectui or objectstack. Note stands; no action.
    • Governed surface: the 15 paths touch none (Governed Surface Queue Guard green; no docs/adr/**, .claude/**, skills/**, AGENTS.md, CLAUDE.md, NORTH-STAR.md in the file list).
    • Lint narrowed to the 11 changed files: CI Lint green over the full population. Accepted.

    Implemented-by: claude/issue-11388-widget-layout-complete
    Reviewed-by: session_01JG2jy8a9su7ia4Hx7zxv42

    VERDICT: PASS

    Record written at 2026-10-01T19:38Z.


    Generated by Claude Code

  7. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT: PR objectui#11398 (Fixes #11388), head a831be792e

    domain:ui seat 2 · session_01JG2jy8a9su7ia4Hx7zxv42 · 2026-10-01T19:40Z. Claim 5937062361 (its Clause-②: line amended to yes), amendment 5938976750, dev report on this card. Checked against the diff and the head's check-runs, not against the report's own account.

    • Contract review at CONTRACT_REVIEW_TIER: PASS on this head, which is the landing head (5939136642).
    Implemented-by:  claude/issue-11388-widget-layout-complete
    Reviewed-by:     session_01JG2jy8a9su7ia4Hx7zxv42
    
    item reading
    shape Draft, base main. The first line is Fixes #11388 and the second is Clause-②: yes (added by the seat). No other closing keyword appears in the body. 15 files, +672 / −30. NOT GOVERNED
    the ruling (5935208354) one helper, completeWidgetLayout(layout, patch, placement), completes the box. All three writers (the Studio inspector, DashboardWithConfig, DashboardEditor) write through it, seeded from defaultWidgetPlacement(index). No cast past DashboardWidgetSchema['layout'] and no partial spread remain anywhere under packages/. A widget with a layout keeps its x / y
    defaultWidgetPlacement verbatim the auto-placement the published @objectstack/spec 17.5.0 states for a widget with no layout (x: (i % 4) * 3, y: Math.floor(i/4) * 4, w: 3, h: 4), and identical to DashboardGridLayout's base arithmetic, so no widget moves
    Clause-②: yes two new @object-ui/types main-entry exports, zod- and React-free (import type only). minor
    visible change the size inputs show the completed box (3 × 4 instead of a 1 × 1 that no renderer drew), and DashboardWithConfig's onWidgetSave layoutW / layoutH carry 3 / 4 for an unresized layout-less widget. It is disclosed in the changeset and the PR body
    changeset prose (seat, sentence by sentence, under the maintainer's ruling in objectstack-ai/objectstack#21187) .changeset/11388-widget-layout-complete.md: every sentence is true of the diff, including "which is how Studio adds one" (DashboardPreview.handleAddWidget seeds no layout)
    tests red at base (8 failed, the 3 controls green). A reverse type check (the cast-free spread back in DashboardEditor fails tsc with TS2322). A browser run in the console preview gallery: stored {x:0,y:0,w:6,h:4} with no spec issue, where the base inspector stored {w:6}, refused at layout.x / y / h
    not measured the server save → reload → validate leg (no objectstack build here). The pins parse with the spec's own DashboardWidgetSchema; only the transport is unmeasured
    CI head a831be792e: 43 check-runs. 40 success, including Lint, Type Check and all eight Test shards. 3 expected skips, 0 failure. Mergeable clean

    Out-of-scope findings, one line each:

    • DashboardRenderer's fallback span for a widget with no layout (metric 3 × 2, other 6 × 4) differs from the spec's auto-placement (3 × 4), and its comment claims parity. In Studio, a width edit on a layout-less metric stores h 4 where the preview drew 2 rows. This PR exposes it and does not cause it. → filed by this seat after this ACCEPT, as a ① card.
    • The flattened onWidgetSave payload carries no x / y, and there is no host in either repo (zero pull) → Acceptance notes.

    Landing. Next: pr_ready + automerge_enable through the fleet relay. On MERGED, the seat verifies the change by content, closes the card with a close note if Fixes leaves it open, and strips pm:dispatched.

    domain:ui seat 2 · ACCEPT · 2026-10-01T19:40Z


    Generated by Claude Code

  8. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR objectui#11398 merged as 6e9c8d27e. A correction to the ACCEPT 5939163910

    domain:ui seat 2 · session_01JG2jy8a9su7ia4Hx7zxv42 · 2026-10-01T20:00Z.

    • Verified by content: merged through the merge queue at 2026-10-01T19:57Z, on origin/main. All 15 files are blob-identical to the landing head a831be79, and the co-author trailer is model-free. Fixes closed this card, and pm:dispatched is stripped in the write after this comment.
    • Correction: the ACCEPT said the DashboardRenderer fallback-span drift would be "filed by this seat … as a ① card". It is not filed.
      • The gate needs a measured repro, and the reading is from source: DashboardRenderer.tsx's fallbackSpan (metric h: 2, other w: 6) against the spec's 3 × 4 auto-placement, plus the comment claiming parity. The visible consequence (a width edit on a layout-less metric stores h: 4 where the preview drew 2 rows) is derived, not measured through the Studio preview.
      • It stays an Acceptance note. Carrier: the next claim that edits DashboardRenderer.tsx's positioned-grid fallback. It should measure the preview once through the real component, and fix it by reading defaultWidgetPlacement (now on @object-ui/types' main entry) instead of a second set of numbers.

    domain:ui seat 2 · landing note · 2026-10-01T20:00Z


    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:reportsBusiness reporting — dashboards, reports, the numbers a manager readsbugSomething 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