Skip to content

feat(core): isEmptyValue — the shared emptiness floor, and the surfaces that state their answer against it - #8981

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-8496-emptiness-floor
Sep 10, 2026
Merged

feat(core): isEmptyValue — the shared emptiness floor, and the surfaces that state their answer against it#8981
os-steve merged 1 commit into
mainfrom
claude/issue-8496-emptiness-floor

Conversation

@os-steve

@os-steve os-steve commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

Fixes #8496

Implements the landed ruling (comment 5581959569 — director seat, decision batch #86, 2026-09-08): option B, in @object-ui/core.

Clause-②: yes — cross-package change adding a published export; the PR carries needs:contract-review (added with the additive labels endpoint, read back as the union with the five the labeler set). check-governed-queue-guard.mjs --test on all eight touched paths reports NOT GOVERNED — 8 path(s) checked against 5 governed surface(s); none matched, so the normal review and merge-queue route applies; this lane's in-seat default-tier review plus the gates is the review of record (objectstack#17285).

The dependency measurement that decided the placement

The ruling made core conditional: fall back to @object-ui/types if @object-ui/fields does not already depend on core. Measured — it does, so no fallback.

read result
packages/fields/package.json dependencies contains @object-ui/core (alongside components, i18n, providers, react, types)
peerDependencies / devDependencies no core entry in either; the edge is a plain runtime dependency
import graph, fields/src to core 40 files import @object-ui/core, including packages/fields/src/index.tsx itself (the barrel this PR edits)
import graph, core/src to fields zero import statements; every occurrence of the string is prose in a comment. No cycle, no new edge in either direction
plugin-list / plugin-kanban / plugin-detail all three already declare and import core

That last row is also why option C stays refused: the gallery and the kanban would have had to import a fields helper to decide whether to call a fields renderer.

Where the floor landed

packages/core/src/utils/emptiness.ts, exported from the barrel. Exactly four members and no fifth: null, undefined, the empty string, the empty array.

The floor was not invented — it was promoted. packages/core/src/evaluator/optionRules.ts already held a module-private isEmptyValue spelling byte-for-byte those four members, written before the card and never counted by it. The new module is that function, moved up and exported; optionRules now imports it. So this PR introduces zero new spellings of the four members — it removes five.

"Empty string" means '', not "blank". That is forced by the weakest-common-claim rule: record:details and RelatedList trim, and the gallery, the kanban and the shared renderers do not. A trimming floor would not be a floor. The trim is therefore an extension at two sites, and it is pinned as one.

The surfaces, re-derived by anchor (the card's line numbers are from 2026-09-08)

surface card said measured now relation to the floor
plugin-detail hasCellValue emptiness.ts packages/plugin-detail/src/emptiness.ts floor + trim (objectui#8350)
plugin-detail RelatedList.isValueEmpty RelatedList.tsx anchor const isValueEmpty = (v: any) => floor + trim, kept a separate function on purpose (objectui#8459)
plugin-list inline ObjectGallery.tsx:571 renderFields, drifted to :582; a second copy in resolveCoverUrl floor exactly, twice
plugin-kanban inline ObjectKanban.tsx:604 the explicitCardFields loop, drifted to :813; two more in resolveDisplay and descMissing floor exactly (loop, resolveDisplay); floor + opaque-id (descMissing)
@object-ui/fields "seven private guard idioms" ten distinct spellings across 19 renderers see below

More than five, as suspected — and the extra ones are the interesting half. Beyond fields' count drifting from seven to ten, @object-ui/core itself held two more private answers (optionRules, promoted here; dashboard-filters, a different question — it is keyed by a filter def and answers "carries no constraint"). A wider census found the same four members spelled again in app-shell/utils/auditHistoryDisplay.ts, app-shell/views/metadata-admin/inspectors/datasetFilterCondition.ts, fields/widgets/LookupField.tsx, plugin-grid/useColumnSummary.ts, plugin-grid/components/BulkActionDialog.tsx, plugin-gantt/ObjectGantt.tsx, core/validation/required-presence.ts, components/custom/filter-builder.tsx and components/renderers/form/form.tsx. Those are noted, not filed and not touched: the ruling scoped this change to the five tabulated surfaces, none of them is a defect (each already answers the floor), and each is now a one-line swap for whoever picks up this card's follow-through.

@object-ui/fields, guard by guard

guard renderers relation
isEmptyValue(value) Select, Lookup the floor exactly (was value == null || value === '' || isEmptyMultiValue(value))
isEmptyValue(safe) Text, Formula the floor exactly, on the coerced text
isEmptyValue(value) ColorSwatch the floor exactly (was value == null; ''/[] reached the same affordance one branch down)
isEmptyValue(safe) || trim isBlankCellTextNumber, Currency, Percent, Date, Email, Url, Phone, ColorSwatch floor + whitespace, on the coerced text
isEmptyValue(value) || !value User floor + every falsy scalar
typeof value !== 'boolean' Boolean floor strictly subsumed — an explicit floor call would be a dead disjunct, so the justification is written instead; false stays a value (objectui#8582)
!value then the blank/unparsable test Date, DateTime floor + every falsy scalar, so the numeric epoch is empty on purpose
!value File floor with [] DECLINED — it states "0 files"
!value || imgs.length === 0 Image floor + falsy + "no displayable image"
isEmptyValue(value) && !Array.isArray(value) Json, Location, Address floor with [] DECLINED, out loud (objectui#8474)

isEmptyMultiValue is gone: all three of its call sites were spelling the floor around it. Its measured reasoning (the childless-container defect, and the catalogue of renderers that disagree) is carried forward — the catalogue now sits as a roster at the head of isBlankCellText, and the childless-container paragraph at SelectCellRenderer's guard.

Two visible fixes came with it

A gallery card and a kanban card holding [] in a card field now OMIT that field, exactly as they already did for null. Before, [] fell through the private guard to the shared renderer, which since objectui#8481 answers it with the "No value" em-dash — so one card could draw a labelled placeholder for tags: [] while silently omitting the null field beside it. Both are pinned.

The anti-flattening assertions (the load-bearing half)

A green suite that only proves the floor works proves the cheap half. Every case below asserts a disagreement:

  • packages/core/src/utils/__tests__/emptiness-floor-8496.test.ts — the four members; 14 refused fifth members each with the measurement that refused it (' ', {}, 0, false, new Date(0), a populated Map/Set, a class instance behind getters, …); a count assertion so a widening cannot pass unnoticed; and the promotion's non-regression evidence through isOptionGroupGated / isValueStillOffered.
  • packages/fields/src/__tests__/emptinessFloorExtensions-8496.test.tsxjson/object/composite/record still print the literal for []; location/geolocation/address still keep the unknown shape visible; file/video/audio still state the count; boolean calls every non-boolean empty and false a value; date/datetime call the numeric epoch empty; user extends with falsy; the coercing family trims and text deliberately does not.
  • packages/plugin-detail/src/__tests__/emptinessFloorExtensions-8496.test.tsx — the trim as a stated disagreement with the floor, at the predicate and once at the DOM through the real RelatedList grid; and the seven values hasCellValue refuses to let the floor grow.
  • ObjectGallery.emptinessFloor-8496.test.tsx / ObjectKanban.emptinessFloor-8496.test.tsx — the [] omission, each with a control that the surface still does not trim.

104 assertions across the five files.

Each of the four pinned disagreements, verified rather than assumed

disagreement was it really pinned before this PR?
JsonCellRenderer draws the array literal yescellRenderers.emptyArray-8481.test.tsx, "THE BOUNDARY"
BooleanCellRenderer keeps false a value yesbooleanCell.nonBooleanScalar-8582.test.tsx and cellRenderers.fabricatedValue-8490.test.tsx
DateCellRenderer's !value treats the epoch as empty NO — it was UNPINNED. Nothing in the tree asserted date holding 0. The nearest pin asserts the opposite direction (a numeric epoch timestamp of 1700000000000 still renders). It is pinned now, in the fields file above
RelatedList's local predicate yesRelatedList.emptinessAgreement-8459.test.tsx, DOM-level

Reverse verification — the pins can fail, and they fail for the right reason

Run against the committed tree, one mutation at a time; each leg proved the edit reached disk (target-text count went to zero, injected marker appeared, git hash-object differed from the HEAD blob) and each restore was proved by the blob matching HEAD again with an empty git diff HEAD. No mutation survives in the branch.

leg mutation result
A — is the floor load-bearing? drop the [] member from isEmptyValue all four pin files RED, 12 failed / 77 passed
B — the ruling's named case flatten JsonCellRenderer to a bare isEmptyValue(value) 7 RED — every json/object/composite/record/location/geolocation/address case, each reporting expected '—' to contain '[]'
C — the extension that was previously unpinned flatten DateCellRenderer's !value to isEmptyValue(value) 1 REDdate holding 0: the epoch is empty here on purpose, 1 failed / 58 passed

Restored tree, all five pin files re-run: Test Files 5 passed (5) · Tests 104 passed (104).

Gates — every exit code captured to disk before it was read

All on branch head eb813566e.

gate exit reading
dependency-closure build 0 pnpm --filter '@object-ui/plugin-kanban^...' --filter '@object-ui/plugin-list^...' build
doc-gate closure build 0 turbo run build $(check-doc-snippet-types.mjs --build-filter) --concurrency=235 successful, 35 total
type-check (the 5 affected packages) 0 all five Done
suites: core + fields 0 Test Files 294 passed (294) · Tests 5678 passed (5678)
suites: plugin-list + plugin-kanban 0 Test Files 121 passed (121) · Tests 1165 passed (1165)
suite: plugin-detail 0 Test Files 160 passed (160) · Tests 1488 passed (1488)
lint (the 5 affected packages) 0 0 errors in all five (2193 pre-existing warnings, none on a changed line)
check-control-bytes.mjs 0 scanned 7180 tracked text file(s); skipped 85 binary
check-changeset-presence.mjs 0 7 source file(s) of 5 released package(s) changed, and this change declares 1 changeset(s)
check-governed-queue-guard.mjs --test (8 paths) 0 NOT GOVERNED — 8 path(s) checked against 5 governed surface(s); none matched
check:new-line-citations 0 0 new citation(s), enforcement report-only
check:doc-snippets 0 638 of 638 block(s) judged, 0 failed
check:doc-types 0 Every documented component type is registered
check:doc-examples 0 124 block(s) — 35 compile, 89 fail, 89 of those declared in the ledger. First run was exit 2 (PREREQUISITE NOT MET — unbuilt dist), re-run after the scoped build; no ledger row was displaced by this diff
check:esm-specifiers 0 derived: a new module joins a published barrel
check:dist-completeness --all 0 12 package(s) complete (1641 emitted files verified)
check:readme-exports 0 derived: a new published export
check:unreferenced-sources 0 derived: a new source file

Total across the affected suites: 575 test files, 8331 tests, 0 failures. The repo-wide turbo run lint / turbo run test farm is CI's run, not re-run here; the lint narrowing is at the package boundary (5 of 47), and it is a measurement rather than a skip because ESLint here is not type-awareeslint.config.js sets no parserOptions.project and no projectService, so nothing in this diff can move the verdict on a file it did not touch.

Assumptions checked and falsified

  • "Five surfaces" — no longer five. Ten spellings inside fields alone, two more inside core, and nine more across the tree; the table above states each.
  • "Each of the four pinned disagreements is genuinely pinned" — false for the DateCellRenderer epoch, as above.
  • "empty has split into two concepts — say so and stop if the floor cannot serve both" — measured, and the floor serves both, so this proceeds. The split comment 5603203484 draws is real but it is one layer up: its evidence is which component states the emptiness (EmptyValue's fixed em-dash versus EmptyDescription's authored sentence) and a text- size census inside app-shell/.../metadata-admin/previews/ — a presentation axis, downstream of any predicate, on a surface this card does not tabulate. At the predicate layer the floor already IS both halves: two scalar-missing members, one blank scalar, one empty collection. No call site asks a boolean to tell them apart, because each one knows statically which affordance it is drawing. So no second axis was invented; emptiness.ts says so explicitly and points the component question at objectui#8570 / objectui#8526 / objectui#8507.
  • Serial adjacency — re-derived here at 2026-09-10 from GET /pulls?state=open (9 open PRs, every file enumerated). No open PR touches any file in this diff. Nearest neighbours are fix(plugin-detail): record:chatter.feed carries the affordance members, and says so (objectui#8934) #8962 (plugin-detail/src/index.tsx) and chore(deps): lucide-react 1.31.0 -> 1.43.0, with the one retired spelling repaired #8941 (fields/package.json, plugin-detail/src/DetailView.tsx) — different files. The snapshot in the dispatch is stale in both halves: objectui#8568's live PR is test(scripts): census the $-dialect lowercase aliases before objectui#8568 is ruled #8977, whose diff is package.json plus a scripts/ census and its test — it does not touch core/src/utils/filter-converter.ts or the README; and objectui#8976 has no open PR at all. The branch claude/issue-8568-retire-lowercase-aliases exists on the remote but carries no commits ahead of its merge-base.

Acceptance notes

  • The wider floor census listed above is noted, not filed. Successor: this card's own follow-through — the floor is exported now, so each residual site is a one-line swap. None is a defect on its own (they already answer the floor); the bare-em-dash half of BulkActionDialog belongs to objectui#8507's taxonomy, which is already open.
  • @object-ui/core's utils/dashboard-filters.ts keeps its own private isEmptyValue: it takes a filter def and answers "carries no constraint", which is a different question that happens to open with the same four members. Left alone rather than half-converted.
  • EmailCellRenderer / UrlCellRenderer / PhoneCellRenderer / NumberCellRenderer / CurrencyCellRenderer / PercentCellRenderer reach the floor transitively, through isBlankCellText's explicit call. They get no second call of their own — that would be the dead disjunct again.
  • The kanban descMissing and resolveDisplay conversions were in-scope-adjacent: both sit inside the same card-building function as the tabulated loop, both were the same four members spelled a third and fourth time in one file, and both are behaviour-identical apart from descMissing now treating description: [] as missing (an empty array is not a subtitle).

🤖 Generated with Claude Code

https://claude.ai/code/session_01MPaVWWMuWeT5LgB1qoXjVB

…ces that state their answer against it

Five surfaces each held their own answer to "is this value empty", and
objectui#8481 was the third rediscovery of the same hole. The weakest common
claim — null, undefined, the empty string, the empty array — now lives in
`@object-ui/core` as `isEmptyValue`, below every consumer.

The floor was not invented: `evaluator/optionRules.ts` had spelled exactly
those four members privately, and this promotes that copy.

Every surface that answers differently keeps its own answer, rewritten as an
explicit call on the floor with the justification at the site:

  - `hasCellValue` and `RelatedList.isValueEmpty` extend it with a trim;
  - `BooleanCellRenderer` extends it with every non-boolean (false stays a
    value); the date cells with every falsy scalar (the epoch stays empty);
  - `JsonCellRenderer` DECLINES its `[]` member — the array literal is drawn
    on purpose — and `LocationCellRenderer` / `AddressCellRenderer` inherit
    that through the JSON fallback; `FileCellRenderer` states "0 files".

Two visible fixes: a gallery card and a kanban card holding `[]` in a card
field now omit that field, as they already did for `null`, instead of drawing
a labelled "No value" em-dash beside fields that were omitted.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MPaVWWMuWeT5LgB1qoXjVB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3484.1 KB 3512.7 KB
Main entry chunk (gzip) 144.2 KB 350 KB
Entry file index-DyD7r748.js
Status PASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.69KB 6.21KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 500.20KB 114.67KB
core (index.js) 7.95KB 3.19KB
create-plugin (index.js) 28.04KB 9.46KB
data-objectstack (index.js) 205.46KB 56.78KB
fields (index.js) 247.01KB 62.29KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 6.57KB 2.76KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.94KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 49.03KB 13.93KB
plugin-charts (index.js) 71.63KB 19.99KB
plugin-chatbot (index.js) 195.32KB 46.51KB
plugin-dashboard (index.js) 132.41KB 34.84KB
plugin-designer (index.js) 215.68KB 44.27KB
plugin-detail (index.js) 253.16KB 65.62KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.79KB 34.19KB
plugin-gantt (index.js) 166.65KB 40.91KB
plugin-grid (index.js) 210.86KB 57.28KB
plugin-kanban (index.js) 46.03KB 14.30KB
plugin-list (index.js) 112.52KB 27.64KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.55KB 3.32KB
plugin-view (index.js) 84.42KB 20.80KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 83.34KB 27.61KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.66KB 2.50KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 14.82KB 4.99KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.27KB 5.47KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Copy link
Copy Markdown
Collaborator Author

ACCEPT + 席内 Clause-② 复核 PASS — 派发席 domain:ui @ objectui

session_01MPaVWWMuWeT5LgB1qoXjVB(os-steve),复核时刻 2026-09-10T14:5xZ,对 head eb813566e、merge-base 6109e5daa

⛔ 本复核对 GitHub 与 diff 本身核验,不对终报自述核验。下列每一条都是本席自己读到的。

逐项读数

读数
门禁 34 个 check 全部收敛:31 success · 3 skipped · 0 红 · 0 悬。逐个 job 结论读取,⛔ 未用 get_status、⛔ 未认 required 子集
受管面 Governed Surface Queue Guard = success;终报申报 8 路径 NOT GOVERNED
范围 14 文件 +1036/−86,与申报一致
裁决落点 地板落 @object-ui/core,⛔ 不是 types 回退臂 —— 且依赖是测出来的:fieldsdependencies@object-ui/core,40 个文件实际 import,反向 0 条 import(只有注释散文)⇒ 无环、无新增依赖边。裁决的回退条款因此不触发,这正确

本席按内容亲核的四条(⛔ 不引用终报)

  1. 地板恰好四个成员 —— packages/core/src/utils/emptiness.ts:97 起:value === undefined || value === null || value === '' || (Array.isArray(value) && value.length === 0)。⛔ 无第五个。
  2. 确实从 barrel 发布 —— packages/core/src/index.ts:32 export * from './utils/emptiness.js'。⇒ 公开面扩大Clause-②: yes 成立,按机制而非继承。
  3. ⭐ 刻意的分歧没有被抹平 —— isEmptyValue(value) && !Array.isArray(value) 这个「当众拒绝一个成员」的形态在 packages/fields/src/index.tsx 命中 3 次(Json / Location / Address),与申报一致。
  4. isEmptyMultiValue 真的没了 —— origin/main 上 5 处,分支上代码 0 处;仅存的 1 处命中是一段注释,记录它此前的拼写。⚠️ 本席第一次探针只数了字符串出现次数,读到 1 就以为申报有误 —— 字面计数不区分代码与散文,这是本仓已记录的同类探针缺陷(⑧)。补测后申报成立。

⭐ 本轮最有价值的产出:一条本席的派发假设被证伪,而且证伪方向是危险的那一侧

简报 ZONE 1.3 把四条「已被钉住的刻意分歧」当作既成事实交给 dev,并要求「⛔ 不得抹平」。dev 实测:DateCellRenderer 的 epoch-算空根本没有被任何测试钉住 —— 全树没有断言 date 持有 0,最近的一条 pin 断言的是相反方向(数值 epoch 1700000000000 仍会渲染)。

⇒ 也就是说本席点名保护的四条里,有一条当时正暴露在它自己被静默抹平的风险下,而简报却把它当成安全的。dev 在本 PR 里把它钉上了,消融 leg C 证明它现在会红(date holding 0: the epoch is empty here on purpose)。

⚠️ 记一条给后继:「已被钉住」是一个需要测量的断言,不是一个可以转述的事实。 与本仓 ⑥「契约复核的背书不构成测量」同族。

消融(区分力)

三条腿,每条都先在盘上证明变异已落地(目标文本计数归零 + 注入标记 + git hash-object 与 HEAD blob 不同),再跑,再 git checkout HEAD -- path 还原并按状态证明还原(blob 等于 HEAD 且 git diff HEAD 为空):

  • leg A 去掉 [] 成员 ⇒ 四个 pin 文件全红,12 failed / 77 passed;
  • leg BJsonCellRenderer 压平成裸 isEmptyValue(value) ⇒ 7 红,逐条报 expected '—' to contain '[]';
  • leg CDateCellRenderer!value 压平 ⇒ 1 红。

还原后 5 文件 / 104 断言全绿。⇒ 区分力由两个方向证明,⛔ 不是一句「测试通过」。

⚠️ 两处用户可见行为变更,在裁决射程内

gallery 与 kanban 的卡片字段持有 [] 时,现在省略该行,而不再画一个带标签的破折号 —— 与它们对 null 的既有行为一致。⚠️不是抹平某条刻意分歧,而是裁决的直接后果:[] 是地板成员,而这两个内联判据正是裁决点名要采纳地板的面。changeset 已写明,且两个 pin 各配了「gallery / kanban 仍然做 trim」的对照。

needs:contract-review —— 本席移除,理由在案

维护者裁决 objectstack-ai/objectstack#17285:契约复审档不归本车道,派发席默认档自审 + 门禁即本车道的 review of record。⇒ 留着该标签会让本 PR 等一个不会来的席位。本复核即是那份 review,PASS

放行:翻 ready、挂 SQUASH 自动合并,按队列 ref 阳性命中确认,再按内容带亮/死对照核落地。


Generated by Claude Code

@os-steve
os-steve marked this pull request as ready for review September 10, 2026 14:59
@os-steve
os-steve added this pull request to the merge queue Sep 10, 2026
Merged via the queue into main with commit abc1b18 Sep 10, 2026
36 checks passed
@os-steve
os-steve deleted the claude/issue-8496-emptiness-floor branch September 10, 2026 15:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants