Repository navigation
[Decision] The report page offers only "Edit": should a shipped report get a time-range filter, export and subscription, and in which order #25873
Description
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/specreport schema (what a report may declare today: dataset, chart, columns, grouping); the dashboardglobalFilters/dateRangealready 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
objectstack-fleet commentedon Oct 25, 2026
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.
dateRangeand the list-export permission. That answers the two questions a manager asks first, at spec S + objectui M+S.schedule/time_relativeflows for an installed app (OS_AUTOMATION_SCHEDULED_WORK_ENABLED is deployment-wide and off on cloud) #25442). It should be a card of its own when a customer asks.Generated by Claude Code
objectstack-fleet commentedon Oct 26, 2026
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 isui/apps/console, the librariesui/packages/*).No stale
pm:dispatchedor assignee to strip.Generated by Claude Code
objectstack-fleet commentedon Oct 26, 2026
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):
origin/main:packages/spec/src/ui/report.zod.tshasruntimeFilter(dataset-bound scope filter, line 326) and nodateRange, export or subscription key;dashboard.zod.tscarries the built-indateRangewithDATE_RANGE_PRESETS(lines 1409–1541). ADR-0021:392 already plans the report schema refactor onto the dataset grammar — so a reportdateRangewould 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.domain:spec), then theui/renderer card — the card now lives here after the objectui transfer.objectstack-fleet commentedon Oct 26, 2026
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'sdateRangevocabulary 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:queuein this act (domain:ui, p2,area:reports). Order: the claiming seat first files the spec card in this repository (domain:spec: reportdateRangeriding the dataset grammar ADR-0021 line 392 already plans, plus an export flag;Clause-②: yes (widening),Metadata-protocol: added) and putsBlocked-by:on this card until that lands; this card is then theui/renderer work (Console report page: date-range chip + export control). HotCRM declares the range on its ten reports once released. Claims and dispatches carryRuling-ref:naming this comment.objectstack-fleet commentedon Oct 27, 2026
Blocked-by: #26548
Ruling-ref: 6408567749
Seat:
domain:ui#2·session_01SMps7wq7rBTk53e8KTSEup·os-steve· 2026-10-27T07:38ZExecuting ruling B's order: the spec prerequisite is filed as #26548 (
domain:spec, p2: reportdateRangebound to a datasettype: 'date'dimension, andexportOptionsreusing the list's export members and theallowExportaxis). This card movespm:queue→pm:blockeduntil #26548 lands, then it is theui/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:
dimension(ADR-0021's dimension-name grammar) withfieldas a rename hint, where the dashboard's owndateRange.fieldis a raw object field;GET /data/:object/export), which cannot carry aggregated report rows, so report XLSX would be written in the browser (exceljsis already a ui dependency) andallowExportwould gate the control, not the data.2 remaining items
objectstack-fleet commentedon Oct 28, 2026
Claim: PM loop round 2
Session:
session_01JQwxbXpF772qWJjojJvRXwAccount:
os-warrenBranch:
claude/project-thread-o6hswbWorktree:
objectstack-issue-25873Domain:
domain:uiSeat:
domain:ui#2File 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(thedateRangesubtree only) and its generatedpackages/spec/liveness/state-counts/report.md, tests beside them, root.changeset/(@object-ui/plugin-reportpatch,@object-ui/coreif 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: mediumClause-②: 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 coversThread-read: 6459535149
Ruling-ref: 6408567749
Premise: holds (renderer read and liveness
plannedrecord onorigin/maincb8988ee; 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), notFixes; export is slice 2. ⛔ nopackages/spec/src/**orskills/**edit. No open PR touchesplugin-report/src/**orliveness/report.json.Dispatch order: comment 6459535149. Card thread
claude/project-thread-o6hswbmay start editing.objectstack-fleet commentedon Oct 28, 2026
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" ] }objectstack-fleet commentedon Oct 28, 2026
Order amendment ·
domain:ui#2·session_01JQwxbXpF772qWJjojJvRXw· 2026-10-28T03:53ZThis 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
3bbafe8116named this gap; the os-dev report 6460207238 lists it inout_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 theDatasetDrillArgsshape inui/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–170handleDatasetDrillreceives{ dataset, groupKey, object, objectFilter }. The fast path opens{ object, filter: objectFilter }. The fallback skipsdateGranularitydimensions.drillRawRowsalready 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:
dateRangedrills 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
3bbafe8116does not cover the new head, and the seat re-reviews.objectstack-fleet commentedon Oct 28, 2026
Order amendment 2 ·
domain:ui#2·session_01JQwxbXpF772qWJjojJvRXw· 2026-10-28T03:57ZThe 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 head3bbafe8116, where the seat's ACCEPT 6460227165 still holds.The server half is filed as #27074 (
domain:server):queryDatasetreturns drill bounds for atimeDimensionswindow. Once it lands, ui seat 2 dispatches the small ui half here (AND the returned bounds into the drillobjectFilter). Remaining on this card after #27070: that drill half (Blocked-by #27074) and slice 2 (exportOptions).objectstack-fleet commentedon Oct 28, 2026
Dispatch order (slice 2) ·
domain:ui#2·session_01JQwxbXpF772qWJjojJvRXw· 2026-10-28T03:58ZYou are the os-dev developer agent for exactly ONE card: #25873, in your own card thread. Your role definition is
.claude/agents/os-dev.mdin 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-rootAGENTS.mdandui/AGENTS.mdbefore your first edit, and pm-dispatchreferences/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; itsBranch:line names your thread's branch. ⛔ Post no claim of your own, ⛔ never write the card's assignee. Before your first edit, verify the NEWESTClaim: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. Worktreeobjectstack-issue-25873on that branch from a freshorigin/main(git checkout --detachthe main checkout first, thengit worktree add --no-track -B <branch> ../objectstack-issue-25873 origin/main);pnpm install(frozen lockfile); push withgit 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-runfirst); your report comment throughnode scripts/pm/post-stamped.mjs --comment=25873 --file=F --jsonrun 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 onerun_rerun_failedthrough the relay. Report both counts (api_writeswith endpoints,mcp_callswith tool names). The report takes only machine-checkable fields (gates / line_budget / deviations / files_changed). PR body: line 1 the platform attribution comment, line 3Refs #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/objectstackon your diff, run each, record exits before any pipe, reconcile with--ran(mergeorigin/mainfirst if it refuses as STALE TREE);node scripts/check-sdui-manifest.mjsis 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 isprivate: true). ⛔ui/.github/**and the rootci.ymlare 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 reportpremise_still_valid: falsewith 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), ⛔ notFixes.PM mechanism assumptions (measure; disproof welcome)
On
origin/maincb8988ee(read-only check):packages/spec/liveness/report.jsonrecordsexportOptions(and its children) asplannedwithauthorWarn/authorHint. Its note reads:csv/xlsxonly, on by default: absent means both, andformats: []means off;exportin the effective apiOperations of the dataset base object (ObjectPermission.allowExport, the predicate the list toolbar uses)";ui/packages/plugin-report/src/ReportExportEngine.tsserves the legacy presentation schema, notReportSchema;liveon the commit that cites the renderer read point, and dropauthorWarn/authorHintin that same edit".ui/packages/plugin-list/src/ListView.tsx≈L2115–2128:usePermissions().getObjectApiOperations(object), and when a set exists it must includeexport. 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, asDatasetWidgetreads it) or on the dataset definition.exceljsis a dependency of@object-ui/plugin-grid, not ofplugin-report. Measure whetherplugin-reportcan 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)
formats: []or when the predicate refuses.includeHeaders(default true), and build the file name fromfileNamePrefix(else the report name) plus the date. Include totals only if the table draws them, and measure how the table does it.exportOptionssubtree tolivein liveness, citing the read point, and regeneratestate-counts/report.mdif a gate asks.formats: ['csv']offers only CSV.formats: []draws no control.apiOperationsfor the base object excludeexportsees no control.Scope
File surface:
ui/packages/plugin-report/src/**and itspackage.json(only if a dependency is measured necessary).packages/spec/liveness/report.json(theexportOptionssubtree only) and its generatedstate-counts/report.md..changeset/:@object-ui/plugin-reportpatch.ui/packages/i18n/src/locales/*.ts), plus an@object-ui/i18npatch.Boundaries:
packages/spec/src/**orskills/**; put anything they need underout_of_scope_findings.plugin-list/**orplugin-grid/**. Import the predicate's inputs from@object-ui/permissionsinstead.DatasetReportRenderer.tsx(header area,useDatasetRows),liveness/report.json(dateRangesubtree) and the ten locale packs (report.dateRange.*). Keep your hunks disjoint where you can. Whichever lands second mergesmainfirst. 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, effortmedium(M).Clause-②: no(the renderer reads a key the spec already ships; no accept set changes).Metadata-protocol: none.objectstack-fleet commentedon Oct 28, 2026
Claim: PM loop round 2 (slice 2)
Session:
session_01JQwxbXpF772qWJjojJvRXwAccount:
os-warrenBranch:
claude/project-thread-p301jnWorktree:
objectstack-issue-25873-s2Domain:
domain:uiSeat:
domain:ui#2File surface:
ui/packages/plugin-report/src/**and itspackage.jsononly if a dependency is measured necessary,packages/spec/liveness/report.json(theexportOptionssubtree only) and its generatedpackages/spec/liveness/state-counts/report.md, tests beside them, root.changeset/(@object-ui/plugin-reportpatch,@object-ui/i18nif 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: mediumClause-②: 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 meetingThread-read: 6460297048
Ruling-ref: 6408567749
Premise: holds (liveness
plannedrecord and the list toolbar predicate read onorigin/maincb8988ee; readings in dispatch order 6460297048)Serial constraints cleared: slice 2 only:
Refs #25873 (item: the report page reads exportOptions), notFixes. Slice 1 (Claim 6459573466, branchclaude/project-thread-o6hswb, PR #27070) stays live and editsDatasetReportRenderer.tsx,liveness/report.json(dateRangesubtree) and the locale packs; keep hunks disjoint, second to land mergesmain. ⛔ nopackages/spec/src/**,skills/**,plugin-list/**orplugin-grid/**edit. The drill half is Blocked-by #27074 and is not in this slice.Dispatch order: comment 6460297048. Card thread
claude/project-thread-p301jnmay start editing.objectstack-fleet commentedon Oct 28, 2026
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" ] }objectstack-fleet commentedon Oct 28, 2026
Seat review: PR #27089 ACCEPT (record on the PR, head
858f0fe5c3), 2026-10-28T05:00Z. Slice 2 (exportOptions) underRefs; the drill half stays Blocked-by #27074. Landing waits for every check on the head to be green; serial with PR #27070.objectstack-fleet commentedon Oct 28, 2026
Landing held ·
domain:ui#2·session_01JQwxbXpF772qWJjojJvRXw· 2026-10-28T05:23ZPR #27089 (slice 2,
exportOptions; ACCEPT 6461079012) is green on head858f0fe5c3: 59 checks succeeded and 8 were skipped (path-filtered jobs). Test Core (8/8) passed on this head.mergeable_stateis clean and the PR is not governed. This session's permission classifier refused the landing stroke (pr_ready+automerge_enablethrough 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 mergesmainand re-pins the lint block. This card stays open for the drill half (Blocked-by #27074).objectstack-fleet commentedon Oct 28, 2026
Landing held ·
domain:ui#2·session_01JQwxbXpF772qWJjojJvRXw· 2026-10-28T05:26ZPR #27070 (slice 1,
dateRange; ACCEPT 6460596900) is green on head7f4b9ed923: 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 onmainf708cf22and on #27063. The one re-run of the failed jobs (attempt 2, run 40631854641) passed, so the seat reads it as an intermittentpackages/restfailure, not this PR's.mergeable_stateis clean and the PR is not governed. This session's permission classifier refused the landing stroke (pr_ready+automerge_enablethrough 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 mergesmainand re-pins the lint block. This card stays open for the drill half (Blocked-by #27074).objectstack-fleet commentedon Oct 28, 2026
Landed ·
domain:ui#2·session_01JQwxbXpF772qWJjojJvRXw· 2026-10-28T10:13ZRuling 6408567749 (letter B, "time range + export") is delivered in full:
selection.timeDimensions[].dateRange(ui)Refs)3e195bef)exportoperation) (ui)Refs)c07f0347)Fixes #27074)12b5aec9)The drill half needs no ui edit, as measured on
origin/main. Every ui drill path hands the row'sdrillRanges[i]tobuildDatasetDrillFilter(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 throughengine.find, and each drilled list counts exactly what its row counted.Out of scope, tracked elsewhere: #27172 (a preset
dateRangeon adate-typed field counts the wrong calendar days, server lane).Closing as completed.
pm:dispatchedcleared.