Skip to content
Open
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: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ jobs:
python-version: '3.12'
- name: Starter and executable discovery regressions
run: python -m unittest discover -s tools -p "test_*.py" -v
- name: Script app verify skill (runner logic, no card-host)
run: python -m unittest discover -s skills/splash-app-verify/scripts -p "test_*.py" -v

rust-sdk:
# The Rust SDK for app components (OctoSense ADR 0014): the macro, the
Expand Down
5 changes: 4 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ says who runs each one and when.
loaded, not only that it opens. Judge behavior, layout and appearance
separately. Keep the evidence of every failure, and say which checks a
person ran and which you ran.
For a script app, use [the script app verify skill](skills/splash-app-verify/SKILL.md):
the contract and scenarios before the code, and the scenarios run after
every edit.
For new Glance cards or card UX acceptance, also use
[the app-card UX skill](skills/octoscript-app-card-ux/SKILL.md): summary-to-workspace
transitions, shared editing state, keyboard/scrolling checks and source-bound
Expand Down Expand Up @@ -343,4 +346,4 @@ The full list is in [docs/SCRIPT-API.md](docs/SCRIPT-API.md#gotchas).
- Loop with `for i in n`; there is no `range()`.
- Write `name := Widget{}` to address a widget as `ui.name`.
- `draw_bg +: {…}` merges your fields into the existing `draw_bg`.
- In `on_render`, keep `if` and `for` separate (no `else for`).
- In `on_render`, keep `if` and `for` separate (no `else for`, braced or not).
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,17 @@ release tag, submission) are listed in [AGENTS.md](AGENTS.md#how-to-work);
explains the ones every flow shares. An agent never fabricates an approval,
a review result or a submission.

### Script app verify skill

Use [splash-app-verify](skills/splash-app-verify/SKILL.md) when writing or
changing a script app (`main.splash`). It moves the work to the front: write the
contract and the scenarios before the code, keep the app in a narrow testable
shape, and run the scenarios in `card-host` after every edit, with host
services answered from the scenario. A failed scenario writes a repair request
in the shape [MODEL-VALIDATION](docs/MODEL-VALIDATION.md) asks for. It checks
behavior and placement, not looks, and does not replace the gate or the visual
review.

### App card UX skill

Use [octoscript-app-card-ux](skills/octoscript-app-card-ux/SKILL.md) when designing
Expand Down
4 changes: 4 additions & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ macOS 上的原生公开签名目录安装及 0.2.0 → 0.2.1 更新保留了本

需要人来把关的节点(Release 工作流、发布者身份与隐私文本、平台声明、付费图像生成、视觉确认、Release 的 tag、提交)都列在 [AGENTS.md](AGENTS.md#how-to-work) 中;[flows/README.zh-CN.md](flows/README.zh-CN.md#每个流程都遵循同一份约定) 说明所有流程共有的那些节点。Agent 绝不伪造人的确认、评审结果或提交记录。

### 脚本应用验证技能

编写或修改脚本应用(`main.splash`)时,使用 [splash-app-verify](skills/splash-app-verify/SKILL.md)。它把工作前移:先写契约和测试场景再写代码,让应用保持可测试的收窄结构,每次修改后在 `card-host` 中运行全部场景,宿主服务的回复由场景提供。场景失败时,按 [MODEL-VALIDATION](docs/MODEL-VALIDATION.zh-CN.md) 要求的格式写出修复请求。它检查行为和位置,不评判外观,也不替代准入检查和视觉评审。

### AppCard UX 技能

设计或验收速览卡片时,使用 [octoscript-app-card-ux](skills/octoscript-app-card-ux/SKILL.md)。它补充摘要 → 展开卡片 → 完整工作区的任务流程、Chat/编辑/审阅共用状态、键盘和滚动检查,以及保留指定模型创作归属的修复循环。验收矩阵区分本地行为、真实 Agent 执行、外部业务效果和视觉批准,历史分数不能代替新卡片的验收。
Expand Down
5 changes: 5 additions & 0 deletions docs/MODEL-VALIDATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ from the final source: a successful tool call or a model's summary is not enough
Use this guide alongside the selected [flow](../flows/README.md), not as a
replacement for its admission, visual-review or publishing gates.

For script apps, the [script app verify skill](../skills/splash-app-verify/SKILL.md)
turns this loop into scenario files that run in `card-host` after every edit,
with host services answered from the scenario, and writes the executable
feedback below when one fails.

For Glance card work, the [app-card UX skill](../skills/octoscript-app-card-ux/SKILL.md)
adds a task journey and acceptance matrix for workspace expansion, shared editing
state, phone keyboard reachability and retained context. It reuses this model
Expand Down
2 changes: 2 additions & 0 deletions docs/MODEL-VALIDATION.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

宿主提供相应工具后,编码模型可以编写应用、查看实际渲染结果、注入输入并修复失败。完成与否取决于最终源码的验证证据,不能仅凭工具调用成功或模型自己的总结。本指南配合所选[设计流程](../flows/README.zh-CN.md)使用,不替代准入、视觉评审或发布检查。

脚本应用可使用[脚本应用验证技能](../skills/splash-app-verify/SKILL.md):它把这个循环写成场景文件,每次修改后在 `card-host` 中运行,宿主服务的回复由场景提供;场景失败时写出下文的可执行反馈。

速览卡片可进一步使用 [AppCard UX 技能](../skills/octoscript-app-card-ux/SKILL.md),补充工作区展开、共享编辑状态、手机键盘可达性和上下文保留的任务流程与验收矩阵。该技能复用本文的模型验证循环,不另建模型服务或截图协议。

## 选择测试方式
Expand Down
6 changes: 5 additions & 1 deletion docs/SCRIPT-API.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ host.request("mail.list", {account: account.id folder: folder.id offset: 0 limit
| Missing | `to_lower to_upper contains starts_with ends_with` | use `search(p) == 0`, `search(p) >= 0`, or a regex |
| Arrays | `push pop clear len remove(i) retain(fn)` | no `sort map filter join insert index_of slice` |
| Objects | `o.k`, `o[k]`, `delete`, `extend`, `o += {k: v}` | |
| Type checks | `v.is_string()`, `is_array()`, `is_object()`, `is_number()`, `is_bool()`, `is_nil()`; also `is_nan is_color is_regex is_err is_id is_opcode` | Every value has every one of these (`MP/platform/script/src/native.rs`: the `types` list and the loop after it), so they are safe on parsed JSON of unknown shape. `is_array` and `is_string` **✓ run** in `card-host` on Linux ([splash-app-verify example](../skills/splash-app-verify/examples/titled-notes/bundle/main.splash)). |
| Numbers → text | `"" + n` | |
| Loops | `for i in n` (0..n-1), `for v in array`, `for i v in array`, `for k v in object`, `a..b` ranges, `loop {}`, `while` | there is **no `range()`** (**✓ run**: `for i in 3` sums to 3; `for k v in {a:1 b:2}` gives `ab`) |

Expand Down Expand Up @@ -328,7 +329,10 @@ installed apps; disclosure changes do not expose the host profile.
an exponent.
- **`on_render` empty state:** write `if list.len() == 0 { Empty } for i in list.len() { Row }`.
Do not write `if … {…} else for …`: the empty branch draws nothing and stale
rows stay on screen (**✓ run**).
rows stay on screen (**✓ run**). The braced form `if … {…} else { for … }`
fails the same way: the empty label is never drawn (**✓ run**, a scenario in
[splash-app-verify](../skills/splash-app-verify/SKILL.md) against a copy of its
example with only that change).
- **Backgrounds:** `View{show_bg: true draw_bg.color: …}` draws no background
in `card-host`, whether always visible or shown later with
`set_visible(true)` (**✓ run**); `SolidView{draw_bg.color: …}` does. Use
Expand Down
190 changes: 190 additions & 0 deletions skills/splash-app-verify/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
---
name: splash-app-verify
description: Build or change an OctoSense Splash script app (main.splash) so it is right by construction and verified like backend code - write observable scenarios before the code, keep the app in a narrow testable shape, and run the scenarios in card-host with faked host services after every edit. Use when creating or changing a script app, when a model keeps breaking its UI, or before hand-off.
---

# Splash app verify

A UI app is hard for a model to check: its logic lives in the screen, it calls
host services that a development machine does not have, and seeing it means
starting a whole window. So models write blind and a person finds the bugs
later, when they cost the most. This skill moves the work to the front:

- **Prevent first.** Fix what "done" looks like, and the shape the code may
take, before writing it. Most UI bugs come from an open-ended process: a
layout nobody budgeted, a host reply nobody planned for, logic spread through
click handlers. A narrow process leaves less room for them.
- **Test like a backend.** Each behavior is a scenario file. A runner starts the
real app in `card-host`, answers its `host.request` calls from the scenario,
clicks and types through the remote bridge, and checks what the screen, the
storage jail and the log show. Run it after every edit.
- **Repair one thing at a time.** A failed scenario writes `FEEDBACK.md`: the
steps, what was expected, what the screen had, the evidence and the scope of
the fix, in the shape [MODEL-VALIDATION](../../docs/MODEL-VALIDATION.md)
asks for. Hand it to the author, a person or a model, unchanged.

This skill adds to the chosen flow ([script-app](../../flows/script-app/FLOW.md));
it does not replace the gate, the visual review or publishing.

## 0. Set up once

```sh
skills/splash-app-verify/scripts/setup.sh # Linux: Xvfb + Mesa; builds card-host if missing
python3 skills/splash-app-verify/scripts/uitest.py doctor
```

`doctor` must print `[ok]` for `card-host` and the display. `setup.sh` records
the `card-host` it finds or builds, so later shells need no variable;
`--card-host` and `OCTO_CARD_HOST` still win. Build `card-host` from current
App Hub `main`: it carries the Splash runtime the current hosts run. On a
machine with no display the runner starts its own Xvfb; nothing appears on
anyone's screen.

## 1. Write the contract before any code

Write `BRIEF.md` (outside `bundle/`) with these tables. Every later step checks
against them, so a gap here becomes a gap in the app.

| Table | One row per | Columns |
| --- | --- | --- |
| Screens and states | screen × state: first open with data, empty, loading, ready, each failure | what the person sees: the exact texts that must and must not show |
| Actions | button, input or gesture | widget id (`name :=`), visible text, what changes, what is saved |
| Host services | `host.request` service | capability, the documented reply shape, every failure the app must show: not granted, no service, an error, no answer, an authorization that ended. Name the host: on OctoSense a capability is a disclosure; Rinx refuses a mini-app the services it was not granted |
| Storage | file in the jail | format, version, what happens when it is missing or corrupt |
| Sizes | size the app is given: a phone, and the rectangle each shell gives it inside its own window | what must be on the first screen without scrolling; what may need a scroll |

Cite where each host service and API comes from ([SCRIPT-API](../../docs/SCRIPT-API.md),
[HOST-SERVICES](../../docs/HOST-SERVICES.md), the runtime source). If nothing
documents it, the app cannot use it.

Then turn the tables into scenarios, **before** `main.splash` exists or before
you change it. At least:

1. the first screen with its data loaded;
2. the main path, end to end;
3. the empty state;
4. each host-service failure from the table;
5. a restart: storage seeded, the saved state comes back;
6. the main path at the smallest and the largest size.

The format is in [references/SCENARIOS.md](references/SCENARIOS.md). Put the
normal host replies and repeated sequences (load, confirm) in shared `_parts`,
so each scenario states only what differs; a Rinx mini-app's shared part sets
`"host_profile": "rinx"`. Run them once on the old source,
or on the bare template. A new scenario must fail first; one that passes
before the feature exists tests nothing.

## 2. Keep the app in a narrow shape

Write the app in the shape [references/SHAPE.md](references/SHAPE.md) shows. Each
rule there closes off a class of bug:

- State in top-level `let`s. Decisions in plain functions that take and return
data. The screen updated only from a few `sync_*` functions.
- One function per host service, handling every reply in the contract, and
ignoring a reply that arrives after its request was replaced.
- A `name :=` id on every widget a scenario or a function touches.
- Lists drawn with the documented empty-state form: `if list.len() == 0 { … }`
then `for …`, never in an `else`.
- Layout from `Fill`/`Fit`, wrapping rows and one scroll container, with a
budget per region for the smallest size. No fixed heights around text.
- Only documented APIs; `//` comments; `#x` colors; no reserved words as names.

## 3. Build one path at a time; test after every edit

1. Pick the next red scenario in contract order: first screen, then the main
action, then its failures, then the next action.
2. Make the smallest change that can turn it green.
3. Run all scenarios:
```sh
python3 skills/splash-app-verify/scripts/uitest.py run <app>/bundle <app>/tests
```
4. Green: commit, take the next one. Red: read `FEEDBACK.md`, fix only what it
names, run again. If a scenario that was green turns red, undo the last edit
before anything else.
5. `ENV` (exit status 2): the machine failed, not the app. The run writes
`ENVIRONMENT.md` instead of `FEEDBACK.md`; it is not a repair request. Do not
change the app for it: fix the display, the card-host or the disk
(`uitest.py doctor`), then run again.

Keep exactly one scenario you are working on red. Do not edit a scenario to
make it pass unless the brief changed, and then say so in the commit.

## 4. Sizes, then look at it

When all scenarios pass at every size, take the screenshots the flow asks for
(`tools/octo shot`) and have a person, or a model that can read images, review
them against the brief. The runner checks text, placement inside the viewport,
enablement, counts, host calls, files and runtime errors. It does not judge
looks, and a clipped glyph inside a widget that is itself in view can pass.

## 5. Real host, last

The fixtures cover every reply in the contract, including the ones a real host
makes hard to produce: another person posting, a grant that expired, a
switched account, a rate limit. A real host is for acceptance, not discovery:
one pass of the main path and the failures you can trigger safely.

- **Prepare it before you need it.** List the accounts, rooms and permissions
each real check needs. Set them up a day ahead and try each once with a
request that changes nothing.
- **Diagnose before you work around.** When the host does something the
contract does not say, shrink it to the smallest request that shows it, read
the client and server source for that request, and write the cause down with
file and line before changing anything. Report it under "Gaps found" with
the reproduction. If it is the host's bug, keep the app's contract.
- **No unattended effects.** A scheduled check may probe and notify. Sending,
creating rooms, inviting and anything else that changes the world waits for
a person.
- **Bind evidence to source.** Record the `main.splash` SHA-256 each real run
used. A run on an earlier source is history, not proof of the current one.

## 6. Hand off

Report, from `build/uitest/summary.json` and the flow's checks:

- which scenarios pass, at which sizes, against which `main.splash` SHA-256;
- what the fixtures stood in for and was not run for real: the host services,
permission sheets, a phone keyboard, provider behavior;
- what ran on a real host, against which source;
- anything a person must still look at.

## What the runner does

`scripts/uitest.py run <bundle> <scenario.json | dir>`:

1. copies the bundle and prepends a fake `host` to the copy's `main.splash`, so
`host.request` gets the scenario's replies; the code under test is otherwise
unchanged, and the bundle under test is never edited;
2. starts `card-host` on the copy (hidden; under a private Xvfb on a headless
Linux machine) at each size the scenario lists;
3. performs the steps through the remote routes and checks each expectation;
a runtime error in the log fails the step;
4. stops at the first failure and writes `snap.json`, `screen.png`,
`card-host.log`, `calls.json` and `FEEDBACK.md` under `build/uitest/`;
every run also keeps the app's jail files there. When the environment
failed instead (no display, a card-host that exited or would not stop,
evidence it cannot read), it writes `ENVIRONMENT.md`, never `FEEDBACK.md`.

Exit status: 0 all passed, 1 a scenario failed, 2 the environment, the
evidence or a scenario file is broken (not the app's fault). Scenario files
are checked when they load: an unknown key, a value of the wrong kind or a
file name that leaves the jail stops the run before any app starts. The test
copy keeps the bundle's capabilities and storage as written; the fake host
reports calls through card-host's log. Under each result the runner notes what
is true of the run but not a failure, such as a service called without being
declared. `summary.json` is rewritten by every run and names its `run_id` and
`status`.

It sees what `/snap` lists, which is what is on screen; scroll to a part of
the app before checking it ([what the runner sees](references/SCENARIOS.md#what-the-runner-sees)).

A card-host that ignores `/quit` is stopped: on Linux and macOS its whole
process group (terminate, then kill), on Windows the process itself. One that
survives that is reported as an environment error, never left running
silently. The recorded runs are on Linux; the macOS and Windows paths are
covered by unit tests only.

Examples: [examples/notes](examples/notes) tests the App Flow template app;
[examples/titled-notes](examples/titled-notes) is a small app in the shape,
with its scenarios.
4 changes: 4 additions & 0 deletions skills/splash-app-verify/agents/openai.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
interface:
display_name: "Splash App Verify"
short_description: "Scenarios first, then code: test script apps in card-host after every edit"
default_prompt: "Use $splash-app-verify to write the contract and scenarios for this script app, then build or change main.splash one path at a time and run every scenario in card-host after each edit."
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "first screen: empty list and the add row in view",
"sizes": ["412x892", "990x613"],
"steps": [
{"expect": {"text": ["My Notes", "No notes yet.", "Tap a note to remove it."],
"in_view": [{"id": "entry"}, {"text": "Add"}]}}
]
}
10 changes: 10 additions & 0 deletions skills/splash-app-verify/examples/notes/tests/02-add-note.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "add a note: it shows, the empty text goes, it is saved",
"steps": [
{"type": {"into": {"id": "entry"}, "text": "Buy milk"}},
{"click": {"text": "Add"}},
{"wait": {"text": ["Buy milk"], "absent": ["No notes yet."],
"files": [{"name": "notes.json", "contains": ["Buy milk"]}]}},
{"expect": {"count": [{"of": {"text": "Buy milk"}, "n": 1}]}}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "an empty input adds nothing",
"steps": [
{"click": {"text": "Add"}},
{"expect": {"text": ["No notes yet."], "files": [{"name": "notes.json", "missing": true}]}}
]
}
7 changes: 7 additions & 0 deletions skills/splash-app-verify/examples/notes/tests/04-restart.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "restart: saved notes come back",
"storage": {"notes.json": ["First", "Second"]},
"steps": [
{"wait": {"text": ["First", "Second"], "absent": ["No notes yet."]}}
]
}
10 changes: 10 additions & 0 deletions skills/splash-app-verify/examples/notes/tests/05-remove.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "tap a note to remove it",
"storage": {"notes.json": ["Keep", "Drop"]},
"steps": [
{"wait": {"text": ["Keep", "Drop"]}},
{"click": {"text": "Drop"}},
{"wait": {"text": ["Keep"], "absent": ["Drop"]}},
{"expect": {"files": [{"name": "notes.json", "contains": ["Keep"]}]}}
]
}
Loading