From 8af37ee5bcc544739d6c64666561d6e34fd1d438 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 02:25:27 +0000 Subject: [PATCH 1/2] docs(skills): teach the report-index navigation item beside the named report A `type: 'report'` item with no `reportName` opens the app's report index (`/reports`), and the console no longer adds an index entry itself (PR #27566), so an app has one only where its navigation declares it. The objectstack-ui navigation table and example, the objectui app-composition route table, and a new objectstack-ui eval case now teach that form. Fixes #27609 Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_016WV4cyZb8pD9989yV2qXKE --- .changeset/27609-skills-report-index-nav.md | 12 ++++++++++++ scripts/check-skills-token-ratchet.mjs | 9 +++++++-- skills/objectstack-ui/SKILL.md | 2 +- .../evals/views-apps-actions-pages.json | 10 ++++++++++ skills/objectstack-ui/rules/navigation.md | 6 +++--- skills/objectui/guides/app-composition.md | 4 ++-- 6 files changed, 35 insertions(+), 8 deletions(-) create mode 100644 .changeset/27609-skills-report-index-nav.md diff --git a/.changeset/27609-skills-report-index-nav.md b/.changeset/27609-skills-report-index-nav.md new file mode 100644 index 00000000000..0432d281492 --- /dev/null +++ b/.changeset/27609-skills-report-index-nav.md @@ -0,0 +1,12 @@ +--- +'@objectstack/skills': patch +--- + +docs(skills): objectstack-ui and objectui teach the report-index navigation item beside the named report + +Clause-②: no +Metadata-protocol: none + +- **Before.** The skills taught only the named report item, `{ type: 'report', reportName }`. Since PR #27566 a `type: 'report'` item with no `reportName` opens the app's report index (`/reports`), and the console no longer adds an index entry on its own, so an agent following the skills shipped an app with no report index entry. +- **Now.** The `objectstack-ui` navigation item table marks `reportName` optional and says what omitting it opens; `rules/navigation.md` shows the index entry beside the named one and says the console adds none itself; the `objectui` app-composition route table lists `{type:'report'}` → `/reports`. A new eval case (10 in `evals/views-apps-actions-pages.json`) asks for an app's report index entry and expects `{ type: 'report' }` with no `reportName`. +- No schema, key or behaviour changes. diff --git a/scripts/check-skills-token-ratchet.mjs b/scripts/check-skills-token-ratchet.mjs index c223c18ebf8..6600dc80124 100644 --- a/scripts/check-skills-token-ratchet.mjs +++ b/scripts/check-skills-token-ratchet.mjs @@ -1123,8 +1123,13 @@ export const CEILINGS = new Map([ // spells its own `must_not_contain` string (-2 bytes). 7154 -> 8034 bytes -> 2009 tokens, // ceiling +220, pinned AT the measurement. Same ruling as the line above (2026-10-27, tap A: // 「A:先削重复,余额按实测提高」). - // ceil($(wc -c < skills/objectstack-ui/evals/views-apps-actions-pages.json) / 4) == ceil(8034 / 4) - ['skills/objectstack-ui/evals/views-apps-actions-pages.json', 2009], + // 2009 -> 2108 (#27609): case 10, the app's report index entry — `{ type: 'report' }` with no + // `reportName` (PR #27566), which the console no longer adds on its own — +398 bytes; the + // file's cases still share no sentence, so there was no duplicate to cut (0 bytes). + // 8034 -> 8432 bytes -> 2108 tokens, ceiling +99, pinned AT the measurement. Same ruling as + // the lines above (2026-10-27, tap A: 「A:先削重复,余额按实测提高」). + // ceil($(wc -c < skills/objectstack-ui/evals/views-apps-actions-pages.json) / 4) == ceil(8432 / 4) + ['skills/objectstack-ui/evals/views-apps-actions-pages.json', 2108], ['skills/objectstack-upgrade/evals/protocol-major-upgrade.json', 1033], // the remaining skills' eval notes diff --git a/skills/objectstack-ui/SKILL.md b/skills/objectstack-ui/SKILL.md index 354c99dac94..152c439e21c 100644 --- a/skills/objectstack-ui/SKILL.md +++ b/skills/objectstack-ui/SKILL.md @@ -168,7 +168,7 @@ second at load, per ADR-0019 (app-as-consumer-unit) D3. More audiences are | `group` | `label`, `icon`, `expanded`, `children[]` | Collapsible group of items | | `object` | `objectName`, `viewName?`, `recordId?`, `filters?`, `label`, `icon` | Link to an object list, a named view, a record deep-link, or a `filters` slice on the bare data surface | | `dashboard` | `dashboardName`, `label`, `icon` | Link to a dashboard | -| `report` | `reportName`, `label`, `icon` | Link to a report | +| `report` | `reportName?`, `label`, `icon` | A report; no `reportName` = the app's report index | | `page` | `pageName`, `label`, `icon` | Link to a custom Page | | `url` | `url`, `label`, `icon` | External or custom URL | | `action` | `actionDef` (`{ actionName, params? }`), `label`, `icon` | Run an action instead of navigating | diff --git a/skills/objectstack-ui/evals/views-apps-actions-pages.json b/skills/objectstack-ui/evals/views-apps-actions-pages.json index 658e7e480e6..b5d5b0ced90 100644 --- a/skills/objectstack-ui/evals/views-apps-actions-pages.json +++ b/skills/objectstack-ui/evals/views-apps-actions-pages.json @@ -90,6 +90,16 @@ "must_contain": ["disabled:", "disabledReason"], "must_not_contain": ["description:", "disabledMessage", "disabledTooltip", "visible:"] } + }, + { + "id": 10, + "prompt": "Give the crm app a Reports entry listing every report it ships.", + "expected_output": "`{ type: 'report' }` with no `reportName` is the app's report index; the console adds none unless navigation declares it.", + "files": [], + "assertions": { + "must_contain": ["type: 'report'"], + "must_not_contain": ["reportName"] + } } ] } diff --git a/skills/objectstack-ui/rules/navigation.md b/skills/objectstack-ui/rules/navigation.md index 58f31c56b59..a83fd6539b2 100644 --- a/skills/objectstack-ui/rules/navigation.md +++ b/skills/objectstack-ui/rules/navigation.md @@ -30,12 +30,12 @@ export const CrmApp = App.create({ { id: 'nav_opportunity', type: 'object', objectName: 'opportunity', label: 'Opportunities', icon: 'target' }, // Open a specific named view instead of the object default: { id: 'nav_pipeline', type: 'object', objectName: 'opportunity', viewName: 'pipeline_kanban', label: 'Sales Pipeline', icon: 'columns-3' }, - // One-off parameterized slice — lands on the bare data surface - // (`/:objectName/data`, objectui ADR-0055) with removable URL filter - // chips, NOT anchored to a saved view. Don't author a view for these: + // One-off slice on the bare data surface (Run Modes, below): { id: 'nav_my_open', type: 'object', objectName: 'opportunity', filters: { owner_id: '{current_user_id}', status: 'open' }, label: 'My Open Deals', icon: 'user-check' }, { id: 'nav_dash', type: 'dashboard', dashboardName: 'sales_dashboard', label: 'Sales Dashboard', icon: 'chart-bar' }, { id: 'nav_report', type: 'report', reportName: 'opportunities_by_stage', label: 'Opps by Stage', icon: 'bar-chart-3' }, + // No `reportName` = the app's report index; the console adds none itself: + { id: 'nav_reports', type: 'report', label: 'Reports', icon: 'library' }, ], }, { diff --git a/skills/objectui/guides/app-composition.md b/skills/objectui/guides/app-composition.md index ede02c0789a..56f5d7c8953 100644 --- a/skills/objectui/guides/app-composition.md +++ b/skills/objectui/guides/app-composition.md @@ -19,6 +19,7 @@ source of truth for that mapping. | `{type:'object', objectName, filters}` | `/:objectName/data?filter[k]=v` | Bare data surface. No saved-view tab bar; conditions render as removable chips; full filter/sort/group toolbar; "Save as view" is the exit | | `{type:'dashboard', dashboardName}` | `/dashboard/:name` | Dashboard renderer | | `{type:'report', reportName}` | `/report/:name` | Report renderer | +| `{type:'report'}` | `/reports` | The app's report index; none unless navigation declares it | | `{type:'page', pageName}` | `/page/:name` | Bare SDUI rendering only -- no object shell, so view switching, actions and record routing are hand-assembled in the page schema | | `{type:'url', url}` | external | `target` controls the tab | | `{type:'component', componentRef, params?}` | `/component/:ns/:name?…` | `componentRef` is colon-joined (`metadata:resource`); `params` ride as querystring. `metadata:*` refs are special-cased onto the `/metadata[/:type[/:name]]` routes | @@ -30,8 +31,7 @@ source of truth for that mapping. - Nav items are a **discriminated union on `type`**. There is no `path` and no `kind` — those keys are ignored by `resolveHref` and rejected/stripped at - save. Emit the typed target field for the chosen type (`objectName`, - `pageName`, `dashboardName`, `reportName`, `url`, `componentRef`). + save. Emit the chosen type's target field. - Object-item targets are exclusive, not ordered (spec `objectNavTargetExclusivity`): `filters` is mutually exclusive with `recordId`/`viewName`; `runAction` is refused with `recordId` (it composes with `viewName` or `filters`); `recordId` + `viewName` is tolerated. From 8400c22560ad4c11be9e5fef4689fe2694c78dc9 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 03:19:03 +0000 Subject: [PATCH 2/2] fix(skills): eval case 10 refuses `reportName:` written, not the word An answer that follows the skill and says it omits `reportName` spelled the forbidden string and failed. The case now forbids the key written with a value, as the file's other absent-key cases do; the prompt drops one byte so the file holds its ceiling. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_016WV4cyZb8pD9989yV2qXKE --- skills/objectstack-ui/evals/views-apps-actions-pages.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skills/objectstack-ui/evals/views-apps-actions-pages.json b/skills/objectstack-ui/evals/views-apps-actions-pages.json index b5d5b0ced90..2a448de0f1b 100644 --- a/skills/objectstack-ui/evals/views-apps-actions-pages.json +++ b/skills/objectstack-ui/evals/views-apps-actions-pages.json @@ -93,12 +93,12 @@ }, { "id": 10, - "prompt": "Give the crm app a Reports entry listing every report it ships.", + "prompt": "Give the crm app a Reports entry listing all reports it ships.", "expected_output": "`{ type: 'report' }` with no `reportName` is the app's report index; the console adds none unless navigation declares it.", "files": [], "assertions": { "must_contain": ["type: 'report'"], - "must_not_contain": ["reportName"] + "must_not_contain": ["reportName:"] } } ]