Files
wursor/IMPLEMENTATION.md
T
SinachPat 4403c8c5e8 docs: PRD v1.3 — Tauri/Monaco shell, Grok model
- Shell decision changed from Electron + Code-OSS to Tauri (Rust) + Monaco Editor
  - Fast startup (~0.3-0.8s), low memory (100-200MB) via system webview
  - Rust backend for tool bus, knowledge graph parser, permission engine, runtime manager
  - Rationale documented vs Electron and Zed
- Model changed from Claude to Grok (xAI), BYO API key
- IMPLEMENTATION.md rewritten for Rust/Tauri stack: crates layout, Rust TDD tests, cargo-based CI
- Phase 1 roadmap, test strategy, decisions section updated to match
2026-08-13 13:12:04 +01:00

1024 lines
38 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Implementation Guide — Wursor v1
**Version:** 1.1
**Source:** [PRD.md](./PRD.md) v1.3
**Method:** Test-driven development (TDD) — every module is written against its tests before its implementation.
---
## Table of Contents
1. [Architecture Overview](#1-architecture-overview)
2. [Project Structure](#2-project-structure)
3. [Build Phases](#3-build-phases)
4. [Phase 1 — Foundation (Weeks 18)](#4-phase-1--foundation-weeks-18)
- [Sprint 1: Tauri shell + Monaco Editor + project scaffold](#sprint-1)
- [Sprint 2: Rust tool bus](#sprint-2)
- [Sprint 3: wp-env runtime manager](#sprint-3)
- [Sprint 4: Agent chat + diff review](#sprint-4)
- [Sprint 5: Permission engine + WP-CLI](#sprint-5)
- [Sprint 6: P0 playbooks + first-run](#sprint-6)
- [Sprint 7: Integration + exit criteria](#sprint-7)
- [Sprint 8: Polish + alpha readiness](#sprint-8)
5. [Phase 2 — Intelligence (Weeks 916)](#5-phase-2--intelligence-weeks-916)
6. [TDD Rules](#6-tdd-rules)
7. [CI/CD Pipeline](#7-cicd-pipeline)
8. [Glossary](#8-glossary)
---
## 1. Architecture Overview
```
┌──────────────────────────────────────────────────────────┐
│ Tauri Shell (Rust native process) │
│ ┌──────────────────────┐ ┌───────────────────────────┐ │
│ │ System Webview (TS) │ │ Rust Backend │ │
│ │ ┌────────────────┐ │ │ ┌─────────────────────┐ │ │
│ │ │ Monaco Editor │ │ │ │ Tool bus (spawn, │ │ │
│ │ │ (editor core) │ │ │ │ exec, stream) │ │ │
│ │ ├────────────────┤ │ │ ├─────────────────────┤ │ │
│ │ │ Wursor panels │ │ │ │ Knowledge graph │ │ │
│ │ │ - preview │ │ │ │ parser (PHP/JSON │ │ │
│ │ │ - diff │ │ │ │ scan, 10k files │ │ │
│ │ │ - state diff │ │ │ │ in ~200ms) │ │ │
│ │ │ - chat │ │ │ ├─────────────────────┤ │ │
│ │ │ - status bar │ │ │ │ Permission engine │ │ │
│ │ └────────────────┘ │ │ │ + secret redaction │ │ │
│ │ Web frontend │ │ ├─────────────────────┤ │ │
│ │ (TypeScript/HTML) │ │ │ Runtime manager │ │ │
│ └──────────────────────┘ │ │ (Docker socket, │ │ │
│ │ │ wp-env lifecycle) │ │ │
│ │ ├─────────────────────┤ │ │
│ │ │ State Diff engine │ │ │
│ │ │ (create, evaluate, │ │ │
│ │ │ rollback, persist) │ │ │
│ │ └─────────────────────┘ │ │
│ └───────────────────────────┘ │
└───────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────┐
│ Site Runtime (wp-env / Docker) │
│ WordPress + MySQL + WP-CLI │
└──────────────────────────────────────────────────────────┘
```
**Key structural decisions:**
- **Monorepo** with a `crates/` directory for Rust packages and a `webview/` directory for the TypeScript frontend
- Rust crates communicate via `tauri::command` IPC to the webview
- Each Rust crate has its own `tests/` directory (integration) and inline `#[cfg(test)]` unit tests
- Integration tests use fixture-based WordPress repos in CI
- E2E tests use Playwright against the Tauri webview
---
## 2. Project Structure
```
wursor/
├── src-tauri/ # Tauri Rust backend
│ ├── src/
│ │ ├── main.rs # Tauri app entry
│ │ ├── lib.rs # Plugin registration
│ │ ├── commands/ # #[tauri::command] IPC handlers
│ │ │ ├── fs.rs # File read/write
│ │ │ ├── project.rs # Project open/detect
│ │ │ ├── runtime.rs # wp-env start/stop/status
│ │ │ ├── tools.rs # Tool bus dispatch
│ │ │ ├── permissions.rs # Policy query
│ │ │ └── verify.rs # Screenshot/HTTP check
│ │ ├── tool-bus/
│ │ │ ├── mod.rs # Tool registry
│ │ │ ├── registry.rs # Name → {schema, handler, category}
│ │ │ ├── tools/
│ │ │ │ ├── fs.rs # fs.read, fs.write, fs.apply_patch
│ │ │ │ ├── wpcli.rs # wpcli.run (categorized)
│ │ │ │ ├── site.rs # site.browse, site.screenshot, site.request
│ │ │ │ ├── db.rs # db.query (read-only)
│ │ │ │ ├── lint.rs # lint.phpcs
│ │ │ │ ├── test_runner.rs# test.phpunit
│ │ │ │ └── index.rs # index.search, index.graph_lookup (stub)
│ │ │ └── executor.rs # Shell spawn, timeout, stream
│ │ ├── knowledge-index/
│ │ │ ├── mod.rs
│ │ │ ├── scanner.rs # Static PHP/JSON scan (Rust-native parser)
│ │ │ ├── enricher.rs # WP-CLI runtime enrichment
│ │ │ ├── graph.rs # Node, edge, store
│ │ │ ├── freshness.rs # Staleness tracking
│ │ │ └── queries.rs # Graph query API
│ │ ├── state-diff/
│ │ │ ├── mod.rs
│ │ │ ├── lifecycle.rs # create → review → stage → apply → verify → commit
│ │ │ ├── evaluator.rs # Blast radius, intent
│ │ │ ├── rollback.rs # Inverse generation
│ │ │ └── serializer.rs # .state-diff.json format
│ │ ├── runtime-manager/
│ │ │ ├── mod.rs
│ │ │ ├── interface.rs # RuntimeAdapter trait
│ │ │ ├── adapters/
│ │ │ │ └── wp_env.rs # wp-env adapter (v1 only)
│ │ │ ├── lifecycle.rs # State machine
│ │ │ └── logs.rs # Log tailing
│ │ ├── permission-engine/
│ │ │ ├── mod.rs
│ │ │ ├── tiers.rs # Permission tiers enum
│ │ │ ├── evaluator.rs # Tool call → policy check
│ │ │ ├── redactor.rs # Secret redaction (.env, wp-config)
│ │ │ └── config.rs # User policy (.wursor/policy.json)
│ │ ├── verify/
│ │ │ ├── mod.rs
│ │ │ ├── screenshot.rs # Screenshot capture
│ │ │ ├── http_check.rs # URL load + status + error sniff
│ │ │ ├── editor_check.rs # Block editor route check
│ │ │ └── reporter.rs # Result formatting
│ │ ├── agent-bridge/
│ │ │ ├── mod.rs
│ │ │ ├── client.rs # Grok API client (BYO key)
│ │ │ ├── tool_schemas.rs # Tool schemas → Grok format
│ │ │ ├── context.rs # System prompt + WP context
│ │ │ └── fallback.rs # Error handling + retry
│ │ └── playbooks/
│ │ ├── mod.rs
│ │ ├── registry.rs # Playbook registry
│ │ ├── dynamic_block.rs
│ │ ├── child_theme.rs
│ │ ├── cpt.rs
│ │ └── plugin.rs
│ ├── tests/ # Integration tests
│ │ ├── tool_bus_test.rs
│ │ ├── runtime_manager_test.rs
│ │ ├── knowledge_index_test.rs
│ │ ├── state_diff_test.rs
│ │ ├── permission_engine_test.rs
│ │ └── playbooks_test.rs
│ ├── fixtures/ # Test WP repos
│ │ ├── classic-theme/
│ │ ├── block-theme/
│ │ └── single-plugin/
│ ├── Cargo.toml
│ └── tauri.conf.json
├── webview/ # Tauri webview frontend (TypeScript)
│ ├── src/
│ │ ├── main.tsx # App entry
│ │ ├── App.tsx # Root component
│ │ ├── components/
│ │ │ ├── Editor.tsx # Monaco Editor wrapper
│ │ │ ├── Preview.tsx # Site preview iframe
│ │ │ ├── ChatPanel.tsx # Agent chat
│ │ │ ├── DiffPanel.tsx # Side-by-side file diff
│ │ │ ├── StateDiffPanel.tsx # State Diff lifecycle UI
│ │ │ ├── StatusBar.tsx # Site status, permissions
│ │ │ ├── Terminal.tsx # Integrated terminal
│ │ │ └── FirstRun.tsx # Onboarding wizard
│ │ ├── hooks/
│ │ │ ├── useToolBus.ts # Invoke Rust commands
│ │ │ ├── useRuntime.ts # Runtime state
│ │ │ └── useAgent.ts # Agent chat state
│ │ ├── utils/
│ │ │ ├── monaco-setup.ts # Monaco theme, WP stubs
│ │ │ └── tauri-api.ts # @tauri-apps/api wrappers
│ │ └── styles/
│ │ └── global.css
│ ├── __tests__/
│ │ ├── App.test.tsx
│ │ ├── ChatPanel.test.tsx
│ │ ├── DiffPanel.test.tsx
│ │ └── FirstRun.test.tsx
│ ├── index.html
│ ├── vite.config.ts
│ ├── tsconfig.json
│ └── package.json
├── e2e/ # End-to-end tests
│ ├── tauri/
│ │ ├── open-project.test.ts
│ │ ├── detect-wp.test.ts
│ │ ├── boot-preview.test.ts
│ │ ├── playbook-dynamic-block.test.ts
│ │ └── verify-preview.test.ts
│ ├── fixtures/
│ │ ├── sample-theme/
│ │ └── sample-plugin/
│ └── playwright.config.ts
├── Cargo.toml # Workspace root
├── package.json # Scripts, dev tooling
├── pnpm-workspace.yaml
├── rust-toolchain.toml
└── .github/workflows/
├── ci.yml # Rust tests + webview tests on PR
└── e2e.yml # E2E on release branch
```
---
## 3. Build Phases
| Phase | Weeks | Output | Exit criteria |
|-------|-------|--------|---------------|
| **Phase 1** | 18 | Tauri shell, Monaco Editor, Rust tool bus, wp-env, chat, playbooks, first-run | Clean machine → live preview ≤10 min; P0 playbook completes |
| **Phase 2** | 916 | Knowledge graph, State Diffs, quality gates, staging pull, closed alpha | All §11 baselines collected; State Diff lifecycle demoed |
| **Phase 3** | 1728 | Block/FSE workshop, deploy connectors, team playbooks, paid beta | — |
| **Phase 4** | 29+ | WooCommerce, multisite, maintenance agents, ecosystem | — |
---
## 4. Phase 1 — Foundation (Weeks 18)
8 sprints, one per week. Every sprint produces a passing integration test.
---
### Sprint 1: Tauri shell + Monaco Editor + project scaffold
**Goal:** A working Tauri window with Monaco Editor that opens a folder and shows a Wursor sidebar.
#### TDD sequence
**Step 1 — Write the test that defines "done"**
```typescript
// e2e/tauri/open-project.test.ts
import { test, expect } from '@playwright/test';
import { _electron as tauri } from 'tauri-playwright';
test('opens a folder and shows Wursor sidebar', async () => {
const app = await tauri.launch({
args: ['--project', '/path/to/fixtures/sample-theme'],
});
const window = await app.webview();
await expect(window.locator('.wursor-sidebar')).toBeVisible();
await expect(window.locator('.monaco-editor')).toBeVisible();
await app.close();
});
```
**Step 2 — Write the Rust unit tests**
```rust
// src-tauri/src/commands/project.rs (tests module)
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_open_valid_project() {
let result = open_project("/path/to/fixtures/sample-theme");
assert!(result.is_ok());
assert_eq!(result.unwrap().project_type, "classic-theme");
}
#[test]
fn test_open_invalid_path() {
let result = open_project("/nonexistent");
assert!(result.is_err());
}
}
```
**Step 3 — Write the webview unit tests**
```typescript
// webview/__tests__/App.test.tsx
describe('App', () => {
it('renders Monaco Editor after project open', () => { /* ... */ });
it('shows Wursor sidebar', () => { /* ... */ });
it('displays project name in the title bar', () => { /* ... */ });
});
```
**Step 4 — Implement**
- **`src-tauri/src/main.rs`** — Tauri builder, register commands
- **`src-tauri/src/commands/project.rs`** — `open_project` command
- **`webview/src/App.tsx`** — Root layout: editor + sidebar
- **`webview/src/components/Editor.tsx`** — Monaco Editor wrapper (`@monaco-editor/react`)
- **`webview/src/components/ChatPanel.tsx`** — Sidebar (placeholder)
- **`webview/src/utils/monaco-setup.ts`** — Dark theme, PHP stubs, `block.json` schemas
- **`src-tauri/tauri.conf.json`** — Window config, permissions
- **`src-tauri/Cargo.toml`** — Dependencies: tauri, serde, tokio, etc.
#### Deliverables
- Tauri app that opens a folder and shows Monaco Editor + sidebar
- `e2e/tauri/open-project.test.ts` passing
- Rust unit tests for `open_project` command
- Webview unit tests for `App` component
---
### Sprint 2: Rust tool bus
**Goal:** All tools from §8.3 are registered Rust commands with schemas, handlers, and shell execution.
#### TDD sequence
**Step 1 — Write the Rust unit tests**
```rust
// src-tauri/src/tool-bus/registry.rs
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_register_tool() {
let mut registry = ToolRegistry::new();
registry.register("fs.read", fs_schema(), fs_handler());
assert!(registry.get("fs.read").is_some());
}
#[test]
fn test_duplicate_tool_name_errors() {
let mut registry = ToolRegistry::new();
registry.register("fs.read", fs_schema(), fs_handler());
assert!(registry.register("fs.read", fs_schema(), fs_handler()).is_err());
}
}
// src-tauri/src/tool-bus/tools/fs.rs
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_fs_read_workspace_file() {
let result = fs_read(FsReadArgs { path: "fixtures/sample.txt".into() }).await;
assert!(result.is_ok());
assert_eq!(result.unwrap().content, "hello");
}
#[tokio::test]
async fn test_fs_read_rejects_path_traversal() {
let result = fs_read(FsReadArgs { path: "../../etc/passwd".into() }).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_fs_read_missing_file() {
let result = fs_read(FsReadArgs { path: "nonexistent.txt".into() }).await;
assert!(result.is_err());
}
}
// src-tauri/src/tool-bus/tools/wpcli.rs
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_wpcli_run_safe_command() {
let result = wpcli_run(WpCliArgs {
command: "wp option get blogname".into(),
category: "safe".into(),
}).await;
assert!(result.is_ok());
assert_eq!(result.unwrap().exit_code, 0);
}
#[tokio::test]
async fn test_wpcli_rejects_destructive_without_confirm() {
let result = wpcli_run(WpCliArgs {
command: "wp db drop".into(),
category: "destructive".into(),
}).await;
assert!(result.is_err());
assert!(result.unwrap_err().contains("confirmation required"));
}
}
// src-tauri/src/tool-bus/executor.rs
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_execute_shell_command() {
let output = execute("echo hello", None).await.unwrap();
assert_eq!(output.stdout.trim(), "hello");
}
#[tokio::test]
async fn test_execute_timeout() {
let result = execute("sleep 60", Some(Duration::from_millis(100))).await;
assert!(result.is_err());
}
}
```
**Step 2 — Implement**
- **`src-tauri/src/tool-bus/registry.rs`** — `HashMap<String, Tool>` with schema + handler
- **`src-tauri/src/tool-bus/tools/fs.rs`** — Path-scoped file operations
- **`src-tauri/src/tool-bus/tools/wpcli.rs`** — WP-CLI spawn with categorized allowlist
- **`src-tauri/src/tool-bus/tools/site.rs`** — HTTP fetch (reqwest) + screenshot via headless webview
- **`src-tauri/src/tool-bus/tools/db.rs`** — MySQL read-only query via mysql crate
- **`src-tauri/src/tool-bus/tools/lint.rs`** — PHPCS spawn
- **`src-tauri/src/tool-bus/tools/test_runner.rs`** — PHPUnit spawn
- **`src-tauri/src/tool-bus/tools/index.rs`** — Stub (throws "not implemented" until Phase 2)
- **`src-tauri/src/tool-bus/executor.rs`** — `tokio::process::Command` wrapper with timeout + streaming
#### Deliverables
- Rust tool registry with all 9 tools
- Each tool has unit tests for happy path, error path, and security boundary
- `cargo test --package tool-bus` passes
---
### Sprint 3: wp-env runtime manager
**Goal:** Start/stop/reset a WordPress site via wp-env from Rust, show status in the webview.
#### TDD sequence
**Step 1 — Write the tests**
```rust
// src-tauri/src/runtime-manager/lifecycle.rs
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_start_wp_env() {
let manager = RuntimeManager::new(WpEnvAdapter::new());
let status = manager.start().await.unwrap();
assert_eq!(status, RuntimeStatus::Running);
}
#[tokio::test]
async fn test_stop_wp_env() {
let manager = RuntimeManager::new(WpEnvAdapter::new());
manager.start().await.unwrap();
let status = manager.stop().await.unwrap();
assert_eq!(status, RuntimeStatus::Stopped);
}
#[tokio::test]
async fn test_status_when_stopped() {
let manager = RuntimeManager::new(WpEnvAdapter::new());
assert_eq!(manager.status().await, RuntimeStatus::Stopped);
}
#[tokio::test]
async fn test_log_streaming() {
let manager = RuntimeManager::new(WpEnvAdapter::new());
let mut logs = manager.stream_logs().await.unwrap();
let entry = logs.next().await;
assert!(entry.is_some());
}
}
// src-tauri/src/runtime-manager/adapters/wp_env.rs
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_spawn_wp_env_start() {
let adapter = WpEnvAdapter::new();
let result = adapter.start().await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_parse_wp_env_output() {
let output = "WordPress development site.\n\n http://localhost:8888\n\n✔ Okay!";
let parsed = WpEnvAdapter::parse_output(output);
assert_eq!(parsed.url, "http://localhost:8888");
}
#[tokio::test]
async fn test_handle_wp_env_not_found() {
let adapter = WpEnvAdapter::new();
// Simulate missing wp-env by clearing PATH
let result = adapter.start().await;
assert!(result.is_err());
assert!(result.unwrap_err().contains("wp-env not found"));
}
}
```
```typescript
// webview/__tests__/Preview.test.tsx
describe('Preview', () => {
it('shows a loading state while runtime starts', () => { /* ... */ });
it('renders the site iframe when running', () => { /* ... */ });
it('shows error state when runtime fails', () => { /* ... */ });
});
```
**Step 2 — Implement**
- **`src-tauri/src/runtime-manager/interface.rs`** — `RuntimeAdapter` trait
- **`src-tauri/src/runtime-manager/adapters/wp_env.rs`** — Spawns `npx wp-env start`, parses URL/creds
- **`src-tauri/src/runtime-manager/lifecycle.rs`** — State machine: Stopped → Starting → Running → Stopping → Stopped
- **`src-tauri/src/runtime-manager/logs.rs`** — Tail `wp-env logs` via tokio process
- **`src-tauri/src/commands/runtime.rs`** — `start_runtime`, `stop_runtime`, `runtime_status` IPC commands
- **`webview/src/components/Preview.tsx`** — iframe pointing to site URL
- **`webview/src/components/StatusBar.tsx`** — Runtime status indicator
- **`webview/src/hooks/useRuntime.ts`** — React hook subscribing to runtime state
#### Deliverables
- Rust runtime manager with wp-env adapter
- Preview panel showing the live site
- Status bar showing runtime state
- `cargo test --package runtime-manager` passing
---
### Sprint 4: Agent chat + diff review
**Goal:** Chat panel that sends tasks to Grok, receives tool calls, dispatches through the Rust tool bus, and shows diffs.
#### TDD sequence
**Step 1 — Write the tests**
```rust
// src-tauri/src/agent-bridge/client.rs
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_send_message_to_grok() {
let client = GrokClient::new("test-key");
let response = client.send("Add a paragraph to index.php").await.unwrap();
assert_eq!(response.message_type, "tool_call");
}
#[tokio::test]
async fn test_handle_api_error() {
let client = GrokClient::new("invalid-key");
let result = client.send("hello").await;
assert!(result.is_err());
assert!(result.unwrap_err().contains("API error"));
}
}
// src-tauri/src/agent-bridge/tool_schemas.rs
#[cfg(test)]
mod tests {
#[test]
fn test_converts_tool_registry_to_grok_format() {
let schemas = to_grok_format(&registry);
assert_eq!(schemas[0].name, "fs.read");
assert!(schemas[0].input_schema.is_object());
}
}
// src-tauri/src/agent-bridge/context.rs
#[cfg(test)]
mod tests {
#[tokio::test]
async fn test_builds_system_prompt_with_wp_semantics() {
let ctx = ContextBuilder::new()
.with_project_rules("fixtures/WORDPRESS.md")
.build();
assert!(ctx.contains("WordPress"));
assert!(ctx.contains("template hierarchy"));
}
}
```
```typescript
// webview/__tests__/ChatPanel.test.tsx
describe('ChatPanel', () => {
it('sends a message and displays the response', () => { /* ... */ });
it('shows tool calls as expandable cards', () => { /* ... */ });
it('shows diffs in a side-by-side view', () => { /* ... */ });
it('shows error state when Grok is unreachable', () => { /* ... */ });
});
```
**Step 2 — Implement**
- **`src-tauri/src/agent-bridge/client.rs`** — Grok API client (messages API, tool use, streaming)
- **`src-tauri/src/agent-bridge/tool_schemas.rs`** — Convert tool registry → Grok `tools` array
- **`src-tauri/src/agent-bridge/context.rs`** — Build system prompt with WP semantics, project rules, graph state
- **`src-tauri/src/agent-bridge/fallback.rs`** — Error handling, retry with exponential backoff
- **`webview/src/components/ChatPanel.tsx`** — Chat UI (message list, input, tool call cards, streaming)
- **`webview/src/components/DiffPanel.tsx`** — Side-by-side diff view (Monaco diff editor)
- **`webview/src/hooks/useAgent.ts`** — React hook for agent state
#### Deliverables
- Working chat panel that sends to Grok and executes tool calls
- Diff view showing file changes
- `cargo test --package agent-bridge` passing
- Chat panel unit tests passing
---
### Sprint 5: Permission engine + WP-CLI
**Goal:** WP-CLI commands categorized and gated by permission tiers. Secrets redacted from agent context.
#### TDD sequence
**Step 1 — Write the tests**
```rust
// src-tauri/src/permission-engine/tiers.rs
#[cfg(test)]
mod tests {
#[test]
fn test_tiers_ordered() {
assert!(Tier::ReadFs < Tier::EditFs);
assert!(Tier::EditFs < Tier::WpCliSafe);
assert!(Tier::WpCliSafe < Tier::WpCliDestructive);
}
#[test]
fn test_all_tiers_defined() {
assert_eq!(Tier::variants().len(), 7);
}
}
// src-tauri/src/permission-engine/evaluator.rs
#[cfg(test)]
mod tests {
#[tokio::test]
async fn test_allows_call_within_tier() {
let policy = Policy::default();
assert!(policy.evaluate("fs.read", Tier::ReadFs).await.is_ok());
}
#[tokio::test]
async fn test_blocks_call_above_tier() {
let policy = Policy::default().with_tier(Tier::ReadFs);
assert!(policy.evaluate("wpcli.run", Tier::WpCliSafe).await.is_err());
}
#[tokio::test]
async fn test_requires_confirm_for_destructive() {
let policy = Policy::default().with_tier(Tier::WpCliDestructive);
let result = policy.evaluate("wpcli.run", Tier::WpCliDestructive).await.unwrap();
assert!(result.confirmation_required);
}
#[tokio::test]
async fn test_blocks_production_writes() {
let policy = Policy::default().with_environment(Environment::Production);
assert!(policy.evaluate("fs.write", Tier::EditFs).await.is_err());
}
}
// src-tauri/src/permission-engine/redactor.rs
#[cfg(test)]
mod tests {
#[tokio::test]
async fn test_redacts_env_values() {
let text = "DB_PASSWORD=secret123";
let secrets = vec!["secret123".to_string()];
assert!(!redact(text, &secrets).contains("secret123"));
}
#[tokio::test]
async fn test_redacts_wp_config_constants() {
let text = "define('DB_PASSWORD', 'secret123');";
let result = redact(text, &[]).await;
assert!(result.contains("[REDACTED]"));
}
#[tokio::test]
async fn test_preserves_variable_names() {
let text = "DB_PASSWORD=secret123";
let result = redact(text, &["secret123".to_string()]);
assert!(result.contains("DB_PASSWORD"));
}
}
```
**Step 2 — Implement**
- **`src-tauri/src/permission-engine/tiers.rs`** — Ordered enum, 7 tiers
- **`src-tauri/src/permission-engine/evaluator.rs`** — Policy evaluator: current tier, environment, confirmation flag
- **`src-tauri/src/permission-engine/redactor.rs`** — Scan text for secrets, redact before sending to agent
- **`src-tauri/src/permission-engine/config.rs`** — User-defined policy from `.wursor/policy.json`
- Wire into **`src-tauri/src/tool-bus/tools/wpcli.rs`** — check permission before executing
#### Deliverables
- Rust permission engine with all 7 tiers
- Secret redaction for `.env` and `wp-config.php`
- WP-CLI tool categorized and gated
- `cargo test --package permission-engine` passing
---
### Sprint 6: P0 playbooks + first-run
**Goal:** Four P0 playbooks executable from the chat panel. First-run experience guides project open.
#### TDD sequence
**Step 1 — Write the tests**
```rust
// src-tauri/src/playbooks/dynamic_block.rs
#[cfg(test)]
mod tests {
#[tokio::test]
async fn test_detects_build_setup() {
let playbook = DynamicBlockPlaybook::new();
let config = playbook.detect("fixtures/sample-theme").await.unwrap();
assert_eq!(config.build_tool, "@wordpress/scripts");
}
#[tokio::test]
async fn test_scaffolds_block_with_metadata() {
let playbook = DynamicBlockPlaybook::new();
let result = playbook.scaffold("testimonial").await.unwrap();
assert!(result.files.contains("block.json"));
assert!(result.files.contains("render.php"));
}
#[tokio::test]
async fn test_verifies_block_in_editor() {
let playbook = DynamicBlockPlaybook::new();
assert!(playbook.verify("testimonial").await.unwrap());
}
}
// src-tauri/src/playbooks/child_theme.rs
#[cfg(test)]
mod tests {
#[tokio::test]
async fn test_creates_style_css_with_template_header() {
let playbook = ChildThemePlaybook::new();
let result = playbook.scaffold("twentytwentyfour", "my-child").await.unwrap();
assert!(result.style_css.contains("Template: twentytwentyfour"));
}
}
// src-tauri/src/playbooks/cpt.rs
#[cfg(test)]
mod tests {
#[tokio::test]
async fn test_registers_cpt_with_rest_support() {
let playbook = CptPlaybook::new();
let result = playbook.register("book", "books").await.unwrap();
assert!(result.php.contains("show_in_rest"));
}
#[tokio::test]
async fn test_flushes_rewrite_rules() {
let playbook = CptPlaybook::new();
assert!(playbook.flush_rewrites().await.is_ok());
}
#[tokio::test]
async fn test_verifies_rest_endpoint() {
let playbook = CptPlaybook::new();
let result = playbook.verify_rest("book").await.unwrap();
assert!(result.status_ok);
}
}
// src-tauri/src/playbooks/plugin.rs
#[cfg(test)]
mod tests {
#[tokio::test]
async fn test_creates_plugin_headers() {
let playbook = PluginPlaybook::new();
let result = playbook.scaffold("my-plugin", "My Plugin").await.unwrap();
assert!(result.main_php.contains("Plugin Name: My Plugin"));
}
}
```
```typescript
// webview/__tests__/FirstRun.test.tsx
describe('FirstRun', () => {
it('shows three project paths on first launch', () => { /* ... */ });
it('detects Docker and wp-env, guides install if missing', () => { /* ... */ });
it('opens a project and shows the workspace within 10 minutes', () => { /* ... */ });
});
```
**Step 2 — Implement**
- **`src-tauri/src/playbooks/registry.rs`** — Playbook registration
- **`src-tauri/src/playbooks/dynamic_block.rs`** — detect → scaffold → register → build → verify → diff
- **`src-tauri/src/playbooks/child_theme.rs`** — scaffold → enqueue → override → screenshot
- **`src-tauri/src/playbooks/cpt.rs`** — register → flush → seed → REST check → diff
- **`src-tauri/src/playbooks/plugin.rs`** — headers → optional Composer/PHPUnit
- **`webview/src/components/FirstRun.tsx`** — Onboarding wizard (3 paths: WP repo, folder, sample)
- **`src-tauri/src/commands/project.rs`** — Dependency check command
#### Deliverables
- 4 P0 playbooks with Rust unit tests
- First-run dialog (3 paths)
- `cargo test --package playbooks` passing
---
### Sprint 7: Integration + exit criteria
**Goal:** All Phase 1 pieces work together. Exit criteria tests pass end-to-end.
#### Integration tests
```typescript
// e2e/tauri/phase1-exit-criteria.test.ts
import { test, expect } from '@playwright/test';
test('clean machine → live preview in ≤10 min', async () => {
const app = await tauri.launch({ args: [] });
const window = await app.webview();
const startTime = Date.now();
// Follow first-run dialog → install Docker → install wp-env → open project → boot
await window.locator('.wursor-first-run-open-repo').click();
await window.locator('.wursor-project-picker').fill('/path/to/fixtures/sample-theme');
await window.locator('.wursor-confirm-open').click();
// Wait for runtime to boot
await expect(window.locator('.wursor-status-indicator')).toHaveText('running', { timeout: 600000 });
const elapsed = Date.now() - startTime;
expect(elapsed).toBeLessThan(10 * 60 * 1000);
await app.close();
});
test('P0 playbook completes with verified preview + accepted diff', async () => {
const app = await tauri.launch({ args: ['/path/to/fixtures/sample-theme'] });
const window = await app.webview();
// Wait for runtime
await expect(window.locator('.wursor-status-indicator')).toHaveText('running', { timeout: 60000 });
// Run playbook
await window.locator('.wursor-chat-input').fill('Create a dynamic block named "testimonial"');
await window.locator('.wursor-chat-send').click();
// Wait for diff
await expect(window.locator('.wursor-diff-view')).toBeVisible({ timeout: 120000 });
// Wait for verify
await expect(window.locator('.wursor-verify-result')).toBeVisible({ timeout: 60000 });
// Accept
await window.locator('.wursor-accept-diff').click();
await expect(window.locator('.wursor-accepted-badge')).toBeVisible();
await app.close();
});
```
```rust
// src-tauri/tests/tool_bus_test.rs
#[cfg(test)]
mod integration_tests {
#[tokio::test]
async fn test_tool_bus_integration() {
let registry = build_registry();
let tool = registry.get("fs.read").unwrap();
let result = (tool.handler)(serde_json::json!({"path": "fixtures/sample.txt"})).await;
assert!(result.is_ok());
}
}
```
#### Deliverables
- Both exit criteria tests passing
- `cargo test` passing (all Rust unit + integration tests)
- `pnpm test:webview` passing (all webview tests)
---
### Sprint 8: Polish + alpha readiness
**Goal:** Error states from §8.5 handled, app packaging works, build ready for internal alpha.
#### Tasks
- **Error states** — Wire each state from PRD §8.5 into the webview UI
- **App packaging** — `tauri build` produces signed `.dmg` (macOS) and `.msi` (Windows)
- **Auto-update** — Tauri updater with GitHub releases
- **Telemetry** — Minimal events (preview load time, playbook run, verify result) with consent dialog
- **Documentation** — `README.md` with install instructions and quickstart
- **Bug bash** — Internal team runs through first-run + playbook flow
#### Deliverables
- Signed app bundles for macOS + Windows
- Auto-update mechanism
- Error states all wired in the webview
- Minimal telemetry with consent
- `README.md` updated for alpha users
---
## 5. Phase 2 — Intelligence (Weeks 916)
*High-level outline — full breakdown follows Phase 1 exit.*
| Sprint | Focus | Crates |
|--------|-------|--------|
| 9 | Knowledge graph static scanner | `src-tauri/src/knowledge-index/scanner.rs` |
| 10 | Knowledge graph runtime enricher | `src-tauri/src/knowledge-index/enricher.rs` |
| 11 | Knowledge graph queries + UI | `src-tauri/src/knowledge-index/queries.rs`, tree view in webview |
| 12 | State Diff lifecycle | `src-tauri/src/state-diff/lifecycle.rs` |
| 13 | State Diff UI + rollback | `src-tauri/src/state-diff/rollback.rs`, StateDiffPanel |
| 14 | Quality gates (PHPCS, PHPUnit) | `src-tauri/src/tool-bus/tools/lint.rs`, `test_runner.rs` |
| 15 | Staging pull connector | `src-tauri/src/tool-bus/tools/staging.rs` |
| 16 | Closed alpha ship + baseline collection | Telemetry review, §11 baselines |
---
## 6. TDD Rules
These rules apply to every sprint:
1. **Write the test first.** No implementation code is written without a failing test.
2. **One assertion per test.** Each test verifies exactly one behavior.
3. **Tests are deterministic.** No network calls in unit tests (mock Grok API, mock wp-env).
4. **Integration tests use fixtures.** Sample WP repos live in `src-tauri/fixtures/` and `e2e/fixtures/`.
5. **Red → Green → Refactor.** Write the failing test (red), make it pass (green), then clean up (refactor).
6. **Coverage floor.** Rust: `cargo-tarpaulin` enforces ≥ 90% line coverage. Webview: vitest enforces ≥ 90%.
7. **No skipped tests in main.** `#[ignore]` and `test.only` only in feature branches.
### Test naming convention
Rust: `{module}_{behavior}` (e.g. `fs_read_rejects_path_traversal`)
TypeScript: `{module}.{behavior}.test.ts` (e.g. `fs.read-workspace-file.test.ts`)
---
## 7. CI/CD Pipeline
```yaml
# .github/workflows/ci.yml — runs on every PR
name: CI
on: [pull_request]
jobs:
rust:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- run: cargo test --all-features
- run: cargo tarpaulin --out Xml --skip-clean
# Enforces 90% coverage floor
webview:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
- run: pnpm install
- run: pnpm test -- --coverage # Enforces 90% coverage
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- run: cargo clippy --all-targets -- -D warnings
- uses: pnpm/action-setup@v2
- run: pnpm install && pnpm lint
# .github/workflows/e2e.yml — runs on release branch
name: E2E
on:
push:
branches: [release/*]
jobs:
e2e:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [macos-latest, windows-latest]
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: pnpm/action-setup@v2
- run: pnpm install
- run: cargo build --release
- run: pnpm test:e2e
```
---
## 8. Glossary
| Term | Definition |
|------|------------|
| **Tool bus** | Rust registry and executor for all agent-callable tools (fs, wpcli, site, db, etc.) |
| **Runtime adapter** | Rust trait that abstracts wp-env (v1) behind a common interface for future backends |
| **Playbook** | A reusable, multi-step agent workflow (scaffold block, create CPT, etc.) |
| **State Diff** | A reviewable mutation plan for WP content/state (CLI commands, SQL, or migration) |
| **Permission tier** | A capability level (read FS → edit FS → WP-CLI safe → destructive → etc.) |
| **Verify** | Required proof step: screenshot, HTTP check, or editor route confirmation |
| **Knowledge graph** | Rust-indexed map of themes, plugins, blocks, hooks, and REST routes |
---
*End of Implementation Guide v1.1 — Wursor*