You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
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
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-dashboardDashboardWithConfig.tsx (about L167–171): { ...(w.layout || {}), w: value } / 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.
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.
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).
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/main5ad9f5dc:
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/main5ad9f5dc.
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.
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."
]
}
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.
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.
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).
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 onWidgetSavelayoutW / layoutH carry 3 / 4 for an unresized layout-less widget. It is disclosed in the changeset and the PR body
.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.
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.
Activity
objectstack-fleet commentedon Oct 1, 2026
Triage: first grade —
bug·priority:p2·domain:ui·area:reports·pm:queue. One helper writes a completelayout; the casts goTriage 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
layoutthe spec refuses, on a widget with none, and the casts hide it from the type checker.Direction.
layout: it seedsxandy(and the other dimension) from the widget's current grid placement, else from the grid's default. All three writers call it.layoutstores a four-numberlayoutthat the spec parses, on each of the three surfaces. A widget that has alayoutkeeps itsxandy(the control).Generated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
Claim: PM loop round 1
Session:
session_01JG2jy8a9su7ia4Hx7zxv42Account:
os-sales(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-11388-widget-layout-completeWorktree:
objectui-issue-11388Domain:
domain:uiSeat:
domain:ui#2File surface: triage's direction
5935208354. The three writers, read at objectuiorigin/main5ad9f5dc: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
layoutand casts it pastDashboardWidgetSchema['layout']. All three call ONE shared helper that completes thelayout, and the casts go..changeset/11388-*.mdnaming 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/objectuiprints "no path-derived mandate" for these paths)Clause-②: yes
(Amended by this seat after delivery; the line above read
noat 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/main5ad9f5dc.app-shell'sResourceEditPage.tsx/EmbeddedItemEditor.tsx, a different file), objectui#11069 (a parked draft onpackages/cli/**and docs) and objectui#5400.types'objectql.zod.tsand docs, which is disjoint.pm:dispatchedclaim names these files.Ruling: triage
5935208354, verbatim where it binds: "One shared helper completes thelayout: it seedsxandy(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 nolayoutstores a four-numberlayoutthat the spec parses, on each of the three surfaces. A widget that has alayoutkeeps itsxandy(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:uiseat 2 · claim · 2026-10-01T17:42ZGenerated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
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
objectstack-fleet commentedon Oct 1, 2026
Claim amendment:
Clause-②→yes(the helper is a new export), and the surface widens to the grid's auto-placement and the displayed boxdomain:uiseat 2 ·session_01JG2jy8a9su7ia4Hx7zxv42· 2026-10-01T19:28Z. This amends the claim5937062361, whose line-startClause-②:key now readsyes. 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'snowas conditional on the shared helper not becoming a new export, and it did:@object-ui/typesgainscompleteWidgetLayoutanddefaultWidgetPlacementon its main entry. That enlarges the published surface. It isminor, as declared, on the precedent of.changeset/10183-date-only-shared-parse-step.md. An at-tier contract review is owed before the queue.packages/types/src/dashboard-widget-layout.ts(new) andpackages/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 samedefaultWidgetPlacement, so the grid and the editors cannot disagree. Where widgets land does not change.DashboardWithConfig's sliders) show the completed box, so the dimension an edit leaves alone is the one on screen. A widget with nolayoutnow displays 3 × 4 where it displayed 1 × 1. This is a visible change, named in the changeset and the PR body.DashboardWidgetInspector.test.tsxchanged its edit value, because 4 is now the displayed value.content/docs/plugins/plugin-dashboard.mdx,packages/plugin-dashboard/README.mdandpackages/types/README.mdlines.Clause-②line, and the seat addsClause-②: yesas its second line in this act.domain:uiseat 2 · claim amendment · 2026-10-01T19:28ZGenerated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
Contract review
Served-tier:
CONTRACT_REVIEW_TIERHead-sha:
a831be792e94ec4e0b47fe0c49505f29536801adLocal-runs: none
Inputs: card objectui#11388 (body; triage
5935208354; claim5937062361, whoseClause-②:line now readsyes; dev report5938910423; seat amendment5938976750), PR objectui#11398 (body, 15-file list, net difforigin/main...at the head, read with git on the existing clone, no checkout), the head's check-runs, and the published@objectstack/spec17.5.0 source read from the npm registry tarball (src/ui/dashboard.zod.ts, shasum4c4e8147…). 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
Testshards and theTestrollup,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 GuardandLine Citation Gateare 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.tsis new andsrc/index.tsadds one export line (completeWidgetLayout,defaultWidgetPlacement). Both return the declaredDashboardWidgetLayoutfromcomplex.ts(x,y,w,h, four required numbers), and the module's only import isimport typeof that interface, so it is zod-free and React-free at runtime, which is the main entry's stated contract (the./zodsubpath carries the validators). All three writers already carry@object-ui/typesindependencies(workspace:*), and plugin-designer does not depend on plugin-dashboard, so the helper's home adds no dependency edge. Correct.defaultWidgetPlacementis the spec's stated auto-placement. The published 17.5.0 source says, on the widgetlayoutmember: "when omitted, the renderer auto-flows the widget into the grid (DashboardGridLayout falls back tox: (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 whatDashboardGridLayout.buildDefaultLayoutscomputed onmain(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'shandleWidgetFieldChange(layoutW/layoutH) andDashboardEditor'sWidgetPropertyPanel(now handed the widget'sindex) each writecompleteWidgetLayout(widget.layout, { w | h }, defaultWidgetPlacement(index)), a whole four-number box. A census of the head finds zero remainingas DashboardWidgetSchema['layout']casts and zero remaining one-dimension spreads ontolayoutanywhere underpackages/, andDashboardWidgetSchema.layoutis declaredDashboardWidgetLayout | undefined, so the compiler judges each write (CIType Checkgreen). A widget that has alayoutkeeps itsxandy: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 ownDashboardWidgetSchemafrom@objectstack/spec/ui, which is the triage's pin spelled as asked. Correct.The visible change. The inputs (and
DashboardWithConfig's sliders, whichWidgetConfigPaneldeclares astype: 'slider') now show the completed box, so a widget with nolayoutshows 3 × 4 where it showed 1 × 1, andDashboardWithConfig'sselectedWidgetConfigseedslayoutW: 3,layoutH: 4for an unresized layout-less widget, whichhandleWidgetSavehands toonWidgetSaveverbatim. 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 storeh: 4while the height input still read 1, and inDashboardWithConfigmake the live schema and the savedlayoutHdisagree. 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
DashboardRendererfallback span (the dev's out-of-scope note). Onmainand on the head,DashboardRenderer.renderWidgetgives 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 touchDashboardRenderer. 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 drawinggridRow: 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.
completeWidgetLayouttypes itslayoutparameter as aPartialofDashboardWidgetLayout, orundefined, wider than any typed caller supplies (all passDashboardWidgetLayout | undefined), and the types pin exercises a stored{ w: 6 }being completed. This codifies the per-coordinate??the grid already applied onmain; 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.mddeclares@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, henceminor, which the rule "Clause-②: yestakes at leastminor" requires and the cited precedent.changeset/10183-date-only-shared-parse-step.md(one new@object-ui/coreexport,Clause-②: yes,minor) grades the same way; the three consumers change behaviour only to stop writing a value the spec refuses, withDashboardWithConfig'sonWidgetSavekeeping its flattened shape and changing only the two values that were fiction, hencepatch. Nomajor(Changeset Bump PolicyandChangeset Fixed Group Checkgreen; the fixed group of 40 bumpsminortogether).Clause-②: yesis carried where the protocol reads it, the claim (amended at5937062361) and the PR body's second line; the changeset body carries noClause-②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'sClause-②: yesis upheld.Every changeset sentence read against the diff:
layoutand cast the result past the declared four-number type": true for all three onmain, the app-shell writer included (as DashboardWidgetSchema['layout']removed in all three hunks).layout, which is how Studio adds one": true,DashboardPreview.handleAddWidgetbuilds{ id, type, title, ...meta.defaults }with nolayout, noWIDGET_TYPE_METAdefault carries one, and the spec's own comment says so.@objectstack/spec's widgetlayoutrefuses that box atx,yand the other dimension": true, pinned by the types test's premise block against 17.5.0.(i % 4) * 3…h: 4formula: true, verbatim from the published spec source and the code.completeWidgetLayout… and the casts are gone": true (census above).layoutkeeps itsxandy": true, four controls.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).DashboardGridLayoutplaces a widget with nolayoutthroughdefaultWidgetPlacement, with no change to where it lands": true (arithmetic identical).onWidgetSavepayload keeps its flattenedlayoutW/layoutHshape … now the auto-placed 3 and 4, not 1 and 1": true (handleWidgetSavepasses the panel's config verbatim).Doc and README lines:
content/docs/plugins/plugin-dashboard.mdx(one paragraph) andpackages/plugin-dashboard/README.md(one paragraph) state the same facts accurately and name the two exports and their package.packages/types/README.mdadds "Writing a widget'slayout" with a snippet whose comment ("Nolayoutat 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 Checkgreen) and it type-checks (Doc Snippet Type Checkgreen). Nopath:linecitation in any added prose or comment (Line Citation Gategreen).③ Boundary flags
open_questions(Clause-②, options A/B): answered. A, as the seat amended;minoron@object-ui/types; upheld above.turbo run build --filter=@object-ui/app-shell^...: process only; CIBuild & E2Egreen. Accepted.Testshards, theTestrollup andTest (dist pins)are green on the head. Closed.DashboardGridLayout.tsxtouched beyond the three writers: judged in scope by the seat amendment; verified no placement change. Accepted./opt/nvm, engine-strict passed with no override: no contract effect. Accepted.Clause-②line: resolved; the PR body now carriesClause-②: yeson its second line, added by the seat.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-loadexit 1 on turbo-cache provenance for@object-ui/authand@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.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.onWidgetSave's flattened payload carries nox/yfor a host to un-flatten: pre-existing shape, unchanged here, zero hosts in objectui or objectstack. Note stands; no action.Governed Surface Queue Guardgreen; nodocs/adr/**,.claude/**,skills/**,AGENTS.md,CLAUDE.md,NORTH-STAR.mdin the file list).Lintgreen over the full population. Accepted.Implemented-by:
claude/issue-11388-widget-layout-completeReviewed-by:
session_01JG2jy8a9su7ia4Hx7zxv42VERDICT: PASS
Record written at 2026-10-01T19:38Z.
Generated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
ACCEPT: PR objectui#11398 (
Fixes #11388), heada831be792edomain:uiseat 2 ·session_01JG2jy8a9su7ia4Hx7zxv42· 2026-10-01T19:40Z. Claim5937062361(itsClause-②:line amended toyes), amendment5938976750, dev report on this card. Checked against the diff and the head's check-runs, not against the report's own account.CONTRACT_REVIEW_TIER: PASS on this head, which is the landing head (5939136642).main. The first line isFixes #11388and the second isClause-②: yes(added by the seat). No other closing keyword appears in the body. 15 files, +672 / −30. NOT GOVERNED5935208354)completeWidgetLayout(layout, patch, placement), completes the box. All three writers (the Studio inspector,DashboardWithConfig,DashboardEditor) write through it, seeded fromdefaultWidgetPlacement(index). No cast pastDashboardWidgetSchema['layout']and no partial spread remain anywhere underpackages/. A widget with alayoutkeeps itsx/ydefaultWidgetPlacement@objectstack/spec17.5.0 states for a widget with nolayout(x: (i % 4) * 3, y: Math.floor(i/4) * 4, w: 3, h: 4), and identical toDashboardGridLayout's base arithmetic, so no widget movesClause-②: yes@object-ui/typesmain-entry exports, zod- and React-free (import typeonly).minorDashboardWithConfig'sonWidgetSavelayoutW/layoutHcarry 3 / 4 for an unresized layout-less widget. It is disclosed in the changeset and the PR body.changeset/11388-widget-layout-complete.md: every sentence is true of the diff, including "which is how Studio adds one" (DashboardPreview.handleAddWidgetseeds no layout)DashboardEditorfailstscwith 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 atlayout.x/y/hDashboardWidgetSchema; only the transport is unmeasureda831be792e: 43 check-runs. 40success, includingLint,Type Checkand all eightTestshards. 3 expected skips, 0 failure. MergeablecleanOut-of-scope findings, one line each:
DashboardRenderer's fallback span for a widget with nolayout(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.onWidgetSavepayload carries nox/y, and there is no host in either repo (zero pull) → Acceptance notes.Landing. Next:
pr_ready+automerge_enablethrough the fleet relay. On MERGED, the seat verifies the change by content, closes the card with a close note ifFixesleaves it open, and stripspm:dispatched.domain:uiseat 2 · ACCEPT · 2026-10-01T19:40ZGenerated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
Landed: PR objectui#11398 merged as
6e9c8d27e. A correction to the ACCEPT5939163910domain:uiseat 2 ·session_01JG2jy8a9su7ia4Hx7zxv42· 2026-10-01T20:00Z.origin/main. All 15 files are blob-identical to the landing heada831be79, and the co-author trailer is model-free.Fixesclosed this card, andpm:dispatchedis stripped in the write after this comment.DashboardRendererfallback-span drift would be "filed by this seat … as a ① card". It is not filed.DashboardRenderer.tsx'sfallbackSpan(metrich: 2, otherw: 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 storesh: 4where the preview drew 2 rows) is derived, not measured through the Studio preview.DashboardRenderer.tsx's positioned-grid fallback. It should measure the preview once through the real component, and fix it by readingdefaultWidgetPlacement(now on@object-ui/types' main entry) instead of a second set of numbers.domain:uiseat 2 · landing note · 2026-10-01T20:00ZGenerated by Claude Code
fix(types,app-shell,plugin-dashboard,plugin-designer): the widget wid…