Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

finding(plugin-dashboard): two more private copies of the reference-bearing field family — #5312's "LAST private copy" claim is false #5692

Closed

Description

@os-sales

Measured while implementing #4914's ruled gate (the six predicate faces). Filed unassigned, out of that card's ruled scope — recording it rather than widening a 7-package diff.

What is there

@object-ui/core's EXPANDABLE_FIELD_TYPES (packages/core/src/utils/expand-fields.ts) is documented as the reference-bearing field family, with a long docblock recording what happened the last time this table was hand-copied per consumer (#4770 / #4790 / #4815). Two consumers were re-pointed at it and each carries an identity pin (a spy on its .has) so a member-identical private copy fails rather than quietly re-forking.

packages/plugin-dashboard holds two more copies that were never converted, and neither derives from nor pins against the shared set:

  1. packages/plugin-dashboard/src/recordFields.tsx — const LOOKUP_TYPES = new Set([...]), read by isLookupType.
  2. packages/plugin-dashboard/src/ObjectDataTable.tsx — an inline const isLookup = (t) => t === 'lookup' || … inside computeLookupExpand.

Both are $expand-driving decisions, which is EXPANDABLE_FIELD_TYPES' own stated job ("buildExpandFields below ("expandable")").

Why it is not merely duplication — the sets genuinely differ

EXPANDABLE_FIELD_TYPES (core) LOOKUP_TYPES (recordFields) inline isLookup (ObjectDataTable)
lookup ✅ ✅ ✅
master_detail ✅ ✅ ✅
user ✅ ✅ ✅
tree ✅ ❌ ❌
reference ❌ ✅ ✅

So they are not in a subset relation in either direction — the exact shape #4790 recorded for the form layer's copy, and the reason a gate could not say so. Today a tree (self-referencing hierarchy) column is $expand-ed by the form/grid road and not by the dashboard table, while a reference column is expanded by the dashboard and is not a member of the family core publishes.

The claim this falsifies

#5312 (closed) states paramToField was "now the LAST private copy of the reference-bearing rule". It was not — these two predate it and were not in that sweep's file surface. Worth recording so the next consolidation does not start from the same false premise.

Not fixed here, deliberately

#4914's ruling scoped a retirement gate ahead of these predicates, not a re-homing of the family they read. Converting them is a behaviour change in both directions (tree gains expansion, reference loses it) and needs its own call on which set is right — most likely EXPANDABLE_FIELD_TYPES gains reference, since two independent consumers reached for it. The gate landed by #4914's PR runs ahead of both predicates and is unaffected by whichever way this goes.

Related: #4770, #4790, #4815, #5312 (the four earlier copies), #4914 (this measurement's origin).

