Skip to content

Commit 4889e22

Browse files
docs(skills): the objectstack-ui report example declares its download in exportOptions under the base object's allowExport (#27318)
Fixes #26777 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:`skills/objectstack-ui/rules/dashboards.md` 〈Report Configuration〉的 `defineReport` 示例在 `dateRange` 下加一行 `exportOptions: { formats: ['csv', 'xlsx'] }`,行尾注释点名可选的 `includeHeaders` / `fileNamePrefix`;注释块加一句:`exportOptions` 只声明下载,谁能下载是基对象权限集上的 `allowExport`,`joined` 报表既不接受 `dateRange` 也不接受 `exportOptions`。token 棘轮用同文件折叠支付:删掉〈Server-side date bucketing〉已教过的 `dateGranularity` 一条和〈Drilldown〉已教过的 `drilldown` 一句;文件 6221 → 6214 tokens,上限 6221 不动。 - 为什么改:PR #26774 为卡 #26633 落地的 eval 要求报表声明 `exportOptions` 并以 `allowExport` 命名下载权限,而技能规则只教 `dateRange`;照技能写报表的 agent 没有下载声明的样例,可能自造 `permissions.export` 这类键或漏掉导出。改后 eval 若失败只会是 agent 的错,不再是教学的缺口。 - 风险与回滚:只动一个受管 markdown 规则文件和一个 patch changeset,不改 schema、键或运行时;折掉的两处文字在同文件另两节原样存在。回滚即 revert 本 PR。 - 先看:`skills/objectstack-ui/rules/dashboards.md` L419–421(注释块新句,措辞即契约)、L401(示例新行)、原 L406–409 与 L414–415 的删除(折叠是否真有第二处);`.changeset/26777-report-export-options-rule.md`。 - 两分钟验证:`node scripts/check-skills-token-ratchet.mjs`,预期打印 `skills/objectstack-ui/rules/dashboards.md is 6214 tokens (ceiling 6221; headroom 7)` 并以 0 退出。 - 需要你做的:批准本 PR(`skills/**` 受管面,Tier H;PR 保持 draft,由席位落地)。 ## What changes One governed rule file and one changeset. **`skills/objectstack-ui/rules/dashboards.md` 〈Report Configuration〉** — the report section the skill routes to (`SKILL.md` L148 points at this file for report config): - The `defineReport` example gains one line under `dateRange` (the diff below): `exportOptions` with `formats: ['csv', 'xlsx']`, and a trailing comment naming the optional `includeHeaders` / `fileNamePrefix`. Every key is read off `packages/spec/src/ui/report.zod.ts` on `origin/main`: the report's `exportOptions` is `ReportExportOptionsSchema.optional()` whose describe reads "Report export control: `{ formats?, includeHeaders?, fileNamePrefix? }`, csv / xlsx, on by default; who may export is the base object's export permission (refused on a joined report)"; `formats` admits exactly `'csv' | 'xlsx'` (the reverse leg under Tests shows `'pdf'` refused by the type). - The note block gains one sentence: `exportOptions` declares the download, never who may download — that is the base object's permission-set `allowExport`; a `joined` report takes neither `dateRange` nor `exportOptions`. It is the schema's own docblock shortened — "Who may export is NOT declared here — there is no report permission key. The control is offered to a viewer whose effective API operations on the dataset's base object include `export`: the `ObjectPermission.allowExport` axis" — plus the two joined-container refusals `JOINED_CONTAINER_DATE_RANGE_REFUSED` and `JOINED_CONTAINER_EXPORT_OPTIONS_REFUSED`. The example writes no `allowExport: true` anywhere: `allowExport` is named in words as a permission-set key, which is what the eval's `must_not_contain` guards. - Paid under the token ratchet by two folds of wording the same file already teaches, measured line by line (table below): - the `dateGranularity` bullet (old L406–409): its meaning lives in 〈Server-side date bucketing — `dateGranularity` (ADR-0021)〉 — the lead paragraph (the dataset dimension carries it; selecting it groups server-side; without it every timestamp becomes its own category) and the five-value bucket table — in the Level A table under 〈Dataset-Bound Widgets〉 (date-bucket `day/week/month/quarter/year` fits a dataset; a computed bucket escalates), and in the example's own comment two lines above the call (`close_date` with `dateGranularity: 'quarter'`); - the `drilldown` sentence (old L414–415): its meaning lives in 〈Drilldown〉 — "Reports drill the same way. A `summary` / `matrix` report (`drilldown` defaults `true`) opens the identical in-place drawer on row/cell click". - Not folded: the `rows` / `columns` axis sentence and the `order` nuances (`by` must be a selected name; list the `columns` key first) have no second home in the file beyond the example's inline comments, so they stay. One further candidate was left for a later shrink: the example's `order` comment ("by measure; dates default to chronological") restates the note's ASCENDING sentence. **`.changeset/26777-report-export-options-rule.md`**: `@objectstack/skills` patch — the published bundle is built from `skills/**` (`packages/skills` ships `dist`, its `build` is `sync-catalog.mjs`) — both declaration lines at line start, one sentence. Not touched, by the ruling: `scripts/check-skills-token-ratchet.mjs` (the `dashboards.md` row stays 6221 — the standing ruling 「A:先削重复,余额按实测提高」 has no locatable comment id, so no raise; the file now sits 7 tokens under its ceiling, so none is needed either) and `skills/objectstack-ui/evals/**` (the eval already expects this teaching). ### The edited region, before → after ```diff @@ -400,2 +400,3 @@ export const PipelineCoverageReport = defineReport({ dateRange: { dimension: 'close_date', defaultRange: 'this_year' }, + exportOptions: { formats: ['csv', 'xlsx'] }, // the download; optional `includeHeaders`, `fileNamePrefix` order: [{ by: 'amount_sum', direction: 'desc' }], // by measure; dates default to chronological @@ -405,6 +406,2 @@ -> **`dateGranularity`** lives on the dataset's date **dimension** -> (`day | week | month | quarter | year`); selecting that dimension buckets the -> field server-side in a single aggregate query — do **not** pre-compute virtual -> columns for this. > **`rows`** are the report's grouping dimensions (selected from the dataset by @@ -413,4 +410,3 @@ > put both axes in `rows`. Multi-level grouping on either axis = multiple -> dimension names in that array. `drilldown` (default `true`) makes cells -> click-through to the underlying records. +> dimension names in that array. > **`order`** sorts the result server-side — a list of `{ by, direction }`, most @@ -422,2 +418,5 @@ > report orders per block (`blocks[].order`), never on the container. +> **`exportOptions`** declares the download, never who may download — that is the +> base object's permission-set `allowExport`; a `joined` report takes neither +> `dateRange` nor `exportOptions`. ``` **Skills readings** (`skills/**` touched; before = `origin/main` a0aa8a1, the branch base; after = head; tokens in the ratchet's `ceil(utf8 bytes / 4)` convention): | Reading | Before | After | | --- | --- | --- | | `rules/dashboards.md` lines / bytes / tokens | 461 / 24881 / 6221 (ceiling 6221, headroom 0) | 460 / 24853 / 6214 (ceiling 6221, headroom 7) | | added lines | — | L401 108 B (27.00 tok) · L419 84 B (21.00) · L420 78 B (19.50) · L421 35 B (8.75) → 305 B, 76.25 tok | | folded lines | old L406 66 B (16.50 tok) · L407 80 B (20.00) · L408 83 B (20.75) · L409 20 B (5.00) · L414–415 tail 84 B (21.00) → 333 B, 83.25 tok | — | | net on the file | — | −28 B = −7 tokens; no re-wrap counted as payment | | `skills/objectstack-ui/**` lines | 2286 | 2285 | | all `SKILL.md` lines | 4615 | 4615 | | whole `skills/**` tree lines | 19268 | 19267 | | bundle total tokens (ratchet report) | 215826 | 215819 | ## Why The eval landed by PR #26774 for card #26633 (`skills/objectstack-ui/evals/report-period-export.json`) scores a report-authoring answer on `exportOptions` with `'csv'` / `'xlsx'` and on `allowExport` named in words, and refuses `permissions.export`, `canExport`, `allowExport: true` and `'pdf'`. On `origin/main` the rules teach `dateRange` and nothing about the download (`git grep -n "exportOptions\|allowExport" skills` → hits only under `evals/`), so an agent authoring from the skill had no example of the declaration and the eval could fail for a gap in the teaching rather than in the agent. The premise held on the branch base a0aa8a1: the same grep, the same zero hits under `rules/**`. ## Tests - `node scripts/check-skills-token-ratchet.mjs` — before: `skills/objectstack-ui/rules/dashboards.md is 6221 tokens (ceiling 6221; headroom 0)`; after: `… is 6214 tokens (ceiling 6221; headroom 7)`, `82 authored bundle file(s) within their ceilings`, exit 0 both runs. Its `--self-test` exit 0. - `pnpm --filter @objectstack/spec check:skill-examples` (the gate that type-checks every `os:check` block; it is `packages/spec`'s script, run by CI after the workspace build, and not in the `dispatch-gates --commands` list for this diff) — after building `@objectstack/spec` and the `@objectstack/client-react...` closure under `scripts/pm/os-verify-lock.sh` (VERDICT command-exit 0, held 73 s and 264 s): `✅ 270 prose examples type-check across 3 surface(s) — every marked block parsed, so tsc ran the SEMANTIC pass on all of them`, exit 0. So `formats: ['csv', 'xlsx']` as string literals is accepted by `defineReport`'s input type (hypothesis 4 holds). - Reverse leg, proving the gate reads this very block and not a cached reading — run after the commit, on the committed file, through `node scripts/ablation-replace.mjs --file skills/objectstack-ui/rules/dashboards.md --anchor "formats: ['csv', 'xlsx'] }" --replacement "formats: ['csv', 'pdf'] }" -- pnpm --filter @objectstack/spec check:skill-examples`: mutation landed on disk (anchor 1 → 0, blob b522363 → afe444cb), the gate went red with `error TS2322: Type '"pdf"' is not assignable to type '"csv" | "xlsx"'`, command exited 1; restored by the tool, then proven: the on-disk blob hash equals the HEAD blob (b522363), `git diff HEAD` empty, `git status` clean. Expected direction red; observed red. No permanent test file left. - No package source is touched, so no package `test` / `typecheck` is owed; no test file edited. The eval JSON is executed by no gate (the repo has no eval runner). ## Acceptance notes - Hypotheses from the dispatch, measured: (1) the schema block carries `exportOptions` with `formats` `'csv' | 'xlsx'`, `includeHeaders?`, `fileNamePrefix?`, and the joined refusals at `report.zod.ts` L187–207 — confirmed, quoted above; (2) `content/docs/references/ui/report.mdx` L71 and L116–122 teach the same keys — confirmed (a generated reference page; the rule text here is shorter); (3) the folds — confirmed for `dateGranularity` and `drilldown`; the `rows` / `columns` / `order` wording was not a restatement and stays; (4) the `os:check` compile — confirmed, with the reverse leg; (5) the derived gate list carried 43 families and did not name `check:pm-governed-prose` (the diff touches neither `AGENTS.md` nor the governed register), so it was not run. - `origin/main` moved from a0aa8a1 (the branch base) to d4427ae (one `ui/packages/components` test commit) while the branch was open; merged in as 94cb20a with no overlap on any touched surface, and the gate union below ran on that head. - The ratchet script's header says lowering a ceiling to a shrunken file's count is "always legitimate and encouraged"; the row was left at 6221 because the ruling section names the script as not touched. Noted for the seat. - Nothing found worth filing: no reproducible defect, no contract violation, no authoring trap met on this card. ## Verification - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at head `94cb20ad` (the final commit, derived from the merge base with no paths passed) printed 43 commands, identical to the derivation before the `origin/main` merge. All 43 ran sequentially in the worktree after that commit, each exit captured before any pipe (`bash -c "$cmd" >> log 2>&1; EXIT=$?`): 42 exit 0 — among them `node scripts/check-skills-token-ratchet.mjs` and its `--self-test`, `node scripts/check-empty-changeset.mjs --base origin/main`, `node scripts/check-adr-0087-registration.mjs --base origin/main`, `node scripts/check-changeset-no-major.mjs --base origin/main`, `node scripts/check-closing-keyword-parity.mjs`, `pnpm check:changeset-gate-self-tests`, `pnpm check:pm-governed-merges`, `pnpm check:published-files`, `pnpm check:skill-compatibility`, `pnpm check:skill-frame-sync`, `pnpm check:skill-identifier-liveness`, `pnpm check:doc-authoring`, `pnpm check:role-word`, `pnpm check:corpus-claim-drift`, `pnpm check:nul-bytes`, `pnpm check:pm-skill-id-lint`, `pnpm check:dts-closure`, `pnpm check:lean-entry-closure`; 1 exit 3 — `pnpm check:dual-build-cjs-loads`, PREREQUISITE NOT MET ("some package has no dist/": 33 packages, it asks for a whole-workspace `pnpm build`), NOT MEASURED locally; CI's `Lint & Repo Gates` runs it after its build. - `dispatch-gates --ran`: `✓ dispatch-gates --ran: 43 derived famil(ies) accounted for — 42 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)`, exit 0. - Local-only families: `pnpm check:dev-prereqs` read this box as not fully built (68 of 106 packages without `dist`; exit 1) — a state of the worktree, not of the diff, since only `@objectstack/spec` and the `client-react` closure were built here; `check:skill-frame-freshness` is not implicated (no four-axis framework file touched). - The wide-population families, the repo-wide `pnpm lint`, the type-check lanes and the path-scheduled jobs are CI's runs on this PR. - Head at the time of the union: `git rev-parse --short HEAD` = 94cb20a. ## Human Minimum Each item is one rule from [CONTRIBUTING.md › Human Minimum](https://github.com/objectstack-ai/objectstack/blob/main/CONTRIBUTING.md#human-minimum), which links the AGENTS.md text that states it. - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (No schema touched.) - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes (the repo-wide run is CI's; no source file edited). - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched: n/a, not touched. - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English. - [x] The *Before Submitting* list in CONTRIBUTING.md is done. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01QSeKYmhU5KEj1nukYhp1Tg --- _Generated by [Claude Code](https://claude.ai/code/session_01QSeKYmhU5KEj1nukYhp1Tg)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent b4eecd5 commit 4889e22

