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 makes holds kebab-case kinds.
  • Starters show what’s real. A makes kind 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 report in 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.