Activity

  1. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Aug 22, 2026
  2. huangyiirene commented on Aug 22, 2026

    @huangyiirene
    Collaborator

    Concentration-round first-touch (triage seat): promoted finding → pm:queue, Task S–M, measurement-first — the "which set is right" call has a prior factual question the fix hangs on: is reference a live field type on these paths at all, or an alias/dead spelling? (The neighbouring #5654 measurement shows PARAM_TYPE_ALIASES folds reference → lookup on a sibling surface — if the same holds here, the two private copies converge onto core's published set unchanged, tree regains expansion in the dashboard as family-behaviour restoration, and no set-widening decision exists.) Only if reference is measured as a genuinely distinct live type does the "core gains reference" question become real — report that fork rather than picking, since it widens $expand behaviour for every consumer of the shared set. Either way: both private copies end re-pointed at EXPANDABLE_FIELD_TYPES with the identity-pin treatment (#4770/#4790/#4815 lineage), and #5312's "LAST private copy" docblock claim gets corrected in the same change.


    Generated by Claude Code

  3. claude commented on Aug 23, 2026

    @claude
    Contributor

    Claiming this card.

    • session id: 01EuPCi56cnGyykygi3z9w4m (dispatched by the domain:ui PM seat)
    • branch: claude/issue-5692-dashboard-expandable-family

    Approach per the PM ruling: converge both private copies onto @object-ui/core's EXPANDABLE_FIELD_TYPES with the identity-pin treatment, and decide the reference membership question by measurement (live stored field type vs. alias spelling that folds to lookup) with controls on the same pipeline, rather than by preference.


    Generated by Claude Code


    Generated by Claude Code

  4. claude commented on Aug 23, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 5692,
      "status": "done",
      "branch": "claude/issue-5692-dashboard-expandable-family",
      "pr": "https://github.com/objectstack-ai/objectui/pull/5888",
      "premise_still_valid": true,
      "summary": "Both private copies of the reference-bearing field family in packages/plugin-dashboard (LOOKUP_TYPES in recordFields.tsx, and the inline isLookup inside computeLookupExpand in ObjectDataTable.tsx) now read @object-ui/core's EXPANDABLE_FIELD_TYPES, each carrying the identity pin (a spy on .has) the two already-converted consumers carry. The membership question was decided by MEASUREMENT: `reference` is absent from @objectstack/spec's closed FieldType vocabulary and refused by FieldSchema.safeParse, measured on the same read as four live positive controls (lookup/master_detail/user/tree) and two dead negative controls (the retired `owner`, plus a nonsense spelling) — all six controls held. So `reference` is a DEAD ALIAS SPELLING, not a live stored field type, EXPANDABLE_FIELD_TYPES is UNCHANGED (no widening on plausibility), and the dashboard simply loses a branch no producer can reach. `tree` gains expansion as ruled. Note stated plainly in the PR rather than overclaimed: only computeLookupExpand is on a live path; isLookupType is exported but has no production consumer in packages/ or apps/ and is not re-exported from the package entry, so its convergence is a contract change, not a behaviour change (filed as #5876). Core's docblock ledger is corrected in-fence — its hand-kept 'four consumers' count was itself already stale by two, and #5312's 'LAST private copy' claim was false by two more. #4914's landed gate is untouched and still runs ahead of both predicates.",
      "tests": "All legs on final commit 91c3f6b3f, tree clean (git status --porcelain = 0 lines). Heavy legs through scripts/pm/os-verify-lock.sh; exit codes captured before any pipe; each gate quoted by its OWN printed verdict line. [SUITES] `pnpm exec vitest run packages/plugin-dashboard/ packages/core/src/utils/__tests__/ --maxWorkers=2` -> 'Test Files 112 passed (112) / Tests 1338 passed (1338)', 'os-verify-lock: VERDICT command-exit 0 · held the lock 180s'. [CLOSURE BUILD FIRST] `pnpm --workspace-concurrency=2 --filter \"@object-ui/plugin-dashboard^...\" build` -> 'VERDICT command-exit 0 · held the lock 71s'. [TYPECHECK] `pnpm --filter @object-ui/plugin-dashboard --filter @object-ui/core type-check` -> 'Scope: 2 of 47 workspace projects', both script names echoed ('tsc --noEmit && tsc -p tsconfig.test.json' ... 'Done' for each) so this is NOT a zero-match silent green, 'VERDICT command-exit 0'. [GATES, each by its own verdict line] check-changeset-presence: '5 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)'; check-changeset-no-major: 'No changeset declares a major bump.'; check-changeset-fixed: 'All workspace packages are in the changeset fixed group.'; check-control-bytes: 'OK (scanned 4890 tracked text file(s); skipped 85 binary)'; check-phantom-dependencies: 'Every in-scope import is declared by the package that publishes it.'; check:spec-symbols: 'spec symbol derivation: 1300 files scanned against 4959 spec export names'; check:self-import: 'No package names itself inside its own src/.'; check:esm-specifiers: 'no un-ledgered package emits an extensionless relative specifier'. [LINT — NARROWING DECLARED] ran the two touched packages' own lint, not the repo-scale `pnpm lint` (CI-owned): 'Scope: 2 of 47', core '510 problems (0 errors, 510 warnings)', plugin-dashboard '381 problems (0 errors, 381 warnings)', VERDICT command-exit 0. 0 ERRORS; the warnings are the repo's standing no-explicit-any backlog on untouched lines. The narrowing is sound, not merely convenient: eslint.config.js's languageOptions sets only ecmaVersion + globals — NO project/projectService/parserOptions.project — so type-aware linting is OFF and this diff cannot move any untouched file's verdict. check:eager-closure was NOT run and is declared: it reads apps/console/dist/eager-closure.json emitted by a console vite build and refuses as a 'broken gauge' when absent; that build is CI's. Structural delta is nil anyway — the only new cross-package import is @object-ui/core into recordFields.tsx, and ObjectDataTable.tsx (which imports recordFields.tsx) already imported @object-ui/core before this change. [MEASUREMENT PROBE, subject + controls on the same read] PROBE-A (spec FieldType vocabulary, size 49): SUBJECT includes('reference')=false; control+ lookup/master_detail/user/tree = true OK x4; control- owner=false OK, zzz_not_a_field_type=false OK. PROBE-B (FieldSchema.safeParse, the real parse-time producer gate): SUBJECT {type:'reference'}.success=false ('Invalid option: expected one of ...'); control+ x4 = true; control- x2 = false. 'VERDICT: controls all held. `reference` is NOT A member of the spec's closed FieldType vocabulary.' The probe is also carried forward as an EXECUTABLE pin in the new test file, so if the spec ever adds the spelling the suite goes red and the membership question reopens. [REVERSE VERIFICATION — 4 legs, all discriminating] NO REBUILD LEG IS OWED, and why: root vitest.config.mts aliases '@object-ui/core' -> packages/core/src, and the tests import the subjects by RELATIVE path inside the same package's src, so no dist/ is on the pins' resolution path and a stale build cannot fake either colour; the green run's identity pin PASSING is itself proof test and subject hold the same Set instance. Every leg carried `trap '<restore>' EXIT INT TERM`, and EACH MUTATION WAS CONFIRMED ON DISK before its reading was accepted — removed-text occurrences = 0 AND injected-text present, grepped per leg (an editor exit code proves neither). LEG 1 (restore recordFields' ORIGINAL copy): 4 red — its identity pin, its tree pin, its reference pin, and lookupTypes.retiredFieldType's LIVE_RELATIONS control; 14 passed. LEG 1b (MEMBER-IDENTICAL copy new Set(['lookup','master_detail','tree','user'])): EXACTLY 1 RED, the identity pin; 17 passed. LEG 2 (restore ObjectDataTable's ORIGINAL inline disjunction): 3 red — its identity pin, its tree expand pin, its reference pin; 15 passed. LEG 2b (member-identical inline copy): EXACTLY 1 RED, the identity pin; 17 passed. RESTORE LEG: git diff HEAD --stat EMPTY, porcelain 0, all three mutation markers absent from disk, 18/18 green. Legs 1b/2b are the point: every membership assertion is satisfied by a private set holding the same four strings, so a value check would pass ON the defect. [LEGS THAT DO NOT DISCRIMINATE, named] (a) in legs 1b and 2b, lookupTypes.retiredFieldType.test.ts stayed FULLY GREEN — it cannot detect a member-identical fork by construction, which is exactly the gap the new identity pin closes, not a defect in that file; (b) legs 1/1b leave computeLookupExpand's pins green and legs 2/2b leave isLookupType's pins green — that per-face isolation is why these are two pins rather than one; (c) the ordinary-relation regression controls (lookup/master_detail/user, both column modes) stayed GREEN in ALL FOUR legs, which is what makes them controls rather than duplicates of the pins. One honest caveat on instrumentation: the leg-2 disk-check printed injected-text occurrences = 7 because grep -c counts LINES and that leg's injected anchor spanned two lines (the trailing '};' matched elsewhere); the load-bearing half of that check — removed-text occurrences = 0 — is exact, and the leg's test result is unambiguous. [FIXTURE TRIAGE] lookupTypes.retiredFieldType.test.ts was triaged as REPLACE, not re-spell: its LIVE_RELATIONS pinned `reference`, the very branch this change deleted (it would have kept passing only while the fork survived), and its assertion `expect(isLookupType(RETIRED)).not.toBe(isLookupType('reference'))` named that branch directly — both rewritten against a still-live relation. Fixture scan was by the RULE's consumption radius, not by edited package: computeLookupExpand/isLookupType have no consumer outside plugin-dashboard (grepped packages/ and apps/), and EXPANDABLE_FIELD_TYPES is unchanged so no other consumer of the family is affected; ObjectDataTable.columnIdentity.test.tsx's fixtures use only lookup/text and were verified unaffected.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #5874: four MORE private copies of the reference-bearing family (plugin-kanban ObjectKanban.tsx:346, plugin-detail RecordDetailDrawer.tsx:227 and HeaderHighlight.tsx:147, app-shell resolveActionParams.ts:489) — each missing `user` and `tree`, each carrying the undeclarable `reference`; excludes #5654's already-filed face",
        "filed as #5875: #5312's 'fourth and last private copy' claim still stated verbatim in app-shell paramToField.ts and paramToField.test.ts prose — outside this card's fence, so corrected only in core's canonical ledger here",
        "filed as #5876: `isLookupType` is exported from plugin-dashboard/src/recordFields.tsx with no production consumer in packages/ or apps/ and is not re-exported from the package entry — the live $expand decision is computeLookupExpand's"
      ]
    }

    Generated by Claude Code


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:queue

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions