diff --git a/examples/astryx.dspack.json b/examples/astryx.dspack.json index 79b21a9..7fe0f7f 100644 --- a/examples/astryx.dspack.json +++ b/examples/astryx.dspack.json @@ -284,6 +284,7 @@ "data": { "type": "array", "description": "Row records: each { cells: string[] } aligned with columns.", + "required": true, "items": { "type": "object", "properties": { @@ -302,6 +303,7 @@ "columns": { "type": "array", "description": "Header labels for the data columns, e.g. [\"Ingredient\", \"Amount\"]. (The full Astryx TableColumn object shape is not yet contract-specified.)", + "required": true, "items": { "type": "string" } @@ -376,6 +378,139 @@ "composition": { "notes": "Takes arbitrary children (ReactNode); surfaces express them as child nodes." } + }, + "list": { + "name": "List", + "description": "A vertical collection of items with consistent spacing, dividers, and optional markers.", + "whenToUse": "Use it to display ordered or unordered groups of related content — including groups of selectable options. Lists imply a meaningful collection.", + "whenNotToUse": "Do not use a list for a single item or for laying out unrelated content. Records with uniform fields belong in a table.", + "props": { + "density": { + "type": "enum", + "values": [ + "compact", + "balanced", + "spacious" + ], + "description": "Item density.", + "default": "balanced" + }, + "hasDividers": { + "type": "boolean", + "description": "Renders dividers between items." + } + }, + "categories": [ + "table-list", + "container" + ], + "x-source": "https://github.com/facebook/astryx/blob/v0.1.4/packages/core/src/List/List.doc.mjs", + "composition": { + "notes": "Takes arbitrary children (ReactNode); surfaces express items as child nodes — for a comparison, selectable-card children." + } + }, + "selectable-card": { + "name": "SelectableCard", + "description": "A card that toggles between selected and unselected states with an accent border.", + "whenToUse": "Use for plan pickers, filter chips, or option grids — one named option among alternatives, with its selected state visible.", + "whenNotToUse": "Do not use for navigation; use ClickableCard for that (not in this contract).", + "props": { + "label": { + "type": "string", + "description": "The option's name. Every option is named.", + "required": true + }, + "isSelected": { + "type": "boolean", + "description": "Whether this option is currently selected. Presentational: selection state lives in the shared data model and re-delivers this prop." + }, + "isDisabled": { + "type": "boolean", + "description": "Whether the option can be chosen." + }, + "variant": { + "type": "enum", + "values": [ + "default", + "transparent", + "muted", + "blue", + "cyan", + "gray", + "green", + "orange", + "pink", + "purple", + "red", + "teal", + "yellow" + ], + "description": "Card background variant, verbatim from SelectableCard's union; color variants categorize rather than signal status.", + "default": "default" + } + }, + "categories": [ + "container", + "data-input" + ], + "x-source": "https://github.com/facebook/astryx/blob/v0.1.4/packages/core/src/SelectableCard/SelectableCard.doc.mjs", + "composition": { + "notes": "Takes arbitrary children (ReactNode); an option's content — attributes, badges, actions — are child nodes." + } + }, + "metadata-list": { + "name": "MetadataList", + "description": "Key-value pairs for object attributes, in a structured layout. DATA-DRIVEN: items are an array prop, the same Astryx data-props idiom as Table.", + "whenToUse": "MetadataList displays key-value pairs for object attributes like quality, condition, and status. Use it for detail panels, settings summaries, and record information — the attributes that make options comparable.", + "whenNotToUse": "Do not use for extensive form input (use a form layout) or for data without a clear key-value structure.", + "props": { + "items": { + "type": "array", + "description": "Attribute records, each { label, value } — e.g. { \"label\": \"Storage\", \"value\": \"1 TB\" }.", + "required": true, + "x-drift": "upstream MetadataList is children-based (MetadataListItem); the contract carries items as data-props because sub-components carry no props under grammar-constrained generation", + "items": { + "type": "object", + "properties": { + "label": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "label", + "value" + ] + } + }, + "columns": { + "type": "enum", + "values": [ + "single", + "multi" + ], + "description": "Column layout for the attribute grid.", + "default": "single" + }, + "orientation": { + "type": "enum", + "values": [ + "vertical", + "horizontal" + ], + "description": "Label/value flow direction.", + "default": "vertical" + } + }, + "categories": [ + "content" + ], + "x-source": "https://github.com/facebook/astryx/blob/v0.1.4/packages/core/src/MetadataList/MetadataList.doc.mjs", + "composition": { + "notes": "Upstream MetadataList takes MetadataListItem children; the contract carries items as data-props because sub-components carry no props under grammar-constrained generation — the renderer synthesizes the item children." + } } }, "categories": { @@ -423,6 +558,21 @@ "id": "structured-editing", "name": "Structured editing", "description": "Surfaces the user and agent edit together: a structured object rendered for co-editing, with explicit actions to change it." + }, + { + "id": "record-collection", + "name": "Record collection", + "description": "Surfaces that present many records of the same kind — support tickets, orders, members — for scanning and action. Records share uniform fields, so the collection renders as a table; each record carries a status the user reads at a glance, summarized with badges where attention is needed and plain text otherwise." + }, + { + "id": "data-collection", + "name": "Data collection", + "description": "Surfaces that ask the user for information and submit it — signup, contact, profile edits, an RSVP. The ask is stated, the fields are labeled, and the submit action is explicit: at minimum, something to fill in and a way to send it." + }, + { + "id": "transactional-review", + "name": "Transactional review", + "description": "Surfaces that present alternatives for a consequential choice — plans, offers, bookings, products — compared on stated attributes and committed with an explicit action. Options are named, comparable, and at least two; the commitment is a labeled action." } ], "rules": [ @@ -550,6 +700,198 @@ "interaction" ], "x-source": "https://github.com/facebook/astryx/blob/v0.1.4/packages/core/src/Dialog/Dialog.doc.mjs" + }, + { + "id": "rule.record-collection-requires-table", + "type": "component-choice", + "severity": "must", + "appliesTo": { + "intents": [ + "record-collection" + ] + }, + "require": [ + "table" + ], + "rationale": "\"Table displays structured data in rows and columns with consistent dimensionality\"; \"Use Table for data sets with uniform structure; for simpler or inconsistent data, consider a list or card layout instead\". A record collection is uniform by definition — same fields on every record — so it renders as a table, not as prose or ad-hoc stacks. (The catalog's list component serves heterogeneous and selectable content, per the same docs; uniform records still choose tables. A table-or-list choice set is inexpressible in current rule types — component-choice require is conjunctive — noted as candidate dspack-gen work if a scenario ever needs it.) — Astryx Table docs, v0.1.4", + "examples": [ + "ex.support-ticket-triage" + ], + "tags": [ + "structure" + ], + "x-source": "https://github.com/facebook/astryx/blob/v0.1.4/packages/core/src/Table/Table.doc.mjs" + }, + { + "id": "rule.table-carries-data", + "type": "required-props", + "severity": "must", + "appliesTo": { + "intents": [ + "record-collection" + ] + }, + "component": "table", + "requiredProps": [ + { + "prop": "columns" + }, + { + "prop": "data" + } + ], + "rationale": "The catalog's Table is \"DATA-DRIVEN: rows and columns are array props (`data`, `columns`), not sub-components\" — a table without its `data` prop renders an empty shell, records nowhere to be seen. Born from observed live runs: gpt-oss reliably emitted the table shell and omitted the rows until this rule made the pipeline ask for them. — Astryx Table docs, v0.1.4", + "examples": [ + "ex.support-ticket-triage" + ], + "tags": [ + "structure" + ], + "x-source": "https://github.com/facebook/astryx/blob/v0.1.4/packages/core/src/Table/Table.doc.mjs" + }, + { + "id": "rule.data-collection-requires-input-and-action", + "type": "component-choice", + "severity": "must", + "appliesTo": { + "intents": [ + "data-collection" + ] + }, + "require": [ + "text-input", + "button" + ], + "rationale": "\"TextInput collects short-form text like names, emails, or search queries\"; Button \"triggers an action when clicked. Use it for form submissions, confirmations, navigation, or any interaction that needs a clear call to action\". An ask without a field is not a form, and a form that cannot be submitted does not ship: a data-collection surface carries at least one input and an explicit action. No layout, field count, or form shape is mandated. — Astryx TextInput and Button docs, v0.1.4", + "examples": [ + "ex.signup-basic" + ], + "tags": [ + "structure" + ], + "x-source": "https://github.com/facebook/astryx/blob/v0.1.4/packages/core/src/TextInput/TextInput.doc.mjs" + }, + { + "id": "rule.review-presents-options", + "type": "component-choice", + "severity": "must", + "appliesTo": { + "intents": [ + "transactional-review" + ] + }, + "require": [ + "list", + "selectable-card", + "button" + ], + "rationale": "SelectableCard is for \"plan pickers, filter chips, or option grids\"; List renders \"ordered or unordered groups of related content\" — \"lists imply a meaningful collection\"; Button \"triggers an action when clicked … any interaction that needs a clear call to action\". A review surface presents its options as a collection of named selectable cards with an explicit committing action — not prose, not a bare recommendation. — Astryx SelectableCard, List, and Button docs, v0.1.4", + "examples": [ + "ex.plan-comparison" + ], + "tags": [ + "structure" + ], + "x-source": "https://github.com/facebook/astryx/blob/v0.1.4/packages/core/src/SelectableCard/SelectableCard.doc.mjs" + }, + { + "id": "rule.review-offers-alternatives", + "type": "required-composition", + "severity": "must", + "appliesTo": { + "intents": [ + "transactional-review" + ] + }, + "component": "list", + "requiredSubComponents": [ + { + "id": "selectable-card", + "min": 2 + } + ], + "rationale": "A choice of one is not a comparison: a review's option list carries at least two selectable cards. (Under this intent, lists are option groups; the floor applies to every list on the surface. Exact counts and upper bounds are inexpressible in current rule types and remain guidance.)", + "examples": [ + "ex.plan-comparison" + ], + "tags": [ + "structure" + ], + "x-source": "https://github.com/facebook/astryx/blob/v0.1.4/packages/core/src/List/List.doc.mjs" + }, + { + "id": "rule.option-states-attributes", + "type": "required-composition", + "severity": "must", + "appliesTo": { + "intents": [ + "transactional-review" + ] + }, + "component": "selectable-card", + "requiredSubComponents": [ + { + "id": "metadata-list", + "min": 1 + } + ], + "rationale": "\"MetadataList displays key-value pairs for object attributes\" — an option you can compare states what it offers as structured attributes, not free prose. Every selectable card under this intent carries a metadata list. — Astryx MetadataList docs, v0.1.4", + "examples": [ + "ex.plan-comparison" + ], + "tags": [ + "structure" + ], + "x-source": "https://github.com/facebook/astryx/blob/v0.1.4/packages/core/src/MetadataList/MetadataList.doc.mjs" + }, + { + "id": "rule.metadata-carries-items", + "type": "required-props", + "severity": "must", + "appliesTo": { + "intents": [ + "transactional-review" + ] + }, + "component": "metadata-list", + "requiredProps": [ + { + "prop": "items" + } + ], + "rationale": "The catalog's MetadataList is data-driven: attributes are `{ label, value }` records. A metadata list without its items renders an empty shell, attributes nowhere to be seen — the same class of failure rule.table-carries-data guards against for tables.", + "examples": [ + "ex.plan-comparison" + ], + "tags": [ + "structure" + ], + "x-source": "https://github.com/facebook/astryx/blob/v0.1.4/packages/core/src/MetadataList/MetadataList.doc.mjs" + }, + { + "id": "rule.option-carries-label", + "type": "required-props", + "severity": "must", + "appliesTo": { + "intents": [ + "transactional-review" + ] + }, + "component": "selectable-card", + "requiredProps": [ + { + "prop": "label" + } + ], + "rationale": "Every option is named: SelectableCard's label is how an alternative is identified, compared, and announced. An unnamed option cannot be chosen deliberately. — Astryx SelectableCard docs, v0.1.4", + "examples": [ + "ex.plan-comparison" + ], + "tags": [ + "accessibility", + "structure" + ], + "x-source": "https://github.com/facebook/astryx/blob/v0.1.4/packages/core/src/SelectableCard/SelectableCard.doc.mjs" } ], "examples": [ @@ -818,6 +1160,285 @@ ] } } + }, + { + "id": "ex.support-ticket-triage", + "intent": "record-collection", + "name": "Support-ticket triage", + "prompt": "a triage view of open support tickets with status and priority", + "description": "A card leading with a badge strip summarizing queue status, above a compact table of the open tickets — the collection renders as a table; statuses that need attention are badged, the rest stay plain text.", + "surface": { + "dspackSurface": "0.1", + "system": "Astryx", + "intent": "record-collection", + "root": { + "component": "card", + "id": "triage-card", + "children": [ + { + "component": "text", + "id": "title", + "props": { + "type": "display-3" + }, + "text": "Open support tickets" + }, + { + "component": "text", + "id": "intro", + "props": { + "type": "body" + }, + "text": "Newest first. Statuses update as agents respond." + }, + { + "component": "badge", + "id": "badge-urgent", + "props": { + "variant": "error", + "label": "2 urgent" + } + }, + { + "component": "badge", + "id": "badge-waiting", + "props": { + "variant": "warning", + "label": "3 waiting" + } + }, + { + "component": "badge", + "id": "badge-replied", + "props": { + "variant": "info", + "label": "4 replied" + } + }, + { + "component": "table", + "id": "tickets", + "props": { + "columns": [ + "Ticket", + "Subject", + "Status", + "Priority" + ], + "data": [ + { + "cells": [ + "#4812", + "Cannot export session receipts", + "Waiting on support", + "Urgent" + ] + }, + { + "cells": [ + "#4809", + "Replay stalls at event 12", + "Replied", + "High" + ] + }, + { + "cells": [ + "#4801", + "Billing address will not save", + "Waiting on customer", + "Normal" + ] + } + ], + "density": "compact" + } + } + ] + } + } + }, + { + "id": "ex.signup-basic", + "intent": "data-collection", + "name": "Signup, name and email", + "prompt": "a signup form asking for name and email, with a clear call to action", + "description": "A card stating the ask, two labeled fields, and one explicit primary action — something to fill in and a way to send it, every field labeled.", + "surface": { + "dspackSurface": "0.1", + "system": "Astryx", + "intent": "data-collection", + "root": { + "component": "card", + "id": "signup-card", + "children": [ + { + "component": "text", + "id": "title", + "props": { + "type": "display-3" + }, + "text": "Create your account" + }, + { + "component": "text", + "id": "intro", + "props": { + "type": "body" + }, + "text": "Name and email is all it takes. You can add the rest later." + }, + { + "component": "text-input", + "id": "name-input", + "props": { + "label": "Your name", + "placeholder": "Ada Lovelace", + "isRequired": true + } + }, + { + "component": "text-input", + "id": "email-input", + "props": { + "type": "email", + "label": "Email address", + "placeholder": "ada@example.com", + "description": "We send a confirmation link, nothing else.", + "isRequired": true + } + }, + { + "component": "button", + "id": "submit", + "props": { + "label": "Create account", + "variant": "primary" + } + } + ] + } + } + }, + { + "id": "ex.plan-comparison", + "intent": "transactional-review", + "name": "Plan comparison", + "prompt": "compare the available plans and choose one", + "description": "Two named plans as selectable cards in a list, each stating its attributes as a metadata list, with one explicit committing action. Deliberately not a hotel: the intent is domain-neutral, and the scenario that validates it comes from a different domain.", + "surface": { + "dspackSurface": "0.1", + "system": "Astryx", + "intent": "transactional-review", + "root": { + "component": "card", + "id": "compare-card", + "children": [ + { + "component": "text", + "id": "title", + "props": { + "type": "display-3" + }, + "text": "Choose a plan" + }, + { + "component": "text", + "id": "intro", + "props": { + "type": "body" + }, + "text": "Two plans, compared on what they include. Pick one to continue." + }, + { + "component": "list", + "id": "plans", + "props": { + "density": "balanced" + }, + "children": [ + { + "component": "selectable-card", + "id": "plan-basic", + "props": { + "label": "Basic", + "isSelected": false + }, + "children": [ + { + "component": "metadata-list", + "id": "basic-attrs", + "props": { + "items": [ + { + "label": "Price", + "value": "$5/mo" + }, + { + "label": "Storage", + "value": "50 GB" + }, + { + "label": "Support", + "value": "Community" + } + ] + } + } + ] + }, + { + "component": "selectable-card", + "id": "plan-pro", + "props": { + "label": "Pro", + "isSelected": true, + "variant": "blue" + }, + "children": [ + { + "component": "metadata-list", + "id": "pro-attrs", + "props": { + "items": [ + { + "label": "Price", + "value": "$12/mo" + }, + { + "label": "Storage", + "value": "1 TB" + }, + { + "label": "Support", + "value": "Priority" + } + ] + } + }, + { + "component": "badge", + "id": "pro-badge", + "props": { + "variant": "info", + "label": "Most popular" + } + } + ] + } + ] + }, + { + "component": "button", + "id": "choose", + "props": { + "label": "Choose plan", + "variant": "primary" + } + } + ] + } + } } ] }