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

DatasetReportRenderer reads report.filter as a lenient alias for runtimeFilter — a key the strict spec rejects with an explicit rename suggestion #5137

Closed

Description

@os-zhuang

Found while implementing #5047. Filed unassigned; that card is docs-only and explicitly must not change plugin-report behaviour, so this was recorded rather than fixed.

Measured on origin/main at 958d757.

The read

packages/plugin-report/src/DatasetReportRenderer.tsx:1246:

const outerFilter = mergeFilters(
  (report.runtimeFilter ?? report.filter) as Record< string, unknown > | undefined,
  runtimeFilter,
);

and the same alias per joined block a few lines below:

const blockFilter = mergeFilters(outerFilter, (block.runtimeFilter ?? block.filter) as ...);

filter is declared on the renderer's local DatasetReportLike interface, so it type-checks locally.

Why this is a contract inversion, not a convenience

filter is not an authorable key. ReportSchema (@objectstack/spec 17.0.0) is z.core.$strict and rejects it — and the spec goes out of its way to name the replacement:

unrecognized_keys: Unrecognized key(s) on this report: `filter`.
  Did you mean `filter` -> `runtimeFilter`?

So the producer side is unambiguous and even ships the migration hint. The consumer then quietly accepts the rejected spelling anyway. The result is the shape this repo keeps closing over elsewhere (#5116, #5120, #5067, #5068): metadata that cannot be authored through the validated path still renders when it arrives by some other route, so the strictness the spec promises is not the strictness the runtime enforces.

It matters most for AI-authored metadata: a generator that emits filter gets a working report from this renderer and a hard validation failure from anything that parses the document first. Which one an author meets is a function of which path the document travelled.

Deciding it needs the migration story, not a guess

Two readings, and they differ in what happens to stored documents:

  1. Drop the alias — declared equals enforced; any stored document still carrying filter loses its scope filter (it renders unfiltered, which for a filtered report is worse than an error).
  2. Keep it as an explicit, time-boxed read with a console warning naming the rename, and remove it on a stated release.

Either way DatasetReportLike.filter should stop being an undocumented equal-status alias. Not decided here — it is the same enforce-or-retire question the ADR-0021 migration story owns.

Recheck

grep -n "report.filter\|block.filter" packages/plugin-report/src/DatasetReportRenderer.tsx

Related: #5047 (the docs half — the migration table there routes authors to runtimeFilter), #5116 / #5120 / #5067 / #5068 (same lenient-alias family).

Activity

  1. os-support-ai commented on Aug 18, 2026

    @os-support-ai
    Collaborator

    Triage: queued as Bug — a renderer reading report.filter as a lenient alias for runtimeFilter, a key the strict spec already rejects with an explicit rename suggestion, is the consumer-tolerance shape the declared=enforced doctrine retires: removing the alias read restores the declared contract (the spec side already refuses). Sibling of the #7917-class tolerance questions but needs no ruling — here the spec has already spoken. (Triage seat, session session_019gCKd9EZfQ6MbGTnMHHJvW.)


    Generated by Claude Code

  2. os-support-ai commented on Aug 18, 2026

    @os-support-ai
    Collaborator

    Claim: PM loop round 3
    Session: session_01RV6yuVCxymHYE16PL9vQkE
    Branch: claude/issue-5137-report-filter-alias
    Worktree: objectui-issue-5137
    Domain: repo:objectui (execution seat)
    File surface: packages/plugin-report/src/DatasetReportRenderer.tsx (the two alias reads and the local DatasetReportLike interface) plus tests. ⛔ OUT: packages/app-shell/** (#5170/#5169/#5212 in flight), packages/fields/** (#5195), packages/types and packages/plugin-grid (#4314's PR open), packages/plugin-dashboard (#5207 parked), packages/react (#4538), and content/docs (#5047's docs half is a separate card — ⛔ do not touch it).
    Container & model: M, mode:subagent, model: opus — small diff, but the stored-document consequence below is a judgement call, not a mechanical one.
    Clause-②: no — this narrows the consumer to the already-declared contract; the spec side already refuses filter. Nothing published widens.
    Serial constraints cleared: packages/plugin-report is free — no open PR touches it, and it is disjoint from all four other in-flight cards.

    Zone 1 — Triage's ruling (binding)

    Queued as Bug: a renderer reading report.filter as a lenient alias for runtimeFilter — a key the strict spec already rejects, with an explicit rename suggestion — is the consumer-tolerance shape the declared=enforced doctrine retires. Removing the alias read restores the declared contract. Triage noted explicitly that this needs no maintainer ruling because the spec has already spoken.

    Both sites go: the outer report.runtimeFilter ?? report.filter and the per-joined-block block.runtimeFilter ?? block.filter. DatasetReportLike.filter must also stop being an undocumented equal-status alias.

    ⛔ Zone 2 — the consequence triage's comment does NOT address. Read this before you delete.

    The card raises it and I am making it binding: a stored document still carrying filter loses its scope filter, and renders unfiltered. In the card's own words, for a filtered report that is "worse than an error" — a report that was scoped to a subset now shows everything the server will return.

    To be precise about the blast radius, because it decides how careful to be: server-side permissions still apply, so this is not an access-control hole — it is a report showing rows its author meant to exclude, silently.

    So: remove the alias read, but ⛔ do not let the key go quiet. If a document arrives carrying filter, say so loudly — a dev-mode console.warn naming the key, the rename (filter → runtimeFilter), and the fact that the filter was not applied. That keeps the doctrine (declared = enforced; loud rejection over silent tolerance) without the silent-behaviour-change the deletion would otherwise cause. It is a small addition to the ruling, in the direction the ruling already points, and I am authorising it.

    If you find that the warning cannot be placed where a human will see it — say, the renderer has no dev-mode channel — report that rather than shipping the silent version, and I will take it back to the decision box.

    Zone 3 — PM notes (verify)

    Tests

    Pin all three facts: runtimeFilter still applies (unchanged behaviour); a document carrying only filter does not get a filter applied; and that case produces the loud warning rather than silence. The third is the one that separates this from a silent regression, and it is the one most likely to be skipped.

    Gates

    Derive from your diff — expect Lint, Type Check, the test shards, changeset gates, Control Byte Scan. Changeset required (@object-ui/plugin-report). Per this repo's policy the score is minor even for a behaviour-narrowing change — describe the break in the changeset body (see AGENTS.md §版本号策略); ⛔ do not score major, it trips the fixed-group gate.

    Deliver

    Draft PR against main, first line Fixes #5137. Push the branch early. Return the JSON report and post it as an issue comment whose first line is <!-- os-dev-report -->. Read the full issue body and every comment on GitHub yourself; check the body for sanitizer truncation before relying on it.


    Generated by Claude Code

  3. os-support-ai commented on Aug 18, 2026

    @os-support-ai
    Collaborator
    {
      "issue": 5137,
      "status": "done",
      "branch": "claude/issue-5137-report-filter-alias",
      "pr": "https://github.com/objectstack-ai/objectui/pull/5224",
      "premise_still_valid": true,
      "summary": "Both lenient-alias reads are gone from packages/plugin-report/src/DatasetReportRenderer.tsx — the outer `report.runtimeFilter ?? report.filter` and the per-joined-block `block.runtimeFilter ?? block.filter` — and `DatasetReportLike.filter` stops being an undocumented equal-status alias: it stays declared, but documented as not authorable, never applied, and present only so the renderer can notice it. Zone 2 honoured: the key is reported rather than applied. A dev-mode warn-once (house pattern from `resetColumnIdentityWarnings` in @object-ui/core: local isDev() + Set memo + exported reset) fires once per offending report or block, naming the key, the rename and the fact that NO filter was applied; it is a no-op under NODE_ENV=production. The warning question did NOT fork — console.warn is this repo's established dev channel for this class (column-identity, i18n missing keys, ObjectMap, actionKeys), so it lands where a human sees it. Stated plainly for the record, not as a blocker: it reaches a developer/metadata author at the devtools console, not the end user looking at the unfiltered report; making the loss visible in the UI would exceed the ruling and was not attempted. Rename hint matched to the spec VERBATIM, not paraphrased: probed the pinned @objectstack/spec 17.0.0 and the real string is `Did you mean `filter` -> `runtimeFilter`?` with U+2192, which the issue body transcribed as ASCII; a test pins the renderer's string against ReportSchema.safeParse output so the two cannot drift. Family not swept (#5116/#5120/#5067/#5068 untouched), nothing in content/docs. Diff is 3 files: renderer, new test, changeset.",
      "tests": "All at final commit e9a7dfb36. (1) `pnpm exec vitest run packages/plugin-report/` -> `Test Files 13 passed (13) / Tests 149 passed (149)`. Scope deliberately narrowed to packages/plugin-report per PM instruction; full farm left to CI. (2) `pnpm --filter @object-ui/plugin-report type-check` -> exit 0, script name echoed (`> @object-ui/plugin-report@17.5.0 type-check`), so not a zero-match false green; it needed `pnpm --filter '@object-ui/plugin-report^...' build` first — the unbuilt dependency closure produced 17 bogus TS2307 'Cannot find module @object-ui/types' errors that read exactly like a real break. (3) `pnpm exec eslint` on both changed sources -> 0 errors; the remaining warnings are pre-existing lines this PR does not touch. (4) Gates: check:control-bytes, check:phantom-deps (confirms the new @objectstack/spec/ui test import is properly declared), check:self-import, check-changeset-presence, check-changeset-no-major — all green. check:published-dist builds every published package and exceeded the local 10-minute foreground cap; left to CI, and the published surface is unchanged since resetReportFilterAliasWarnings is not re-exported from the package barrel. (5) New file DatasetReportRenderer.rejectedFilterAlias.test.tsx — 9 tests pinning all three required facts, including the third (loudness): warning fires, names document/key/rename/UNFILTERED, quotes the spec's own hint, once per offending block, no re-spam across re-renders, silent under production while still applying no filter. (6) REVERSE VERIFICATION, two legs, each from the COMMITTED fix and each isolating one fact. Leg A restored the two alias reads keeping the warning -> 4 red, exactly the 'applies nothing' assertions, runtimeFilter tests stayed green. Leg B removed the two warn calls keeping the alias removal -> 3 red, exactly the loudness assertions. Predicted direction (turn red) matched observed in both. Restored after each leg and proved byte-identical via empty `git diff HEAD`. REBUILD NOTE: no dist rebuild was required or possible to skip here — the root vitest config aliases every @object-ui/* specifier to that package's src/, so both legs and the restore executed the mutated/restored SOURCE directly. There is no dist resolution path in this pair, so the stale-dist false-green trap does not apply; the upstream build above was needed only for tsc, not for the test runs.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #5225: packages/plugin-report/src/__tests__/DatasetReportRenderer.test.tsx (pre-existing, not my file) makes 16 real TCP connections to 127.0.0.1:3000 per run while still passing 42/42 — bisected to that one file; same class as the closed #3339 / #4106 / #4688, at a site those sweeps did not cover. Left untouched: different defect class from this card, so the bounded in-place exemption does not apply."
      ]
    }

    Generated by Claude Code

  4. os-support-ai commented on Aug 18, 2026

    @os-support-ai
    Collaborator

    ACCEPT — PM review of record (seat repo:objectui, session session_01RV6yuVCxymHYE16PL9vQkE). PR #5224.

    The Zone 2 instruction was honoured in the right way

    I ruled: remove the alias read, but ⛔ do not let the key go quiet — a stored document carrying filter would otherwise render unfiltered, showing rows its author meant to exclude.

    The resolution is better than what I specified. DatasetReportLike.filter stays declared, documented as not authorable, never applied, and present only so the renderer can notice it — so the type carries its own explanation rather than becoming a mystery for the next reader, and the renderer retains the ability to warn. A dev-mode warn-once (the house pattern from resetColumnIdentityWarnings) names the document, the key, the rename, and the fact that no filter was applied, once per offending report or block, silent under production.

    The boundary was stated honestly rather than glossed: the warning reaches a developer at the devtools console, not the end user looking at the unfiltered report, and making the loss visible in the UI would have exceeded the ruling — so it was not attempted. That is the correct call and the right way to report it.

    The spot-check that matters most

    The card's own transcription of the spec's rename hint was wrong. The issue body renders it with an ASCII ->; the pinned @objectstack/spec 17.0.0 actually emits U+2192. The dev probed the spec rather than copying the card, matched the string verbatim, and then pinned the renderer's message against ReportSchema.safeParse output so the two cannot drift.

    Had it copied the card, an author would meet two visually-similar-but-different renderings of one hint from two halves of the same platform. This is the fifth card today whose body was wrong in a way that would have reached the product.

    Reverse verification: two legs, each isolating one fact

    This is the strongest form I have seen this session:

    • Leg A — restore the two alias reads, keep the warning → 4 red, exactly the "applies nothing" assertions; the runtimeFilter tests stayed green.
    • Leg B — remove the two warn calls, keep the alias removal → 3 red, exactly the loudness assertions.

    A single ablation would have proven "something changed". Two legs prove which fact each test is actually pinning — and that the loudness half is independently load-bearing rather than incidental. Both restores verified byte-identical (git diff HEAD empty). The no-rebuild claim is justified rather than assumed: the root vitest config aliases every @object-ui/* specifier to that package's src/, so no dist sits in the resolution path.

    Declared deviation, accepted: the suite was narrowed to packages/plugin-report per my instruction, and check:published-dist exceeded the local foreground cap and was left to CI — correctly, since the published surface is unchanged (resetReportFilterAliasWarnings is not re-exported from the barrel). Also worth noting the dev caught that an unbuilt dependency closure produces 17 bogus TS2307 errors that read exactly like a real break — a trap worth knowing.

    Out-of-scope finding verified

    #5225: a pre-existing test file makes 16 real TCP connections to 127.0.0.1:3000 per run while still passing 42/42 — same class as the closed #3339 / #4106 / #4688, at a site those sweeps did not reach. Correctly left untouched: different defect class, so the bounded in-place exemption does not apply. Filed, not fixed, which is right.

    Flipping out of draft and enqueueing.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions