feat(detail): derived related lists consume the FK's relatedListFilter — AND-composed query, badge counts the same set - #6946

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-4664-relatedlistfilter-consumption
Aug 31, 2026
Merged

feat(detail): derived related lists consume the FK's relatedListFilter — AND-composed query, badge counts the same set#6946
os-sam merged 2 commits into
mainfrom
claude/issue-4664-relatedlistfilter-consumption

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#4664

Auto-derived related lists now consume the field-level relatedListFilter
declaration: the list query is the declared filter AND-composed with
{ [referenceField]: parentId }, and the related-list tab badge counts the
same composed predicate
, so the badge and the rows answer one question.

Scope guard — condition 2, measured before any code

The card refuses to start unless the pinned @objectstack/spec actually
carries the key. Measured, not assumed:

readingvalue
manifest pinroot package.json"@objectstack/spec": "^17.0.0"
lockfile resolution17.2.0 (30 entries, one resolution)
installed packagenode_modules/.pnpm/@objectstack+spec@17.2.0_.../package.json17.2.0
key on the FIELD shaperelatedListFilter?: FilterCondition | undefined, beside relatedList / relatedListTitle / relatedListColumns
key namerelatedListFilter — exactly as the card assumed, no drift
shapeFilterConditionSchema.optional(), i.e. the canonical Query-DSL FilterCondition (same dialect as a query where)

The installed key's own describe() text is the contract this PR implements:

Declarative default filter for the detail-page related list: AND-composed
with the parent-relationship condition { [referenceField]: parentId } — an
authored constraint, never a user-editable suggestion. The related-list tab
badge count honors the same composed filter, so counts match the visible
rows.

Premise holds. No spec pin was touched.

What changed

Five source files, one value, one read site — no new dialect:

  1. app-shell/src/utils/deriveRelatedLists.ts — reads relatedListFilter off
    the FK onto the derived descriptor. A plain non-empty object is a
    declaration; an array, a string, null, a number, {} are silence. Not a
    tolerant reader: nothing is aliased, coerced or defaulted (AGENTS.md #0.1),
    the same declared-vs-absent discrimination relatedListTitle and
    relatedListColumns already do next to it.
  2. app-shell/src/views/RecordDetailView.tsx — forwards it into the page
    synthesizer's related shape.
  3. plugin-detail/src/synth/buildDefaultPageSchema.ts — emits it onto the
    record:related_list node's existingfilter key.

That key is where the reuse is honest and where it stops. filter has had a
read site since objectstack#7118: RelatedList lowers it through the repo's one
filter sink and ANDs it with the parent scope, wrapping both sources under a
single and rather than letting either replace the other. The derived producer
lands on the same key as the component-level authored filter and as a
dataSource binding's composed filter — three producers, one read site, one
lowering. Nothing was added to the list path; it already did the right thing
with a value it never received.

The badge half needed new code, because the tab strip's probe read only
objectName and relationshipField off the node it was badging:

  1. components/src/renderers/layout/containers.tsx — the page:tabs count
    probe now also reads the node's filter.
  2. components/src/hooks/related-count-store.ts — composes it with the parent
    scope through the samemergeFilterNodes sink the row query uses, so the
    two $filter values are one composition used twice rather than two
    implementations that agree by luck. Cache identity gains the scope, placed
    INSIDE the relField segment so invalidate's two structural reads on these
    keys (startsWith(object::), endsWith(::parentId)) keep working — a scope
    segment appended after the parent id would have orphaned the entry, and its
    failure mode is a badge that never refreshes after a write, with no error to
    notice.

With nothing declared, the synthesized node, the row query and the badge probe
are byte-identical to before ({ [referenceField]: parentId }, the untouched
MongoDB-style object — not a freshly lowered AST that means the same thing).

Consumption only. No authoring UI for the filter, as the card requires.

Verification

Every exit code below was captured before any pipe, and each gate is quoted by
the line the gate itself printed.

Proving the AND, not just the filter. The page test's fake backend
EVALUATES the composed predicate over a four-row fixture built so each conjunct
alone admits a row the other excludes:

rowparentstatusparent scopedeclared filterAND
Live Itemtv-1openyesyesyes
Archived Itemtv-1archivedyesnono
Other Parent Itemtv-2opennoyesno
Other Parent Archivedtv-2archivednonono

Parent scope alone answers 2, the declared filter alone answers 2, the
conjunction answers 1 — so dropping either conjunct changes the number the test
reads, in both directions. Those three numbers are asserted from the fixture
itself, so a later edit that flattens it into a single-condition fixture fails
loudly instead of quietly weakening every assertion above it. The evaluator
THROWS on any filter shape it does not recognise: a permissive evaluator would
answer "all rows" for a malformed filter, which is exactly what the
counter-probes would read as a pass.

Proving badge and list agree, in one test, with the unfiltered count
differing from the filtered one: the list renders Live Item and neither
Archived Item (excluded by the declared conjunct) nor Other Parent Item
(excluded by the parent conjunct), while the tab badge reads 1. The
parent-scoped unfiltered count is 2, so a badge ignoring the filter reads 2
there.

Ablation — direction predicted in writing before either run. These tests
resolve through the root vitest.config.mts alias table, which maps every
@object-ui/* specifier to packages/*/src; there is no dist/ seam for a
mutation to be lost behind and therefore no rebuild leg. Each leg proved the
mutation on disk (anchored count of the injected marker AND of the deleted text,
plus git hash-object differing from the HEAD blob) and proved the restore both
ways (git diff HEAD empty AND hash equal to the HEAD blob), under
trap ... EXIT INT TERM with absolute paths from git rev-parse --show-toplevel
and git checkout HEAD -- ABSPATH, never bare.

Leg A — drop the filter from the BADGE path alone (containers.tsx).
Predicted: the two badge legs red, the row leg green, the other three files
green. Observed, exactly:

× SUBJECT — the badge probe sends the SAME composed filter as the rows
AssertionError: expected { task_version: 'tv-1' } to deeply equal [ 'and', …(2) ]
× PARITY — the badge and the rendered rows agree, and 1 !== the unfiltered 2
AssertionError: expected '2' to be '1'
Test Files 1 failed | 3 passed (4)
Tests 2 failed | 25 passed (27)

The second line is the defect this card exists to prevent, reproduced at
fixture scale: the badge said 2 above a list showing 1.

Leg B — the converse: drop it from the LIST path alone
(record-related-list.tsx). Predicted: the row leg red, the badge-equality leg
red as the mirror image of A, parity red the other way round, the other three
files green. Observed, exactly:

× SUBJECT — the row query is the declared filter AND the parent scope
AssertionError: expected [ 'ci-live', 'ci-archived' ] to deeply equal [ 'ci-live' ]
× SUBJECT — the badge probe sends the SAME composed filter as the rows
AssertionError: expected [ 'and', …(2) ] to deeply equal { task_version: 'tv-1' }
× PARITY — the badge and the rendered rows agree, and 1 !== the unfiltered 2
Test Files 1 failed | 3 passed (4)
Tests 3 failed | 24 passed (27)

Gates, all at the final commit 729d29c6c:

  • Root-form vitest, before/after over a set derived by MEASUREMENT (every test
    file that reads one of the five changed modules, by grep, rather than by
    guess): before — 110 files / 1559 tests passed, at BASE 40c479af2 in a
    comparison worktree; after — 114 files / 1586 tests passed. Delta +4
    files / +27 tests, exactly the four test files this PR adds. No pre-existing
    file or test count moved. The reported file count (114) equals the derived
    list's own count (114) — the sanity check against a run that silently
    substitutes another package's suite.
  • type-check (hyphenated) for all three touched packages, script name echoed
    by pnpm as tsc --noEmit && tsc -p tsconfig.test.json, all three Done.
    The .test.ts files are genuinely inside those programs, not excluded:
    tsc -p tsconfig.test.json --listFiles names all four new test files
    (app-shell 2, components 1, plugin-detail 1).
  • pnpm exec eslint ., plain form (no --no-inline-config), whole repo:
    exit 0, ✖ 11554 problems (0 errors, 11554 warnings) — 0 errors; the
    warnings are the repo-wide pre-existing no-explicit-any population.
  • node scripts/check-changeset-presence.mjs: exit 0 —
    ✅ 9 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s).
  • node scripts/check-changeset-no-major.mjs: exit 0 —
    ✅ No changeset declares a 'major' bump. The changeset is minor on the
    three packages and states the new consumption in words.

Boundaries named rather than forced

  • The reference rail is deliberately NOT wired.record:reference_rail
    renders its own total-count badge from a fixed
    { [relationshipField]: parentId } query, so on a page whose FK declares a
    filter the Related tab badge and the rail card for the same child disagree.
    It is not fixable here: ReferenceRailEntrySchema is strictObject with no
    filter key, and the spec's own guidance on that shape says the rail
    "honours no per-entry filter" and that "record:related_list is the
    component whose filter is real; if the rail is ever granted one, this entry
    shape is where it gets declared and enforced." Granting it is an upstream
    spec decision, not this card. Filed separately.
  • Logical combinators do not survive the shared sink.relatedListFilter
    is typed FilterCondition, whose type explicitly declares $and / $or /
    $not — but convertFiltersToAST in @object-ui/core has no combinator
    branch: { $or: [...] } lowers to ['$or','=',[...]] (a leaf naming a field
    $or) and { $not: {...} } throws FilterOperatorError naming a nonsense
    operator. Pre-existing on every consumer of that sink since
    objectstack#7118; this key newly makes it reachable by an author following
    the spec's own type. Fixing it means changing the shared sink for every
    caller, which is its own card. Filed separately; not patched around here,
    because a renderer-side workaround is exactly the second de-facto contract
    #0.1 forbids.

⛔ Left in draft, not enqueued, no auto-merge — the PM lands it.

Generated by Claude Code


Generated by Claude Code

The spec key `relatedListFilter` (objectstack#8704 / PR #8955, shipped in
@objectstack/spec 17.1.0) declares a related list's own scope on the
relationship. This repo accepted it at every gate and consumed it nowhere.
- deriveRelatedLists reads it off the FK; RecordDetailView forwards it; the
page synthesizer emits it onto the record:related_list node's EXISTING
`filter` key, where RelatedList already ANDs it with
`{ [referenceField]: parentId }` (objectstack#7118). No new dialect.
- The tab badge counts the same composed predicate: page:tabs reads the node's
filter and the count store composes it with the parent scope through the same
mergeFilterNodes sink the row query uses. Counts cache per scope, so a
filtered and an unfiltered probe never badge each other.
With nothing declared, the node, the row query and the badge probe are
byte-identical to before. Consumption only; no authoring UI.
@claude

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
ContributorAuthor

The two boundaries named in the description are now filed, both unassigned and unlabelled for PM triage:

Neither is worked around in this PR. Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3179.8 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-B0Pj123L.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.32KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)175.69KB48.80KB
fields (index.js)243.65KB61.63KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.30KB63.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

…rose
The doc-version-claims ratchet (Test shard 4/4) flagged the hard-coded
version this PR's own diff added: a version literal in prose that nothing
in the repo re-verifies. Per the gate's own prescribed remedy (PR #3688 /
PR #3698's precedent), delete the literal and point at the truth instead —
this package's own `@objectstack/spec` dependency in package.json, which
the package manager keeps honest at install time.
Fixes the red `Test (shard 4/4)` check on PR #6946; no other files touched.
@os-samClaude

Copy link
Copy Markdown
Collaborator

CI fix for the red Test (shard 4/4) check.

Reproduced failure (at head 729d29c6c, before the fix):

FAIL scripts/__tests__/doc-version-claims.test.ts > doc version claims - the ratchet > records every version literal on the scanned surfaces
AssertionError: These doc surfaces state a version and nothing in this repository can tell whether it is still true:
- packages/plugin-detail/README.md:262 "@objectstack/spec` 17.1.0"

This came from this PR's own diff (+20/-1 to packages/plugin-detail/README.md), which added a hard-coded 17.1.0 version literal in prose.

Fix: deleted the literal and pointed at the truth instead, following the gate's own prescribed remedy and the house form from PR #3688 / PR #3698. Minimal, single-file diff:

 **A third producer writes that same key: the relationship itself.** A `lookup` /
