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
2 changes: 1 addition & 1 deletion docs/AUDIT.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ followed it. Statuses: Complete, Partial, Missing, Intentionally changed.
| X-ray and provenance (FM-4) | Complete | Canvas click-to-trace, provenance card (node, catalog entry, creating/updating events, rules), reverse violet wash for the playhead delivery. This pass added a keyboard path: "trace" buttons in the inspector components tab. |
| Governed repair / "argues back" (FM-1) | Complete | fixture-001 carries two real repairs (`rule.destructive-requires-alertdialog`, `rule.alertdialog-action-label-specific`); gate ticker + gates tab show findings and verbatim repair messages. |
| Break-it mode (FM-8) | Complete | 7 conditions across 6 failure kinds through the ordinary RunView. This pass added offline behavior: three conditions replay their recorded catch when the local agent is absent (the deployed static site); live-only conditions say so plainly. |
| Scenarios | Partial by plan | 3 ready (project-deletion, appointment-booking, recipe-creator), 3 planned (onboarding, support-triage, hotel-reservations) presented honestly with their blockers. New scenarios need owner-authored contract intents/rules (plan §17, open question 2) — deferred, not a defect. |
| Scenarios | Partial by plan | 4 ready (project-deletion, appointment-booking, recipe-creator, support-triage), 2 planned (onboarding, hotel-reservations) presented honestly with their blockers. support-triage shipped 2026-07-21 with owner-authored record-collection governance and two live gpt-oss recordings (fixtures 010/011). Remaining scenarios need owner-authored contract intents/rules (plan §17, open question 2) — deferred, not a defect. |
| Recipe shared-state experience (FM-6) | Complete | Real dishes, per-serving amounts, constraint swaps; this pass added numbered cooking instructions that the constraint swaps rewrite, on both the deterministic and generated paths, and re-recorded fixture-006 (live `gpt-oss`, 2026-07-11) with the model laying out an instructions table and the labeled enhancement seeding responder content into it. |
| Live BYO-key mode | Intentionally changed | The plan's "browser BYO key" became "no credentials in the browser": modelRef selects `scripted` or local `ollama:*`; hosted keys live only in the agent's env. Stricter than the plan; documented in the UI. |
| HITL (FM-7) | Partial | Appointment booking pauses for slot selection and validates round-trips; the agent's question is not itself a governed AlertDialog. Post-MVP. |
Expand Down
54 changes: 54 additions & 0 deletions docs/IMPLEMENTATION_LOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -905,3 +905,57 @@ Fail-first evidence: 6 new unit assertions failed on the old responder
interactive e2e specs failed on a clean origin/main worktree with
"element(s) not found" for [data-a2ui-surface=scheduling_question]; both
captured before implementation and pasted in the PR.

## 2026-07-21 — support-ticket triage: fourth ready scenario (record-collection)

The first scenario shipped since the plan's "new scenarios need
owner-authored contract intents/rules" deferral, and the template for
the next one: replay-first, non-interactive, zero per-scenario code
anywhere (no agent SCENARIOS entry, no capabilities, no authored
surface, no web UI) — a registry record, owner-authored governance, and
two live recordings.

Contract (astryx.dspack.json): intent `record-collection`; rule
`rule.record-collection-requires-table` (component-choice, must,
rationale verbatim from the Astryx Table docs via `npx astryx component
Table --json`); rule `rule.table-carries-data` (required-props: a
data-driven table must carry `columns` and `data`); worked example
`ex.support-ticket-triage` (badge strip above a compact tickets table —
the contract's table is data-driven strings, so badges live beside it,
never in cells). Status badges are deliberately example-steered, not
ruled: the linter renders a should-severity miss as "Required component
does not appear" under a passing gate — a contradiction — and Badge's
own docs endorse plain text when no action is needed.

