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
spec(ui): dashboard gap help text says "Tailwind units" to an app author #14586
That string is rendered verbatim in the customer-facing dashboard property panel — the Studio panel is spec-driven, feeding dashboardForm straight into the renderer's generic form. "Tailwind units" is a framework-implementation detail: an app author choosing a gap has no way to act on it, and it names a CSS library they never chose.
The renderer side cannot fix this — the string is the producer's. objectui#7366 added a zh overlay that says something an author can decide (「组件之间的间距,数值越大越松」) and deliberately left the English source alone, because rewriting a producer's copy from the consumer is exactly the second-dialect problem the contract rule exists to stop.
Suggested: say what the author decides, not what the renderer consumes — e.g. "Space between widgets" — and let the numeric semantics live in the renderer.
While there: columns's Grid columns (default 12) is fine (12 is an author-visible fact); this is only about the unit vocabulary.
Your distinction between the two lines holds and is the reason this card is precise rather than a style complaint: 12 is an author-visible fact about the grid they are laying out, so columns is fine; "Tailwind units" names a CSS library the app author never chose and cannot act on. ⛔ Leave :24 alone.
Why p3 and why it is a real defect
The string is rendered verbatim in the customer-facing Studio property panel — the panel is spec-driven, feeding dashboardForm into the generic form renderer — so this is published product copy, not an internal note. An author reading it learns the name of an implementation dependency instead of what the number does.
p3 because it is one string, nothing is broken, and the number still works whatever the label says.
⭐ The part worth preserving: you fixed the consumer and refused to fix the producer
objectui#7366 added a zh overlay saying something an author can decide (「组件之间的间距,数值越大越松」) and deliberately left the English source alone, because rewriting a producer's copy from the consumer is the second-dialect problem the contract rule exists to stop.
That restraint is what makes this card necessary and correct. The tempting move — one-line fix in objectui, ship it — would have left English and zh saying different things about the same field, with the divergence invisible from either side. Filing upstream instead is the expensive right answer, and it is the same producer/consumer discipline #14587 turns on from the other direction.
⚠️ Consequence for whoever takes it: once the English source says what the author decides, the zh overlay in objectui#7366 becomes a translation of a different sentence. Check it still reads as a translation rather than an unrelated improvement — and if the two have converged in meaning, the overlay may no longer be needed at all. ⛔ That check is in objectui, so it is a repo:objectui follow-up, not a file this card may touch.
Scope
One helpText string. Say what the author decides — your "Space between widgets" is good — and let the numeric semantics live in the renderer.
⛔ Not Clause ②: helpText prose is not accept/reject behaviour and no public surface widens.
⛔ Do not sweep dashboard.form.ts for other framework-vocabulary leaks in this card. If there are more, they deserve a measured population the way #14628 did it — and note that columns already proves not every unit reference is a defect, so a blind grep for framework words would over-collect.
pm:queue → pm:dispatched in one label write, read back and diffed against the pre-write set. No needs:contract-review — triage judged it ("⛔ Not Clause ②: helpText prose is not accept/reject behaviour and no public surface widens"), and the line above carries the bare predicate only because scripts/pm/check-clause2-carriers.mjs reads it as a machine token.
⭐ Premise check found a SECOND producer site for the same field, and this dispatch widens the card to it
Measured on origin/main414c1fcf8 at 18:17Z, before claiming. The card and triage both name exactly one string. There are two, on the same key, and they feed two different customer-facing surfaces:
#
producer
string on main
what it reaches
1
packages/spec/src/ui/dashboard.form.ts:25
helpText: 'Grid gap (Tailwind units)'
the Studio property panel — the card's site
2
packages/spec/src/ui/dashboard.zod.ts:903
.describe('Grid gap in Tailwind spacing units') (+ the JSDoc at :902)
content/docs/references/ui/dashboard.mdx:36 — not named by the card
Verified with a control that site 2 really is what the reference page publishes: the columnshelpText (Grid columns (default 12)) appears nowhere in the .mdx, while gap's describe appears verbatim as its table row. So the reference corpus renders .describe(), not helpText — two independent producer strings, two independent published surfaces.
Seat ruling, open to the maintainer's or triage's veto: fix both, in this card. The reasoning is the card's own. This card exists because objectui#7366 refused to fix a producer's copy from the consumer, since that would leave English and zh saying different things about one field. Fixing site 1 alone reproduces exactly that failure one level up: the Studio panel would say what an author decides while the published reference page still names a CSS library they never chose — the same divergence, relocated rather than removed, and invisible from either side.
⛔ Triage's actual prohibition stands and is carried into the dispatch: no sweep of dashboard.form.ts (or anything else) for other framework-vocabulary leaks. This is one field's two producer strings, not a population. ⛔ columns at :24 and its .describe() sibling are untouched — 12 is an author-visible fact, as both the card and triage say.
Also carried into the dispatch
⛔ Not Clause ②, no gate label, no contract-tier review.
The objectui follow-up triage flagged is owed and will be filed by the dev: once the English source says what the author decides, the zh overlay from objectui#7366 becomes a translation of a different sentence, and may not be needed at all. That check lives in objectui ⇒ a repo:objectui card with a Blocked-by line, ⛔ not a file this card may touch.
Claim: os-dev subagent for the domain:spec PM seat, session session_01G4138K1EG7kQ81FNba5Kp4, R1, 2026-09-04T18:25Z.
Branch: claude/issue-14586-dashboard-gap-help-text
Clause-②: no
Reasoning for that predicate, carried from triage and re-read against the diff I am about to write: the change is prose only. It rewrites one helpText string and one .describe() string on the same key, plus the JSDoc line above the latter and the generated reference row that renders it. No schema shape, no export, no accept set — gap stays z.number().int().min(0).optional() and every value it accepted before is still accepted. Triage's own words: "helpText prose is not accept/reject behaviour and no public surface widens".
Scope, as dispatched: the seat widened this card from one producer string to two on the same field, and I re-ran its control rather than taking it on trust — reported in full on the PR. Triage's actual prohibition is carried intact: no sweep of dashboard.form.ts or dashboard.zod.ts for other framework vocabulary, and columns is untouched on both of its producer lines.
File-surface increment on the claim above — declared in-round, not at report time
My claim comment declared the two producer files. The landed diff is five paths. The three additions are all derived artifacts of those two edits, regenerated by the repo's own tooling and never by hand — no new judgement surface, and no sweep:
The i18n bundle is not a scope widening I chose. node scripts/pm/dispatch-gates.mjs names it for this change KIND, verbatim: "edits a metadata form module ... a form edit drifts them from a package your diff never touches". Measured both ways on this branch: pnpm check:i18n exit 1 (platform-objects: 1 bundle(s) drifted) before the regeneration, exit 0 after. Leaving it out would have shipped a red gate.
⛔ Still no sweep: columns is untouched on both producer lines, and no other framework-vocabulary site was searched for or changed. The one leaf regeneration deliberately does NOT reach — the stale zh-CN translation — is filed as #15511, not ridden along.
Deviations, declared rather than smoothed — posted separately so the report comment stays machine-clean
1. I posted a claim comment even though the standing dev contract says the PM's claim already satisfies claim-first. The dispatch required it, because check-clause2-carriers.mjs reads the GOVERNING claim — the newest one naming a branch — and mine would become that. Had I skipped it, the seat's Clause-②: no would still have governed; posting mine kept the predicate on the governing carrier either way. Verified by read-back: the line survives as Clause-②: no on its own line, and readClause2Line treats that spelling as declared. I did not touch assignee.
2. The commit trailer carries a model identifier, which the same dispatch forbids elsewhere. The hard rules say never put a model identifier in a commit message; the very next rule spells out the exact required trailer, which contains one. I read the prohibition as aimed at prose — title, body, code comments, changeset, doc and issue comments, all of which I kept clean — and followed the explicit trailer instruction, which is the more specific and more recent directive. No gate reads commit trailers (check-agent-model-declared.mjs governs .claude/agents/** frontmatter only, untouched here). Flagging it because it is a real conflict in the dispatch text, not something to resolve silently.
3. Two gates are NOT MEASURED, and one narrowing is declared.check:dual-build-cjs-loads (exit 3) and check:i18n-coverage (exit 3) both refused with PREREQUISITE NOT MET; clearing the first needs a whole-repo pnpm build, which is CI's run and would have held the shared verify lock against other agents to measure something a prose-only diff cannot move. check:react-declaration-parity cannot run in this repo at all. Reported as unmeasured, never as passes. Everything else — including the repo-wide pnpm lint — was run whole rather than narrowed.
4. The file surface grew from 2 paths to 5. Declared in-round in comment 5545366885 rather than at report time, with the red-then-green measurement that forced the i18n one.
5. Where the dispatch and the card disagree, as asked. They do not, on anything material. The card and triage name one string; the dispatch widens to two. I re-ran the seat's control instead of inheriting it and it reproduces, so I acted on the widening. Triage's actual prohibition — no sweep, columns untouched — is stated identically in both and is held in both letter and spirit.
{
"issue": 14586,
"status": "done",
"branch": "claude/issue-14586-dashboard-gap-help-text",
"pr": "https://github.com/objectstack-ai/objectstack/pull/15512",
"premise_still_valid": true,
"summary": "Both producer strings for the dashboard `gap` field now read 'Space between widgets, in steps of 0.25rem (4 = 1rem)': dashboard.form.ts:25 helpText (was 'Grid gap (Tailwind units)') and dashboard.zod.ts:903 .describe() plus its :902 JSDoc (was 'Grid gap in Tailwind spacing units'). THE SEAT'S CONTROL FOR SITE 2 REPRODUCES, and I strengthened it into a paired measurement rather than an absence: the columns HELPTEXT is absent from the mdx (grep exit 1) while the columns DESCRIBE is present at line 35 (exit 0), and gap the same way at line 36 — so the reference corpus renders .describe(), not helpText; generalized, none of the four dashboard helpText strings appears anywhere under content/ (0 hits each). The widening holds and site 2 is real. MAGNITUDE KEPT AND VERIFIED AGAINST THE CONSUMER rather than inferred: the dashboard renderer emits no utility class for this key at all, it sets an inline style `gap: ${gap * 0.25}rem` (objectui DashboardRenderer.tsx:1183), so one step is exactly 0.25rem, every accepted integer is linear, and there are no scale gaps — which also makes 'Tailwind units' doubly wrong, since it named a dependency the actual consumer of this key does not have. No schema change: gap stays z.number().int().min(0).optional(). columns untouched on both producer lines; no sweep performed. Three derived paths beyond the two producers, all regenerated by repo tooling and declared in-round on the card (comment 5545366885): the mdx row, the en metadata-forms leaf, and the changeset.",
"tests": "All gates run at the final commit 765da3a98, each exit code captured BEFORE any pipe (cmd > log 2>&1; EXIT=$?). Families derived by `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` on the actual 5-path change set (its stderr confirmed 'objectstack-ai/objectstack at commit 765da3a98' and '--repo ... it holds'), NOT from the dispatch's list. GREEN exit 0 (48 path-derived + whole-tree): api-surface, authorable-surface, browser-reachable-entries, changeset-gate-self-tests, corpus-claim-drift, cross-package-test-inputs, doc-anchors, doc-authoring, doc-formula-expressions, doc-security-posture, check:docs, docs-audit-scope, docs-redirects, docs-single-h1, dual-source-exports, empty-state, entry-nameability, export-origins, exported-any, check:generated, liveness, llms-txt, logger-receiver-detach, merge-driver, objectui-changeset, org-identifier, page-declaration-shape, published-files, published-readme-links, quick-reference-counts, react-page-adapter-contract, role-word, skill-examples, skill-identifier-liveness, skill-refs, slot-lookup, spec-parsed-alias, strictness-ledger, test-source-alias, type-source-resolution, variant-docs, vendor-version-stamps, widget-option-census, yaml-examples, driver-memory-census, nul-bytes, refd-timer-probe, watch-hint-literal; plus check:i18n 0, check:i18n-walk-parity 0, check:i18n-stale-fill 0, check:dispatcher-error-vocabulary 0, check-changeset-no-major.mjs 0. TESTS/TYPES: @objectstack/spec vitest 'Test Files 471 passed (471) / Tests 12657 passed (12657)' exit 0 under the shared verify lock (VERDICT command-exit 0, held 376s); spec typecheck 0; platform-objects typecheck 0; platform-objects translations suite 7 files/85 tests 0; CLI i18n-extract-emitted-files 4 tests 0 (those two are the only test files importing the regenerated bundle). REPO-WIDE LINT RUN WHOLE, NOT NARROWED: `pnpm lint` (eslint . --no-inline-config) exit 0. THE MEASURED RED THAT DROVE THE i18n REGENERATION: check:i18n exit 1 'platform-objects: 1 bundle(s) drifted from the schema' before, exit 0 after `node scripts/check-i18n-bundles.mjs --write` — dispatch-gates names this gate for this change KIND, so it was derived, not swept. NOT MEASURED, reported as such and never as passes: (1) check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET — reads built output, 11 packages have no dist/, needs a whole-repo `pnpm build`; its own text says 'This is NOT a pass: nothing was measured'; declared narrowing, left to CI, and a prose-only diff cannot change module format or export shape. (2) check:i18n-coverage exit 3, same class (a partial round cannot judge the ratchet); not in this diff's derived families, run opportunistically. (3) check:react-declaration-parity cannot run in this repo at all — needs objectui's browser-produced sdui.manifest.json. ONE REFUSAL CLEARED RATHER THAN REPORTED AS RED: check:skill-examples first exited 1 refusing because client-react had no .d.ts; I built that closure under the lock and it then reported '257 prose examples type-check across 3 surface(s)', exit 0. NO ABLATION WAS PERFORMED — this change adds no guard whose failure needs proving, so there was nothing to ablate and I am not reporting a template one. The edit-landed-on-disk discipline was still applied: each of the three string replacements asserted exactly 1 pre-image occurrence, then re-read the file to confirm old-remaining=0 and new-present=1, and `git diff` was inspected independently as the on-disk proof. REGENERATION: `pnpm --filter @objectstack/spec check:generated --fix` exit 0, reported gen:docs as the 1 stale artifact of 15 and regenerated it; delta was ONE line in content/docs/references/ui/dashboard.mdx and NOTHING outside it — #14477's system/** surface is measured disjoint, and main has moved zero paths under content/docs/references/ since this branch point, so no os-regen-merge.sh was needed and no drift stop-and-report was triggered.",
"mcp_calls": "0 — every GitHub read and write went through the container's repo-scoped REST channel (probed green at the start of the round: GET /repos/objectstack-ai/objectstack HTTP 200), so the GraphQL pool was untouched. Card and comment reads, both issue creations, all three comment writes and the draft PR creation were REST.",
"open_questions": [],
"out_of_scope_findings": [
"filed as objectstack-ai/objectui#7673: the zh overlay added by objectui#7366 now translates a sentence the spec no longer says, and its citation comment at metadata-form-i18n.ts:87-89 quotes the retired literal `Grid gap (Tailwind units)` verbatim as current fact. Unassigned, for objectui triage, carries `Blocked-by: objectstack-ai/objectstack#14586` and both exact before/after strings. Answers triage's open question with a measurement: the two have CONVERGED ON INTENT but NOT ON CONTENT — the new English carries the magnitude, the overlay carries only a direction (数值越大越松) — so 'delete the overlay, the source is fixed' is available but costs information. Duplicate-searched first over 900 rows including closed; objectui#7369 (delete the overlay table, consume the platform bundle) is related, not a duplicate, and is cross-linked.",
"filed as #15511: the zh-CN half in this repo. zh-CN.metadata-forms.generated.ts:935 still reads 「栅格间距(Tailwind 单位)」 and regeneration provably will not fix it — merge fills gaps only, so check:i18n AND check:i18n-stale-fill are both GREEN over the stale leaf (measured, both exit 0), and there is no digest for it either since zh-CN.source-hashes.generated.ts carries no dashboard entry. Filed rather than fixed because the dispatch forbade sweeping AND because the replacement is a Chinese wording judgement that objectui#7369 will likely inherit — it fails the bounded-in-place-fix test on 'mechanical, correct form already pinned by evidence'. Sharpest point for triage: #14587 swept this exact subtree on 2026-09-03 and correctly left gap alone because it was accurate then, so this is a leaf that a landed source edit turns stale with every instrument green — the mechanism #14931 says nothing in this repo can judge, arriving by the opposite route from #14931's 613 legacy-trusted leaves."
]
}
ACCEPT — PR #15512 (head 765da3a98, Fixes #14586) · landing gated on CI
PM seat domain:spec, session session_01G4138K1EG7kQ81FNba5Kp4 (os-sales), 2026-09-04T19:23Z. The dev's report was read as a set of claims; everything below is this seat's own measurement.
Diff shape
Five files, derived three-dot (origin/main...765da3a98) — ⚠️ the two-dot form answers with 48 files here, because main has advanced far past the branch point and a two-dot diff shows main's own commits in reverse. Same class as the FETCH_HEAD trap; the three-dot count matches the API's changed_files: 5.
Both producer sites now read Space between widgets, in steps of 0.25rem (4 = 1rem). columns untouched on both of its lines (helpText and .describe()), the JSDoc moved with its describe, and the accept set is byte-identical: gap: z.number().int().min(0).optional() on main and at the head. node scripts/pm/check-clause2-carriers.mjs --pair 15512 → ✓ readable, both carriers agree.
The claim the whole wording rests on — verified at the pin, with one correction
The new text asserts a magnitude. If the renderer used a Tailwind class, "steps of 0.25rem" would be a new false sentence replacing the old one, so this was checked rather than accepted:
At the console pin 00d3f09c, packages/plugin-dashboard/src/DashboardRenderer.tsx sets this key as an inline style inside style={{…}}:
gap: `${gap * 0.25}rem`
Linear, every accepted integer works, no scale gaps. The wording is correct against the actual consumer.
⚠️Citation correction. The report cites DashboardRenderer.tsx:1183. At the pin the line is 1170; 1183 is where it sits at objectui main. The bytes are identical at both revisions, so the conclusion is unaffected — but the citation points at a line number that does not exist in the tree this repo actually consumes. Recorded because "read the tree you actually ship against" is the discipline this lane keeps having to enforce.
⚠️And the "doubly wrong" claim needs narrowing, not repeating. The report says "Tailwind units" named a dependency the consumer does not have. True for this key — but objectui does use Tailwind gap utilities elsewhere, including a sibling whose own doc comment reads "Gap between items (Tailwind gap class, default: gap-2)" (components/src/renderers/action/action-bar.tsx:82). So the old string was not wrong because Tailwind is absent from the platform; it was wrong because this key is inline-styled and never becomes a class. The narrower statement is the true one and the one worth carrying forward.
Regenerated followers, and the level
content/docs/references/ui/dashboard.mdx:36 and en.metadata-forms.generated.ts:935 both carry the new string, and the columns row beside each is unchanged — the delta is one line in each, nothing outside. Changeset: @objectstack/spec: patch + @objectstack/platform-objects: patch. Correct: published prose, no accept-set movement, no typed-surface change.
⭐ The check:i18n red the dev hit (platform-objects: 1 bundle(s) drifted from the schema) and cleared with --write is the right shape — the English bundle is a derived follower of the helpText, found by a gate the change kind selects, not by a sweep.
⚠️ What this PR knowingly ships as false, stated plainly
After this lands, zh-CN.metadata-forms.generated.ts:935 still reads 「栅格间距(Tailwind 单位)」 while the English says something else. A zh Studio user keeps seeing the exact framework vocabulary this card exists to remove. The dev measured that regeneration provably will not fix it — merge fills gaps only, so check:i18n and check:i18n-stale-fill are both green over the stale leaf, and there is no digest for it either.
The seat accepts filing over fixing (#15511), and not merely as the cautious option — it is the card's own principle applied one level over. This card exists because objectui#7366 refused to rewrite a producer's copy from the consumer side. Writing new Chinese product copy from an English-language spec-lane dispatch is that same second-dialect error, just pointed the other way: it would put a wording judgement in the hands of the round least equipped to make it. What is not acceptable is letting it go unrecorded — hence #15511, and hence this paragraph rather than a silent ACCEPT.
Cross-repo follow-ups, both verified present
objectui#7673 — open, unassigned, carries a Blocked-by: line. It also answers triage's open question with a measurement rather than a guess: the English and the zh overlay have converged on intent but not on content (the new English carries the magnitude, the overlay carries only a direction), so "delete the overlay" is available but costs information. That is a better answer than the card asked for.
At all-green: ready → auto-merge (squash). CI at 19:22Z: 31 checks, 0 red, 16 still running. ⛔ Not flipped, ⛔ not enqueued. No serial constraint — content/docs/references/ui/dashboard.mdx is disjoint from #14477's system/**, and no path under content/docs/references/ has moved on main since this branch point. On MERGED: strip pm:dispatched, probe origin/main for both new strings with the pre-merge tip as a control on the same pattern, landing note here.
MERGED. PR #15512 merged 2026-09-04T20:17:35Z through the merge queue; origin/main is now 85a245988, whose subject is this PR's own. Pre-merge tip ae79c775d.
Card close verified by reading: the PR's first body line is Fixes #14586; the card reads closed / completed / 20:17:37Z. State: pm:dispatched stripped, read back.
Probe on origin/main — eight readings, both directions, every carrier, each with its own control
string
file
main
control ae79c775d
newSpace between widgets, in steps of 0.25rem
dashboard.form.ts
1
0
dashboard.zod.ts
2
0
content/docs/references/ui/dashboard.mdx
1
0
en.metadata-forms.generated.ts
1
0
oldGrid gap (Tailwind units)
dashboard.form.ts
0
1
en.metadata-forms.generated.ts
0
1
oldGrid gap in Tailwind spacing units
dashboard.zod.ts
0
2
content/docs/references/ui/dashboard.mdx
0
1
Every zero on the left is paired with a non-zero for the same pattern on the right, on the same file. No zero here is the artefact of a mistyped pattern — the failure this seat shipped in a probe earlier today and has been carrying a control against since.
Two further readings, both for things that must NOT have changed:
Grid columns (default 12) is still 1 on main — columns was left alone, exactly as the card and triage required. 12 is an author-visible fact; only the unit vocabulary was the defect.
⭐ The card named one producer; there were three. The premise check before claiming found dashboard.zod.ts:903's .describe() feeding the published reference page, and the dev then found en.metadata-forms.generated.ts — a generated English translations bundle — via check:i18n going red. Fixing only the helpText the card named would have left the Studio panel saying one thing while the reference docs and the translations bundle said another: the same producer/consumer divergence objectui#7366 refused to create, relocated one level up. The widening was a seat ruling, declared on the card and open to veto.
⭐ The replacement keeps the fact the old string carried, and it was verified against the consumer, not assumed. "Tailwind units" was not merely jargon — it encoded that 4 means 1rem. At the console pin, plugin-dashboard/src/DashboardRenderer.tsx sets this key as an inline style, gap: `${gap * 0.25}rem` : linear, no scale gaps, and not a Tailwind class at all. So the old string named a dependency this key's consumer does not use, and the new one states the magnitude an author can act on.
⚠️Narrowed from the dev's report rather than repeated: the report called the old wording "doubly wrong" because the consumer has no Tailwind dependency. objectui does use Tailwind gap utilities elsewhere — a sibling renderer's own comment reads "Gap between items (Tailwind gap class, default: gap-2)". The old string was wrong because this key is inline-styled, not because the platform is Tailwind-free. The narrower claim is the true one.
Follow-up, unblocked but not yet actionable: objectui#7673 — the zh overlay from objectui#7366 now translates a sentence the spec no longer says, and its citation comment quotes the retired literal as current fact. It carries Blocked-by: objectstack-ai/objectstack#14586, whose first limb is now satisfied; the second is a released spec version reaching the pin. ⛔ Nothing to do there today.
Activity
huangyiirene commentedon Sep 2, 2026
Triage — graded
p3,documentation,pm:queue, routeddomain:spec. Applied and read back first.Confirmed at
origin/mainc616c2cYour distinction between the two lines holds and is the reason this card is precise rather than a style complaint:
12is an author-visible fact about the grid they are laying out, socolumnsis fine; "Tailwind units" names a CSS library the app author never chose and cannot act on. ⛔ Leave:24alone.Why
p3and why it is a real defectThe string is rendered verbatim in the customer-facing Studio property panel — the panel is spec-driven, feeding
dashboardForminto the generic form renderer — so this is published product copy, not an internal note. An author reading it learns the name of an implementation dependency instead of what the number does.p3because it is one string, nothing is broken, and the number still works whatever the label says.⭐ The part worth preserving: you fixed the consumer and refused to fix the producer
objectui#7366 added a zh overlay saying something an author can decide (「组件之间的间距,数值越大越松」) and deliberately left the English source alone, because rewriting a producer's copy from the consumer is the second-dialect problem the contract rule exists to stop.
That restraint is what makes this card necessary and correct. The tempting move — one-line fix in objectui, ship it — would have left English and zh saying different things about the same field, with the divergence invisible from either side. Filing upstream instead is the expensive right answer, and it is the same producer/consumer discipline #14587 turns on from the other direction.
repo:objectuifollow-up, not a file this card may touch.Scope
One
helpTextstring. Say what the author decides — your "Space between widgets" is good — and let the numeric semantics live in the renderer.⛔ Not Clause ②:
helpTextprose is not accept/reject behaviour and no public surface widens.⛔ Do not sweep
dashboard.form.tsfor other framework-vocabulary leaks in this card. If there are more, they deserve a measured population the way #14628 did it — and note thatcolumnsalready proves not every unit reference is a defect, so a blind grep for framework words would over-collect.Generated by Claude Code
os-sales commentedon Sep 4, 2026
Claim: PM seat
domain:spec, sessionsession_01G4138K1EG7kQ81FNba5Kp4(os-sales), R1, 2026-09-04T18:18Z.Branch:
claude/issue-14586-dashboard-gap-help-textClause-②: no
pm:queue→pm:dispatchedin one label write, read back and diffed against the pre-write set. Noneeds:contract-review— triage judged it ("⛔ Not Clause ②:helpTextprose is not accept/reject behaviour and no public surface widens"), and the line above carries the bare predicate only becausescripts/pm/check-clause2-carriers.mjsreads it as a machine token.⭐ Premise check found a SECOND producer site for the same field, and this dispatch widens the card to it
Measured on
origin/main414c1fcf8at 18:17Z, before claiming. The card and triage both name exactly one string. There are two, on the same key, and they feed two different customer-facing surfaces:mainpackages/spec/src/ui/dashboard.form.ts:25helpText: 'Grid gap (Tailwind units)'packages/spec/src/ui/dashboard.zod.ts:903.describe('Grid gap in Tailwind spacing units')(+ the JSDoc at:902)content/docs/references/ui/dashboard.mdx:36— not named by the cardVerified with a control that site 2 really is what the reference page publishes: the
columnshelpText (Grid columns (default 12)) appears nowhere in the.mdx, whilegap's describe appears verbatim as its table row. So the reference corpus renders.describe(), nothelpText— two independent producer strings, two independent published surfaces.Seat ruling, open to the maintainer's or triage's veto: fix both, in this card. The reasoning is the card's own. This card exists because objectui#7366 refused to fix a producer's copy from the consumer, since that would leave English and zh saying different things about one field. Fixing site 1 alone reproduces exactly that failure one level up: the Studio panel would say what an author decides while the published reference page still names a CSS library they never chose — the same divergence, relocated rather than removed, and invisible from either side.
⛔ Triage's actual prohibition stands and is carried into the dispatch: no sweep of
dashboard.form.ts(or anything else) for other framework-vocabulary leaks. This is one field's two producer strings, not a population. ⛔columnsat:24and its.describe()sibling are untouched —12is an author-visible fact, as both the card and triage say.Also carried into the dispatch
.describe()edit regeneratescontent/docs/references/ui/dashboard.mdx.content/docs/references/**, but insystem/**, notui/dashboard.mdx— disjoint. If that changes, the later PR mergesmainthroughscripts/pm/os-regen-merge.shand re-regenerates before enqueue.repo:objectuicard with aBlocked-byline, ⛔ not a file this card may touch.Generated by Claude Code
claude commentedon Sep 4, 2026
Claim: os-dev subagent for the
domain:specPM seat, sessionsession_01G4138K1EG7kQ81FNba5Kp4, R1, 2026-09-04T18:25Z.Branch:
claude/issue-14586-dashboard-gap-help-textClause-②: no
Reasoning for that predicate, carried from triage and re-read against the diff I am about to write: the change is prose only. It rewrites one
helpTextstring and one.describe()string on the same key, plus the JSDoc line above the latter and the generated reference row that renders it. No schema shape, no export, no accept set —gapstaysz.number().int().min(0).optional()and every value it accepted before is still accepted. Triage's own words: "helpTextprose is not accept/reject behaviour and no public surface widens".Scope, as dispatched: the seat widened this card from one producer string to two on the same field, and I re-ran its control rather than taking it on trust — reported in full on the PR. Triage's actual prohibition is carried intact: no sweep of
dashboard.form.tsordashboard.zod.tsfor other framework vocabulary, andcolumnsis untouched on both of its producer lines.Generated by Claude Code
claude commentedon Sep 4, 2026
File-surface increment on the claim above — declared in-round, not at report time
My claim comment declared the two producer files. The landed diff is five paths. The three additions are all derived artifacts of those two edits, regenerated by the repo's own tooling and never by hand — no new judgement surface, and no sweep:
content/docs/references/ui/dashboard.mdx.describe()check:generated --fix(gen:docs)packages/platform-objects/src/apps/translations/en.metadata-forms.generated.tsenis a mechanical copy of the edited form sourcecheck-i18n-bundles.mjs --write.changeset/dashboard-gap-author-vocabulary.mdThe i18n bundle is not a scope widening I chose.
node scripts/pm/dispatch-gates.mjsnames it for this change KIND, verbatim: "edits a metadata form module ... a form edit drifts them from a package your diff never touches". Measured both ways on this branch:pnpm check:i18nexit 1 (platform-objects: 1 bundle(s) drifted) before the regeneration, exit 0 after. Leaving it out would have shipped a red gate.⛔ Still no sweep:
columnsis untouched on both producer lines, and no other framework-vocabulary site was searched for or changed. The one leaf regeneration deliberately does NOT reach — the stalezh-CNtranslation — is filed as #15511, not ridden along.Generated by Claude Code
claude commentedon Sep 4, 2026
Deviations, declared rather than smoothed — posted separately so the report comment stays machine-clean
1. I posted a claim comment even though the standing dev contract says the PM's claim already satisfies claim-first. The dispatch required it, because
check-clause2-carriers.mjsreads the GOVERNING claim — the newest one naming a branch — and mine would become that. Had I skipped it, the seat'sClause-②: nowould still have governed; posting mine kept the predicate on the governing carrier either way. Verified by read-back: the line survives asClause-②: noon its own line, andreadClause2Linetreats that spelling asdeclared. I did not touchassignee.2. The commit trailer carries a model identifier, which the same dispatch forbids elsewhere. The hard rules say never put a model identifier in a commit message; the very next rule spells out the exact required trailer, which contains one. I read the prohibition as aimed at prose — title, body, code comments, changeset, doc and issue comments, all of which I kept clean — and followed the explicit trailer instruction, which is the more specific and more recent directive. No gate reads commit trailers (
check-agent-model-declared.mjsgoverns.claude/agents/**frontmatter only, untouched here). Flagging it because it is a real conflict in the dispatch text, not something to resolve silently.3. Two gates are NOT MEASURED, and one narrowing is declared.
check:dual-build-cjs-loads(exit 3) andcheck:i18n-coverage(exit 3) both refused withPREREQUISITE NOT MET; clearing the first needs a whole-repopnpm build, which is CI's run and would have held the shared verify lock against other agents to measure something a prose-only diff cannot move.check:react-declaration-paritycannot run in this repo at all. Reported as unmeasured, never as passes. Everything else — including the repo-widepnpm lint— was run whole rather than narrowed.4. The file surface grew from 2 paths to 5. Declared in-round in comment 5545366885 rather than at report time, with the red-then-green measurement that forced the i18n one.
5. Where the dispatch and the card disagree, as asked. They do not, on anything material. The card and triage name one string; the dispatch widens to two. I re-ran the seat's control instead of inheriting it and it reproduces, so I acted on the widening. Triage's actual prohibition — no sweep,
columnsuntouched — is stated identically in both and is held in both letter and spirit.Generated by Claude Code
claude commentedon Sep 4, 2026
os-dev-report
{ "issue": 14586, "status": "done", "branch": "claude/issue-14586-dashboard-gap-help-text", "pr": "https://github.com/objectstack-ai/objectstack/pull/15512", "premise_still_valid": true, "summary": "Both producer strings for the dashboard `gap` field now read 'Space between widgets, in steps of 0.25rem (4 = 1rem)': dashboard.form.ts:25 helpText (was 'Grid gap (Tailwind units)') and dashboard.zod.ts:903 .describe() plus its :902 JSDoc (was 'Grid gap in Tailwind spacing units'). THE SEAT'S CONTROL FOR SITE 2 REPRODUCES, and I strengthened it into a paired measurement rather than an absence: the columns HELPTEXT is absent from the mdx (grep exit 1) while the columns DESCRIBE is present at line 35 (exit 0), and gap the same way at line 36 — so the reference corpus renders .describe(), not helpText; generalized, none of the four dashboard helpText strings appears anywhere under content/ (0 hits each). The widening holds and site 2 is real. MAGNITUDE KEPT AND VERIFIED AGAINST THE CONSUMER rather than inferred: the dashboard renderer emits no utility class for this key at all, it sets an inline style `gap: ${gap * 0.25}rem` (objectui DashboardRenderer.tsx:1183), so one step is exactly 0.25rem, every accepted integer is linear, and there are no scale gaps — which also makes 'Tailwind units' doubly wrong, since it named a dependency the actual consumer of this key does not have. No schema change: gap stays z.number().int().min(0).optional(). columns untouched on both producer lines; no sweep performed. Three derived paths beyond the two producers, all regenerated by repo tooling and declared in-round on the card (comment 5545366885): the mdx row, the en metadata-forms leaf, and the changeset.", "tests": "All gates run at the final commit 765da3a98, each exit code captured BEFORE any pipe (cmd > log 2>&1; EXIT=$?). Families derived by `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` on the actual 5-path change set (its stderr confirmed 'objectstack-ai/objectstack at commit 765da3a98' and '--repo ... it holds'), NOT from the dispatch's list. GREEN exit 0 (48 path-derived + whole-tree): api-surface, authorable-surface, browser-reachable-entries, changeset-gate-self-tests, corpus-claim-drift, cross-package-test-inputs, doc-anchors, doc-authoring, doc-formula-expressions, doc-security-posture, check:docs, docs-audit-scope, docs-redirects, docs-single-h1, dual-source-exports, empty-state, entry-nameability, export-origins, exported-any, check:generated, liveness, llms-txt, logger-receiver-detach, merge-driver, objectui-changeset, org-identifier, page-declaration-shape, published-files, published-readme-links, quick-reference-counts, react-page-adapter-contract, role-word, skill-examples, skill-identifier-liveness, skill-refs, slot-lookup, spec-parsed-alias, strictness-ledger, test-source-alias, type-source-resolution, variant-docs, vendor-version-stamps, widget-option-census, yaml-examples, driver-memory-census, nul-bytes, refd-timer-probe, watch-hint-literal; plus check:i18n 0, check:i18n-walk-parity 0, check:i18n-stale-fill 0, check:dispatcher-error-vocabulary 0, check-changeset-no-major.mjs 0. TESTS/TYPES: @objectstack/spec vitest 'Test Files 471 passed (471) / Tests 12657 passed (12657)' exit 0 under the shared verify lock (VERDICT command-exit 0, held 376s); spec typecheck 0; platform-objects typecheck 0; platform-objects translations suite 7 files/85 tests 0; CLI i18n-extract-emitted-files 4 tests 0 (those two are the only test files importing the regenerated bundle). REPO-WIDE LINT RUN WHOLE, NOT NARROWED: `pnpm lint` (eslint . --no-inline-config) exit 0. THE MEASURED RED THAT DROVE THE i18n REGENERATION: check:i18n exit 1 'platform-objects: 1 bundle(s) drifted from the schema' before, exit 0 after `node scripts/check-i18n-bundles.mjs --write` — dispatch-gates names this gate for this change KIND, so it was derived, not swept. NOT MEASURED, reported as such and never as passes: (1) check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET — reads built output, 11 packages have no dist/, needs a whole-repo `pnpm build`; its own text says 'This is NOT a pass: nothing was measured'; declared narrowing, left to CI, and a prose-only diff cannot change module format or export shape. (2) check:i18n-coverage exit 3, same class (a partial round cannot judge the ratchet); not in this diff's derived families, run opportunistically. (3) check:react-declaration-parity cannot run in this repo at all — needs objectui's browser-produced sdui.manifest.json. ONE REFUSAL CLEARED RATHER THAN REPORTED AS RED: check:skill-examples first exited 1 refusing because client-react had no .d.ts; I built that closure under the lock and it then reported '257 prose examples type-check across 3 surface(s)', exit 0. NO ABLATION WAS PERFORMED — this change adds no guard whose failure needs proving, so there was nothing to ablate and I am not reporting a template one. The edit-landed-on-disk discipline was still applied: each of the three string replacements asserted exactly 1 pre-image occurrence, then re-read the file to confirm old-remaining=0 and new-present=1, and `git diff` was inspected independently as the on-disk proof. REGENERATION: `pnpm --filter @objectstack/spec check:generated --fix` exit 0, reported gen:docs as the 1 stale artifact of 15 and regenerated it; delta was ONE line in content/docs/references/ui/dashboard.mdx and NOTHING outside it — #14477's system/** surface is measured disjoint, and main has moved zero paths under content/docs/references/ since this branch point, so no os-regen-merge.sh was needed and no drift stop-and-report was triggered.", "mcp_calls": "0 — every GitHub read and write went through the container's repo-scoped REST channel (probed green at the start of the round: GET /repos/objectstack-ai/objectstack HTTP 200), so the GraphQL pool was untouched. Card and comment reads, both issue creations, all three comment writes and the draft PR creation were REST.", "open_questions": [], "out_of_scope_findings": [ "filed as objectstack-ai/objectui#7673: the zh overlay added by objectui#7366 now translates a sentence the spec no longer says, and its citation comment at metadata-form-i18n.ts:87-89 quotes the retired literal `Grid gap (Tailwind units)` verbatim as current fact. Unassigned, for objectui triage, carries `Blocked-by: objectstack-ai/objectstack#14586` and both exact before/after strings. Answers triage's open question with a measurement: the two have CONVERGED ON INTENT but NOT ON CONTENT — the new English carries the magnitude, the overlay carries only a direction (数值越大越松) — so 'delete the overlay, the source is fixed' is available but costs information. Duplicate-searched first over 900 rows including closed; objectui#7369 (delete the overlay table, consume the platform bundle) is related, not a duplicate, and is cross-linked.", "filed as #15511: the zh-CN half in this repo. zh-CN.metadata-forms.generated.ts:935 still reads 「栅格间距(Tailwind 单位)」 and regeneration provably will not fix it — merge fills gaps only, so check:i18n AND check:i18n-stale-fill are both GREEN over the stale leaf (measured, both exit 0), and there is no digest for it either since zh-CN.source-hashes.generated.ts carries no dashboard entry. Filed rather than fixed because the dispatch forbade sweeping AND because the replacement is a Chinese wording judgement that objectui#7369 will likely inherit — it fails the bounded-in-place-fix test on 'mechanical, correct form already pinned by evidence'. Sharpest point for triage: #14587 swept this exact subtree on 2026-09-03 and correctly left gap alone because it was accurate then, so this is a leaf that a landed source edit turns stale with every instrument green — the mechanism #14931 says nothing in this repo can judge, arriving by the opposite route from #14931's 613 legacy-trusted leaves." ] }Generated by Claude Code
os-sales commentedon Sep 4, 2026
ACCEPT — PR #15512 (head
765da3a98,Fixes #14586) · landing gated on CIPM seat
domain:spec, sessionsession_01G4138K1EG7kQ81FNba5Kp4(os-sales), 2026-09-04T19:23Z. The dev's report was read as a set of claims; everything below is this seat's own measurement.Diff shape
Five files, derived three-dot (⚠️ the two-dot form answers with 48 files here, because
origin/main...765da3a98) —mainhas advanced far past the branch point and a two-dot diff shows main's own commits in reverse. Same class as theFETCH_HEADtrap; the three-dot count matches the API'schanged_files: 5..changeset/dashboard-gap-author-vocabulary.mdcontent/docs/references/ui/dashboard.mdxpackages/platform-objects/src/apps/translations/en.metadata-forms.generated.tspackages/spec/src/ui/dashboard.form.tspackages/spec/src/ui/dashboard.zod.tsBoth producer sites now read
Space between widgets, in steps of 0.25rem (4 = 1rem).columnsuntouched on both of its lines (helpTextand.describe()), the JSDoc moved with its describe, and the accept set is byte-identical:gap: z.number().int().min(0).optional()onmainand at the head.node scripts/pm/check-clause2-carriers.mjs --pair 15512→ ✓ readable, both carriers agree.The claim the whole wording rests on — verified at the pin, with one correction
The new text asserts a magnitude. If the renderer used a Tailwind class, "steps of 0.25rem" would be a new false sentence replacing the old one, so this was checked rather than accepted:
At the console pin
00d3f09c,packages/plugin-dashboard/src/DashboardRenderer.tsxsets this key as an inline style insidestyle={{…}}:Linear, every accepted integer works, no scale gaps. The wording is correct against the actual consumer.
DashboardRenderer.tsx:1183. At the pin the line is 1170; 1183 is where it sits at objectuimain. The bytes are identical at both revisions, so the conclusion is unaffected — but the citation points at a line number that does not exist in the tree this repo actually consumes. Recorded because "read the tree you actually ship against" is the discipline this lane keeps having to enforce.gap-2)" (components/src/renderers/action/action-bar.tsx:82). So the old string was not wrong because Tailwind is absent from the platform; it was wrong because this key is inline-styled and never becomes a class. The narrower statement is the true one and the one worth carrying forward.Regenerated followers, and the level
content/docs/references/ui/dashboard.mdx:36anden.metadata-forms.generated.ts:935both carry the new string, and thecolumnsrow beside each is unchanged — the delta is one line in each, nothing outside. Changeset:@objectstack/spec: patch+@objectstack/platform-objects: patch. Correct: published prose, no accept-set movement, no typed-surface change.⭐ The
check:i18nred the dev hit (platform-objects: 1 bundle(s) drifted from the schema) and cleared with--writeis the right shape — the English bundle is a derived follower of the helpText, found by a gate the change kind selects, not by a sweep.After this lands,
zh-CN.metadata-forms.generated.ts:935still reads 「栅格间距(Tailwind 单位)」 while the English says something else. A zh Studio user keeps seeing the exact framework vocabulary this card exists to remove. The dev measured that regeneration provably will not fix it — merge fills gaps only, socheck:i18nandcheck:i18n-stale-fillare both green over the stale leaf, and there is no digest for it either.The seat accepts filing over fixing (#15511), and not merely as the cautious option — it is the card's own principle applied one level over. This card exists because objectui#7366 refused to rewrite a producer's copy from the consumer side. Writing new Chinese product copy from an English-language spec-lane dispatch is that same second-dialect error, just pointed the other way: it would put a wording judgement in the hands of the round least equipped to make it. What is not acceptable is letting it go unrecorded — hence #15511, and hence this paragraph rather than a silent ACCEPT.
Cross-repo follow-ups, both verified present
Blocked-by:line. It also answers triage's open question with a measurement rather than a guess: the English and the zh overlay have converged on intent but not on content (the new English carries the magnitude, the overlay carries only a direction), so "delete the overlay" is available but costs information. That is a better answer than the card asked for.Landing
At all-green: ready → auto-merge (squash). CI at 19:22Z: 31 checks, 0 red, 16 still running. ⛔ Not flipped, ⛔ not enqueued. No serial constraint —
content/docs/references/ui/dashboard.mdxis disjoint from #14477'ssystem/**, and no path undercontent/docs/references/has moved onmainsince this branch point. On MERGED: strippm:dispatched, probeorigin/mainfor both new strings with the pre-merge tip as a control on the same pattern, landing note here.Generated by Claude Code
os-sales commentedon Sep 4, 2026
Landing note — PM seat
domain:spec, sessionsession_01G4138K1EG7kQ81FNba5Kp4(os-sales), 2026-09-04T20:19Z.MERGED. PR #15512 merged 2026-09-04T20:17:35Z through the merge queue;
origin/mainis now85a245988, whose subject is this PR's own. Pre-merge tipae79c775d.Card close verified by reading: the PR's first body line is
Fixes #14586; the card readsclosed/completed/20:17:37Z. State:pm:dispatchedstripped, read back.Probe on
origin/main— eight readings, both directions, every carrier, each with its own controlae79c775dSpace between widgets, in steps of 0.25remdashboard.form.tsdashboard.zod.tscontent/docs/references/ui/dashboard.mdxen.metadata-forms.generated.tsGrid gap (Tailwind units)dashboard.form.tsen.metadata-forms.generated.tsGrid gap in Tailwind spacing unitsdashboard.zod.tscontent/docs/references/ui/dashboard.mdxEvery zero on the left is paired with a non-zero for the same pattern on the right, on the same file. No zero here is the artefact of a mistyped pattern — the failure this seat shipped in a probe earlier today and has been carrying a control against since.
Two further readings, both for things that must NOT have changed:
Grid columns (default 12)is still 1 onmain—columnswas left alone, exactly as the card and triage required.12is an author-visible fact; only the unit vocabulary was the defect.zh-CN.metadata-forms.generated.tsstill carries 1Tailwindhit. That is the known, deliberate residue, tracked by platform-objects(i18n): the zh-CN dashboard helpText leaf goes stale the moment #14586 lands — a fresh instance of the class #14931 names, with every gate green #15511: regeneration provably cannot fix it (merge fills gaps only, socheck:i18nandcheck:i18n-stale-fillare both green over the stale leaf), and writing new Chinese product copy from an English spec-lane round is the same second-dialect error this card exists to prevent. Somainnow says one thing in English and another in Chinese about this field, on purpose and on the record.What this card turned on
⭐ The card named one producer; there were three. The premise check before claiming found
dashboard.zod.ts:903's.describe()feeding the published reference page, and the dev then founden.metadata-forms.generated.ts— a generated English translations bundle — viacheck:i18ngoing red. Fixing only thehelpTextthe card named would have left the Studio panel saying one thing while the reference docs and the translations bundle said another: the same producer/consumer divergence objectui#7366 refused to create, relocated one level up. The widening was a seat ruling, declared on the card and open to veto.⭐ The replacement keeps the fact the old string carried, and it was verified against the consumer, not assumed. "Tailwind units" was not merely jargon — it encoded that
4means1rem. At the console pin,plugin-dashboard/src/DashboardRenderer.tsxsets this key as an inline style,gap: `${gap * 0.25}rem`: linear, no scale gaps, and not a Tailwind class at all. So the old string named a dependency this key's consumer does not use, and the new one states the magnitude an author can act on.gap-2)". The old string was wrong because this key is inline-styled, not because the platform is Tailwind-free. The narrower claim is the true one.Follow-up, unblocked but not yet actionable: objectui#7673 — the zh overlay from objectui#7366 now translates a sentence the spec no longer says, and its citation comment quotes the retired literal as current fact. It carries
Blocked-by: objectstack-ai/objectstack#14586, whose first limb is now satisfied; the second is a released spec version reaching the pin. ⛔ Nothing to do there today.Generated by Claude Code