View files
One literal-data file per route says what the page is called, what it's for, what the assistant can make there and what to ask. Parsed at build, never run, so the nav and the assistant read the same words.
An assistant that can take someone to a page, or tell them where they are, needs a sentence per page: what it shows and what you do there. That sentence usually lives nowhere. Titles are scattered across breadcrumbs, nav config and document titles. Route components are code, so nothing can read them without running the app. Docs describe last year’s pages.
So each route gets a sibling view file that holds only data:
// routes/orders/index.view.ts
import { defineView } from "@app/views";
export const view = defineView({
kind: "page",
title: "Orders",
description:
"Every order as a table, filterable by status, customer and date. Export to CSV, or open an order to refund it.",
makes: ["report"], // payload kinds this page is the home of
asks: ["Which orders are late?", "Who had the most refunds this month?"],
access: { permission: "orders:read" },
});
A build plugin parses each file’s syntax tree, without importing it, and writes two generated maps. Structure ships with the app. The text is only for the assistant, so it loads lazily.
routes/**/*.view.ts ──parse, never run──▶ views.gen.ts { kind, title, path, gates, makes }
└──▶ viewText.gen.ts { description, asks } (lazy)
│
├─▶ nav titles, breadcrumbs, route gates
├─▶ the assistant's page list: every place it can navigate, with what each is for
├─▶ what's on screen: "Orders: every order as a table, …"
└─▶ blank-screen starters: [Generate report] [Which orders are late?]
Rules
- Literal data only. One import and one
export const view = defineView({ ... }), with no variables, calls, spreads or types. Anything else fails the build with the file and line. That’s what lets a plugin read it without running the app. - Checked where types can’t. A page or modal needs a title and a description. Descriptions stay under 400 characters, there are at most four asks of under 120 characters each (each one is a single line in a narrow panel), and
makesholds kebab-case kinds. - Starters show what’s real. A
makeskind becomes a starter only if the app declares that payload right now. A page without asks gets generated ones, made once per page per session from its title, its description and the descriptions of what’s on screen. They describe the page, not its data, so they don’t need refreshing. - A make starter types, an ask starter sends. “Generate report” puts
/generate reportin the composer for the person to finish. “Which orders are late?” is sent as is.
Why it works
A page’s description is one sentence next to its route, and it pays out four times. The assistant’s sense of “where you are” and “where I can take you” comes from the same words the person reads in the nav, so the two can’t disagree.
How this fits the larger system: The UI is the engine.