English · 日本語
Core concepts and design principles¶
Every module in Bajutsu follows from a small set of principles. Read this before the implementation details to understand why things are shaped the way they are. The full design rationale is in
DESIGN.md.
Related: architecture · selectors · run-loop
1. AI is the author and the investigator, never the judge¶
The non-determinism, cost, and latency of an LLM (large language model) must not enter the CI (continuous integration) gate. This prohibition is the top-level constraint, and it directly produces the two-tier structure (below).
| Command | Tier | AI | How pass/fail is decided |
|---|---|---|---|
record |
Tier 1 | author | explores and proposes the next move → writes a deterministic scenario (recording) |
run |
Tier 2 | none | each step is act → wait → verify; pass/fail comes only from the expect machine assertions (run-loop) |
codegen |
— | none | structural mapping of a scenario to XCUITest (codegen) |
The run path contains no anthropic call at all. The single exception is --dismiss-alerts
(it visually dismisses OS system alerts); it prepares the environment rather than deciding
pass/fail, and runs only when explicitly opted in
(the alert guard).
2. Two tiers (Tier 1 / Tier 2)¶
- Tier 1 = AI live operation: exploration and authoring. Flexible but non-deterministic. The artifact (the scenario YAML) is AI-independent and, from then on, owned and edited by humans.
- Tier 2 = deterministic runner: CI regression. The same scenario follows the same path every time.
Both use the same observe → act → verify loop, but the layer where AI participates is
strictly separated.
The scenario is just YAML, so humans extend it with authoring features that never involve AI: reusable
usecomponents, data-driven rows (data/dataFile), secret variables (${secrets.X}),tags for selection,setLocation/pushdevice steps, and file- / scenario-leveldescriptions (scenarios).
3. Determinism first (four concrete mechanisms)¶
The structure of the code enforces Bajutsu's "deterministic" behavior.
- An ambiguous selector fails immediately. When a single action's target matches two or more
elements, Bajutsu raises
AmbiguousSelectorinstead of tapping whatever matched first (selectors). Of these four mechanisms, ruling out non-determinism structurally matters most. - Condition waits only; no fixed sleep. Waiting polls
query()until a condition holds. Atimeoutis mandatory (no infinite waits) (run-loop). - Start from a clean environment. Each test, by default,
simctl erases before boot/launch, cutting off contamination from the previous test. State is injected via launch env / deeplink (drivers). - Pass/fail is machine-checkable only. There is no "looks like it passed" judgment. The
machine assertions are
exists/value/label/count/enabled/disabled/selected/request/visual(selectors).
The scope is narrow: stable identifiers only stabilize the determinism of selection. Flakiness from timing, state, or the network is handled separately by waits, the environment, and
mocks.
4. Stable selectors (prefer accessibilityIdentifier)¶
Always write selectors by a non-localized, unique, data-derived id. On iOS that is
accessibilityIdentifier; on the web it is data-testid; on Android it is resource-id. The
selector YAML is the same across backends — only the attribute the backend reads to satisfy it
differs. The reason is to eliminate flakiness from layout changes, translation, and coordinate drift.
label is for VoiceOver / AI semantic understanding; because its wording changes with
localization, it is not used as a selector (only as an auxiliary / disambiguator). The naming
convention (<namespace>.<element>) is in
configuration.
5. The stability ladder¶
UI actions are attempted most-stable-first, where "most stable" refers to selection (which element), not actuation. idb (the iOS actuator) actuates by coordinate tap at the frame center regardless, so what changes between rungs is how the element is chosen. The lower the rung, the more fragile.
| Rung | Selection (which element) | Stability |
|---|---|---|
| 1 | resolve uniquely by id |
most stable (independent of layout / translation / coordinates) |
| 2 | resolve by label / traits |
weak to localization |
| 3 | index / raw coordinates |
breaks on layout changes. Last resort |
Actuation is always a frame-center coordinate tap: idb exposes no semantic tap, so the run loop resolves the unique element and taps its frame center.
The actuator — the backend that performs actions — is the first available one in the
most-stable-first backend list; it is fixed once at the start of a run and held for the whole run
(to avoid the non-determinism of two drivers operating one device). The full
driver / backend / actuator / platform
relationship, and which actuators each platform expands to, is in the glossary. Selection is always
by id, so scenarios do not change
(drivers).
6. App-agnostic (push differences into config)¶
The tool core, the drivers, and the runner do not depend on any app. To target a new app, you
change the app-side preparation (adding identifiers, etc.) and one targets.<name> config entry
— nothing else. The same implementation convention guarantees each app's determinism
(onboarding in configuration).
The same move makes Bajutsu platform-agnostic: a platform is a backend behind the Driver
interface (the actuator, §5). Web (playwright) and Android (adb) are new targets built this way
— each a new backend; the scenario format, selector resolution, assertions, the orchestrator,
evidence, and the reporter stay byte-for-byte the same. Per-platform differences live only in the
backend and config.
7. Evidence is rules (fire repeatedly)¶
The request "capture evidence every time a particular action happens" is stored not as a
one-shot instruction but as a trigger-based rule
(capturePolicy). This way the same
evidence reproduces without AI on the second run onward (evidence).
Where each principle shows up in the code (quick map)¶
| Principle | Main implementation |
|---|---|
| Ambiguous selector fails fast | drivers/base.py resolve_unique |
| Condition waits only | orchestrator/waits.py _wait |
| Clean environment | runner/launch.py launch_driver · simctl.py Env.erase |
| Machine assertions | assertions/ |
| Stability order / actuator | backends.py select_actuator · each drivers/*.py capabilities() |
| App-agnostic | config/resolve.py resolve → Effective |
| Evidence rules | orchestrator/loop.py _collect_captures · evidence/core.py |