2 files changed

Lines changed: 15 additions & 6 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@objectstack/skills': patch
3+
---
4+
5+
docs(skills): the objectstack-ui report example declares its download in `exportOptions` under the base object's `allowExport`
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
`rules/dashboards.md` 〈Report Configuration〉 taught a report's `dateRange` and not its `exportOptions`: the example now declares `exportOptions: { formats: ['csv', 'xlsx'] }` (optional `includeHeaders` / `fileNamePrefix`) and the note says who may download is the base object's permission-set `allowExport` and that a `joined` report takes neither key, paid for by folding the `dateGranularity` and `drilldown` restatements that the same file already teaches under Server-side date bucketing and Drilldown.

‎skills/objectstack-ui/rules/dashboards.md‎

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -398,28 +398,27 @@ export const PipelineCoverageReport = defineReport({
398398
runtimeFilter: { stage: { $ne: 'closed_lost' } },
399399
// Period: ONE `type: 'date'` dataset dimension, not a dashboard-style raw `field`.
400400
dateRange: { dimension: 'close_date', defaultRange: 'this_year' },
401+
exportOptions: { formats: ['csv', 'xlsx'] }, // the download; optional `includeHeaders`, `fileNamePrefix`
401402
order: [{ by: 'amount_sum', direction: 'desc' }], // by measure; dates default to chronological
402403
chart: { type: 'bar', xAxis: 'forecast_category', yAxis: 'amount_sum' },
403404
});
404405
```
405406

406-
> **`dateGranularity`** lives on the dataset's date **dimension**
407-
> (`day | week | month | quarter | year`); selecting that dimension buckets the
408-
> field server-side in a single aggregate query — do **not** pre-compute virtual
409-
> columns for this.
410407
> **`rows`** are the report's grouping dimensions (selected from the dataset by
411408
> name). A `summary` groups *down* by `rows`. A `matrix` pivots `rows` (down) ×
412409
> **`columns`** (across, ADR-0021 D2) with `values` in the cells — do **not**
413410
> put both axes in `rows`. Multi-level grouping on either axis = multiple
414-
> dimension names in that array. `drilldown` (default `true`) makes cells
415-
> click-through to the underlying records.
411+
> dimension names in that array.
416412
> **`order`** sorts the result server-side — a list of `{ by, direction }`, most
417413
> significant key first. `by` must be a `rows`/`columns` dimension or a `values`
418414
> measure the report actually selects (anything else is an authoring error). A
419415
> selected date dimension already defaults to ASCENDING, so a month-bucketed
420416
> matrix reads left-to-right in time with no `order` at all; list the `columns`
421417
> key first when the across-axis header sequence is what matters. A `joined`
422418
> report orders per block (`blocks[].order`), never on the container.
419+
> **`exportOptions`** declares the download, never who may download — that is the
420+
> base object's permission-set `allowExport`; a `joined` report takes neither
421+
> `dateRange` nor `exportOptions`.
423422
424423
---
425424

0 commit comments

Comments
 (0)