Recording (Spark, OLLAMA_HOST, ollama:gpt-oss:latest) surfaced a real
generation bug: across ~20 live runs on two models the table's nested
`data` rows were NEVER emitted (optional scalar props were), so every
run shipped an empty table shell — and with the data rule active, runs
exhausted repairs instead. Root cause upstream: dspack-gen's generation
schema marked every prop optional, and grammar-constrained decoders
skip optional heavy branches; a repair round cannot fix what the
grammar never demands. Fix in dspack-gen (feat/required-props-grammar,
targeting 0.1.2): prop descriptors may declare `required: true`, which
reaches the props schema's `required` and makes `props` itself required
on the node branch. The contract now flags table `columns` and `data`
required. With the fix: fixture-011 (clean) is a first-pass five-row
triage table with a status badge strip; fixture-010 (argues-back) is
the flagship catch — the model obeys a prose-list prompt with a lone
text node, S3 fails on the table rule, and the repaired surface ships
the filled table. `--require-repair` rejected 3 compliant runs before a
genuine violation landed (recorded as rejected, not distorted). The
optional emitter-refusal class was attempted once honestly; the model
built a clean table instead, so no third fixture ships.
Comment on lines +941 to +948

Break-it: `records-as-prose` (governed-repair, recordedCatch →
argues-back; no scriptedRef — break-scripts untouched). Docs: AUDIT
count 4 ready / 2 planned; NAV-REFACTOR matrix row filled.