-`master_detail` field may declare `relatedListFilter` (`@objectstack/spec` 17.1.0-— objectstack#8704 / PR #8955), a canonical Query-DSL `FilterCondition` such as+`master_detail` field may declare `relatedListFilter` (added to `@objectstack/spec`+in objectstack#8704 / PR #8955 — see this package's own `@objectstack/spec`+dependency in `package.json` for the version in use), a canonical Query-DSL+`FilterCondition` such as

Did not touch KNOWN_CLAIMS (the entry-required route was explicitly not the intended remedy here) and did not touch the changeset — its own 17.1.0 mention is in a changelog entry, not a scanned surface, and stays accurate as history.

Passing run (root-form vitest, this repo's guard refuses --filter here):

$ pnpm vitest run scripts/__tests__/doc-version-claims.test.ts
Test Files 1 passed (1)
Tests 25 passed (25)

All 25 tests in the file pass, including the previously-red one. git diff --stat confirms exactly one file changed (packages/plugin-detail/README.md, +4/-2) — nothing else in the PR's diff was touched.

Pushed sha: 8dbb79213d6f14df8bf2fd5f8b3939dce95bd77f on claude/issue-4664-relatedlistfilter-consumption.

PR left in draft, not enqueued, no auto-merge — unchanged per the dispatch constraints.

Generated by Claude Code


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3179.8 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-B0Pj123L.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.32KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)175.69KB48.80KB
fields (index.js)243.65KB61.63KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.30KB63.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-sam
os-sam marked this pull request as ready for review August 31, 2026 14:05
@os-sam
os-sam added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit d796c8dAug 31, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-4664-relatedlistfilter-consumption branch August 31, 2026 14:19
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationpackage: componentsplugintests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Auto-derived related lists consume the new field-level relatedListFilter declaration — list query AND-composed, tab badge honors the same filter

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat(detail): derived related lists consume the FK's relatedListFilter — AND-composed query, badge counts the same set - #6946

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-4664-relatedlistfilter-consumption
Aug 31, 2026
Merged

feat(detail): derived related lists consume the FK's relatedListFilter — AND-composed query, badge counts the same set#6946
os-sam merged 2 commits into
mainfrom
claude/issue-4664-relatedlistfilter-consumption

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#4664

Auto-derived related lists now consume the field-level relatedListFilter
declaration: the list query is the declared filter AND-composed with
{ [referenceField]: parentId }, and the related-list tab badge counts the
same composed predicate
, so the badge and the rows answer one question.

Scope guard — condition 2, measured before any code

The card refuses to start unless the pinned @objectstack/spec actually
carries the key. Measured, not assumed:

readingvalue
manifest pinroot package.json"@objectstack/spec": "^17.0.0"
lockfile resolution17.2.0 (30 entries, one resolution)
installed packagenode_modules/.pnpm/@objectstack+spec@17.2.0_.../package.json17.2.0
key on the FIELD shaperelatedListFilter?: FilterCondition | undefined, beside relatedList / relatedListTitle / relatedListColumns
key namerelatedListFilter — exactly as the card assumed, no drift
shapeFilterConditionSchema.optional(), i.e. the canonical Query-DSL FilterCondition (same dialect as a query where)

The installed key's own describe() text is the contract this PR implements:

Declarative default filter for the detail-page related list: AND-composed
with the parent-relationship condition { [referenceField]: parentId } — an
authored constraint, never a user-editable suggestion. The related-list tab
badge count honors the same composed filter, so counts match the visible
rows.

Premise holds. No spec pin was touched.

What changed

Five source files, one value, one read site — no new dialect:

  1. app-shell/src/utils/deriveRelatedLists.ts — reads relatedListFilter off
    the FK onto the derived descriptor. A plain non-empty object is a
    declaration; an array, a string, null, a number, {} are silence. Not a
    tolerant reader: nothing is aliased, coerced or defaulted (AGENTS.md #0.1),
    the same declared-vs-absent discrimination relatedListTitle and
    relatedListColumns already do next to it.
  2. app-shell/src/views/RecordDetailView.tsx — forwards it into the page
    synthesizer's related shape.
  3. plugin-detail/src/synth/buildDefaultPageSchema.ts — emits it onto the
    record:related_list node's existingfilter key.

That key is where the reuse is honest and where it stops. filter has had a
read site since objectstack#7118: RelatedList lowers it through the repo's one
filter sink and ANDs it with the parent scope, wrapping both sources under a
single and rather than letting either replace the other. The derived producer
lands on the same key as the component-level authored filter and as a
dataSource binding's composed filter — three producers, one read site, one
lowering. Nothing was added to the list path; it already did the right thing
with a value it never received.

The badge half needed new code, because the tab strip's probe read only
objectName and relationshipField off the node it was badging:

  1. components/src/renderers/layout/containers.tsx — the page:tabs count
    probe now also reads the node's filter.
  2. components/src/hooks/related-count-store.ts — composes it with the parent
    scope through the samemergeFilterNodes sink the row query uses, so the
    two $filter values are one composition used twice rather than two
    implementations that agree by luck. Cache identity gains the scope, placed
    INSIDE the relField segment so invalidate's two structural reads on these
    keys (startsWith(object::), endsWith(::parentId)) keep working — a scope
    segment appended after the parent id would have orphaned the entry, and its
    failure mode is a badge that never refreshes after a write, with no error to
    notice.

With nothing declared, the synthesized node, the row query and the badge probe
are byte-identical to before ({ [referenceField]: parentId }, the untouched
MongoDB-style object — not a freshly lowered AST that means the same thing).

Consumption only. No authoring UI for the filter, as the card requires.

Verification

Every exit code below was captured before any pipe, and each gate is quoted by
the line the gate itself printed.

Proving the AND, not just the filter. The page test's fake backend
EVALUATES the composed predicate over a four-row fixture built so each conjunct
alone admits a row the other excludes:

rowparentstatusparent scopedeclared filterAND
Live Itemtv-1openyesyesyes
Archived Itemtv-1archivedyesnono
Other Parent Itemtv-2opennoyesno
Other Parent Archivedtv-2archivednonono

Parent scope alone answers 2, the declared filter alone answers 2, the
conjunction answers 1 — so dropping either conjunct changes the number the test
reads, in both directions. Those three numbers are asserted from the fixture
itself, so a later edit that flattens it into a single-condition fixture fails
loudly instead of quietly weakening every assertion above it. The evaluator
THROWS on any filter shape it does not recognise: a permissive evaluator would
answer "all rows" for a malformed filter, which is exactly what the
counter-probes would read as a pass.

Proving badge and list agree, in one test, with the unfiltered count
differing from the filtered one: the list renders Live Item and neither
Archived Item (excluded by the declared conjunct) nor Other Parent Item
(excluded by the parent conjunct), while the tab badge reads 1. The
parent-scoped unfiltered count is 2, so a badge ignoring the filter reads 2
there.

Ablation — direction predicted in writing before either run. These tests
resolve through the root vitest.config.mts alias table, which maps every
@object-ui/* specifier to packages/*/src; there is no dist/ seam for a
mutation to be lost behind and therefore no rebuild leg. Each leg proved the
mutation on disk (anchored count of the injected marker AND of the deleted text,
plus git hash-object differing from the HEAD blob) and proved the restore both
ways (git diff HEAD empty AND hash equal to the HEAD blob), under
trap ... EXIT INT TERM with absolute paths from git rev-parse --show-toplevel
and git checkout HEAD -- ABSPATH, never bare.

Leg A — drop the filter from the BADGE path alone (containers.tsx).
Predicted: the two badge legs red, the row leg green, the other three files
green. Observed, exactly:

× SUBJECT — the badge probe sends the SAME composed filter as the rows
AssertionError: expected { task_version: 'tv-1' } to deeply equal [ 'and', …(2) ]
× PARITY — the badge and the rendered rows agree, and 1 !== the unfiltered 2
AssertionError: expected '2' to be '1'
Test Files 1 failed | 3 passed (4)
Tests 2 failed | 25 passed (27)

The second line is the defect this card exists to prevent, reproduced at
fixture scale: the badge said 2 above a list showing 1.

Leg B — the converse: drop it from the LIST path alone
(record-related-list.tsx). Predicted: the row leg red, the badge-equality leg
red as the mirror image of A, parity red the other way round, the other three
files green. Observed, exactly:

× SUBJECT — the row query is the declared filter AND the parent scope
AssertionError: expected [ 'ci-live', 'ci-archived' ] to deeply equal [ 'ci-live' ]
× SUBJECT — the badge probe sends the SAME composed filter as the rows
AssertionError: expected [ 'and', …(2) ] to deeply equal { task_version: 'tv-1' }
× PARITY — the badge and the rendered rows agree, and 1 !== the unfiltered 2
Test Files 1 failed | 3 passed (4)
Tests 3 failed | 24 passed (27)

Gates, all at the final commit 729d29c6c:

  • Root-form vitest, before/after over a set derived by MEASUREMENT (every test
    file that reads one of the five changed modules, by grep, rather than by
    guess): before — 110 files / 1559 tests passed, at BASE 40c479af2 in a
    comparison worktree; after — 114 files / 1586 tests passed. Delta +4
    files / +27 tests, exactly the four test files this PR adds. No pre-existing
    file or test count moved. The reported file count (114) equals the derived
    list's own count (114) — the sanity check against a run that silently
    substitutes another package's suite.
  • type-check (hyphenated) for all three touched packages, script name echoed
    by pnpm as tsc --noEmit && tsc -p tsconfig.test.json, all three Done.
    The .test.ts files are genuinely inside those programs, not excluded:
    tsc -p tsconfig.test.json --listFiles names all four new test files
    (app-shell 2, components 1, plugin-detail 1).
  • pnpm exec eslint ., plain form (no --no-inline-config), whole repo:
    exit 0, ✖ 11554 problems (0 errors, 11554 warnings) — 0 errors; the
    warnings are the repo-wide pre-existing no-explicit-any population.
  • node scripts/check-changeset-presence.mjs: exit 0 —
    ✅ 9 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s).
  • node scripts/check-changeset-no-major.mjs: exit 0 —
    ✅ No changeset declares a 'major' bump. The changeset is minor on the
    three packages and states the new consumption in words.

Boundaries named rather than forced

  • The reference rail is deliberately NOT wired.record:reference_rail
    renders its own total-count badge from a fixed
    { [relationshipField]: parentId } query, so on a page whose FK declares a
    filter the Related tab badge and the rail card for the same child disagree.
    It is not fixable here: ReferenceRailEntrySchema is strictObject with no
    filter key, and the spec's own guidance on that shape says the rail
    "honours no per-entry filter" and that "record:related_list is the
    component whose filter is real; if the rail is ever granted one, this entry
    shape is where it gets declared and enforced." Granting it is an upstream
    spec decision, not this card. Filed separately.
  • Logical combinators do not survive the shared sink.relatedListFilter
    is typed FilterCondition, whose type explicitly declares $and / $or /
    $not — but convertFiltersToAST in @object-ui/core has no combinator
    branch: { $or: [...] } lowers to ['$or','=',[...]] (a leaf naming a field
    $or) and { $not: {...} } throws FilterOperatorError naming a nonsense
    operator. Pre-existing on every consumer of that sink since
    objectstack#7118; this key newly makes it reachable by an author following
    the spec's own type. Fixing it means changing the shared sink for every
    caller, which is its own card. Filed separately; not patched around here,
    because a renderer-side workaround is exactly the second de-facto contract
    #0.1 forbids.

⛔ Left in draft, not enqueued, no auto-merge — the PM lands it.

Generated by Claude Code


Generated by Claude Code

The spec key `relatedListFilter` (objectstack#8704 / PR #8955, shipped in
@objectstack/spec 17.1.0) declares a related list's own scope on the
relationship. This repo accepted it at every gate and consumed it nowhere.
- deriveRelatedLists reads it off the FK; RecordDetailView forwards it; the
page synthesizer emits it onto the record:related_list node's EXISTING
`filter` key, where RelatedList already ANDs it with
`{ [referenceField]: parentId }` (objectstack#7118). No new dialect.
- The tab badge counts the same composed predicate: page:tabs reads the node's
filter and the count store composes it with the parent scope through the same
mergeFilterNodes sink the row query uses. Counts cache per scope, so a
filtered and an unfiltered probe never badge each other.
With nothing declared, the node, the row query and the badge probe are
byte-identical to before. Consumption only; no authoring UI.
@claude

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
ContributorAuthor

The two boundaries named in the description are now filed, both unassigned and unlabelled for PM triage:

Neither is worked around in this PR. Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3179.8 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-B0Pj123L.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.32KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)175.69KB48.80KB
fields (index.js)243.65KB61.63KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.30KB63.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

…rose
The doc-version-claims ratchet (Test shard 4/4) flagged the hard-coded
version this PR's own diff added: a version literal in prose that nothing
in the repo re-verifies. Per the gate's own prescribed remedy (PR #3688 /
PR #3698's precedent), delete the literal and point at the truth instead —
this package's own `@objectstack/spec` dependency in package.json, which
the package manager keeps honest at install time.
Fixes the red `Test (shard 4/4)` check on PR #6946; no other files touched.
@os-samClaude

Copy link
Copy Markdown
Collaborator

CI fix for the red Test (shard 4/4) check.

Reproduced failure (at head 729d29c6c, before the fix):

FAIL scripts/__tests__/doc-version-claims.test.ts > doc version claims - the ratchet > records every version literal on the scanned surfaces
AssertionError: These doc surfaces state a version and nothing in this repository can tell whether it is still true:
- packages/plugin-detail/README.md:262 "@objectstack/spec` 17.1.0"

This came from this PR's own diff (+20/-1 to packages/plugin-detail/README.md), which added a hard-coded 17.1.0 version literal in prose.

Fix: deleted the literal and pointed at the truth instead, following the gate's own prescribed remedy and the house form from PR #3688 / PR #3698. Minimal, single-file diff:

 **A third producer writes that same key: the relationship itself.** A `lookup` /
-`master_detail` field may declare `relatedListFilter` (`@objectstack/spec` 17.1.0-— objectstack#8704 / PR #8955), a canonical Query-DSL `FilterCondition` such as+`master_detail` field may declare `relatedListFilter` (added to `@objectstack/spec`+in objectstack#8704 / PR #8955 — see this package's own `@objectstack/spec`+dependency in `package.json` for the version in use), a canonical Query-DSL+`FilterCondition` such as

Did not touch KNOWN_CLAIMS (the entry-required route was explicitly not the intended remedy here) and did not touch the changeset — its own 17.1.0 mention is in a changelog entry, not a scanned surface, and stays accurate as history.

Passing run (root-form vitest, this repo's guard refuses --filter here):

$ pnpm vitest run scripts/__tests__/doc-version-claims.test.ts
Test Files 1 passed (1)
Tests 25 passed (25)

All 25 tests in the file pass, including the previously-red one. git diff --stat confirms exactly one file changed (packages/plugin-detail/README.md, +4/-2) — nothing else in the PR's diff was touched.

Pushed sha: 8dbb79213d6f14df8bf2fd5f8b3939dce95bd77f on claude/issue-4664-relatedlistfilter-consumption.

PR left in draft, not enqueued, no auto-merge — unchanged per the dispatch constraints.

Generated by Claude Code


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3179.8 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-B0Pj123L.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.32KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)175.69KB48.80KB
fields (index.js)243.65KB61.63KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.30KB63.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-sam
os-sam marked this pull request as ready for review August 31, 2026 14:05
@os-sam
os-sam added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit d796c8dAug 31, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-4664-relatedlistfilter-consumption branch August 31, 2026 14:19
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationpackage: componentsplugintests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Auto-derived related lists consume the new field-level relatedListFilter declaration — list query AND-composed, tab badge honors the same filter

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(detail): derived related lists consume the FK's relatedListFilter — AND-composed query, badge counts the same set - #6946

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-4664-relatedlistfilter-consumption
Aug 31, 2026
Merged

feat(detail): derived related lists consume the FK's relatedListFilter — AND-composed query, badge counts the same set#6946
os-sam merged 2 commits into
mainfrom
claude/issue-4664-relatedlistfilter-consumption

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#4664

Auto-derived related lists now consume the field-level relatedListFilter
declaration: the list query is the declared filter AND-composed with
{ [referenceField]: parentId }, and the related-list tab badge counts the
same composed predicate
, so the badge and the rows answer one question.

Scope guard — condition 2, measured before any code

The card refuses to start unless the pinned @objectstack/spec actually
carries the key. Measured, not assumed:

readingvalue
manifest pinroot package.json"@objectstack/spec": "^17.0.0"
lockfile resolution17.2.0 (30 entries, one resolution)
installed packagenode_modules/.pnpm/@objectstack+spec@17.2.0_.../package.json17.2.0
key on the FIELD shaperelatedListFilter?: FilterCondition | undefined, beside relatedList / relatedListTitle / relatedListColumns
key namerelatedListFilter — exactly as the card assumed, no drift
shapeFilterConditionSchema.optional(), i.e. the canonical Query-DSL FilterCondition (same dialect as a query where)

The installed key's own describe() text is the contract this PR implements:

Declarative default filter for the detail-page related list: AND-composed
with the parent-relationship condition { [referenceField]: parentId } — an
authored constraint, never a user-editable suggestion. The related-list tab
badge count honors the same composed filter, so counts match the visible
rows.

Premise holds. No spec pin was touched.

What changed

Five source files, one value, one read site — no new dialect:

  1. app-shell/src/utils/deriveRelatedLists.ts — reads relatedListFilter off
    the FK onto the derived descriptor. A plain non-empty object is a
    declaration; an array, a string, null, a number, {} are silence. Not a
    tolerant reader: nothing is aliased, coerced or defaulted (AGENTS.md #0.1),
    the same declared-vs-absent discrimination relatedListTitle and
    relatedListColumns already do next to it.
  2. app-shell/src/views/RecordDetailView.tsx — forwards it into the page
    synthesizer's related shape.
  3. plugin-detail/src/synth/buildDefaultPageSchema.ts — emits it onto the
    record:related_list node's existingfilter key.

That key is where the reuse is honest and where it stops. filter has had a
read site since objectstack#7118: RelatedList lowers it through the repo's one
filter sink and ANDs it with the parent scope, wrapping both sources under a
single and rather than letting either replace the other. The derived producer
lands on the same key as the component-level authored filter and as a
dataSource binding's composed filter — three producers, one read site, one
lowering. Nothing was added to the list path; it already did the right thing
with a value it never received.

The badge half needed new code, because the tab strip's probe read only
objectName and relationshipField off the node it was badging:

  1. components/src/renderers/layout/containers.tsx — the page:tabs count
    probe now also reads the node's filter.
  2. components/src/hooks/related-count-store.ts — composes it with the parent
    scope through the samemergeFilterNodes sink the row query uses, so the
    two $filter values are one composition used twice rather than two
    implementations that agree by luck. Cache identity gains the scope, placed
    INSIDE the relField segment so invalidate's two structural reads on these
    keys (startsWith(object::), endsWith(::parentId)) keep working — a scope
    segment appended after the parent id would have orphaned the entry, and its
    failure mode is a badge that never refreshes after a write, with no error to
    notice.

With nothing declared, the synthesized node, the row query and the badge probe
are byte-identical to before ({ [referenceField]: parentId }, the untouched
MongoDB-style object — not a freshly lowered AST that means the same thing).

Consumption only. No authoring UI for the filter, as the card requires.

Verification

Every exit code below was captured before any pipe, and each gate is quoted by
the line the gate itself printed.

Proving the AND, not just the filter. The page test's fake backend
EVALUATES the composed predicate over a four-row fixture built so each conjunct
alone admits a row the other excludes:

rowparentstatusparent scopedeclared filterAND
Live Itemtv-1openyesyesyes
Archived Itemtv-1archivedyesnono
Other Parent Itemtv-2opennoyesno
Other Parent Archivedtv-2archivednonono

Parent scope alone answers 2, the declared filter alone answers 2, the
conjunction answers 1 — so dropping either conjunct changes the number the test
reads, in both directions. Those three numbers are asserted from the fixture
itself, so a later edit that flattens it into a single-condition fixture fails
loudly instead of quietly weakening every assertion above it. The evaluator
THROWS on any filter shape it does not recognise: a permissive evaluator would
answer "all rows" for a malformed filter, which is exactly what the
counter-probes would read as a pass.

Proving badge and list agree, in one test, with the unfiltered count
differing from the filtered one: the list renders Live Item and neither
Archived Item (excluded by the declared conjunct) nor Other Parent Item
(excluded by the parent conjunct), while the tab badge reads 1. The
parent-scoped unfiltered count is 2, so a badge ignoring the filter reads 2
there.

Ablation — direction predicted in writing before either run. These tests
resolve through the root vitest.config.mts alias table, which maps every
@object-ui/* specifier to packages/*/src; there is no dist/ seam for a
mutation to be lost behind and therefore no rebuild leg. Each leg proved the
mutation on disk (anchored count of the injected marker AND of the deleted text,
plus git hash-object differing from the HEAD blob) and proved the restore both
ways (git diff HEAD empty AND hash equal to the HEAD blob), under
trap ... EXIT INT TERM with absolute paths from git rev-parse --show-toplevel
and git checkout HEAD -- ABSPATH, never bare.

Leg A — drop the filter from the BADGE path alone (containers.tsx).
Predicted: the two badge legs red, the row leg green, the other three files
green. Observed, exactly:

× SUBJECT — the badge probe sends the SAME composed filter as the rows
AssertionError: expected { task_version: 'tv-1' } to deeply equal [ 'and', …(2) ]
× PARITY — the badge and the rendered rows agree, and 1 !== the unfiltered 2
AssertionError: expected '2' to be '1'
Test Files 1 failed | 3 passed (4)
Tests 2 failed | 25 passed (27)

The second line is the defect this card exists to prevent, reproduced at
fixture scale: the badge said 2 above a list showing 1.

Leg B — the converse: drop it from the LIST path alone
(record-related-list.tsx). Predicted: the row leg red, the badge-equality leg
red as the mirror image of A, parity red the other way round, the other three
files green. Observed, exactly:

× SUBJECT — the row query is the declared filter AND the parent scope
AssertionError: expected [ 'ci-live', 'ci-archived' ] to deeply equal [ 'ci-live' ]
× SUBJECT — the badge probe sends the SAME composed filter as the rows
AssertionError: expected [ 'and', …(2) ] to deeply equal { task_version: 'tv-1' }
× PARITY — the badge and the rendered rows agree, and 1 !== the unfiltered 2
Test Files 1 failed | 3 passed (4)
Tests 3 failed | 24 passed (27)

Gates, all at the final commit 729d29c6c:

  • Root-form vitest, before/after over a set derived by MEASUREMENT (every test
    file that reads one of the five changed modules, by grep, rather than by
    guess): before — 110 files / 1559 tests passed, at BASE 40c479af2 in a
    comparison worktree; after — 114 files / 1586 tests passed. Delta +4
    files / +27 tests, exactly the four test files this PR adds. No pre-existing
    file or test count moved. The reported file count (114) equals the derived
    list's own count (114) — the sanity check against a run that silently
    substitutes another package's suite.
  • type-check (hyphenated) for all three touched packages, script name echoed
    by pnpm as tsc --noEmit && tsc -p tsconfig.test.json, all three Done.
    The .test.ts files are genuinely inside those programs, not excluded:
    tsc -p tsconfig.test.json --listFiles names all four new test files
    (app-shell 2, components 1, plugin-detail 1).
  • pnpm exec eslint ., plain form (no --no-inline-config), whole repo:
    exit 0, ✖ 11554 problems (0 errors, 11554 warnings) — 0 errors; the
    warnings are the repo-wide pre-existing no-explicit-any population.
  • node scripts/check-changeset-presence.mjs: exit 0 —
    ✅ 9 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s).
  • node scripts/check-changeset-no-major.mjs: exit 0 —
    ✅ No changeset declares a 'major' bump. The changeset is minor on the
    three packages and states the new consumption in words.

Boundaries named rather than forced

  • The reference rail is deliberately NOT wired.record:reference_rail
    renders its own total-count badge from a fixed
    { [relationshipField]: parentId } query, so on a page whose FK declares a
    filter the Related tab badge and the rail card for the same child disagree.
    It is not fixable here: ReferenceRailEntrySchema is strictObject with no
    filter key, and the spec's own guidance on that shape says the rail
    "honours no per-entry filter" and that "record:related_list is the
    component whose filter is real; if the rail is ever granted one, this entry
    shape is where it gets declared and enforced." Granting it is an upstream
    spec decision, not this card. Filed separately.
  • Logical combinators do not survive the shared sink.relatedListFilter
    is typed FilterCondition, whose type explicitly declares $and / $or /
    $not — but convertFiltersToAST in @object-ui/core has no combinator
    branch: { $or: [...] } lowers to ['$or','=',[...]] (a leaf naming a field
    $or) and { $not: {...} } throws FilterOperatorError naming a nonsense
    operator. Pre-existing on every consumer of that sink since
    objectstack#7118; this key newly makes it reachable by an author following
    the spec's own type. Fixing it means changing the shared sink for every
    caller, which is its own card. Filed separately; not patched around here,
    because a renderer-side workaround is exactly the second de-facto contract
    #0.1 forbids.

⛔ Left in draft, not enqueued, no auto-merge — the PM lands it.

Generated by Claude Code


Generated by Claude Code

The spec key `relatedListFilter` (objectstack#8704 / PR #8955, shipped in
@objectstack/spec 17.1.0) declares a related list's own scope on the
relationship. This repo accepted it at every gate and consumed it nowhere.
- deriveRelatedLists reads it off the FK; RecordDetailView forwards it; the
page synthesizer emits it onto the record:related_list node's EXISTING
`filter` key, where RelatedList already ANDs it with
`{ [referenceField]: parentId }` (objectstack#7118). No new dialect.
- The tab badge counts the same composed predicate: page:tabs reads the node's
filter and the count store composes it with the parent scope through the same
mergeFilterNodes sink the row query uses. Counts cache per scope, so a
filtered and an unfiltered probe never badge each other.
With nothing declared, the node, the row query and the badge probe are
byte-identical to before. Consumption only; no authoring UI.
@claude

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
ContributorAuthor

The two boundaries named in the description are now filed, both unassigned and unlabelled for PM triage:

Neither is worked around in this PR. Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3179.8 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-B0Pj123L.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.32KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)175.69KB48.80KB
fields (index.js)243.65KB61.63KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.30KB63.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

…rose
The doc-version-claims ratchet (Test shard 4/4) flagged the hard-coded
version this PR's own diff added: a version literal in prose that nothing
in the repo re-verifies. Per the gate's own prescribed remedy (PR #3688 /
PR #3698's precedent), delete the literal and point at the truth instead —
this package's own `@objectstack/spec` dependency in package.json, which
the package manager keeps honest at install time.
Fixes the red `Test (shard 4/4)` check on PR #6946; no other files touched.
@os-samClaude

Copy link
Copy Markdown
Collaborator

CI fix for the red Test (shard 4/4) check.

Reproduced failure (at head 729d29c6c, before the fix):

FAIL scripts/__tests__/doc-version-claims.test.ts > doc version claims - the ratchet > records every version literal on the scanned surfaces
AssertionError: These doc surfaces state a version and nothing in this repository can tell whether it is still true:
- packages/plugin-detail/README.md:262 "@objectstack/spec` 17.1.0"

This came from this PR's own diff (+20/-1 to packages/plugin-detail/README.md), which added a hard-coded 17.1.0 version literal in prose.

Fix: deleted the literal and pointed at the truth instead, following the gate's own prescribed remedy and the house form from PR #3688 / PR #3698. Minimal, single-file diff:

 **A third producer writes that same key: the relationship itself.** A `lookup` /
-`master_detail` field may declare `relatedListFilter` (`@objectstack/spec` 17.1.0-— objectstack#8704 / PR #8955), a canonical Query-DSL `FilterCondition` such as+`master_detail` field may declare `relatedListFilter` (added to `@objectstack/spec`+in objectstack#8704 / PR #8955 — see this package's own `@objectstack/spec`+dependency in `package.json` for the version in use), a canonical Query-DSL+`FilterCondition` such as

Did not touch KNOWN_CLAIMS (the entry-required route was explicitly not the intended remedy here) and did not touch the changeset — its own 17.1.0 mention is in a changelog entry, not a scanned surface, and stays accurate as history.

Passing run (root-form vitest, this repo's guard refuses --filter here):

$ pnpm vitest run scripts/__tests__/doc-version-claims.test.ts
Test Files 1 passed (1)
Tests 25 passed (25)

All 25 tests in the file pass, including the previously-red one. git diff --stat confirms exactly one file changed (packages/plugin-detail/README.md, +4/-2) — nothing else in the PR's diff was touched.

Pushed sha: 8dbb79213d6f14df8bf2fd5f8b3939dce95bd77f on claude/issue-4664-relatedlistfilter-consumption.

PR left in draft, not enqueued, no auto-merge — unchanged per the dispatch constraints.

Generated by Claude Code


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3179.8 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-B0Pj123L.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.32KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)175.69KB48.80KB
fields (index.js)243.65KB61.63KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.30KB63.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-sam
os-sam marked this pull request as ready for review August 31, 2026 14:05
@os-sam
os-sam added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit d796c8dAug 31, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-4664-relatedlistfilter-consumption branch August 31, 2026 14:19
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationpackage: componentsplugintests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Auto-derived related lists consume the new field-level relatedListFilter declaration — list query AND-composed, tab badge honors the same filter

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(detail): derived related lists consume the FK's relatedListFilter — AND-composed query, badge counts the same set - #6946

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-4664-relatedlistfilter-consumption
Aug 31, 2026
Merged

feat(detail): derived related lists consume the FK's relatedListFilter — AND-composed query, badge counts the same set#6946
os-sam merged 2 commits into
mainfrom
claude/issue-4664-relatedlistfilter-consumption

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#4664

Auto-derived related lists now consume the field-level relatedListFilter
declaration: the list query is the declared filter AND-composed with
{ [referenceField]: parentId }, and the related-list tab badge counts the
same composed predicate
, so the badge and the rows answer one question.

Scope guard — condition 2, measured before any code

The card refuses to start unless the pinned @objectstack/spec actually
carries the key. Measured, not assumed:

readingvalue
manifest pinroot package.json"@objectstack/spec": "^17.0.0"
lockfile resolution17.2.0 (30 entries, one resolution)
installed packagenode_modules/.pnpm/@objectstack+spec@17.2.0_.../package.json17.2.0
key on the FIELD shaperelatedListFilter?: FilterCondition | undefined, beside relatedList / relatedListTitle / relatedListColumns
key namerelatedListFilter — exactly as the card assumed, no drift
shapeFilterConditionSchema.optional(), i.e. the canonical Query-DSL FilterCondition (same dialect as a query where)

The installed key's own describe() text is the contract this PR implements:

Declarative default filter for the detail-page related list: AND-composed
with the parent-relationship condition { [referenceField]: parentId } — an
authored constraint, never a user-editable suggestion. The related-list tab
badge count honors the same composed filter, so counts match the visible
rows.

Premise holds. No spec pin was touched.

What changed

Five source files, one value, one read site — no new dialect:

  1. app-shell/src/utils/deriveRelatedLists.ts — reads relatedListFilter off
    the FK onto the derived descriptor. A plain non-empty object is a
    declaration; an array, a string, null, a number, {} are silence. Not a
    tolerant reader: nothing is aliased, coerced or defaulted (AGENTS.md #0.1),
    the same declared-vs-absent discrimination relatedListTitle and
    relatedListColumns already do next to it.
  2. app-shell/src/views/RecordDetailView.tsx — forwards it into the page
    synthesizer's related shape.
  3. plugin-detail/src/synth/buildDefaultPageSchema.ts — emits it onto the
    record:related_list node's existingfilter key.

That key is where the reuse is honest and where it stops. filter has had a
read site since objectstack#7118: RelatedList lowers it through the repo's one
filter sink and ANDs it with the parent scope, wrapping both sources under a
single and rather than letting either replace the other. The derived producer
lands on the same key as the component-level authored filter and as a
dataSource binding's composed filter — three producers, one read site, one
lowering. Nothing was added to the list path; it already did the right thing
with a value it never received.

The badge half needed new code, because the tab strip's probe read only
objectName and relationshipField off the node it was badging:

  1. components/src/renderers/layout/containers.tsx — the page:tabs count
    probe now also reads the node's filter.
  2. components/src/hooks/related-count-store.ts — composes it with the parent
    scope through the samemergeFilterNodes sink the row query uses, so the
    two $filter values are one composition used twice rather than two
    implementations that agree by luck. Cache identity gains the scope, placed
    INSIDE the relField segment so invalidate's two structural reads on these
    keys (startsWith(object::), endsWith(::parentId)) keep working — a scope
    segment appended after the parent id would have orphaned the entry, and its
    failure mode is a badge that never refreshes after a write, with no error to
    notice.

With nothing declared, the synthesized node, the row query and the badge probe
are byte-identical to before ({ [referenceField]: parentId }, the untouched
MongoDB-style object — not a freshly lowered AST that means the same thing).

Consumption only. No authoring UI for the filter, as the card requires.

Verification

Every exit code below was captured before any pipe, and each gate is quoted by
the line the gate itself printed.

Proving the AND, not just the filter. The page test's fake backend
EVALUATES the composed predicate over a four-row fixture built so each conjunct
alone admits a row the other excludes:

rowparentstatusparent scopedeclared filterAND
Live Itemtv-1openyesyesyes
Archived Itemtv-1archivedyesnono
Other Parent Itemtv-2opennoyesno
Other Parent Archivedtv-2archivednonono

Parent scope alone answers 2, the declared filter alone answers 2, the
conjunction answers 1 — so dropping either conjunct changes the number the test
reads, in both directions. Those three numbers are asserted from the fixture
itself, so a later edit that flattens it into a single-condition fixture fails
loudly instead of quietly weakening every assertion above it. The evaluator
THROWS on any filter shape it does not recognise: a permissive evaluator would
answer "all rows" for a malformed filter, which is exactly what the
counter-probes would read as a pass.

Proving badge and list agree, in one test, with the unfiltered count
differing from the filtered one: the list renders Live Item and neither
Archived Item (excluded by the declared conjunct) nor Other Parent Item
(excluded by the parent conjunct), while the tab badge reads 1. The
parent-scoped unfiltered count is 2, so a badge ignoring the filter reads 2
there.

Ablation — direction predicted in writing before either run. These tests
resolve through the root vitest.config.mts alias table, which maps every
@object-ui/* specifier to packages/*/src; there is no dist/ seam for a
mutation to be lost behind and therefore no rebuild leg. Each leg proved the
mutation on disk (anchored count of the injected marker AND of the deleted text,
plus git hash-object differing from the HEAD blob) and proved the restore both
ways (git diff HEAD empty AND hash equal to the HEAD blob), under
trap ... EXIT INT TERM with absolute paths from git rev-parse --show-toplevel
and git checkout HEAD -- ABSPATH, never bare.

Leg A — drop the filter from the BADGE path alone (containers.tsx).
Predicted: the two badge legs red, the row leg green, the other three files
green. Observed, exactly:

× SUBJECT — the badge probe sends the SAME composed filter as the rows
AssertionError: expected { task_version: 'tv-1' } to deeply equal [ 'and', …(2) ]
× PARITY — the badge and the rendered rows agree, and 1 !== the unfiltered 2
AssertionError: expected '2' to be '1'
Test Files 1 failed | 3 passed (4)
Tests 2 failed | 25 passed (27)

The second line is the defect this card exists to prevent, reproduced at
fixture scale: the badge said 2 above a list showing 1.

Leg B — the converse: drop it from the LIST path alone
(record-related-list.tsx). Predicted: the row leg red, the badge-equality leg
red as the mirror image of A, parity red the other way round, the other three
files green. Observed, exactly:

× SUBJECT — the row query is the declared filter AND the parent scope
AssertionError: expected [ 'ci-live', 'ci-archived' ] to deeply equal [ 'ci-live' ]
× SUBJECT — the badge probe sends the SAME composed filter as the rows
AssertionError: expected [ 'and', …(2) ] to deeply equal { task_version: 'tv-1' }
× PARITY — the badge and the rendered rows agree, and 1 !== the unfiltered 2
Test Files 1 failed | 3 passed (4)
Tests 3 failed | 24 passed (27)

Gates, all at the final commit 729d29c6c:

  • Root-form vitest, before/after over a set derived by MEASUREMENT (every test
    file that reads one of the five changed modules, by grep, rather than by
    guess): before — 110 files / 1559 tests passed, at BASE 40c479af2 in a
    comparison worktree; after — 114 files / 1586 tests passed. Delta +4
    files / +27 tests, exactly the four test files this PR adds. No pre-existing
    file or test count moved. The reported file count (114) equals the derived
    list's own count (114) — the sanity check against a run that silently
    substitutes another package's suite.
  • type-check (hyphenated) for all three touched packages, script name echoed
    by pnpm as tsc --noEmit && tsc -p tsconfig.test.json, all three Done.
    The .test.ts files are genuinely inside those programs, not excluded:
    tsc -p tsconfig.test.json --listFiles names all four new test files
    (app-shell 2, components 1, plugin-detail 1).
  • pnpm exec eslint ., plain form (no --no-inline-config), whole repo:
    exit 0, ✖ 11554 problems (0 errors, 11554 warnings) — 0 errors; the
    warnings are the repo-wide pre-existing no-explicit-any population.
  • node scripts/check-changeset-presence.mjs: exit 0 —
    ✅ 9 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s).
  • node scripts/check-changeset-no-major.mjs: exit 0 —
    ✅ No changeset declares a 'major' bump. The changeset is minor on the
    three packages and states the new consumption in words.

Boundaries named rather than forced

  • The reference rail is deliberately NOT wired.record:reference_rail
    renders its own total-count badge from a fixed
    { [relationshipField]: parentId } query, so on a page whose FK declares a
    filter the Related tab badge and the rail card for the same child disagree.
    It is not fixable here: ReferenceRailEntrySchema is strictObject with no
    filter key, and the spec's own guidance on that shape says the rail
    "honours no per-entry filter" and that "record:related_list is the
    component whose filter is real; if the rail is ever granted one, this entry
    shape is where it gets declared and enforced." Granting it is an upstream
    spec decision, not this card. Filed separately.
  • Logical combinators do not survive the shared sink.relatedListFilter
    is typed FilterCondition, whose type explicitly declares $and / $or /
    $not — but convertFiltersToAST in @object-ui/core has no combinator
    branch: { $or: [...] } lowers to ['$or','=',[...]] (a leaf naming a field
    $or) and { $not: {...} } throws FilterOperatorError naming a nonsense
    operator. Pre-existing on every consumer of that sink since
    objectstack#7118; this key newly makes it reachable by an author following
    the spec's own type. Fixing it means changing the shared sink for every
    caller, which is its own card. Filed separately; not patched around here,
    because a renderer-side workaround is exactly the second de-facto contract
    #0.1 forbids.

⛔ Left in draft, not enqueued, no auto-merge — the PM lands it.

Generated by Claude Code


Generated by Claude Code

The spec key `relatedListFilter` (objectstack#8704 / PR #8955, shipped in
@objectstack/spec 17.1.0) declares a related list's own scope on the
relationship. This repo accepted it at every gate and consumed it nowhere.
- deriveRelatedLists reads it off the FK; RecordDetailView forwards it; the
page synthesizer emits it onto the record:related_list node's EXISTING
`filter` key, where RelatedList already ANDs it with
`{ [referenceField]: parentId }` (objectstack#7118). No new dialect.
- The tab badge counts the same composed predicate: page:tabs reads the node's
filter and the count store composes it with the parent scope through the same
mergeFilterNodes sink the row query uses. Counts cache per scope, so a
filtered and an unfiltered probe never badge each other.
With nothing declared, the node, the row query and the badge probe are
byte-identical to before. Consumption only; no authoring UI.
@claude

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
ContributorAuthor

The two boundaries named in the description are now filed, both unassigned and unlabelled for PM triage:

Neither is worked around in this PR. Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3179.8 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-B0Pj123L.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.32KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)175.69KB48.80KB
fields (index.js)243.65KB61.63KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.30KB63.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

…rose
The doc-version-claims ratchet (Test shard 4/4) flagged the hard-coded
version this PR's own diff added: a version literal in prose that nothing
in the repo re-verifies. Per the gate's own prescribed remedy (PR #3688 /
PR #3698's precedent), delete the literal and point at the truth instead —
this package's own `@objectstack/spec` dependency in package.json, which
the package manager keeps honest at install time.
Fixes the red `Test (shard 4/4)` check on PR #6946; no other files touched.
@os-samClaude

Copy link
Copy Markdown
Collaborator

CI fix for the red Test (shard 4/4) check.

Reproduced failure (at head 729d29c6c, before the fix):

FAIL scripts/__tests__/doc-version-claims.test.ts > doc version claims - the ratchet > records every version literal on the scanned surfaces
AssertionError: These doc surfaces state a version and nothing in this repository can tell whether it is still true:
- packages/plugin-detail/README.md:262 "@objectstack/spec` 17.1.0"

This came from this PR's own diff (+20/-1 to packages/plugin-detail/README.md), which added a hard-coded 17.1.0 version literal in prose.

Fix: deleted the literal and pointed at the truth instead, following the gate's own prescribed remedy and the house form from PR #3688 / PR #3698. Minimal, single-file diff:

 **A third producer writes that same key: the relationship itself.** A `lookup` /
-`master_detail` field may declare `relatedListFilter` (`@objectstack/spec` 17.1.0-— objectstack#8704 / PR #8955), a canonical Query-DSL `FilterCondition` such as+`master_detail` field may declare `relatedListFilter` (added to `@objectstack/spec`+in objectstack#8704 / PR #8955 — see this package's own `@objectstack/spec`+dependency in `package.json` for the version in use), a canonical Query-DSL+`FilterCondition` such as

Did not touch KNOWN_CLAIMS (the entry-required route was explicitly not the intended remedy here) and did not touch the changeset — its own 17.1.0 mention is in a changelog entry, not a scanned surface, and stays accurate as history.

Passing run (root-form vitest, this repo's guard refuses --filter here):

$ pnpm vitest run scripts/__tests__/doc-version-claims.test.ts
Test Files 1 passed (1)
Tests 25 passed (25)

All 25 tests in the file pass, including the previously-red one. git diff --stat confirms exactly one file changed (packages/plugin-detail/README.md, +4/-2) — nothing else in the PR's diff was touched.

Pushed sha: 8dbb79213d6f14df8bf2fd5f8b3939dce95bd77f on claude/issue-4664-relatedlistfilter-consumption.

PR left in draft, not enqueued, no auto-merge — unchanged per the dispatch constraints.

Generated by Claude Code


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3179.8 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-B0Pj123L.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.32KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)175.69KB48.80KB
fields (index.js)243.65KB61.63KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.30KB63.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-sam
os-sam marked this pull request as ready for review August 31, 2026 14:05
@os-sam
os-sam added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit d796c8dAug 31, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-4664-relatedlistfilter-consumption branch August 31, 2026 14:19
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationpackage: componentsplugintests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Auto-derived related lists consume the new field-level relatedListFilter declaration — list query AND-composed, tab badge honors the same filter

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat(detail): derived related lists consume the FK's relatedListFilter — AND-composed query, badge counts the same set - #6946

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-4664-relatedlistfilter-consumption
Aug 31, 2026
Merged

feat(detail): derived related lists consume the FK's relatedListFilter — AND-composed query, badge counts the same set#6946
os-sam merged 2 commits into
mainfrom
claude/issue-4664-relatedlistfilter-consumption

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#4664

Auto-derived related lists now consume the field-level relatedListFilter
declaration: the list query is the declared filter AND-composed with
{ [referenceField]: parentId }, and the related-list tab badge counts the
same composed predicate
, so the badge and the rows answer one question.

Scope guard — condition 2, measured before any code

The card refuses to start unless the pinned @objectstack/spec actually
carries the key. Measured, not assumed:

readingvalue
manifest pinroot package.json"@objectstack/spec": "^17.0.0"
lockfile resolution17.2.0 (30 entries, one resolution)
installed packagenode_modules/.pnpm/@objectstack+spec@17.2.0_.../package.json17.2.0
key on the FIELD shaperelatedListFilter?: FilterCondition | undefined, beside relatedList / relatedListTitle / relatedListColumns
key namerelatedListFilter — exactly as the card assumed, no drift
shapeFilterConditionSchema.optional(), i.e. the canonical Query-DSL FilterCondition (same dialect as a query where)

The installed key's own describe() text is the contract this PR implements:

Declarative default filter for the detail-page related list: AND-composed
with the parent-relationship condition { [referenceField]: parentId } — an
authored constraint, never a user-editable suggestion. The related-list tab
badge count honors the same composed filter, so counts match the visible
rows.

Premise holds. No spec pin was touched.

What changed

Five source files, one value, one read site — no new dialect:

  1. app-shell/src/utils/deriveRelatedLists.ts — reads relatedListFilter off
    the FK onto the derived descriptor. A plain non-empty object is a
    declaration; an array, a string, null, a number, {} are silence. Not a
    tolerant reader: nothing is aliased, coerced or defaulted (AGENTS.md #0.1),
    the same declared-vs-absent discrimination relatedListTitle and
    relatedListColumns already do next to it.
  2. app-shell/src/views/RecordDetailView.tsx — forwards it into the page
    synthesizer's related shape.
  3. plugin-detail/src/synth/buildDefaultPageSchema.ts — emits it onto the
    record:related_list node's existingfilter key.

That key is where the reuse is honest and where it stops. filter has had a
read site since objectstack#7118: RelatedList lowers it through the repo's one
filter sink and ANDs it with the parent scope, wrapping both sources under a
single and rather than letting either replace the other. The derived producer
lands on the same key as the component-level authored filter and as a
dataSource binding's composed filter — three producers, one read site, one
lowering. Nothing was added to the list path; it already did the right thing
with a value it never received.

The badge half needed new code, because the tab strip's probe read only
objectName and relationshipField off the node it was badging:

  1. components/src/renderers/layout/containers.tsx — the page:tabs count
    probe now also reads the node's filter.
  2. components/src/hooks/related-count-store.ts — composes it with the parent
    scope through the samemergeFilterNodes sink the row query uses, so the
    two $filter values are one composition used twice rather than two
    implementations that agree by luck. Cache identity gains the scope, placed
    INSIDE the relField segment so invalidate's two structural reads on these
    keys (startsWith(object::), endsWith(::parentId)) keep working — a scope
    segment appended after the parent id would have orphaned the entry, and its
    failure mode is a badge that never refreshes after a write, with no error to
    notice.

With nothing declared, the synthesized node, the row query and the badge probe
are byte-identical to before ({ [referenceField]: parentId }, the untouched
MongoDB-style object — not a freshly lowered AST that means the same thing).

Consumption only. No authoring UI for the filter, as the card requires.

Verification

Every exit code below was captured before any pipe, and each gate is quoted by
the line the gate itself printed.

Proving the AND, not just the filter. The page test's fake backend
EVALUATES the composed predicate over a four-row fixture built so each conjunct
alone admits a row the other excludes:

rowparentstatusparent scopedeclared filterAND
Live Itemtv-1openyesyesyes
Archived Itemtv-1archivedyesnono
Other Parent Itemtv-2opennoyesno
Other Parent Archivedtv-2archivednonono

Parent scope alone answers 2, the declared filter alone answers 2, the
conjunction answers 1 — so dropping either conjunct changes the number the test
reads, in both directions. Those three numbers are asserted from the fixture
itself, so a later edit that flattens it into a single-condition fixture fails
loudly instead of quietly weakening every assertion above it. The evaluator
THROWS on any filter shape it does not recognise: a permissive evaluator would
answer "all rows" for a malformed filter, which is exactly what the
counter-probes would read as a pass.

Proving badge and list agree, in one test, with the unfiltered count
differing from the filtered one: the list renders Live Item and neither
Archived Item (excluded by the declared conjunct) nor Other Parent Item
(excluded by the parent conjunct), while the tab badge reads 1. The
parent-scoped unfiltered count is 2, so a badge ignoring the filter reads 2
there.

Ablation — direction predicted in writing before either run. These tests
resolve through the root vitest.config.mts alias table, which maps every
@object-ui/* specifier to packages/*/src; there is no dist/ seam for a
mutation to be lost behind and therefore no rebuild leg. Each leg proved the
mutation on disk (anchored count of the injected marker AND of the deleted text,
plus git hash-object differing from the HEAD blob) and proved the restore both
ways (git diff HEAD empty AND hash equal to the HEAD blob), under
trap ... EXIT INT TERM with absolute paths from git rev-parse --show-toplevel
and git checkout HEAD -- ABSPATH, never bare.

Leg A — drop the filter from the BADGE path alone (containers.tsx).
Predicted: the two badge legs red, the row leg green, the other three files
green. Observed, exactly:

× SUBJECT — the badge probe sends the SAME composed filter as the rows
AssertionError: expected { task_version: 'tv-1' } to deeply equal [ 'and', …(2) ]
× PARITY — the badge and the rendered rows agree, and 1 !== the unfiltered 2
AssertionError: expected '2' to be '1'
Test Files 1 failed | 3 passed (4)
Tests 2 failed | 25 passed (27)

The second line is the defect this card exists to prevent, reproduced at
fixture scale: the badge said 2 above a list showing 1.

Leg B — the converse: drop it from the LIST path alone
(record-related-list.tsx). Predicted: the row leg red, the badge-equality leg
red as the mirror image of A, parity red the other way round, the other three
files green. Observed, exactly:

× SUBJECT — the row query is the declared filter AND the parent scope
AssertionError: expected [ 'ci-live', 'ci-archived' ] to deeply equal [ 'ci-live' ]
× SUBJECT — the badge probe sends the SAME composed filter as the rows
AssertionError: expected [ 'and', …(2) ] to deeply equal { task_version: 'tv-1' }
× PARITY — the badge and the rendered rows agree, and 1 !== the unfiltered 2
Test Files 1 failed | 3 passed (4)
Tests 3 failed | 24 passed (27)

Gates, all at the final commit 729d29c6c:

  • Root-form vitest, before/after over a set derived by MEASUREMENT (every test
    file that reads one of the five changed modules, by grep, rather than by
    guess): before — 110 files / 1559 tests passed, at BASE 40c479af2 in a
    comparison worktree; after — 114 files / 1586 tests passed. Delta +4
    files / +27 tests, exactly the four test files this PR adds. No pre-existing
    file or test count moved. The reported file count (114) equals the derived
    list's own count (114) — the sanity check against a run that silently
    substitutes another package's suite.
  • type-check (hyphenated) for all three touched packages, script name echoed
    by pnpm as tsc --noEmit && tsc -p tsconfig.test.json, all three Done.
    The .test.ts files are genuinely inside those programs, not excluded:
    tsc -p tsconfig.test.json --listFiles names all four new test files
    (app-shell 2, components 1, plugin-detail 1).
  • pnpm exec eslint ., plain form (no --no-inline-config), whole repo:
    exit 0, ✖ 11554 problems (0 errors, 11554 warnings) — 0 errors; the
    warnings are the repo-wide pre-existing no-explicit-any population.
  • node scripts/check-changeset-presence.mjs: exit 0 —
    ✅ 9 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s).
  • node scripts/check-changeset-no-major.mjs: exit 0 —
    ✅ No changeset declares a 'major' bump. The changeset is minor on the
    three packages and states the new consumption in words.

Boundaries named rather than forced

  • The reference rail is deliberately NOT wired.record:reference_rail
    renders its own total-count badge from a fixed
    { [relationshipField]: parentId } query, so on a page whose FK declares a
    filter the Related tab badge and the rail card for the same child disagree.
    It is not fixable here: ReferenceRailEntrySchema is strictObject with no
    filter key, and the spec's own guidance on that shape says the rail
    "honours no per-entry filter" and that "record:related_list is the
    component whose filter is real; if the rail is ever granted one, this entry
    shape is where it gets declared and enforced." Granting it is an upstream
    spec decision, not this card. Filed separately.
  • Logical combinators do not survive the shared sink.relatedListFilter
    is typed FilterCondition, whose type explicitly declares $and / $or /
    $not — but convertFiltersToAST in @object-ui/core has no combinator
    branch: { $or: [...] } lowers to ['$or','=',[...]] (a leaf naming a field
    $or) and { $not: {...} } throws FilterOperatorError naming a nonsense
    operator. Pre-existing on every consumer of that sink since
    objectstack#7118; this key newly makes it reachable by an author following
    the spec's own type. Fixing it means changing the shared sink for every
    caller, which is its own card. Filed separately; not patched around here,
    because a renderer-side workaround is exactly the second de-facto contract
    #0.1 forbids.

⛔ Left in draft, not enqueued, no auto-merge — the PM lands it.

Generated by Claude Code


Generated by Claude Code

The spec key `relatedListFilter` (objectstack#8704 / PR #8955, shipped in
@objectstack/spec 17.1.0) declares a related list's own scope on the
relationship. This repo accepted it at every gate and consumed it nowhere.
- deriveRelatedLists reads it off the FK; RecordDetailView forwards it; the
page synthesizer emits it onto the record:related_list node's EXISTING
`filter` key, where RelatedList already ANDs it with
`{ [referenceField]: parentId }` (objectstack#7118). No new dialect.
- The tab badge counts the same composed predicate: page:tabs reads the node's
filter and the count store composes it with the parent scope through the same
mergeFilterNodes sink the row query uses. Counts cache per scope, so a
filtered and an unfiltered probe never badge each other.
With nothing declared, the node, the row query and the badge probe are
byte-identical to before. Consumption only; no authoring UI.
@claude

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
ContributorAuthor

The two boundaries named in the description are now filed, both unassigned and unlabelled for PM triage:

Neither is worked around in this PR. Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3179.8 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-B0Pj123L.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.32KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)175.69KB48.80KB
fields (index.js)243.65KB61.63KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.30KB63.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

…rose
The doc-version-claims ratchet (Test shard 4/4) flagged the hard-coded
version this PR's own diff added: a version literal in prose that nothing
in the repo re-verifies. Per the gate's own prescribed remedy (PR #3688 /
PR #3698's precedent), delete the literal and point at the truth instead —
this package's own `@objectstack/spec` dependency in package.json, which
the package manager keeps honest at install time.
Fixes the red `Test (shard 4/4)` check on PR #6946; no other files touched.
@os-samClaude

Copy link
Copy Markdown
Collaborator

CI fix for the red Test (shard 4/4) check.

Reproduced failure (at head 729d29c6c, before the fix):

FAIL scripts/__tests__/doc-version-claims.test.ts > doc version claims - the ratchet > records every version literal on the scanned surfaces
AssertionError: These doc surfaces state a version and nothing in this repository can tell whether it is still true:
- packages/plugin-detail/README.md:262 "@objectstack/spec` 17.1.0"

This came from this PR's own diff (+20/-1 to packages/plugin-detail/README.md), which added a hard-coded 17.1.0 version literal in prose.

Fix: deleted the literal and pointed at the truth instead, following the gate's own prescribed remedy and the house form from PR #3688 / PR #3698. Minimal, single-file diff:

 **A third producer writes that same key: the relationship itself.** A `lookup` /
-`master_detail` field may declare `relatedListFilter` (`@objectstack/spec` 17.1.0-— objectstack#8704 / PR #8955), a canonical Query-DSL `FilterCondition` such as+`master_detail` field may declare `relatedListFilter` (added to `@objectstack/spec`+in objectstack#8704 / PR #8955 — see this package's own `@objectstack/spec`+dependency in `package.json` for the version in use), a canonical Query-DSL+`FilterCondition` such as

Did not touch KNOWN_CLAIMS (the entry-required route was explicitly not the intended remedy here) and did not touch the changeset — its own 17.1.0 mention is in a changelog entry, not a scanned surface, and stays accurate as history.

Passing run (root-form vitest, this repo's guard refuses --filter here):

$ pnpm vitest run scripts/__tests__/doc-version-claims.test.ts
Test Files 1 passed (1)
Tests 25 passed (25)

All 25 tests in the file pass, including the previously-red one. git diff --stat confirms exactly one file changed (packages/plugin-detail/README.md, +4/-2) — nothing else in the PR's diff was touched.

Pushed sha: 8dbb79213d6f14df8bf2fd5f8b3939dce95bd77f on claude/issue-4664-relatedlistfilter-consumption.

PR left in draft, not enqueued, no auto-merge — unchanged per the dispatch constraints.

Generated by Claude Code


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3179.8 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-B0Pj123L.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.32KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)175.69KB48.80KB
fields (index.js)243.65KB61.63KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.30KB63.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-sam
os-sam marked this pull request as ready for review August 31, 2026 14:05
@os-sam
os-sam added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit d796c8dAug 31, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-4664-relatedlistfilter-consumption branch August 31, 2026 14:19
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationpackage: componentsplugintests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Auto-derived related lists consume the new field-level relatedListFilter declaration — list query AND-composed, tab badge honors the same filter

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(detail): derived related lists consume the FK's relatedListFilter — AND-composed query, badge counts the same set - #6946

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-4664-relatedlistfilter-consumption
Aug 31, 2026
Merged

feat(detail): derived related lists consume the FK's relatedListFilter — AND-composed query, badge counts the same set#6946
os-sam merged 2 commits into
mainfrom
claude/issue-4664-relatedlistfilter-consumption

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#4664

Auto-derived related lists now consume the field-level relatedListFilter
declaration: the list query is the declared filter AND-composed with
{ [referenceField]: parentId }, and the related-list tab badge counts the
same composed predicate
, so the badge and the rows answer one question.

Scope guard — condition 2, measured before any code

The card refuses to start unless the pinned @objectstack/spec actually
carries the key. Measured, not assumed:

readingvalue
manifest pinroot package.json"@objectstack/spec": "^17.0.0"
lockfile resolution17.2.0 (30 entries, one resolution)
installed packagenode_modules/.pnpm/@objectstack+spec@17.2.0_.../package.json17.2.0
key on the FIELD shaperelatedListFilter?: FilterCondition | undefined, beside relatedList / relatedListTitle / relatedListColumns
key namerelatedListFilter — exactly as the card assumed, no drift
shapeFilterConditionSchema.optional(), i.e. the canonical Query-DSL FilterCondition (same dialect as a query where)

The installed key's own describe() text is the contract this PR implements:

Declarative default filter for the detail-page related list: AND-composed
with the parent-relationship condition { [referenceField]: parentId } — an
authored constraint, never a user-editable suggestion. The related-list tab
badge count honors the same composed filter, so counts match the visible
rows.

Premise holds. No spec pin was touched.

What changed

Five source files, one value, one read site — no new dialect:

  1. app-shell/src/utils/deriveRelatedLists.ts — reads relatedListFilter off
    the FK onto the derived descriptor. A plain non-empty object is a
    declaration; an array, a string, null, a number, {} are silence. Not a
    tolerant reader: nothing is aliased, coerced or defaulted (AGENTS.md #0.1),
    the same declared-vs-absent discrimination relatedListTitle and
    relatedListColumns already do next to it.
  2. app-shell/src/views/RecordDetailView.tsx — forwards it into the page
    synthesizer's related shape.
  3. plugin-detail/src/synth/buildDefaultPageSchema.ts — emits it onto the
    record:related_list node's existingfilter key.

That key is where the reuse is honest and where it stops. filter has had a
read site since objectstack#7118: RelatedList lowers it through the repo's one
filter sink and ANDs it with the parent scope, wrapping both sources under a
single and rather than letting either replace the other. The derived producer
lands on the same key as the component-level authored filter and as a
dataSource binding's composed filter — three producers, one read site, one
lowering. Nothing was added to the list path; it already did the right thing
with a value it never received.

The badge half needed new code, because the tab strip's probe read only
objectName and relationshipField off the node it was badging:

  1. components/src/renderers/layout/containers.tsx — the page:tabs count
    probe now also reads the node's filter.
  2. components/src/hooks/related-count-store.ts — composes it with the parent
    scope through the samemergeFilterNodes sink the row query uses, so the
    two $filter values are one composition used twice rather than two
    implementations that agree by luck. Cache identity gains the scope, placed
    INSIDE the relField segment so invalidate's two structural reads on these
    keys (startsWith(object::), endsWith(::parentId)) keep working — a scope
    segment appended after the parent id would have orphaned the entry, and its
    failure mode is a badge that never refreshes after a write, with no error to
    notice.

With nothing declared, the synthesized node, the row query and the badge probe
are byte-identical to before ({ [referenceField]: parentId }, the untouched
MongoDB-style object — not a freshly lowered AST that means the same thing).

Consumption only. No authoring UI for the filter, as the card requires.

Verification

Every exit code below was captured before any pipe, and each gate is quoted by
the line the gate itself printed.

Proving the AND, not just the filter. The page test's fake backend
EVALUATES the composed predicate over a four-row fixture built so each conjunct
alone admits a row the other excludes:

rowparentstatusparent scopedeclared filterAND
Live Itemtv-1openyesyesyes
Archived Itemtv-1archivedyesnono
Other Parent Itemtv-2opennoyesno
Other Parent Archivedtv-2archivednonono

Parent scope alone answers 2, the declared filter alone answers 2, the
conjunction answers 1 — so dropping either conjunct changes the number the test
reads, in both directions. Those three numbers are asserted from the fixture
itself, so a later edit that flattens it into a single-condition fixture fails
loudly instead of quietly weakening every assertion above it. The evaluator
THROWS on any filter shape it does not recognise: a permissive evaluator would
answer "all rows" for a malformed filter, which is exactly what the
counter-probes would read as a pass.

Proving badge and list agree, in one test, with the unfiltered count
differing from the filtered one: the list renders Live Item and neither
Archived Item (excluded by the declared conjunct) nor Other Parent Item
(excluded by the parent conjunct), while the tab badge reads 1. The
parent-scoped unfiltered count is 2, so a badge ignoring the filter reads 2
there.

Ablation — direction predicted in writing before either run. These tests
resolve through the root vitest.config.mts alias table, which maps every
@object-ui/* specifier to packages/*/src; there is no dist/ seam for a
mutation to be lost behind and therefore no rebuild leg. Each leg proved the
mutation on disk (anchored count of the injected marker AND of the deleted text,
plus git hash-object differing from the HEAD blob) and proved the restore both
ways (git diff HEAD empty AND hash equal to the HEAD blob), under
trap ... EXIT INT TERM with absolute paths from git rev-parse --show-toplevel
and git checkout HEAD -- ABSPATH, never bare.

Leg A — drop the filter from the BADGE path alone (containers.tsx).
Predicted: the two badge legs red, the row leg green, the other three files
green. Observed, exactly:

× SUBJECT — the badge probe sends the SAME composed filter as the rows
AssertionError: expected { task_version: 'tv-1' } to deeply equal [ 'and', …(2) ]
× PARITY — the badge and the rendered rows agree, and 1 !== the unfiltered 2
AssertionError: expected '2' to be '1'
Test Files 1 failed | 3 passed (4)
Tests 2 failed | 25 passed (27)

The second line is the defect this card exists to prevent, reproduced at
fixture scale: the badge said 2 above a list showing 1.

Leg B — the converse: drop it from the LIST path alone
(record-related-list.tsx). Predicted: the row leg red, the badge-equality leg
red as the mirror image of A, parity red the other way round, the other three
files green. Observed, exactly:

× SUBJECT — the row query is the declared filter AND the parent scope
AssertionError: expected [ 'ci-live', 'ci-archived' ] to deeply equal [ 'ci-live' ]
× SUBJECT — the badge probe sends the SAME composed filter as the rows
AssertionError: expected [ 'and', …(2) ] to deeply equal { task_version: 'tv-1' }
× PARITY — the badge and the rendered rows agree, and 1 !== the unfiltered 2
Test Files 1 failed | 3 passed (4)
Tests 3 failed | 24 passed (27)

Gates, all at the final commit 729d29c6c:

  • Root-form vitest, before/after over a set derived by MEASUREMENT (every test
    file that reads one of the five changed modules, by grep, rather than by
    guess): before — 110 files / 1559 tests passed, at BASE 40c479af2 in a
    comparison worktree; after — 114 files / 1586 tests passed. Delta +4
    files / +27 tests, exactly the four test files this PR adds. No pre-existing
    file or test count moved. The reported file count (114) equals the derived
    list's own count (114) — the sanity check against a run that silently
    substitutes another package's suite.
  • type-check (hyphenated) for all three touched packages, script name echoed
    by pnpm as tsc --noEmit && tsc -p tsconfig.test.json, all three Done.
    The .test.ts files are genuinely inside those programs, not excluded:
    tsc -p tsconfig.test.json --listFiles names all four new test files
    (app-shell 2, components 1, plugin-detail 1).
  • pnpm exec eslint ., plain form (no --no-inline-config), whole repo:
    exit 0, ✖ 11554 problems (0 errors, 11554 warnings) — 0 errors; the
    warnings are the repo-wide pre-existing no-explicit-any population.
  • node scripts/check-changeset-presence.mjs: exit 0 —
    ✅ 9 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s).
  • node scripts/check-changeset-no-major.mjs: exit 0 —
    ✅ No changeset declares a 'major' bump. The changeset is minor on the
    three packages and states the new consumption in words.

Boundaries named rather than forced

  • The reference rail is deliberately NOT wired.record:reference_rail
    renders its own total-count badge from a fixed
    { [relationshipField]: parentId } query, so on a page whose FK declares a
    filter the Related tab badge and the rail card for the same child disagree.
    It is not fixable here: ReferenceRailEntrySchema is strictObject with no
    filter key, and the spec's own guidance on that shape says the rail
    "honours no per-entry filter" and that "record:related_list is the
    component whose filter is real; if the rail is ever granted one, this entry
    shape is where it gets declared and enforced." Granting it is an upstream
    spec decision, not this card. Filed separately.
  • Logical combinators do not survive the shared sink.relatedListFilter
    is typed FilterCondition, whose type explicitly declares $and / $or /
    $not — but convertFiltersToAST in @object-ui/core has no combinator
    branch: { $or: [...] } lowers to ['$or','=',[...]] (a leaf naming a field
    $or) and { $not: {...} } throws FilterOperatorError naming a nonsense
    operator. Pre-existing on every consumer of that sink since
    objectstack#7118; this key newly makes it reachable by an author following
    the spec's own type. Fixing it means changing the shared sink for every
    caller, which is its own card. Filed separately; not patched around here,
    because a renderer-side workaround is exactly the second de-facto contract
    #0.1 forbids.

⛔ Left in draft, not enqueued, no auto-merge — the PM lands it.

Generated by Claude Code


Generated by Claude Code

The spec key `relatedListFilter` (objectstack#8704 / PR #8955, shipped in
@objectstack/spec 17.1.0) declares a related list's own scope on the
relationship. This repo accepted it at every gate and consumed it nowhere.
- deriveRelatedLists reads it off the FK; RecordDetailView forwards it; the
page synthesizer emits it onto the record:related_list node's EXISTING
`filter` key, where RelatedList already ANDs it with
`{ [referenceField]: parentId }` (objectstack#7118). No new dialect.
- The tab badge counts the same composed predicate: page:tabs reads the node's
filter and the count store composes it with the parent scope through the same
mergeFilterNodes sink the row query uses. Counts cache per scope, so a
filtered and an unfiltered probe never badge each other.
With nothing declared, the node, the row query and the badge probe are
byte-identical to before. Consumption only; no authoring UI.
@claude

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
ContributorAuthor

The two boundaries named in the description are now filed, both unassigned and unlabelled for PM triage:

Neither is worked around in this PR. Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3179.8 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-B0Pj123L.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.32KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)175.69KB48.80KB
fields (index.js)243.65KB61.63KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.30KB63.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

…rose
The doc-version-claims ratchet (Test shard 4/4) flagged the hard-coded
version this PR's own diff added: a version literal in prose that nothing
in the repo re-verifies. Per the gate's own prescribed remedy (PR #3688 /
PR #3698's precedent), delete the literal and point at the truth instead —
this package's own `@objectstack/spec` dependency in package.json, which
the package manager keeps honest at install time.
Fixes the red `Test (shard 4/4)` check on PR #6946; no other files touched.
@os-samClaude

Copy link
Copy Markdown
Collaborator

CI fix for the red Test (shard 4/4) check.

Reproduced failure (at head 729d29c6c, before the fix):

FAIL scripts/__tests__/doc-version-claims.test.ts > doc version claims - the ratchet > records every version literal on the scanned surfaces
AssertionError: These doc surfaces state a version and nothing in this repository can tell whether it is still true:
- packages/plugin-detail/README.md:262 "@objectstack/spec` 17.1.0"

This came from this PR's own diff (+20/-1 to packages/plugin-detail/README.md), which added a hard-coded 17.1.0 version literal in prose.

Fix: deleted the literal and pointed at the truth instead, following the gate's own prescribed remedy and the house form from PR #3688 / PR #3698. Minimal, single-file diff:

 **A third producer writes that same key: the relationship itself.** A `lookup` /
-`master_detail` field may declare `relatedListFilter` (`@objectstack/spec` 17.1.0-— objectstack#8704 / PR #8955), a canonical Query-DSL `FilterCondition` such as+`master_detail` field may declare `relatedListFilter` (added to `@objectstack/spec`+in objectstack#8704 / PR #8955 — see this package's own `@objectstack/spec`+dependency in `package.json` for the version in use), a canonical Query-DSL+`FilterCondition` such as

Did not touch KNOWN_CLAIMS (the entry-required route was explicitly not the intended remedy here) and did not touch the changeset — its own 17.1.0 mention is in a changelog entry, not a scanned surface, and stays accurate as history.

Passing run (root-form vitest, this repo's guard refuses --filter here):

$ pnpm vitest run scripts/__tests__/doc-version-claims.test.ts
Test Files 1 passed (1)
Tests 25 passed (25)

All 25 tests in the file pass, including the previously-red one. git diff --stat confirms exactly one file changed (packages/plugin-detail/README.md, +4/-2) — nothing else in the PR's diff was touched.

Pushed sha: 8dbb79213d6f14df8bf2fd5f8b3939dce95bd77f on claude/issue-4664-relatedlistfilter-consumption.

PR left in draft, not enqueued, no auto-merge — unchanged per the dispatch constraints.

Generated by Claude Code


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3179.8 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-B0Pj123L.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.32KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)175.69KB48.80KB
fields (index.js)243.65KB61.63KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.30KB63.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-sam
os-sam marked this pull request as ready for review August 31, 2026 14:05
@os-sam
os-sam added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit d796c8dAug 31, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-4664-relatedlistfilter-consumption branch August 31, 2026 14:19
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationpackage: componentsplugintests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Auto-derived related lists consume the new field-level relatedListFilter declaration — list query AND-composed, tab badge honors the same filter

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(detail): derived related lists consume the FK's relatedListFilter — AND-composed query, badge counts the same set - #6946

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-4664-relatedlistfilter-consumption
Aug 31, 2026
Merged

feat(detail): derived related lists consume the FK's relatedListFilter — AND-composed query, badge counts the same set#6946
os-sam merged 2 commits into
mainfrom
claude/issue-4664-relatedlistfilter-consumption

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#4664

Auto-derived related lists now consume the field-level relatedListFilter
declaration: the list query is the declared filter AND-composed with
{ [referenceField]: parentId }, and the related-list tab badge counts the
same composed predicate
, so the badge and the rows answer one question.

Scope guard — condition 2, measured before any code

The card refuses to start unless the pinned @objectstack/spec actually
carries the key. Measured, not assumed:

readingvalue
manifest pinroot package.json"@objectstack/spec": "^17.0.0"
lockfile resolution17.2.0 (30 entries, one resolution)
installed packagenode_modules/.pnpm/@objectstack+spec@17.2.0_.../package.json17.2.0
key on the FIELD shaperelatedListFilter?: FilterCondition | undefined, beside relatedList / relatedListTitle / relatedListColumns
key namerelatedListFilter — exactly as the card assumed, no drift
shapeFilterConditionSchema.optional(), i.e. the canonical Query-DSL FilterCondition (same dialect as a query where)

The installed key's own describe() text is the contract this PR implements:

Declarative default filter for the detail-page related list: AND-composed
with the parent-relationship condition { [referenceField]: parentId } — an
authored constraint, never a user-editable suggestion. The related-list tab
badge count honors the same composed filter, so counts match the visible
rows.

Premise holds. No spec pin was touched.

What changed

Five source files, one value, one read site — no new dialect:

  1. app-shell/src/utils/deriveRelatedLists.ts — reads relatedListFilter off
    the FK onto the derived descriptor. A plain non-empty object is a
    declaration; an array, a string, null, a number, {} are silence. Not a
    tolerant reader: nothing is aliased, coerced or defaulted (AGENTS.md #0.1),
    the same declared-vs-absent discrimination relatedListTitle and
    relatedListColumns already do next to it.
  2. app-shell/src/views/RecordDetailView.tsx — forwards it into the page
    synthesizer's related shape.
  3. plugin-detail/src/synth/buildDefaultPageSchema.ts — emits it onto the
    record:related_list node's existingfilter key.

That key is where the reuse is honest and where it stops. filter has had a
read site since objectstack#7118: RelatedList lowers it through the repo's one
filter sink and ANDs it with the parent scope, wrapping both sources under a
single and rather than letting either replace the other. The derived producer
lands on the same key as the component-level authored filter and as a
dataSource binding's composed filter — three producers, one read site, one
lowering. Nothing was added to the list path; it already did the right thing
with a value it never received.

The badge half needed new code, because the tab strip's probe read only
objectName and relationshipField off the node it was badging:

  1. components/src/renderers/layout/containers.tsx — the page:tabs count
    probe now also reads the node's filter.
  2. components/src/hooks/related-count-store.ts — composes it with the parent
    scope through the samemergeFilterNodes sink the row query uses, so the
    two $filter values are one composition used twice rather than two
    implementations that agree by luck. Cache identity gains the scope, placed
    INSIDE the relField segment so invalidate's two structural reads on these
    keys (startsWith(object::), endsWith(::parentId)) keep working — a scope
    segment appended after the parent id would have orphaned the entry, and its
    failure mode is a badge that never refreshes after a write, with no error to
    notice.

With nothing declared, the synthesized node, the row query and the badge probe
are byte-identical to before ({ [referenceField]: parentId }, the untouched
MongoDB-style object — not a freshly lowered AST that means the same thing).

Consumption only. No authoring UI for the filter, as the card requires.

Verification

Every exit code below was captured before any pipe, and each gate is quoted by
the line the gate itself printed.

Proving the AND, not just the filter. The page test's fake backend
EVALUATES the composed predicate over a four-row fixture built so each conjunct
alone admits a row the other excludes:

rowparentstatusparent scopedeclared filterAND
Live Itemtv-1openyesyesyes
Archived Itemtv-1archivedyesnono
Other Parent Itemtv-2opennoyesno
Other Parent Archivedtv-2archivednonono

Parent scope alone answers 2, the declared filter alone answers 2, the
conjunction answers 1 — so dropping either conjunct changes the number the test
reads, in both directions. Those three numbers are asserted from the fixture
itself, so a later edit that flattens it into a single-condition fixture fails
loudly instead of quietly weakening every assertion above it. The evaluator
THROWS on any filter shape it does not recognise: a permissive evaluator would
answer "all rows" for a malformed filter, which is exactly what the
counter-probes would read as a pass.

Proving badge and list agree, in one test, with the unfiltered count
differing from the filtered one: the list renders Live Item and neither
Archived Item (excluded by the declared conjunct) nor Other Parent Item
(excluded by the parent conjunct), while the tab badge reads 1. The
parent-scoped unfiltered count is 2, so a badge ignoring the filter reads 2
there.

Ablation — direction predicted in writing before either run. These tests
resolve through the root vitest.config.mts alias table, which maps every
@object-ui/* specifier to packages/*/src; there is no dist/ seam for a
mutation to be lost behind and therefore no rebuild leg. Each leg proved the
mutation on disk (anchored count of the injected marker AND of the deleted text,
plus git hash-object differing from the HEAD blob) and proved the restore both
ways (git diff HEAD empty AND hash equal to the HEAD blob), under
trap ... EXIT INT TERM with absolute paths from git rev-parse --show-toplevel
and git checkout HEAD -- ABSPATH, never bare.

Leg A — drop the filter from the BADGE path alone (containers.tsx).
Predicted: the two badge legs red, the row leg green, the other three files
green. Observed, exactly:

× SUBJECT — the badge probe sends the SAME composed filter as the rows
AssertionError: expected { task_version: 'tv-1' } to deeply equal [ 'and', …(2) ]
× PARITY — the badge and the rendered rows agree, and 1 !== the unfiltered 2
AssertionError: expected '2' to be '1'
Test Files 1 failed | 3 passed (4)
Tests 2 failed | 25 passed (27)

The second line is the defect this card exists to prevent, reproduced at
fixture scale: the badge said 2 above a list showing 1.

Leg B — the converse: drop it from the LIST path alone
(record-related-list.tsx). Predicted: the row leg red, the badge-equality leg
red as the mirror image of A, parity red the other way round, the other three
files green. Observed, exactly:

× SUBJECT — the row query is the declared filter AND the parent scope
AssertionError: expected [ 'ci-live', 'ci-archived' ] to deeply equal [ 'ci-live' ]
× SUBJECT — the badge probe sends the SAME composed filter as the rows
AssertionError: expected [ 'and', …(2) ] to deeply equal { task_version: 'tv-1' }
× PARITY — the badge and the rendered rows agree, and 1 !== the unfiltered 2
Test Files 1 failed | 3 passed (4)
Tests 3 failed | 24 passed (27)

Gates, all at the final commit 729d29c6c:

  • Root-form vitest, before/after over a set derived by MEASUREMENT (every test
    file that reads one of the five changed modules, by grep, rather than by
    guess): before — 110 files / 1559 tests passed, at BASE 40c479af2 in a
    comparison worktree; after — 114 files / 1586 tests passed. Delta +4
    files / +27 tests, exactly the four test files this PR adds. No pre-existing
    file or test count moved. The reported file count (114) equals the derived
    list's own count (114) — the sanity check against a run that silently
    substitutes another package's suite.
  • type-check (hyphenated) for all three touched packages, script name echoed
    by pnpm as tsc --noEmit && tsc -p tsconfig.test.json, all three Done.
    The .test.ts files are genuinely inside those programs, not excluded:
    tsc -p tsconfig.test.json --listFiles names all four new test files
    (app-shell 2, components 1, plugin-detail 1).
  • pnpm exec eslint ., plain form (no --no-inline-config), whole repo:
    exit 0, ✖ 11554 problems (0 errors, 11554 warnings) — 0 errors; the
    warnings are the repo-wide pre-existing no-explicit-any population.
  • node scripts/check-changeset-presence.mjs: exit 0 —
    ✅ 9 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s).
  • node scripts/check-changeset-no-major.mjs: exit 0 —
    ✅ No changeset declares a 'major' bump. The changeset is minor on the
    three packages and states the new consumption in words.

Boundaries named rather than forced

  • The reference rail is deliberately NOT wired.record:reference_rail
    renders its own total-count badge from a fixed
    { [relationshipField]: parentId } query, so on a page whose FK declares a
    filter the Related tab badge and the rail card for the same child disagree.
    It is not fixable here: ReferenceRailEntrySchema is strictObject with no
    filter key, and the spec's own guidance on that shape says the rail
    "honours no per-entry filter" and that "record:related_list is the
    component whose filter is real; if the rail is ever granted one, this entry
    shape is where it gets declared and enforced." Granting it is an upstream
    spec decision, not this card. Filed separately.
  • Logical combinators do not survive the shared sink.relatedListFilter
    is typed FilterCondition, whose type explicitly declares $and / $or /
    $not — but convertFiltersToAST in @object-ui/core has no combinator
    branch: { $or: [...] } lowers to ['$or','=',[...]] (a leaf naming a field
    $or) and { $not: {...} } throws FilterOperatorError naming a nonsense
    operator. Pre-existing on every consumer of that sink since
    objectstack#7118; this key newly makes it reachable by an author following
    the spec's own type. Fixing it means changing the shared sink for every
    caller, which is its own card. Filed separately; not patched around here,
    because a renderer-side workaround is exactly the second de-facto contract
    #0.1 forbids.

⛔ Left in draft, not enqueued, no auto-merge — the PM lands it.

Generated by Claude Code


Generated by Claude Code

The spec key `relatedListFilter` (objectstack#8704 / PR #8955, shipped in
@objectstack/spec 17.1.0) declares a related list's own scope on the
relationship. This repo accepted it at every gate and consumed it nowhere.
- deriveRelatedLists reads it off the FK; RecordDetailView forwards it; the
page synthesizer emits it onto the record:related_list node's EXISTING
`filter` key, where RelatedList already ANDs it with
`{ [referenceField]: parentId }` (objectstack#7118). No new dialect.
- The tab badge counts the same composed predicate: page:tabs reads the node's
filter and the count store composes it with the parent scope through the same
mergeFilterNodes sink the row query uses. Counts cache per scope, so a
filtered and an unfiltered probe never badge each other.
With nothing declared, the node, the row query and the badge probe are
byte-identical to before. Consumption only; no authoring UI.
@claude

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
ContributorAuthor

The two boundaries named in the description are now filed, both unassigned and unlabelled for PM triage:

Neither is worked around in this PR. Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3179.8 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-B0Pj123L.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.32KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)175.69KB48.80KB
fields (index.js)243.65KB61.63KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.30KB63.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

…rose
The doc-version-claims ratchet (Test shard 4/4) flagged the hard-coded
version this PR's own diff added: a version literal in prose that nothing
in the repo re-verifies. Per the gate's own prescribed remedy (PR #3688 /
PR #3698's precedent), delete the literal and point at the truth instead —
this package's own `@objectstack/spec` dependency in package.json, which
the package manager keeps honest at install time.
Fixes the red `Test (shard 4/4)` check on PR #6946; no other files touched.
@os-samClaude

Copy link
Copy Markdown
Collaborator

CI fix for the red Test (shard 4/4) check.

Reproduced failure (at head 729d29c6c, before the fix):

FAIL scripts/__tests__/doc-version-claims.test.ts > doc version claims - the ratchet > records every version literal on the scanned surfaces
AssertionError: These doc surfaces state a version and nothing in this repository can tell whether it is still true:
- packages/plugin-detail/README.md:262 "@objectstack/spec` 17.1.0"

This came from this PR's own diff (+20/-1 to packages/plugin-detail/README.md), which added a hard-coded 17.1.0 version literal in prose.

Fix: deleted the literal and pointed at the truth instead, following the gate's own prescribed remedy and the house form from PR #3688 / PR #3698. Minimal, single-file diff:

 **A third producer writes that same key: the relationship itself.** A `lookup` /
-`master_detail` field may declare `relatedListFilter` (`@objectstack/spec` 17.1.0-— objectstack#8704 / PR #8955), a canonical Query-DSL `FilterCondition` such as+`master_detail` field may declare `relatedListFilter` (added to `@objectstack/spec`+in objectstack#8704 / PR #8955 — see this package's own `@objectstack/spec`+dependency in `package.json` for the version in use), a canonical Query-DSL+`FilterCondition` such as

Did not touch KNOWN_CLAIMS (the entry-required route was explicitly not the intended remedy here) and did not touch the changeset — its own 17.1.0 mention is in a changelog entry, not a scanned surface, and stays accurate as history.

Passing run (root-form vitest, this repo's guard refuses --filter here):

$ pnpm vitest run scripts/__tests__/doc-version-claims.test.ts
Test Files 1 passed (1)
Tests 25 passed (25)

All 25 tests in the file pass, including the previously-red one. git diff --stat confirms exactly one file changed (packages/plugin-detail/README.md, +4/-2) — nothing else in the PR's diff was touched.

Pushed sha: 8dbb79213d6f14df8bf2fd5f8b3939dce95bd77f on claude/issue-4664-relatedlistfilter-consumption.

PR left in draft, not enqueued, no auto-merge — unchanged per the dispatch constraints.

Generated by Claude Code


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3179.8 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-B0Pj123L.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.32KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)175.69KB48.80KB
fields (index.js)243.65KB61.63KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.30KB63.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-sam
os-sam marked this pull request as ready for review August 31, 2026 14:05
@os-sam
os-sam added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit d796c8dAug 31, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-4664-relatedlistfilter-consumption branch August 31, 2026 14:19
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationpackage: componentsplugintests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Auto-derived related lists consume the new field-level relatedListFilter declaration — list query AND-composed, tab badge honors the same filter

2 participants

@os-sam@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat(detail): derived related lists consume the FK's relatedListFilter — AND-composed query, badge counts the same set - #6946

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-4664-relatedlistfilter-consumption
Aug 31, 2026
Merged

feat(detail): derived related lists consume the FK's relatedListFilter — AND-composed query, badge counts the same set#6946
os-sam merged 2 commits into
mainfrom
claude/issue-4664-relatedlistfilter-consumption

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes#4664

Auto-derived related lists now consume the field-level relatedListFilter
declaration: the list query is the declared filter AND-composed with
{ [referenceField]: parentId }, and the related-list tab badge counts the
same composed predicate
, so the badge and the rows answer one question.

Scope guard — condition 2, measured before any code

The card refuses to start unless the pinned @objectstack/spec actually
carries the key. Measured, not assumed:

readingvalue
manifest pinroot package.json"@objectstack/spec": "^17.0.0"
lockfile resolution17.2.0 (30 entries, one resolution)
installed packagenode_modules/.pnpm/@objectstack+spec@17.2.0_.../package.json17.2.0
key on the FIELD shaperelatedListFilter?: FilterCondition | undefined, beside relatedList / relatedListTitle / relatedListColumns
key namerelatedListFilter — exactly as the card assumed, no drift
shapeFilterConditionSchema.optional(), i.e. the canonical Query-DSL FilterCondition (same dialect as a query where)

The installed key's own describe() text is the contract this PR implements:

Declarative default filter for the detail-page related list: AND-composed
with the parent-relationship condition { [referenceField]: parentId } — an
authored constraint, never a user-editable suggestion. The related-list tab
badge count honors the same composed filter, so counts match the visible
rows.

Premise holds. No spec pin was touched.

What changed

Five source files, one value, one read site — no new dialect:

  1. app-shell/src/utils/deriveRelatedLists.ts — reads relatedListFilter off
    the FK onto the derived descriptor. A plain non-empty object is a
    declaration; an array, a string, null, a number, {} are silence. Not a
    tolerant reader: nothing is aliased, coerced or defaulted (AGENTS.md #0.1),
    the same declared-vs-absent discrimination relatedListTitle and
    relatedListColumns already do next to it.
  2. app-shell/src/views/RecordDetailView.tsx — forwards it into the page
    synthesizer's related shape.
  3. plugin-detail/src/synth/buildDefaultPageSchema.ts — emits it onto the
    record:related_list node's existingfilter key.

That key is where the reuse is honest and where it stops. filter has had a
read site since objectstack#7118: RelatedList lowers it through the repo's one
filter sink and ANDs it with the parent scope, wrapping both sources under a
single and rather than letting either replace the other. The derived producer
lands on the same key as the component-level authored filter and as a
dataSource binding's composed filter — three producers, one read site, one
lowering. Nothing was added to the list path; it already did the right thing
with a value it never received.

The badge half needed new code, because the tab strip's probe read only
objectName and relationshipField off the node it was badging:

  1. components/src/renderers/layout/containers.tsx — the page:tabs count
    probe now also reads the node's filter.
  2. components/src/hooks/related-count-store.ts — composes it with the parent
    scope through the samemergeFilterNodes sink the row query uses, so the
    two $filter values are one composition used twice rather than two
    implementations that agree by luck. Cache identity gains the scope, placed
    INSIDE the relField segment so invalidate's two structural reads on these
    keys (startsWith(object::), endsWith(::parentId)) keep working — a scope
    segment appended after the parent id would have orphaned the entry, and its
    failure mode is a badge that never refreshes after a write, with no error to
    notice.

With nothing declared, the synthesized node, the row query and the badge probe
are byte-identical to before ({ [referenceField]: parentId }, the untouched
MongoDB-style object — not a freshly lowered AST that means the same thing).

Consumption only. No authoring UI for the filter, as the card requires.

Verification

Every exit code below was captured before any pipe, and each gate is quoted by
the line the gate itself printed.

Proving the AND, not just the filter. The page test's fake backend
EVALUATES the composed predicate over a four-row fixture built so each conjunct
alone admits a row the other excludes:

rowparentstatusparent scopedeclared filterAND
Live Itemtv-1openyesyesyes
Archived Itemtv-1archivedyesnono
Other Parent Itemtv-2opennoyesno
Other Parent Archivedtv-2archivednonono

Parent scope alone answers 2, the declared filter alone answers 2, the
conjunction answers 1 — so dropping either conjunct changes the number the test
reads, in both directions. Those three numbers are asserted from the fixture
itself, so a later edit that flattens it into a single-condition fixture fails
loudly instead of quietly weakening every assertion above it. The evaluator
THROWS on any filter shape it does not recognise: a permissive evaluator would
answer "all rows" for a malformed filter, which is exactly what the
counter-probes would read as a pass.

Proving badge and list agree, in one test, with the unfiltered count
differing from the filtered one: the list renders Live Item and neither
Archived Item (excluded by the declared conjunct) nor Other Parent Item
(excluded by the parent conjunct), while the tab badge reads 1. The
parent-scoped unfiltered count is 2, so a badge ignoring the filter reads 2
there.

Ablation — direction predicted in writing before either run. These tests
resolve through the root vitest.config.mts alias table, which maps every
@object-ui/* specifier to packages/*/src; there is no dist/ seam for a
mutation to be lost behind and therefore no rebuild leg. Each leg proved the
mutation on disk (anchored count of the injected marker AND of the deleted text,
plus git hash-object differing from the HEAD blob) and proved the restore both
ways (git diff HEAD empty AND hash equal to the HEAD blob), under
trap ... EXIT INT TERM with absolute paths from git rev-parse --show-toplevel
and git checkout HEAD -- ABSPATH, never bare.

Leg A — drop the filter from the BADGE path alone (containers.tsx).
Predicted: the two badge legs red, the row leg green, the other three files
green. Observed, exactly:

× SUBJECT — the badge probe sends the SAME composed filter as the rows
AssertionError: expected { task_version: 'tv-1' } to deeply equal [ 'and', …(2) ]
× PARITY — the badge and the rendered rows agree, and 1 !== the unfiltered 2
AssertionError: expected '2' to be '1'
Test Files 1 failed | 3 passed (4)
Tests 2 failed | 25 passed (27)

The second line is the defect this card exists to prevent, reproduced at
fixture scale: the badge said 2 above a list showing 1.

Leg B — the converse: drop it from the LIST path alone
(record-related-list.tsx). Predicted: the row leg red, the badge-equality leg
red as the mirror image of A, parity red the other way round, the other three
files green. Observed, exactly:

× SUBJECT — the row query is the declared filter AND the parent scope
AssertionError: expected [ 'ci-live', 'ci-archived' ] to deeply equal [ 'ci-live' ]
× SUBJECT — the badge probe sends the SAME composed filter as the rows
AssertionError: expected [ 'and', …(2) ] to deeply equal { task_version: 'tv-1' }
× PARITY — the badge and the rendered rows agree, and 1 !== the unfiltered 2
Test Files 1 failed | 3 passed (4)
Tests 3 failed | 24 passed (27)

Gates, all at the final commit 729d29c6c:

  • Root-form vitest, before/after over a set derived by MEASUREMENT (every test
    file that reads one of the five changed modules, by grep, rather than by
    guess): before — 110 files / 1559 tests passed, at BASE 40c479af2 in a
    comparison worktree; after — 114 files / 1586 tests passed. Delta +4
    files / +27 tests, exactly the four test files this PR adds. No pre-existing
    file or test count moved. The reported file count (114) equals the derived
    list's own count (114) — the sanity check against a run that silently
    substitutes another package's suite.
  • type-check (hyphenated) for all three touched packages, script name echoed
    by pnpm as tsc --noEmit && tsc -p tsconfig.test.json, all three Done.
    The .test.ts files are genuinely inside those programs, not excluded:
    tsc -p tsconfig.test.json --listFiles names all four new test files
    (app-shell 2, components 1, plugin-detail 1).
  • pnpm exec eslint ., plain form (no --no-inline-config), whole repo:
    exit 0, ✖ 11554 problems (0 errors, 11554 warnings) — 0 errors; the
    warnings are the repo-wide pre-existing no-explicit-any population.
  • node scripts/check-changeset-presence.mjs: exit 0 —
    ✅ 9 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s).
  • node scripts/check-changeset-no-major.mjs: exit 0 —
    ✅ No changeset declares a 'major' bump. The changeset is minor on the
    three packages and states the new consumption in words.

Boundaries named rather than forced

  • The reference rail is deliberately NOT wired.record:reference_rail
    renders its own total-count badge from a fixed
    { [relationshipField]: parentId } query, so on a page whose FK declares a
    filter the Related tab badge and the rail card for the same child disagree.
    It is not fixable here: ReferenceRailEntrySchema is strictObject with no
    filter key, and the spec's own guidance on that shape says the rail
    "honours no per-entry filter" and that "record:related_list is the
    component whose filter is real; if the rail is ever granted one, this entry
    shape is where it gets declared and enforced." Granting it is an upstream
    spec decision, not this card. Filed separately.
  • Logical combinators do not survive the shared sink.relatedListFilter
    is typed FilterCondition, whose type explicitly declares $and / $or /
    $not — but convertFiltersToAST in @object-ui/core has no combinator
    branch: { $or: [...] } lowers to ['$or','=',[...]] (a leaf naming a field
    $or) and { $not: {...} } throws FilterOperatorError naming a nonsense
    operator. Pre-existing on every consumer of that sink since
    objectstack#7118; this key newly makes it reachable by an author following
    the spec's own type. Fixing it means changing the shared sink for every
    caller, which is its own card. Filed separately; not patched around here,
    because a renderer-side workaround is exactly the second de-facto contract
    #0.1 forbids.

⛔ Left in draft, not enqueued, no auto-merge — the PM lands it.

Generated by Claude Code


Generated by Claude Code

The spec key `relatedListFilter` (objectstack#8704 / PR #8955, shipped in
@objectstack/spec 17.1.0) declares a related list's own scope on the
relationship. This repo accepted it at every gate and consumed it nowhere.
- deriveRelatedLists reads it off the FK; RecordDetailView forwards it; the
page synthesizer emits it onto the record:related_list node's EXISTING
`filter` key, where RelatedList already ANDs it with
`{ [referenceField]: parentId }` (objectstack#7118). No new dialect.
- The tab badge counts the same composed predicate: page:tabs reads the node's
filter and the count store composes it with the parent scope through the same
mergeFilterNodes sink the row query uses. Counts cache per scope, so a
filtered and an unfiltered probe never badge each other.
With nothing declared, the node, the row query and the badge probe are
byte-identical to before. Consumption only; no authoring UI.
@claude

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
ContributorAuthor

The two boundaries named in the description are now filed, both unassigned and unlabelled for PM triage:

Neither is worked around in this PR. Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3179.8 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-B0Pj123L.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.32KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)175.69KB48.80KB
fields (index.js)243.65KB61.63KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.30KB63.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

…rose
The doc-version-claims ratchet (Test shard 4/4) flagged the hard-coded
version this PR's own diff added: a version literal in prose that nothing
in the repo re-verifies. Per the gate's own prescribed remedy (PR #3688 /
PR #3698's precedent), delete the literal and point at the truth instead —
this package's own `@objectstack/spec` dependency in package.json, which
the package manager keeps honest at install time.
Fixes the red `Test (shard 4/4)` check on PR #6946; no other files touched.
@os-samClaude

Copy link
Copy Markdown
Collaborator

CI fix for the red Test (shard 4/4) check.

Reproduced failure (at head 729d29c6c, before the fix):

FAIL scripts/__tests__/doc-version-claims.test.ts > doc version claims - the ratchet > records every version literal on the scanned surfaces
AssertionError: These doc surfaces state a version and nothing in this repository can tell whether it is still true:
- packages/plugin-detail/README.md:262 "@objectstack/spec` 17.1.0"

This came from this PR's own diff (+20/-1 to packages/plugin-detail/README.md), which added a hard-coded 17.1.0 version literal in prose.

Fix: deleted the literal and pointed at the truth instead, following the gate's own prescribed remedy and the house form from PR #3688 / PR #3698. Minimal, single-file diff:

 **A third producer writes that same key: the relationship itself.** A `lookup` /
-`master_detail` field may declare `relatedListFilter` (`@objectstack/spec` 17.1.0-— objectstack#8704 / PR #8955), a canonical Query-DSL `FilterCondition` such as+`master_detail` field may declare `relatedListFilter` (added to `@objectstack/spec`+in objectstack#8704 / PR #8955 — see this package's own `@objectstack/spec`+dependency in `package.json` for the version in use), a canonical Query-DSL+`FilterCondition` such as

Did not touch KNOWN_CLAIMS (the entry-required route was explicitly not the intended remedy here) and did not touch the changeset — its own 17.1.0 mention is in a changelog entry, not a scanned surface, and stays accurate as history.

Passing run (root-form vitest, this repo's guard refuses --filter here):

$ pnpm vitest run scripts/__tests__/doc-version-claims.test.ts
Test Files 1 passed (1)
Tests 25 passed (25)

All 25 tests in the file pass, including the previously-red one. git diff --stat confirms exactly one file changed (packages/plugin-detail/README.md, +4/-2) — nothing else in the PR's diff was touched.

Pushed sha: 8dbb79213d6f14df8bf2fd5f8b3939dce95bd77f on claude/issue-4664-relatedlistfilter-consumption.

PR left in draft, not enqueued, no auto-merge — unchanged per the dispatch constraints.

Generated by Claude Code


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3179.8 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-B0Pj123L.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.32KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)175.69KB48.80KB
fields (index.js)243.65KB61.63KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)247.30KB63.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)202.08KB54.61KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.05KB8.37KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.79KB21.10KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-sam
os-sam marked this pull request as ready for review August 31, 2026 14:05
@os-sam
os-sam added this pull request to the merge queueAug 31, 2026
Merged via the queue into main with commit d796c8dAug 31, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-4664-relatedlistfilter-consumption branch August 31, 2026 14:19
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationpackage: componentsplugintests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Auto-derived related lists consume the new field-level relatedListFilter declaration — list query AND-composed, tab badge honors the same filter

2 participants

@os-sam@claude