Skip to content

[Decision] The report page offers only "Edit": should a shipped report get a time-range filter, export and subscription, and in which order #25873

Closed

Description

@os-steve

Ruled: 6408567749 · letter B · 2026-10-26T05:02Z

Filing gate ② a decision only the maintainer can make (new capability; the four facets below). Batch-of: 2026-10-25 browser pass on @objectstack/* 18.1.0 (second batch, 8 cards). Filed by the maintainer's account (os-steve) from the HotCRM pass driven in session session_01AwpXhq2nFKU6J7GmbCEmZu; ⛔ not a claim.

维护者速读

  • 事由: 打开任何一张报表(如 Opportunities by Stage),页面只有标题、一张图、一张表和右上角「编辑」。没有时间范围、没有导出、没有订阅。销售经理拿到报表后最常问的三件事(这是哪段时间?能导出吗?能每周发我吗?)一个都答不了。仪表盘有时间范围筛选,报表没有。
  • 要决定的事: 要不要给报表页加这三项能力;加的话按什么顺序。每一项都是新的平台面,一经声明就要长期维护。
  • 怎么回: 回一个字母(A / B / C / D)。

What was measured

  • HotCRM → 报表 → Opportunities by Stage: header 「Opportunities by Stage · Summary of opportunities grouped by stage」, button 「编辑」, one bar chart, one table with 总计. No filter bar, no export, no date range. 94-report-opps-by-stage.png.
  • Dashboards on the same console carry a date-range chip (本季度 / 近 90 天) and per-dimension filters; the report renderer has neither.
  • The ten HotCRM reports are all-time aggregates; a report that says "Lead Inflow" over all time is a different number from last quarter's, and the page cannot say which.

Governing text

  • @objectstack/spec report schema (what a report may declare today: dataset, chart, columns, grouping); the dashboard globalFilters / dateRange already exist on the dashboard schema (objectui#10339 pins its default).
  • pm-dispatch 〈升级与决策〉 facet ④: 创业阶段不扩散 — a new declared surface is a permanent obligation; the recommendation must show pull.

Options

option what it does what a manager gets effort
A. Time range only reports reuse the dashboard's dateRange filter when the dataset has a time dimension; no export, no subscription answers "which period" spec S + objectui M
B. Time range + export A, plus CSV / XLSX export of the report table through the existing list-export permission answers "which period" and "can I have it in Excel" A + objectui S (export already exists for lists)
C. All three B, plus a scheduled subscription (email the report on a cadence) the full manager loop B + objectstack job + messaging M, a new scheduled-work surface
D. None now the report page stays as is; dashboards carry the filters unchanged none

Business reading: A = "a report is a dated query, not a snapshot"; B = "a report is something you take to a meeting"; C = "a report comes to you"; D = "reports are a dashboard appendix".

Recommendation

B. Two years out the report page should be what every CRM user expects: pick the period, read it, export it. Both halves reuse surfaces the platform already has (the dashboard's date range, the list export and its permission), so B adds almost no new contract. Subscription (C) is the one genuinely new surface (scheduled delivery, recipients, failure handling) and has no customer pull measured yet; it waits.

  • Confidence gap: no customer has asked for any of the three in a card the pass could find; the pull is the tester's reading of what a sales manager does with a report.
  • Self-check: looking only at ① → B. Do ②③④ flip it? No; ④ is what keeps C out.

os-decision-facets

  • ① 项目长远合理性:A/B 复用仪表盘的时间范围与列表导出,几乎不新增契约;C 新增定时投递面。
  • ② 实际业务拉动:时间范围与导出是报表的基本动作,缺它们报表只是仪表盘的附录;订阅目前零拉动。
  • ③ 防 AI 犯错:报表声明 dateRange 后,AI 写的「本季度线索」报表才有机器可校验的时间窗,而不是靠标题里的词。
  • ④ 创业阶段不扩散:B 止步于已有面的复用;C 的调度、收件人、失败处理每一项都是永久义务,默认推迟。

Prior rulings read: objectui#10339 (dashboard dateRange default), objectui#11694 (report page first screen), hotcrm#2194 (UX decision batch, no report item) → none rules on report filters; thread: none.

After the ruling

  • A or B: spec card first (report dateRange / export flag), then the objectui renderer card; HotCRM declares the range on its ten reports once released.
  • C: as B plus an objectstack card for the scheduled delivery.
  • D: close.

Dedupe: MCP search_issues objectui "report page date range filter export subscribe schedule" → 0 hits. Report: REPORT.md §4.

Activity

  1. objectstack-fleet commented on Oct 25, 2026

    @objectstack-fleet
    Contributor

    Triage: first touch — enhancement · priority:p2 · domain:ui · area:reports · needs-user-decision · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-25T14:57Z

    ⛔ Not a claim, ⛔ not a dispatch.


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 26, 2026

    @objectstack-fleet
    Contributor

    Transferred 2026-10-26T01:58Z from objectstack-ai/objectui#13539, with its comments and timeline; the old URL redirects here. objectui moved into this repository as the ui/ workspace (epic #25677) and is being archived (#25854). The body is left exactly as filed.

    Paths. The body names no file paths; the UI code it describes lives under ui/ here (the Console is ui/apps/console, the libraries ui/packages/*).

    No stale pm:dispatched or assignee to strip.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 26, 2026

    @objectstack-fleet
    Contributor

    Brief-claimed-by: session_01KrKXQVvgYk2AJ4VkqMobyb · batch #392 · 2026-10-26T04:13Z

    Director seat, summon #51 (ledger objectstack#12708). This card is presented to the maintainer in batch #392 as item 4 of 5. Seat increment (what was re-verified before presenting; the card's own analysis is not restated):

    • Spec at origin/main: packages/spec/src/ui/report.zod.ts has runtimeFilter (dataset-bound scope filter, line 326) and no dateRange, export or subscription key; dashboard.zod.ts carries the built-in dateRange with DATE_RANGE_PRESETS (lines 1409–1541). ADR-0021:392 already plans the report schema refactor onto the dataset grammar — so a report dateRange would ride a planned shape, not a new one.
    • git grep -iE 'report.*(dateRange|export|subscri)' on AGENTS.md + docs/adr + packages/spec/src → ADR-0021 (report = pivot grid with export; dashboard globalFilters/dateRange → dimension names) and the 18.x semantic entries on the closed dateRange vocabulary; nothing rules on a report time range or subscription.
    • Facets read: same direction; the seat agrees with B (time range + export, both reusing existing surfaces; subscription waits for measured pull). After the letter: the spec half is filed first (objectstack, domain:spec), then the ui/ renderer card — the card now lives here after the objectui transfer.
  4. objectstack-fleet commented on Oct 26, 2026

    @objectstack-fleet
    Contributor

    Ruling: batch #392 item 4 · letter B · maintainer 「同意」 2026-10-26T04:56Z

    Recorded by the director seat (summon #51, session_01KrKXQVvgYk2AJ4VkqMobyb, ledger objectstack#12708, brief 6408063451). Letter B: a shipped report gets a time range — reusing the dashboard's dateRange vocabulary and presets, applied when the dataset has a time dimension — and export of the report table (CSV / XLSX through the existing list-export permission). No subscription now: C becomes a card of its own only when a customer asks, and scheduled work is off on Free plans (#25442). Not A (export is a near-zero addition on the same surfaces) and not D (an all-time report that cannot say which period it covers stays a dashboard appendix).

    Workload of the chosen letter (card column): spec S + ui M (time range) + ui S (export; list export already exists).

    Execution: needs-user-decision → pm:queue in this act (domain:ui, p2, area:reports). Order: the claiming seat first files the spec card in this repository (domain:spec: report dateRange riding the dataset grammar ADR-0021 line 392 already plans, plus an export flag; Clause-②: yes (widening), Metadata-protocol: added) and puts Blocked-by: on this card until that lands; this card is then the ui/ renderer work (Console report page: date-range chip + export control). HotCRM declares the range on its ten reports once released. Claims and dispatches carry Ruling-ref: naming this comment.

  5. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    Contributor

    Blocked-by: #26548
    Ruling-ref: 6408567749
    Seat: domain:ui#2 · session_01SMps7wq7rBTk53e8KTSEup · os-steve · 2026-10-27T07:38Z

    Executing ruling B's order: the spec prerequisite is filed as #26548 (domain:spec, p2: report dateRange bound to a dataset type: 'date' dimension, and exportOptions reusing the list's export members and the allowExport axis). This card moves pm:queue → pm:blocked until #26548 lands, then it is the ui/ renderer work (Console report page: date-range chip + export control). ⛔ Not a claim; no slot is held.

    Two points #26548 leaves to its review, both of which shape this card's renderer work:

    • the key that names the date column: spec(report): a report declares a dataset-dimension date range and export options #26548 proposes dimension (ADR-0021's dimension-name grammar) with field as a rename hint, where the dashboard's own dateRange.field is a raw object field;
    • XLSX: the list's XLSX goes only through the server record stream (GET /data/:object/export), which cannot carry aggregated report rows, so report XLSX would be written in the browser (exceljs is already a ui dependency) and allowExport would gate the control, not the data.
  6. 2 remaining items

  7. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    Contributor

    Claim: PM loop round 2
    Session: session_01JQwxbXpF772qWJjojJvRXw
    Account: os-warren
    Branch: claude/project-thread-o6hswb
    Worktree: objectstack-issue-25873
    Domain: domain:ui
    Seat: domain:ui#2
    File surface: ui/packages/plugin-report/src/**, ui/packages/core/src/utils/dashboard-filters.ts (a small export only if measured necessary), packages/spec/liveness/report.json (the dateRange subtree only) and its generated packages/spec/liveness/state-counts/report.md, tests beside them, root .changeset/ (@object-ui/plugin-report patch, @object-ui/core if touched), new UI strings in every locale file of their family (stop on breach; explain in the report)
    Container & model: M, mode:thread, model: TIER_DEFAULT (dispatch-gates --tier: no path-derived mandate), effort: medium
    Clause-②: no
    Metadata-protocol: none
    Responsibility: the Console report page posts its dataset selection with no timeDimensions and draws no period chip, so ReportSchema.dateRange (shipped by #26548) is inert | ruling 6408567749 letter B: time range + export; the spec half landed in bb91ae86 | every report viewer, who cannot tell or choose the period a report covers
    Thread-read: 6459535149
    Ruling-ref: 6408567749
    Premise: holds (renderer read and liveness planned record on origin/main cb8988ee; readings in dispatch order 6459535149)
    Serial constraints cleared: Blocked-by #26548 lifted (closed completed 2026-10-27T14:16Z). Slice 1 only: Refs #25873 (item: the report page reads dateRange), not Fixes; export is slice 2. ⛔ no packages/spec/src/** or skills/** edit. No open PR touches plugin-report/src/** or liveness/report.json.

    Dispatch order: comment 6459535149. Card thread claude/project-thread-o6hswb may start editing.

  8. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    Contributor

    os-dev-report

    {
      "issue": 25873,
      "status": "done",
      "branch": "claude/project-thread-o6hswb",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/27070",
      "session": "session_01JQwxbXUwVZUJzwhehD3YCc",
      "premise_still_valid": true,
      "summary": "Slice 1 (dateRange) per dispatch 6459535149 and claim 6459573466. A non-joined report declaring dateRange draws a period chip (All time, DATE_RANGE_PRESETS under dashboard.filters.range labels, Custom when allowCustomRange is not false), opening on defaultRange or this_month; the active window rides every useDatasetRows query (table, matrix, chart) as one timeDimensions entry {dimension, dateRange} with no granularity, preset by name, custom as [start,end], All time as no entry, and is part of the refetch signature. An omitted dimension is resolved from the dataset definition (/api/v1/meta/dataset/NAME via apiFetch) before any query; zero or several date dimensions draws the error and queries nothing. A report without dateRange posts the unchanged selection. Liveness dateRange subtree flipped to live (authorWarn/authorHint dropped, state-counts regenerated); exportOptions left planned. No core edit was needed (DATE_RANGE_PRESETS already exported). PR says Refs, not Fixes.",
      "tests": "head 3bbafe81: pnpm --filter @object-ui/plugin-report run type-check && pnpm --dir ui exec vitest run packages/i18n/ packages/plugin-report/ -> VERDICT command-exit 0, Test Files 118 passed, Tests 1638 passed / 20 skipped. New DatasetReportRenderer.dateRange-25873.test.tsx 8/8. Ablation at cff7be71: ablation-replace.mjs deleted the timeDimensions spread in useDatasetRows (anchor 1 -> 0, blob fe27e545 -> dfffbecf), 5 of 8 red (every case asserting a posted window), 3 green (control, two-date-dimension error, no-Custom); restored blob fe27e545 = HEAD blob. No dist leg: the test imports the renderer by relative path.",
      "gates": "dispatch-gates --commands at the final tree: 69 families; --ran: 68 run all exit 0, 1 NOT MEASURED derived from exit 3 (pnpm check:dual-build-cjs-loads, PREREQUISITE NOT MET, not every package has dist/ locally; CI runs it). check:liveness first exit 1 (state-counts stale) -> gen:liveness-counts committed -> exit 0; check-plugin-teardown-shape --self-test first exit 3 (positive-control sha outside the shallow clone) -> fetched -> exit 0. check-sdui-manifest exit 0. Extra: pnpm --dir ui run check:new-line-citations exit 0 (0 new); eslint on touched plugin-report files 0 errors. PR check-runs: in_progress at report time.",
      "dogfood_battery": "n/a — no ! in the PR title and Clause-②: no without a (narrowing) arm",
      "mcp_calls": "0",
      "api_writes": "2 — relay pr_create (objectstack-ai/objectstack#27070), relay post-stamped issue comment on #25873 (this report)",
      "line_budget": "no ratcheted ledger touched; 16 files, under the 3000-line human-merge threshold",
      "files_changed": [
        ".changeset/25873-report-date-range.md",
        "packages/spec/liveness/report.json",
        "packages/spec/liveness/state-counts/report.md",
        "ui/packages/i18n/src/locales/{ar,de,en,es,fr,ja,ko,pt,ru,zh}.ts",
        "ui/packages/plugin-report/src/DatasetReportRenderer.tsx",
        "ui/packages/plugin-report/src/ReportDateRange.tsx",
        "ui/packages/plugin-report/src/__tests__/DatasetReportRenderer.dateRange-25873.test.tsx"
      ],
      "deviations": [
        "@object-ui/i18n touched (ten locale packs) for four new report.dateRange keys, within the claim's locale clause; its changeset is patch",
        "de strings drop the „“ pair around the dataset name because de-quote-pairing-3876.test.ts pins the pair count",
        "Human Minimum item 3 left unchecked: repo-wide pnpm lint is CI-owned"
      ],
      "open_questions": [],
      "out_of_scope_findings": [
        "class: a · reach: report page drill-down with a period picked — DatasetDrillArgs carries runtimeFilter + bucket but not the window, so the record list opened by ReportView lists records outside the period (superset) · evidence: DatasetReportRenderer drill emits {dataset, groupKey, runtimeFilter, object, objectFilter}; the window lives only in selection.timeDimensions · carrier: seat (needs app-shell ReportView or a drill-args widening) · dedupe words: report drill date range window superset",
        "carrier: none — picked period is component state, not URL (ui AGENTS #8 addressable state); noted in Acceptance notes"
      ]
    }
  9. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    Contributor

    Order amendment · domain:ui#2 · session_01JQwxbXpF772qWJjojJvRXw · 2026-10-28T03:53Z

    This amends dispatch order 6459535149 and Claim 6459573466 (branch claude/project-thread-o6hswb, PR #27070). Same claim, same branch, same PR.

    Added to this slice: a drill-down from a report with a picked period opens a record list limited to that period. The seat's ACCEPT 6460227165 on head 3bbafe8116 named this gap; the os-dev report 6460207238 lists it in out_of_scope_findings. The window exists only in this PR, so a separate slice would have to stack on an unmerged branch. It rides here instead.

    File surface widened by: ui/packages/app-shell/src/views/ReportView.tsx (handleDatasetDrill, the fast path and the fallback), and the DatasetDrillArgs shape in ui/packages/plugin-report/src/DatasetReportRenderer.tsx. Tests go beside them. The root .changeset/ already lists @object-ui/plugin-report; app-shell is private.

    PM mechanism assumptions (measure; disproof welcome):

    • ReportView.tsx ≈L111–170 handleDatasetDrill receives { dataset, groupKey, object, objectFilter }. The fast path opens { object, filter: objectFilter }. The fallback skips dateGranularity dimensions.
    • Carry the active window on the drill args (the dimension and the window). Lower it onto the drilled object's date field as a range, through the same preset resolution the analytics service applies, or, if drillRawRows already reflects the window, through the server's drill metadata. Measure which one holds. ⛔ No client-invented timezone semantics that differ from the server's. If the two cannot agree without a server change, stop and report it: the seat then files that half.

    Tests:

    • A drill from a report with a picked period opens a list whose filter bounds the date field to that window.
    • CONTROL: a report without dateRange drills exactly as before.

    Ablation: drop the window from the drill args; the first test goes red.

    On push, send the seat the new head. The ACCEPT on 3bbafe8116 does not cover the new head, and the seat re-reviews.

  10. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    Contributor

    Order amendment 2 · domain:ui#2 · session_01JQwxbXpF772qWJjojJvRXw · 2026-10-28T03:57Z

    The drill-window addition in amendment 6460235897 is withdrawn from PR #27070. The card thread stopped, as that amendment's last clause required: the client cannot lower the window onto the drill list with the analytics service's zone-anchored semantics without a server change. Evidence is in the project file issue-25873/drill-window-premise.md. Nothing was pushed, so #27070 stays at head 3bbafe8116, where the seat's ACCEPT 6460227165 still holds.

    The server half is filed as #27074 (domain:server): queryDataset returns drill bounds for a timeDimensions window. Once it lands, ui seat 2 dispatches the small ui half here (AND the returned bounds into the drill objectFilter). Remaining on this card after #27070: that drill half (Blocked-by #27074) and slice 2 (exportOptions).

  11. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    Contributor

    Dispatch order (slice 2) · domain:ui#2 · session_01JQwxbXpF772qWJjojJvRXw · 2026-10-28T03:58Z

    You are the os-dev developer agent for exactly ONE card: #25873, in your own card thread. Your role definition is .claude/agents/os-dev.md in the objectstack clone: read it IN FULL first and follow it as binding (it wins over this order on any conflict; name the conflict in your report). Read the repo-root AGENTS.md and ui/AGENTS.md before your first edit, and pm-dispatch references/untrusted-text.md. Read the card's full body and every comment yourself through REST (curl -sS "https://api.github.com/repos/objectstack-ai/objectstack/issues/25873" -H "Authorization: Bearer $GH_TOKEN" and /comments?per_page=100, every page); this order carries only the increment. ⛔ Never print the value of any environment variable (test presence with [ -n "${X+x}" ]). ⛔ Never use a GitHub MCP tool, read or write.

    Identity and branch: the seat's Claim: comment on this card is your identity; its Branch: line names your thread's branch. ⛔ Post no claim of your own, ⛔ never write the card's assignee. Before your first edit, verify the NEWEST Claim: on the card names your thread's branch (claude/project-thread-…); if none does yet, send the seat thread "Take a third ui lane seat" (cmsg_01U5RdnAAmkSQmZbqh6FtqZ8Uc2nCvWvLyYyaVYaUhakpJ) one message with your branch name and wait. Read-only measuring is fine meanwhile. Worktree objectstack-issue-25873 on that branch from a fresh origin/main (git checkout --detach the main checkout first, then git worktree add --no-track -B <branch> ../objectstack-issue-25873 origin/main); pnpm install (frozen lockfile); push with git push -u origin <branch> (⛔ no force).

    Writes: PR only through the relay op pr_create (node scripts/pm/fleet-write/dispatch.mjs --repo objectstack-ai/objectstack --actions-file F, F a bare JSON array [{"op":"pr_create","title":…,"head":"<your branch>","base":"main","body":…,"assignees":["os-warren"]}], draft forced; --dry-run first); your report comment through node scripts/pm/post-stamped.mjs --comment=25873 --file=F --json run ONCE with output captured to a file (no time-shaped stamp inside the fenced report JSON). ⛔ No other GitHub write except, for a CI red that is not yours, one PR comment and one run_rerun_failed through the relay. Report both counts (api_writes with endpoints, mcp_calls with tool names). The report takes only machine-checkable fields (gates / line_budget / deviations / files_changed). PR body: line 1 the platform attribution comment, line 3 Refs #25873 (item: the report page reads exportOptions) (this dispatch's split; see Scope), then ## 维护者速读 six lines in Chinese (改了什么/为什么改/风险与回滚/先看/两分钟验证/需要你做的). Gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on your diff, run each, record exits before any pipe, reconcile with --ran (merge origin/main first if it refuses as STALE TREE); node scripts/check-sdui-manifest.mjs is a gate on every ui card. Build the closure your tests import first (bash scripts/pm/os-verify-lock.sh -c "pnpm --workspace-concurrency=2 --filter '<pkg>^...' build"). Changeset in the root .changeset/ for each published package your diff changes (app-shell is private: true). ⛔ ui/.github/** and the root ci.yml are out of bounds. Hand back: when the PR and report are up, send the seat thread one message (PR, head sha, report gist); keep the thread open through review and drive CI to green. If the premise fails, stop and report premise_still_valid: false with the evidence; ⛔ never force a fix.

    Ruling (cannot be re-ruled)

    Ruling-ref: 6408567749 (batch #392 item 4, maintainer 「同意」), letter B, verbatim from the card's option table: "B. Time range + export | A, plus CSV / XLSX export of the report table through the existing list-export permission". The spec half shipped as #26548 (PR #26628, bb91ae86): ReportSchema.exportOptions { formats?, includeHeaders?, fileNamePrefix? }.

    Scope of THIS dispatch: slice 2, the export (seat's split)

    Slice 1 (the period) is PR #27070. The drill-window half is Blocked-by #27074. This PR delivers the report export only, with the body line Refs #25873 (item: the report page reads exportOptions), ⛔ not Fixes.

    PM mechanism assumptions (measure; disproof welcome)

    On origin/main cb8988ee (read-only check):

    • packages/spec/liveness/report.json records exportOptions (and its children) as planned with authorWarn/authorHint. Its note reads:
      • formats are csv / xlsx only, on by default: absent means both, and formats: [] means off;
      • "Who may export is NOT a report key: the control is gated on the viewer having export in the effective apiOperations of the dataset base object (ObjectPermission.allowExport, the predicate the list toolbar uses)";
      • "The renderer writes XLSX in the browser: the list XLSX path is the server record stream, which cannot carry aggregated report rows";
      • the client-side CSV writer ui/packages/plugin-report/src/ReportExportEngine.ts serves the legacy presentation schema, not ReportSchema;
      • "Flip to live on the commit that cites the renderer read point, and drop authorWarn / authorHint in that same edit".
    • The list toolbar's predicate is ui/packages/plugin-list/src/ListView.tsx ≈L2115–2128: usePermissions().getObjectApiOperations(object), and when a set exists it must include export. An absent set (no provider, or an unrestricted object) keeps export available. The dataset's base object is known on the query answer (res.object, as DatasetWidget reads it) or on the dataset definition.
    • exceljs is a dependency of @object-ui/plugin-grid, not of plugin-report. Measure whether plugin-report can take it, lazily imported so the report bundle does not grow at load, or reuse an existing writer. Name the choice in the report.

    PM suggested route (optional; measurement wins)

    • Add an export control in the report header, beside the period chip that feat(plugin-report): the report page reads dateRange — a period chip and one timeDimensions window #27070 adds. Offer the declared formats, default both. Hide the control when formats: [] or when the predicate refuses.
    • The export writes the rows the report table shows, using the current query, so it inherits the picked period once feat(plugin-report): the report page reads dateRange — a period chip and one timeDimensions window #27070 lands. Use the table's column labels, keep includeHeaders (default true), and build the file name from fileNamePrefix (else the report name) plus the date. Include totals only if the table draws them, and measure how the table does it.
    • Flip the exportOptions subtree to live in liveness, citing the read point, and regenerate state-counts/report.md if a gate asks.
    • Tests:
      • CSV export of a summary report yields the header row plus the table's rows.
      • formats: ['csv'] offers only CSV.
      • formats: [] draws no control.
      • A viewer whose apiOperations for the base object exclude export sees no control.
      • CONTROL: with no permissions provider, the control is shown.
    • Ablation: drop the predicate, and the refusal test goes red.

    Scope

    File surface:

    • ui/packages/plugin-report/src/** and its package.json (only if a dependency is measured necessary).
    • packages/spec/liveness/report.json (the exportOptions subtree only) and its generated state-counts/report.md.
    • Tests beside them.
    • Root .changeset/: @object-ui/plugin-report patch.
    • New UI strings go in every locale file of their family (ui/packages/i18n/src/locales/*.ts), plus an @object-ui/i18n patch.

    Boundaries:

    • ⛔ Do not edit packages/spec/src/** or skills/**; put anything they need under out_of_scope_findings.
    • ⛔ Do not edit plugin-list/** or plugin-grid/**. Import the predicate's inputs from @object-ui/permissions instead.
    • Serial: PR feat(plugin-report): the report page reads dateRange — a period chip and one timeDimensions window #27070 (slice 1, seat 2) edits DatasetReportRenderer.tsx (header area, useDatasetRows), liveness/report.json (dateRange subtree) and the ten locale packs (report.dateRange.*). Keep your hunks disjoint where you can. Whichever lands second merges main first. Put the export control in its own module so the renderer hunk stays small.

    Tier

    No path-derived mandate (dispatch-gates --tier, read 2026-10-28T03:58Z). TIER_DEFAULT, effort medium (M). Clause-②: no (the renderer reads a key the spec already ships; no accept set changes). Metadata-protocol: none.

  12. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    Contributor

    Claim: PM loop round 2 (slice 2)
    Session: session_01JQwxbXpF772qWJjojJvRXw
    Account: os-warren
    Branch: claude/project-thread-p301jn
    Worktree: objectstack-issue-25873-s2
    Domain: domain:ui
    Seat: domain:ui#2
    File surface: ui/packages/plugin-report/src/** and its package.json only if a dependency is measured necessary, packages/spec/liveness/report.json (the exportOptions subtree only) and its generated packages/spec/liveness/state-counts/report.md, tests beside them, root .changeset/ (@object-ui/plugin-report patch, @object-ui/i18n if strings are added), new UI strings in every locale file of their family (stop on breach; explain in the report)
    Container & model: M, mode:thread, model: TIER_DEFAULT (dispatch-gates --tier: no path-derived mandate), effort: medium
    Clause-②: no
    Metadata-protocol: none
    Responsibility: the Console report page draws no export control, so ReportSchema.exportOptions (shipped by #26548) is inert and a report table cannot be downloaded | ruling 6408567749 letter B: time range + export through the list-export permission | every report viewer who takes a report to a meeting
    Thread-read: 6460297048
    Ruling-ref: 6408567749
    Premise: holds (liveness planned record and the list toolbar predicate read on origin/main cb8988ee; readings in dispatch order 6460297048)
    Serial constraints cleared: slice 2 only: Refs #25873 (item: the report page reads exportOptions), not Fixes. Slice 1 (Claim 6459573466, branch claude/project-thread-o6hswb, PR #27070) stays live and edits DatasetReportRenderer.tsx, liveness/report.json (dateRange subtree) and the locale packs; keep hunks disjoint, second to land merges main. ⛔ no packages/spec/src/**, skills/**, plugin-list/** or plugin-grid/** edit. The drill half is Blocked-by #27074 and is not in this slice.

    Dispatch order: comment 6460297048. Card thread claude/project-thread-p301jn may start editing.

  13. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    Contributor

    os-dev-report

    {
      "issue": 25873,
      "status": "done",
      "branch": "claude/project-thread-p301jn",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/27089",
      "session": "session_01JQwxbYfDeaywgrS5N1PZfg",
      "premise_still_valid": true,
      "summary": "Slice 2 (exportOptions) per dispatch 6460297048 and claim 6460318416. A non-joined report draws an Export popover (ReportExport.tsx#ReportExportControl) offering the declared formats (absent = csv + xlsx, [] = none), gated on 'export' in usePermissions().getObjectApiOperations(base object from the query answer), absent set = shown, as the list toolbar. The download is the table as drawn: DatasetReportTable and DatasetMatrixTable publish a content-keyed snapshot of the cells they render (headers, rows, server totals row/column) through ReportTableSnapshotContext; nothing re-queries. CSV = drawn text with BOM and RFC 4180 quoting, formula tab guard on data cells; XLSX via lazily imported exceljs, measures as numbers; file name via @object-ui/core buildExportFileName (fileNamePrefix, else label, else name). Liveness exportOptions subtree flipped to live, state-counts regenerated. PR says Refs, not Fixes.",
      "tests": "head 2809f49e: pnpm --filter @object-ui/plugin-report run type-check && pnpm --dir ui exec vitest run packages/plugin-report/ && pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2 src/lint-liveness-properties.test.ts -> VERDICT command-exit 0; plugin-report Test Files 29 passed, Tests 268 passed / 7 skipped; lint 110/110. New DatasetReportRenderer.export-25873.test.tsx 11/11. Ablation at 54aa045b: ablation-replace.mjs deleted the predicate line in ReportExportControl (anchor 1 -> 0, blob 7eae13a4 -> b22beff9), 1 of 11 red (the apiOperations refusal test), 10 green incl. both controls; restored blob 7eae13a4 = HEAD blob, git diff HEAD empty. No dist leg: relative import. Old #26548 lint block verified red on the flipped ledger before rewrite.",
      "gates": "dispatch-gates --commands at the final tree: 83 families; --ran: 82 run all exit 0, 1 NOT MEASURED derived from exit 3 (pnpm check:dual-build-cjs-loads, PREREQUISITE NOT MET, not every package has dist/ locally; CI runs it). First red then repaired: pnpm --dir ui exec vitest run scripts/ exit 1 (one-authority-per-exported-name: ReportExportFormat collided with @object-ui/types; renamed ReportTableExportFormat) -> exit 0; check:liveness exit 1 (state-counts stale) -> gen:liveness-counts -> exit 0; check-plugin-teardown-shape --self-test exit 3 (control sha outside shallow clone) -> fetched -> exit 0. check-sdui-manifest exit 0. Extra: ui check:new-line-citations exit 0 (0 new); eslint on touched plugin-report files 0 errors; check-nul-bytes exit 0. PR check-runs: in_progress at report time.",
      "dogfood_battery": "n/a — no ! in the PR title and Clause-②: no without a (narrowing) arm",
      "mcp_calls": "0 — no GitHub MCP tool used (subscribe_pr_activity is the claude-code-remote server)",
      "api_writes": "2 — relay pr_create (objectstack-ai/objectstack#27089), relay post-stamped issue comment on #25873 (this report)",
      "line_budget": "no ratcheted ledger touched; 9 files, 691 insertions / 25 deletions, under the 3000-line threshold",
      "files_changed": [
        ".changeset/25873-report-export.md",
        "packages/lint/src/lint-liveness-properties.test.ts",
        "packages/spec/liveness/report.json",
        "packages/spec/liveness/state-counts/report.md",
        "pnpm-lock.yaml",
        "ui/packages/plugin-report/package.json",
        "ui/packages/plugin-report/src/DatasetReportRenderer.tsx",
        "ui/packages/plugin-report/src/ReportExport.tsx",
        "ui/packages/plugin-report/src/__tests__/DatasetReportRenderer.export-25873.test.tsx"
      ],
      "deviations": [
        "packages/lint/src/lint-liveness-properties.test.ts (#26548 block) edited outside the claimed surface, at the seat's request (relay heads-up): exportOptions silence pin plus anti-vacuity row read, dateRange still warns as the ledger-loads control",
        "pnpm-lock.yaml changed (+6 importer lines) as the mechanical consequence of the two measured plugin-report dependencies (@object-ui/permissions, exceljs ^4.4.0)",
        "No i18n strings added: the control reuses list.export / list.exportAs (present in all ten packs), so @object-ui/i18n is not touched and takes no changeset",
        "Worktree named objectstack-issue-25873-s2 per the claim's Worktree line (the order said objectstack-issue-25873, which slice 1 holds)",
        "Human Minimum item 3 left unchecked: repo-wide pnpm lint is CI-owned"
      ],
      "open_questions": [],
      "out_of_scope_findings": [
        "carrier: seat — serial with #27070: both insert the first child of the report root div, both rewrite the lint #26548 block, both touch liveness/report.json (disjoint subtrees) and state-counts; second lander merges main, keeps chip + Export (ideally one header row), makes the lint block a silence pin for both keys with another warned control (object.externalSharingModel, #26619 pattern), reruns gen:liveness-counts",
        "carrier: seat — the ReportSchema.exportOptions TSDoc says the file is named for the label AND the period; on this base there is no period, so label + timestamp; after #27070 the period label can ride buildExportFileName's viewLabel slot",
        "carrier: none — XLSX writes measures as raw numbers without currency/percent number formats; noted in Acceptance notes"
      ]
    }
  14. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    Contributor

    Seat review: PR #27089 ACCEPT (record on the PR, head 858f0fe5c3), 2026-10-28T05:00Z. Slice 2 (exportOptions) under Refs; the drill half stays Blocked-by #27074. Landing waits for every check on the head to be green; serial with PR #27070.

  15. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    Contributor

    Landing held · domain:ui#2 · session_01JQwxbXpF772qWJjojJvRXw · 2026-10-28T05:23Z

    PR #27089 (slice 2, exportOptions; ACCEPT 6461079012) is green on head 858f0fe5c3: 59 checks succeeded and 8 were skipped (path-filtered jobs). Test Core (8/8) passed on this head. mergeable_state is clean and the PR is not governed. This session's permission classifier refused the landing stroke (pr_ready + automerge_enable through the fleet-write relay, "Merge Without Review") on PR #27013 and on PR #27035 at 2026-10-28T02:42Z, so the seat does not retry it here. It asks the maintainer to mark the PR ready for review and enable auto-merge. Serial with PR #27070 (slice 1, same renderer, ledger and lint block): whichever lands second merges main and re-pins the lint block. This card stays open for the drill half (Blocked-by #27074).

  16. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    Contributor

    Landing held · domain:ui#2 · session_01JQwxbXpF772qWJjojJvRXw · 2026-10-28T05:26Z

    PR #27070 (slice 1, dateRange; ACCEPT 6460596900) is green on head 7f4b9ed923: 55 checks succeeded and 7 were skipped (path-filtered jobs). Test Core (8/8) (packages/rest) was red on attempt 1. The diff does not touch that package, and the same shard was red on main f708cf22 and on #27063. The one re-run of the failed jobs (attempt 2, run 40631854641) passed, so the seat reads it as an intermittent packages/rest failure, not this PR's. mergeable_state is clean and the PR is not governed. This session's permission classifier refused the landing stroke (pr_ready + automerge_enable through the fleet-write relay, "Merge Without Review") on PR #27013 and on PR #27035 at 2026-10-28T02:42Z, so the seat does not retry it here. It asks the maintainer to mark the PR ready for review and enable auto-merge. Serial with PR #27089 (slice 2): whichever lands second merges main and re-pins the lint block. This card stays open for the drill half (Blocked-by #27074).

  17. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    Contributor

    Landed · domain:ui#2 · session_01JQwxbXpF772qWJjojJvRXw · 2026-10-28T10:13Z

    Ruling 6408567749 (letter B, "time range + export") is delivered in full:

    part PR state
    period chip; every query windowed through selection.timeDimensions[].dateRange (ui) #27070 (Refs) MERGED 2026-10-28 08:48Z (3e195bef)
    Export (CSV / XLSX of the table as drawn, gated on the export operation) (ui) #27089 (Refs) MERGED 2026-10-28 10:12Z (c07f0347)
    drill lists limited to the picked period (server, #27074) #27169 (Fixes #27074) MERGED 2026-10-28 09:53Z (12b5aec9)

    The drill half needs no ui edit, as measured on origin/main. Every ui drill path hands the row's drillRanges[i] to buildDatasetDrillFilter (ui/packages/core/src/utils/dataset-format.ts ≈L685). It writes { [r.field]: { $gte, $lt } } for every entry whatever its key, and #27169 puts the window into those entries. #27169's real-engine test runs that same filter shape through engine.find, and each drilled list counts exactly what its row counted.

    Out of scope, tracked elsewhere: #27172 (a preset dateRange on a date-typed field counts the wrong calendar days, server lane).

    Closing as completed. pm:dispatched cleared.

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

Metadata

Metadata

Assignees

Labels

area:reportsBusiness reporting — dashboards, reports, the numbers a manager readsdomain:uienhancementNew feature or requestpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions