Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .changeset/27609-skills-report-index-nav.md
Original file line number Diff line number Diff line change
@@ -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.
9 changes: 7 additions & 2 deletions scripts/check-skills-token-ratchet.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion skills/objectstack-ui/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
10 changes: 10 additions & 0 deletions skills/objectstack-ui/evals/views-apps-actions-pages.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 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:"]
}
}
]
}
6 changes: 3 additions & 3 deletions skills/objectstack-ui/rules/navigation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
],
},
{
Expand Down
4 changes: 2 additions & 2 deletions skills/objectui/guides/app-composition.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand All @@ -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.
Expand Down
Loading