Fail-first evidence: pre-contract, recording with the new intent failed
with getIntent's "intent 'record-collection' is not registered"
(pasted in the PR); post-contract the scripted smoke plays the worked
example clean. All four support-triage.spec.ts assertions failed on the
old registry (shelf "(planned)", missing fixtures, permalink "unknown
or not-yet-ready"); upstream, three new grammar-alignment assertions
failed on dspack-gen 0.1.1. shadcn intent parity stays deferred to the
standing owner-authored contract-extension item.
6 changes: 3 additions & 3 deletions docs/NAV-REFACTOR.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ session file, or run it live and download one."*

- **Scenario** = `scenarioId` (React state in `Studio`), canonical ids from
`packages/scenarios/src/registry.ts`: ready `recipe-creator`,
`appointment-booking`, `project-deletion`; planned (not selectable)
`onboarding`, `support-triage`, `hotel-reservations`.
`appointment-booking`, `project-deletion`, `support-triage`; planned
(not selectable) `onboarding`, `hotel-reservations`.
- **Operation** = `view` state: `"replay" | "live" | "break" | "canvas"`.
X-ray and Fork/diff are not top-level views: they are controls inside
`RunView` and operate on whatever run is loaded — already
Expand Down Expand Up @@ -156,8 +156,8 @@ local agent · **gap** = no support yet. Cells marked † change with this work.
| `appointment-booking` | **recorded** (fixture-005) | **live-only** (interactive: `bookingRespond`) | `ambiguous-action`: **live-only** today → **recorded** catch† | **gap** today → **universal**† via fixture-005 | **universal** | **universal** (continuation live-only) |
| `project-deletion` | **recorded** ×3 (fixtures 001/002/003) | **live-only** (generation: `scripted` deterministic or `ollama:*`) | `no-alertdialog` **recorded** (catch=argues-back) · `ok-label` **recorded** (argues-back) · `unsupported-component` **recorded** (refusal) · `malformed-generation` **live-only** (scripted, needs agent) | **recorded** today (the hardcode) → **universal**† via fixture-001 | **universal** | **universal** |
| *(any scenario)* | — | — | `malformed-import`: **universal** (client-side validator; mislabeled project-deletion → `scenarioIndependent`†) | — | — | — |
| `support-triage` | **recorded** ×2 (fixtures 010/011) | **live-only** (generation: `scripted` deterministic or `ollama:*`; zero agent code, same as project-deletion) | `records-as-prose` **recorded** (catch=argues-back) | **universal** via fixture-011 final surface | **universal** | **universal** |
| `onboarding` (planned) | **gap** (not selectable; shelf states needs) | **gap** | **gap** | **gap** | n/a until a run loads | n/a |
| `support-triage` (planned) | **gap** (same) | **gap** | **gap** | **gap** | n/a | n/a |
| `hotel-reservations` (planned) | **gap** (same) | **gap** | **gap** | **gap** | n/a | n/a |

Unused asset worth noting: `packages/replay/fixtures/fixture-004.json`
Expand Down
63 changes: 63 additions & 0 deletions e2e/support-triage.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* Support-ticket triage: the record-collection scenario, replay-first,
* against the static export. Both fixtures are mode:"live" recordings
* (gpt-oss via Ollama) — nothing here is hand-scripted content.
*
* The governance story this scenario carries: the contract steers structural
* component choice — collections render as tables (rule.record-collection-
* requires-table), and a data-driven table must carry its rows
* (rule.table-carries-data).
*/
import { expect, test, type Page } from "@playwright/test";

const scrubber = (page: Page) => page.getByTestId("scrubber");

async function scrubToEnd(page: Page) {
await scrubber(page).focus();
await page.keyboard.press("End");
}

test.beforeEach(async ({ page }) => {
await page.goto("/");
});

test("support-triage is a ready, selectable scenario on the shelf", async ({ page }) => {
const shelf = page.getByTestId("scenario-support-triage");
await expect(shelf).not.toContainText("(planned)");
await shelf.click();
await expect(page.getByTestId("fixture-argues-back")).toBeVisible();
await expect(page.getByTestId("fixture-clean")).toBeVisible();
});

test("clean replay: a live run straight through the gates to a filled table", async ({ page }) => {
await page.getByTestId("scenario-support-triage").click();
await page.getByTestId("fixture-clean").click();
await expect(page.getByTestId("fixture-meta")).toContainText("live run");
await scrubToEnd(page);
await expect(page.getByTestId("audit-outcome")).toContainText("passed");
// The recorded table renders with its rows — the whole point of the
// required-props grammar fix; an empty table shell would fail here.
const canvas = page.locator("[data-canvas]");
await expect(canvas).toContainText("Status");
await expect(canvas).toContainText("Priority");
await expect(canvas).toContainText("2 urgent");
});

test("repair replay: prose list caught by the table rule, repaired surface ships", async ({ page }) => {
await page.getByTestId("scenario-support-triage").click();
await page.getByTestId("fixture-argues-back").click();
await scrubToEnd(page);
const ticker = page.getByTestId("gate-ticker");
await expect(ticker).toContainText(/attempt 0:\s*S1✓\s*S2✓\s*S3✗\s*→ repair/);
await expect(ticker).toContainText(/attempt 1:\s*S1✓\s*S2✓\s*S3✓/);
await expect(ticker).toContainText("outcome passed");
// The design system won: the prose list came back as a filled table.
await expect(page.locator("[data-canvas]")).toContainText("Ticket");
});

test("permalink: a support-triage deep link resolves", async ({ page }) => {
await page.goto("/#s=support-triage&f=clean");
await page.reload();
await expect(page.getByTestId("fixture-meta")).toContainText("live run");
await expect(page.getByTestId("link-error")).toHaveCount(0);
});
152 changes: 152 additions & 0 deletions packages/contracts/astryx.dspack.json
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@
"data": {
"type": "array",
"description": "Row records: each { cells: string[] } aligned with columns.",
"required": true,
"items": {
"type": "object",
"properties": {
Expand All @@ -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"
}
Expand Down Expand Up @@ -423,6 +425,11 @@
"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."
}
],
"rules": [
Expand Down Expand Up @@ -550,6 +557,54 @@
"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. (Valid while table is this catalog's only collection structure; widen to a choice set if a list component lands.) — 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"
}
],
"examples": [
Expand Down Expand Up @@ -818,6 +873,103 @@
]
}
}
},
{
"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"
}
}
]
}
}
}
]
}
Loading
Loading