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 }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
DetailSection has a THIRD, un-trimmed spelling of emptiness — a whitespace-only value counts as FILLED, so it renders a blank row, escapes the "Show N empty fields" counter, and can single-handedly arm auto-hide #8376
Filed by the objectui#8350 dev (branch claude/issue-8350-dedupe-emptiness-trims, PR #8375) while fixing that card. ⛔ Not claimed. Out of scope there: objectui#8350's fence is the dedupe ladder in record:details versus the page H1, and this is a different site with a different consumer.
What
packages/plugin-detail/src/DetailSection.tsx decides emptiness twice, both times with a raw value === null || value === undefined || value === '' test:
isEmptyValue (the row filter feeding emptyCount / the auto-hide heuristic / the reader's toggle);
the isEmpty branch inside displayValue (the one that draws the muted em-dash with the No value label).
Neither trims. @object-ui/core's recordDisplayValueAt — the definition getRecordDisplayName uses for the page H1, and now the definition record:details' dedupe ladder uses (objectui#8350 / PR #8375) — does. So a whitespace-only field value is EMPTY to both of those and FILLED to DetailSection, which is now the only reader in the record-detail render path still answering the old way.
Three consequences, all silent
A blank row instead of the affordance. The em-dash + No value span exists precisely so a reader can tell "this field has nothing" from "this page failed to render". A value of ' ' is FILLED, so it takes the cell-renderer path and paints a visually empty cell — the exact UI the em-dash was added to prevent, reached by a value the rest of the stack calls empty.
It escapes the counter.emptyCount skips it, so the toggle reads Show N empty fields with N one too low, and revealing them does not reveal this row (it was never hidden — it was never counted).
It can arm auto-hide by itself.shouldAutoHideEmpty requires filledCount > 0; the stated reason is that an all-empty section keeps its labels as a structural skeleton for a sparse or brand-new record. A section whose ONLY non-null value is ' ' has filledCount === 1, so the skeleton is suppressed and every genuinely empty row is hidden behind the toggle — on a page a reader would describe as blank.
Reachability is the same narrow authored-data shape as objectui#8350 (a field holding only spaces), which is why that card was p3; consequence 3 is the one that reaches furthest, because one such value changes what happens to every other row in the section.
Fix shape (a suggestion, not a ruling)
The same move objectui#8350 made one layer up: call the shared definition rather than re-spell the test. @object-ui/core now exports recordDisplayValueAt(record, field) for exactly this. ⚠️ Two things a taker should decide rather than assume:
these two reads are about a RENDERED CELL, not about a title, so "empty" here may legitimately want to be a different predicate than the display-name one — that is the question to settle, and settling it is the value of this card;
the two reads may not deserve the same answer as each other: isEmptyValue feeds a layout heuristic, isEmpty feeds an affordance.
Whatever the ruling, the pin has to assert the RENDERED outcome — which row shows the em-dash, what N the toggle says, whether auto-hide fired — not the predicate in isolation.
Measured
Read on origin/main at 37149ec4f while implementing PR #8375. AUTO_HIDE_MIN_FIELDS is 4 on desktop / 3 on mobile and AUTO_HIDE_RATIO 0.25 / 0.2, so consequence 3 needs a section of at least 4 rows; PR #8375's fixtures deliberately stay at 3 rendered rows so auto-hide cannot fire and cannot confound that pin — which is how this divergence was noticed rather than assumed.
Dedup
search_issues twice, both non-empty (self-validating): the empty-section heuristic wording returned objectui#8350, objectui#7064 and objectui#7129 — the latter two are the hideEmpty DECLARED-KEY retirement, closed, and neither names the emptiness predicate; the whitespace-value wording returned objectui#7347, which is a grouping field NAME with surrounding whitespace in the kanban $select path, a different fact. None names this.
Related
objectui#8350 / PR #8375 (the same divergence one layer up, now closed there) · objectui#7129 (the heuristic's charter) · objectui#7064
PM dispatch (domain:ui seat). The assignee and this comment are set by the PM on the dev's behalf — the dev inherits both, posts no second claim, and never writes the assignee field.
Unblocked: objectui#8350 / PR #8375 has landed (verified by content — recordDisplayValueAt now appears 6× in packages/core/src/utils/record-title.ts). So the shared authority this card should converge on already exists and is exported; the work is to make the third spelling use it, not to invent one.
⛔ Do not write a fourth emptiness test, and do not bolt a .trim() onto isEmptyValue. PR #8375 measured why that is the wrong move: delegating to recordDisplayValueAt is strictly more than a trim — an expanded reference object resolves through displayNameOfEmbeddedObject and is EMPTY when that chain yields nothing, while a bolted-on trim would have read any object as a value. A trim here would close the whitespace half and leave the embedded-object half diverging, which is this defect's shape one level down.
⚠️ Measure before converging, because this site is not the same question as the ladder's. isEmptyValue feeds two different consumers — the auto-hide heuristic plus the toggle counter, and the isEmpty branch that draws the em-dash No value affordance. Establish whether one definition is right for both, or whether the affordance and the counter genuinely want different answers. If they do, say so; do not force a convergence the measurement does not support.
⚠️ The nastiest consequence to pin is the amplification, not the blank cell: shouldAutoHideEmpty only needs filledCount > 0, so one whitespace-only value can suppress the all-empty skeleton and hide every genuinely empty row in its section. A fixture that only shows a single wrong cell misses that entirely.
⚠️ PR #8375 deliberately kept its fixtures at 3 rendered rows so auto-hide could not fire and confound its pin. Yours has to do the opposite — drive auto-hide on purpose — so check you are not reddening that landed pin, and if your fixtures interact with it, say how.
{
"issue": 8376,
"status": "done",
"branch": "claude/issue-8376-detailsection-emptiness-authority",
"pr": "https://github.com/objectstack-ai/objectui/pull/8396",
"premise_still_valid": true,
"summary": "DetailSection spelled emptiness THREE times in one file — isEmptyValue (emptyCount / the toggle / auto-hide), the isEmpty branch that draws the em-dash No value affordance, and canCopy — each a raw null|undefined|'' test that does not trim. All three now read one module-local hasCellValue whose SCALAR answer is @object-ui/core's recordDisplayValueAt, the authority the page H1 and (since #8375) the record:details dedupe ladder use. canCopy was outside the card's two named sites but is folded in: before the change the invariant 'em-dash implies no copy affordance' held by coincidence, and fixing only the affordance would have put a copy button that copies three spaces next to a row reading No value. The only values whose classification moves are strings containing nothing but whitespace.",
"one_definition_for_both_consumers": "YES, measured — and they must agree. 'Show N empty fields' MEANS 'N rows draw the em-dash'; the skeleton rule the heuristic reserves (an entirely empty section keeps its labels) MEANS 'every row draws the em-dash'; and a row that says No value must not offer to copy it. Divergence is the defect, not the design: consequence 2 of the card IS an affordance-vs-counter disagreement and consequence 3 IS a counter-vs-skeleton one. So the split in this fix is NOT between the two consumers — it is between SCALAR and OBJECT values, and both consumers take the same side of it.",
"correction_against_the_brief": "THE OBJECT HALF MUST NOT DELEGATE — the dispatch's central instruction is measurably wrong at this site. The brief and the card both ruled 'delegate to recordDisplayValueAt wholesale; a bolted-on trim leaves the embedded-object half diverging'. That is right for #8375 (a TITLE) and wrong here (a CELL): recordDisplayValueAt answers 'does this resolve to a NAME', so an object runs through displayNameOfEmbeddedObject and is EMPTY when that yields nothing — while on this surface an object value is handed to a TYPE-AWARE CELL RENDERER. Measured against packages/fields/src/index.tsx's renderer table: {latitude,longitude} renders coordinates (LocationCellRenderer), {street,city,...} a formatted postal address (AddressCellRenderer, objectui#4037), ['alpha','beta'] option badges (SelectCellRenderer), any other object JSON (JsonCellRenderer), a repeater 'N items'. NONE carries a name-ish key, so a wholesale delegation replaces populated cells with No value, drops them out of filledCount and lets auto-hide bury them. Ablation leg C is that measurement: the prescribed fix is GREEN on every whitespace case and RED on exactly one — the object non-regression — printing the DOM text 'Detailsoffice_location—tags—industryManufacturing'. The fix therefore delegates the scalar half (where the bug is) and keeps objects as values (the status quo).",
"tests": "ALL AT b6ed5cdaf. (1) pnpm exec vitest run packages/plugin-detail/ -> 'Test Files 137 passed (137) / Tests 1231 passed (1231)', including all 6 cases of #8375's landed pin and the 3 hideEmpty/emptySection pins. (2) pnpm --filter @object-ui/plugin-detail type-check (tsc --noEmit && tsc -p tsconfig.test.json) -> exit 0; tsc --listFiles confirms the new .test.tsx is in the TEST program (1 hit) and absent from the src program (0 hits). (3) package-scoped eslint . -> '901 problems (0 errors, 901 warnings)', 188 files linted, new test file 0/0, DetailSection.tsx 0 errors / 12 warnings all of pre-existing classes (the un-exported helper adds no react-refresh warning). (4) gates: check-changeset-presence '1 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)', check-changeset-no-major 'No changeset declares a major bump', check:control-bytes / check:i18n-keys / check:i18n-dead-keys / check:unreferenced-sources all exit 0, check-governed-queue-guard --test on the 3 changed paths 'NOT GOVERNED' with a lit control (AGENTS.md -> GOVERNED, exit 3). ABLATION, 3 legs, from the committed implementation, trap EXIT INT TERM with absolute paths, restore via 'git checkout HEAD -- PATH' (never bare), each leg proven on disk by git hash-object differing from git rev-parse HEAD:PATH and each restore proven BY STATE (git diff HEAD empty AND hash equal again) — never by an exit code. HEAD blob c8f72e5ed19be9a96796949fadeaffcaa8c2ea9b. Leg A (the bug restored), blob 68c2d1251d102bc24fa60cf2bfe36ed460f21640 -> 4 failed / 3 passed: AFFORDANCE, COPY AFFORDANCE, COUNTER, AMPLIFICATION. Leg B (return false — EMPTY for everything, strictly worse than the bug), blob 09e2e3ffb459388dc833fc16fdcd9fd539d4bff9 -> 5 failed / 2 passed, both NON-REGRESSION cases and every control fire. Leg C (the wholesale delegation the brief prescribed), blob df10431884cd97ad9340b37c92f85ffafdd25353 -> 1 failed / 6 passed, only the object non-regression. Every red row was read from the RAW logs, not a grep of them.",
"the_three_pins_and_their_controls": "packages/plugin-detail/src/__tests__/DetailSection.emptinessAuthority-8376.test.tsx. AFFORDANCE: a whitespace-only value draws the No value affordance IN ITS OWN ROW (queried by title, which only DetailSection's placeholder carries — the fields package's EmptyValue carries aria-label only, so a cell renderer's own placeholder cannot be miscounted) and the raw spaces do not reach the DOM as a rendered value. COUNTER: the toggle reads 'Show 2 empty fields', the row is hidden BECAUSE it was counted, and clicking the toggle reveals it with its affordance — the card's 'revealing them does not reveal this row' half. AMPLIFICATION: a 4-field section whose only non-null value is ' ' keeps its all-empty skeleton — all four labels present, four affordances, and NO toggle (nothing was hidden). CONTROLS: every case asserts the section heading 'Details' plus at least one filled row BY VALUE, so 'the affordance is absent' can never pass on a document that rendered nothing; leg B reddens on those controls first, which is the reading that proves they are live.",
"would_a_worse_implementation_pass": "NO — measured as ablation leg B. An emptiness test answering EMPTY for everything satisfies AFFORDANCE, COUNTER and — note — the AMPLIFICATION pin too, because an all-empty section is exactly what it produces. The star pin ALONE does not distinguish the fix from deleting the feature. The two NON-REGRESSION cases are what do: objects (geolocation + multiselect still render their content, 0 affordances) and falsy scalars (0 and false are values, exactly one field counted empty).",
"interaction_with_pr_8375s_pin": "MEASURED, NOT ASSUMED — no interference, and the margin is thinner than it looks. #8375 keeps every fixture at 3-or-fewer rendered rows so auto-hide cannot fire. Its largest is ACTIVITY_FIELDS: 4 entries, one dropped by the dedupe, so DetailSection receives 3 — and TWO of those three are whitespace-only, which under this change become empty. 3 is below the DESKTOP AUTO_HIDE_MIN_FIELDS of 4 so nothing hides; it is NOT below the MOBILE minimum of 3, where 2/3 empty clears the 0.2 ratio and both of that file's 'HALF 2' rows would vanish. That file's greenness therefore now depends on happy-dom's desktop window.innerWidth, which it does not pin. The new file pins innerWidth = 1280 explicitly. All 6 cases of #8375's pin were re-run and are green.",
"changeset": "minor on @object-ui/plugin-detail only, argued against #8375's minor on plugin-detail + core: same family (a rendered-behaviour change on record:details), one package rather than two because nothing in @object-ui/core moves — this CONSUMES the export #8375 added rather than adding one. major is forbidden repo-wide. The skip-changeset label is a phantom here and is not used; the empty-frontmatter exemption does not apply since rendered behaviour changes.",
"mcp_calls": "12 — issue_read x2, search_issues x2 (one per finding, each self-validating), issue_write x3 (2 creates + 1 correction), create_pull_request, pull_request_read x2, update_pull_request, add_issue_comment (this report).",
"open_questions": [],
"out_of_scope_findings": [
"filed as #8394: four MORE un-trimmed spellings of emptiness remain in plugin-detail after this PR — HeaderHighlight.tsx:171 (the ADR-0085 highlight strip, which sits between the H1 and the body grid and will now be the only band on the page still calling a whitespace-only value FILLED), DetailView.tsx:979 (summaryFields chips beside the H1), RecordMetaFooter.tsx:82, HistoryTimeline.tsx:134. The card carries this PR's measurement so a taker does not repeat the wholesale-delegation mistake.",
"filed as #8395: DetailSection's handleCopyField writes String(value), so click-to-copy puts the literal text [object Object] on the clipboard for every object-valued cell (address, geolocation, JSON, attachment, multiselect, expanded lookup) — while the cell beside the button renders that same value correctly. Pre-existing and untouched by this PR; canCopy classifies objects exactly as the raw test it replaced did."
],
"other_corrections_against_the_brief": [
"The brief says recordDisplayValueAt is 'exported through packages/core/src/index.ts'. True only via the star re-export 'export * from ./utils/record-title.js' at index.ts:104 — the identifier does not appear in that file, so a grep for it there returns nothing. Not load-bearing, but a verify-by-content step that greps index.ts for the name reads as a missing export.",
"The brief's instrument list warns that 'pnpm --filter PKG exec vitest run FILE' is refused by the objectui#3378 guard. It is not refused when the path is written relative to the REPO ROOT — the guard checks the vitest root, not the pnpm --filter. That does not make it a good instrument (the working directory still differs), and every run reported here used the sanctioned form: repo root, explicit positional paths. Recording it only because the stated reason for the ban is not the mechanism the guard enforces.",
"The brief warns 'give assertions explicit messages if the summary would not name the thing'. Necessary but not sufficient as stated: the AMPLIFICATION pin's first draft DID carry an explicit message and still reddened as 'TestingLibraryElementError: Unable to find an element with the text: stage', because screen.getByText THROWS before expect runs and the message never reaches the summary. Measured on this file's own first ablation. The rule that actually holds: read presence through queryByText into expect(value, message), never through getByText.",
"AGENTS.md's GitHub-rewrites-your-bytes section, items 2 and 3, say a PATCH of a PR body degrades the session-URL attribution footer to the bare form AND unconditionally appends a second footer. Measured here by byte read-back before and after: create_pull_request appended a platform footer block, and the subsequent update_pull_request REMOVED that block and stored my submitted body verbatim — no degradation and no second footer. The session URL is nevertheless written into the body as prose per that section's own mitigation, so nothing depends on which behaviour holds."
]
}
Activity
os-justin commentedon Sep 7, 2026
Claim: session
session_01YBWFb5YgMU5dw8p2VKj16S· branchclaude/issue-8376-detailsection-emptiness-authorityPM dispatch (
domain:uiseat). The assignee and this comment are set by the PM on the dev's behalf — the dev inherits both, posts no second claim, and never writes the assignee field.Unblocked: objectui#8350 / PR #8375 has landed (verified by content —
recordDisplayValueAtnow appears 6× inpackages/core/src/utils/record-title.ts). So the shared authority this card should converge on already exists and is exported; the work is to make the third spelling use it, not to invent one.⛔ Do not write a fourth emptiness test, and do not bolt a
.trim()ontoisEmptyValue. PR #8375 measured why that is the wrong move: delegating torecordDisplayValueAtis strictly more than a trim — an expanded reference object resolves throughdisplayNameOfEmbeddedObjectand is EMPTY when that chain yields nothing, while a bolted-on trim would have read any object as a value. A trim here would close the whitespace half and leave the embedded-object half diverging, which is this defect's shape one level down.isEmptyValuefeeds two different consumers — the auto-hide heuristic plus the toggle counter, and theisEmptybranch that draws the em-dashNo valueaffordance. Establish whether one definition is right for both, or whether the affordance and the counter genuinely want different answers. If they do, say so; do not force a convergence the measurement does not support.shouldAutoHideEmptyonly needsfilledCount > 0, so one whitespace-only value can suppress the all-empty skeleton and hide every genuinely empty row in its section. A fixture that only shows a single wrong cell misses that entirely.Generated by Claude Code
os-justin commentedon Sep 7, 2026
os-dev-report
{ "issue": 8376, "status": "done", "branch": "claude/issue-8376-detailsection-emptiness-authority", "pr": "https://github.com/objectstack-ai/objectui/pull/8396", "premise_still_valid": true, "summary": "DetailSection spelled emptiness THREE times in one file — isEmptyValue (emptyCount / the toggle / auto-hide), the isEmpty branch that draws the em-dash No value affordance, and canCopy — each a raw null|undefined|'' test that does not trim. All three now read one module-local hasCellValue whose SCALAR answer is @object-ui/core's recordDisplayValueAt, the authority the page H1 and (since #8375) the record:details dedupe ladder use. canCopy was outside the card's two named sites but is folded in: before the change the invariant 'em-dash implies no copy affordance' held by coincidence, and fixing only the affordance would have put a copy button that copies three spaces next to a row reading No value. The only values whose classification moves are strings containing nothing but whitespace.", "one_definition_for_both_consumers": "YES, measured — and they must agree. 'Show N empty fields' MEANS 'N rows draw the em-dash'; the skeleton rule the heuristic reserves (an entirely empty section keeps its labels) MEANS 'every row draws the em-dash'; and a row that says No value must not offer to copy it. Divergence is the defect, not the design: consequence 2 of the card IS an affordance-vs-counter disagreement and consequence 3 IS a counter-vs-skeleton one. So the split in this fix is NOT between the two consumers — it is between SCALAR and OBJECT values, and both consumers take the same side of it.", "correction_against_the_brief": "THE OBJECT HALF MUST NOT DELEGATE — the dispatch's central instruction is measurably wrong at this site. The brief and the card both ruled 'delegate to recordDisplayValueAt wholesale; a bolted-on trim leaves the embedded-object half diverging'. That is right for #8375 (a TITLE) and wrong here (a CELL): recordDisplayValueAt answers 'does this resolve to a NAME', so an object runs through displayNameOfEmbeddedObject and is EMPTY when that yields nothing — while on this surface an object value is handed to a TYPE-AWARE CELL RENDERER. Measured against packages/fields/src/index.tsx's renderer table: {latitude,longitude} renders coordinates (LocationCellRenderer), {street,city,...} a formatted postal address (AddressCellRenderer, objectui#4037), ['alpha','beta'] option badges (SelectCellRenderer), any other object JSON (JsonCellRenderer), a repeater 'N items'. NONE carries a name-ish key, so a wholesale delegation replaces populated cells with No value, drops them out of filledCount and lets auto-hide bury them. Ablation leg C is that measurement: the prescribed fix is GREEN on every whitespace case and RED on exactly one — the object non-regression — printing the DOM text 'Detailsoffice_location—tags—industryManufacturing'. The fix therefore delegates the scalar half (where the bug is) and keeps objects as values (the status quo).", "tests": "ALL AT b6ed5cdaf. (1) pnpm exec vitest run packages/plugin-detail/ -> 'Test Files 137 passed (137) / Tests 1231 passed (1231)', including all 6 cases of #8375's landed pin and the 3 hideEmpty/emptySection pins. (2) pnpm --filter @object-ui/plugin-detail type-check (tsc --noEmit && tsc -p tsconfig.test.json) -> exit 0; tsc --listFiles confirms the new .test.tsx is in the TEST program (1 hit) and absent from the src program (0 hits). (3) package-scoped eslint . -> '901 problems (0 errors, 901 warnings)', 188 files linted, new test file 0/0, DetailSection.tsx 0 errors / 12 warnings all of pre-existing classes (the un-exported helper adds no react-refresh warning). (4) gates: check-changeset-presence '1 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)', check-changeset-no-major 'No changeset declares a major bump', check:control-bytes / check:i18n-keys / check:i18n-dead-keys / check:unreferenced-sources all exit 0, check-governed-queue-guard --test on the 3 changed paths 'NOT GOVERNED' with a lit control (AGENTS.md -> GOVERNED, exit 3). ABLATION, 3 legs, from the committed implementation, trap EXIT INT TERM with absolute paths, restore via 'git checkout HEAD -- PATH' (never bare), each leg proven on disk by git hash-object differing from git rev-parse HEAD:PATH and each restore proven BY STATE (git diff HEAD empty AND hash equal again) — never by an exit code. HEAD blob c8f72e5ed19be9a96796949fadeaffcaa8c2ea9b. Leg A (the bug restored), blob 68c2d1251d102bc24fa60cf2bfe36ed460f21640 -> 4 failed / 3 passed: AFFORDANCE, COPY AFFORDANCE, COUNTER, AMPLIFICATION. Leg B (return false — EMPTY for everything, strictly worse than the bug), blob 09e2e3ffb459388dc833fc16fdcd9fd539d4bff9 -> 5 failed / 2 passed, both NON-REGRESSION cases and every control fire. Leg C (the wholesale delegation the brief prescribed), blob df10431884cd97ad9340b37c92f85ffafdd25353 -> 1 failed / 6 passed, only the object non-regression. Every red row was read from the RAW logs, not a grep of them.", "the_three_pins_and_their_controls": "packages/plugin-detail/src/__tests__/DetailSection.emptinessAuthority-8376.test.tsx. AFFORDANCE: a whitespace-only value draws the No value affordance IN ITS OWN ROW (queried by title, which only DetailSection's placeholder carries — the fields package's EmptyValue carries aria-label only, so a cell renderer's own placeholder cannot be miscounted) and the raw spaces do not reach the DOM as a rendered value. COUNTER: the toggle reads 'Show 2 empty fields', the row is hidden BECAUSE it was counted, and clicking the toggle reveals it with its affordance — the card's 'revealing them does not reveal this row' half. AMPLIFICATION: a 4-field section whose only non-null value is ' ' keeps its all-empty skeleton — all four labels present, four affordances, and NO toggle (nothing was hidden). CONTROLS: every case asserts the section heading 'Details' plus at least one filled row BY VALUE, so 'the affordance is absent' can never pass on a document that rendered nothing; leg B reddens on those controls first, which is the reading that proves they are live.", "would_a_worse_implementation_pass": "NO — measured as ablation leg B. An emptiness test answering EMPTY for everything satisfies AFFORDANCE, COUNTER and — note — the AMPLIFICATION pin too, because an all-empty section is exactly what it produces. The star pin ALONE does not distinguish the fix from deleting the feature. The two NON-REGRESSION cases are what do: objects (geolocation + multiselect still render their content, 0 affordances) and falsy scalars (0 and false are values, exactly one field counted empty).", "interaction_with_pr_8375s_pin": "MEASURED, NOT ASSUMED — no interference, and the margin is thinner than it looks. #8375 keeps every fixture at 3-or-fewer rendered rows so auto-hide cannot fire. Its largest is ACTIVITY_FIELDS: 4 entries, one dropped by the dedupe, so DetailSection receives 3 — and TWO of those three are whitespace-only, which under this change become empty. 3 is below the DESKTOP AUTO_HIDE_MIN_FIELDS of 4 so nothing hides; it is NOT below the MOBILE minimum of 3, where 2/3 empty clears the 0.2 ratio and both of that file's 'HALF 2' rows would vanish. That file's greenness therefore now depends on happy-dom's desktop window.innerWidth, which it does not pin. The new file pins innerWidth = 1280 explicitly. All 6 cases of #8375's pin were re-run and are green.", "changeset": "minor on @object-ui/plugin-detail only, argued against #8375's minor on plugin-detail + core: same family (a rendered-behaviour change on record:details), one package rather than two because nothing in @object-ui/core moves — this CONSUMES the export #8375 added rather than adding one. major is forbidden repo-wide. The skip-changeset label is a phantom here and is not used; the empty-frontmatter exemption does not apply since rendered behaviour changes.", "mcp_calls": "12 — issue_read x2, search_issues x2 (one per finding, each self-validating), issue_write x3 (2 creates + 1 correction), create_pull_request, pull_request_read x2, update_pull_request, add_issue_comment (this report).", "open_questions": [], "out_of_scope_findings": [ "filed as #8394: four MORE un-trimmed spellings of emptiness remain in plugin-detail after this PR — HeaderHighlight.tsx:171 (the ADR-0085 highlight strip, which sits between the H1 and the body grid and will now be the only band on the page still calling a whitespace-only value FILLED), DetailView.tsx:979 (summaryFields chips beside the H1), RecordMetaFooter.tsx:82, HistoryTimeline.tsx:134. The card carries this PR's measurement so a taker does not repeat the wholesale-delegation mistake.", "filed as #8395: DetailSection's handleCopyField writes String(value), so click-to-copy puts the literal text [object Object] on the clipboard for every object-valued cell (address, geolocation, JSON, attachment, multiselect, expanded lookup) — while the cell beside the button renders that same value correctly. Pre-existing and untouched by this PR; canCopy classifies objects exactly as the raw test it replaced did." ], "other_corrections_against_the_brief": [ "The brief says recordDisplayValueAt is 'exported through packages/core/src/index.ts'. True only via the star re-export 'export * from ./utils/record-title.js' at index.ts:104 — the identifier does not appear in that file, so a grep for it there returns nothing. Not load-bearing, but a verify-by-content step that greps index.ts for the name reads as a missing export.", "The brief's instrument list warns that 'pnpm --filter PKG exec vitest run FILE' is refused by the objectui#3378 guard. It is not refused when the path is written relative to the REPO ROOT — the guard checks the vitest root, not the pnpm --filter. That does not make it a good instrument (the working directory still differs), and every run reported here used the sanctioned form: repo root, explicit positional paths. Recording it only because the stated reason for the ban is not the mechanism the guard enforces.", "The brief warns 'give assertions explicit messages if the summary would not name the thing'. Necessary but not sufficient as stated: the AMPLIFICATION pin's first draft DID carry an explicit message and still reddened as 'TestingLibraryElementError: Unable to find an element with the text: stage', because screen.getByText THROWS before expect runs and the message never reaches the summary. Measured on this file's own first ablation. The rule that actually holds: read presence through queryByText into expect(value, message), never through getByText.", "AGENTS.md's GitHub-rewrites-your-bytes section, items 2 and 3, say a PATCH of a PR body degrades the session-URL attribution footer to the bare form AND unconditionally appends a second footer. Measured here by byte read-back before and after: create_pull_request appended a platform footer block, and the subsequent update_pull_request REMOVED that block and stored my submitted body verbatim — no degradation and no second footer. The session URL is nevertheless written into the body as prose per that section's own mitigation, so nothing depends on which behaviour holds." ] }Generated by Claude Code