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 apps/agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"record:catch": "tsx src/record-catch.ts"
},
"dependencies": {
"@aestheticfunction/dspack-gen": "^0.1.1",
"@aestheticfunction/dspack-gen": "^0.1.2",
"@dspack-studio/agui-bridge": "workspace:*",
"@dspack-studio/contracts": "workspace:*",
"@dspack-studio/replay": "workspace:*"
Expand Down
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 | 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. |
| Scenarios | Partial by plan | 5 ready (project-deletion, appointment-booking, recipe-creator, support-triage, onboarding), 1 planned (hotel-reservations) presented honestly with its blockers. support-triage and onboarding shipped 2026-07-21 with owner-authored governance (record-collection, data-collection) and live gpt-oss recordings (fixtures 010-014). hotel-reservations needs new design-system components (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
42 changes: 42 additions & 0 deletions docs/IMPLEMENTATION_LOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -959,3 +959,45 @@ 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.

## 2026-07-21 — signup/onboarding: fifth ready scenario (data-collection)

Same template as support-triage: replay-first, zero per-scenario code, an
atomic PR of governance + recordings + registry + tests + docs.

Honesty split, stated everywhere it matters: labels were ALWAYS enforced
(`rule.input-carries-label` is universal, no appliesTo) — the scenario's
old "Labeled inputs, enforced" tagline over-claimed and was rewritten.
What the new intent adds is structural:
`rule.data-collection-requires-input-and-action` (component-choice,
must) — an ask without a field is not a form, and a form that cannot be
submitted does not ship. No layout, field count, or form shape is
mandated, deliberately: data collection is not equivalent to signup
forms. Rationales quote the Astryx TextInput and Button docs verbatim.

Durability check (owner requirement: the intent must remain durable
beyond the signup example): a non-signup probe — "request access to a
private workspace: name, work email, team" — produced an honest form
under the same intent and rules. It also produced, unforced, the
scenario's refusal story: the model picked `dropdown-menu` for the team
select, which lints clean but the astryx profile cannot project. That
recording ships as the third fixture; it documents the vocabulary's
real limit (no select), which is a component gap (the hotel-reservations
blocker class), not an intent-design flaw.

Recording (Spark, ollama:gpt-oss:latest, dspack-gen 0.1.2 published):
fixture-012 clean — first pass to a labeled name+email form with one
primary action (close to the worked example's shape; the few-shot
steers hard, noted honestly). fixture-013 argues-back — first
`--require-repair` attempt: the model obeys the text-only prompt with a
card and welcome text, S3 fails on the new rule (both missing
components), repair ships a labeled email field and a "Join now"
action. fixture-014 refusal — reproduced on the second roll (first
was a clean pass, discarded).

Fail-first evidence: pre-contract recording fails with getIntent's
"intent 'data-collection' is not registered" (pasted in the PR); the
five onboarding.spec.ts tests fail on the old registry.
`studio-shell.spec.ts`'s planned-reveal test retargeted
`scenario-onboarding` → `scenario-hotel-reservations` (the last planned
entry) — a retarget, not a new assertion; it passes on both builds.
8 changes: 4 additions & 4 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`, `support-triage`; planned
(not selectable) `onboarding`, `hotel-reservations`.
`appointment-booking`, `project-deletion`, `support-triage`,
`onboarding`; planned (not selectable) `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 @@ -157,8 +157,8 @@ local agent · **gap** = no support yet. Cells marked † change with this work.
| `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 |
| `hotel-reservations` (planned) | **gap** (same) | **gap** | **gap** | **gap** | n/a | n/a |
| `onboarding` | **recorded** ×3 (fixtures 012/013/014) | **live-only** (generation: `scripted` deterministic or `ollama:*`; zero agent code) | `ask-without-a-form` **recorded** (catch=argues-back) | **universal** via fixture-012 final surface | **universal** | **universal** |
| `hotel-reservations` (planned) | **gap** (not selectable; shelf states needs) | **gap** | **gap** | **gap** | n/a until a run loads | n/a |

Unused asset worth noting: `packages/replay/fixtures/fixture-004.json`
("Scheduling, generated live", intent `scheduling`, 12 events) is bundled but
Expand Down
74 changes: 74 additions & 0 deletions e2e/onboarding.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/**
* Signup / onboarding: the data-collection scenario, replay-first, against
* the static export. All three fixtures are mode:"live" recordings (gpt-oss
* via Ollama) — nothing here is hand-scripted content.
*
* The governance story, honestly split: labels were always enforced (the
* universal rule.input-carries-label); what the new intent adds is
* structural — an ask needs something to fill in and a way to send it
* (rule.data-collection-requires-input-and-action).
*/
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("onboarding is a ready, selectable scenario on the shelf", async ({ page }) => {
const shelf = page.getByTestId("scenario-onboarding");
await expect(shelf).not.toContainText("(planned)");
await shelf.click();
await expect(page.getByTestId("fixture-argues-back")).toBeVisible();
await expect(page.getByTestId("fixture-clean")).toBeVisible();
await expect(page.getByTestId("fixture-refusal")).toBeVisible();
});

test("clean replay: a live run straight through the gates to a labeled form", async ({ page }) => {
await page.getByTestId("scenario-onboarding").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");
const canvas = page.locator("[data-canvas]");
await expect(canvas).toContainText("Your name");
await expect(canvas).toContainText("Email address");
await expect(canvas).toContainText("Create account");
});

test("repair replay: a text-only ask caught by the form rule, repaired surface ships", async ({ page }) => {
await page.getByTestId("scenario-onboarding").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 ask got its field and its action.
const canvas = page.locator("[data-canvas]");
await expect(canvas).toContainText("Email address");
await expect(canvas).toContainText("Join now");
});

test("refusal replay: a non-signup ask meets the vocabulary's honest limit", async ({ page }) => {
await page.getByTestId("scenario-onboarding").click();
await page.getByTestId("fixture-refusal").click();
await scrubToEnd(page);
await expect(page.getByTestId("audit-outcome")).toContainText("failed-gate (exit 3)");
const panel = page.getByTestId("failure-panel");
await expect(panel).toContainText("unknown component 'dropdown-menu'");
await expect(page.getByTestId("canvas-empty")).toContainText("No surface shipped");
});

test("permalink: an onboarding deep link resolves", async ({ page }) => {
await page.goto("/#s=onboarding&f=clean");
await page.reload();
await expect(page.getByTestId("fixture-meta")).toContainText("live run");
await expect(page.getByTestId("link-error")).toHaveCount(0);
});
3 changes: 2 additions & 1 deletion e2e/studio-shell.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ test("the pipeline diagram names the honest pipeline in reading order", async ({

test("planned scenarios reveal what they need, by keyboard", async ({ page }) => {
await page.goto("/");
const planned = page.getByTestId("scenario-onboarding");
// hotel-reservations is the last remaining planned scenario.
const planned = page.getByTestId("scenario-hotel-reservations");
await expect(planned).toContainText("(planned)");
await planned.focus();
await page.keyboard.press("Enter");
Expand Down
89 changes: 89 additions & 0 deletions packages/contracts/astryx.dspack.json
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,11 @@
"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."
}
],
"rules": [
Expand Down Expand Up @@ -605,6 +610,28 @@
"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"
}
],
"examples": [
Expand Down Expand Up @@ -970,6 +997,68 @@
]
}
}
},
{
"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"
}
}
]
}
}
}
]
}
Loading
Loading