跳到主要內容
HOZU0.26.1
選單

你的 AI 寫應用程式, Hozu 負責檢查。

說出你想要的 app,AI 幫你做出來;AI 每改一次,Hozu 就先檢查一次,不讓東西在你沒注意時壞掉。

↓ 按下「AI 修改」,看 AI 改這個 app,Hozu 同時檢查它的成果。

notes · ada

買牛奶

✔ types ok · 0 errors · lock current

負責檢查的紅色木釘 Peg 正在揮手
A tenon joint: two posts, a beam with two tenons and two red pegs

影片 · 2 分鐘

兩分鐘,不用寫程式。

你負責想像,AI 負責打造,而 Peg(那根鎖住榫接的紅色木釘)會在每次修改送到任何人面前之前先檢查。給 vibe coder 與設計師。

英文字幕 · 語音由 elevenlabs.io 生成

帳單

你的 AI 從沒看過 Hozu。等它熟悉之後,成本和 Nuxt 差不多。

每個模型都早已熟悉 Nuxt;Hozu 則要在每個 session 中從我們的指南學起。即便如此,一次修改花費的是 Nuxt 的 1.32–1.42× token;熟悉指南之後則是 1.02–1.06×:差不多。剩下的差距來自學習,而不是框架本身。

換來的是:在 29 次修改中,Hozu 通過了每一步的所有檢查。Nuxt 則在 3/29 次修改中默默弄壞了原本正常的匯出功能,而它的型別檢查與建置仍然全綠。你的 agent 會先執行檢查器、修好發現的問題,才告訴你「完成了」。

以上是實測 0024 的數據:Hozu 0.14 對 Nuxt 4.5.2,同一個應用程式、同一個模型(Claude Opus)。之後的版本尚未用這種方式測量。更早的版本成本更高:Hozu 0.7 為 2.64–2.73×、0.8 為 1.34–1.72×。

速度

經過檢查,依然是最輕的頁面。

同一個頁面(100 件商品加一個購物車計數器),分別用各個框架打造,並在各自的正式環境伺服器上執行。每次請求都重新渲染。只有會回應點擊的部分才會送出 JavaScript。

同一個頁面、四個框架,每次請求都渲染

Hozu 0.25.0

adapter-node

請求/秒
15,818
JS(gzip)
9.7 KB
可互動
60 ms

SvelteKit 3.0.0

Svelte 5.57.1, adapter-node 6.0.0

請求/秒
6,581
JS(gzip)
33.0 KB
可互動
101 ms

Nuxt 4.5.2

Vue 3.5.43, Nitro 2.13.4

請求/秒
2,953
JS(gzip)
75.8 KB
可互動
91 ms

Next.js 16.3.8

React 19.3.0, App Router

請求/秒
1,616
JS(gzip)
130.9 KB
可互動
164 ms

於 2026-10-08 以 Hozu 0.25.0 測量:在 Apple M4 Pro、Node 22.22.2、Chrome 154 上執行一次;瀏覽器計時時將 CPU 降速四倍。Next.js 以預先渲染的方式提供頁面時,可達每秒 6,664 次請求。接受 gzip 時,Hozu 為 11,454 次、Next.js 為 1,502 次;Nuxt 與 SvelteKit 傳送未壓縮的頁面。JavaScript 大小皆以相同方式 gzip 計算。測量方式 · 只比較函式庫本身(React、Vue、Preact、Svelte)

工作方式

三個步驟,打字交給你的 agent。

  1. 1 · 建立

    一個指令建立應用程式,並把 Hozu 指南放在旁邊。

  2. 2 · 告訴你的 agent

    「幫我的筆記加上分享功能。」或用 Hozu DevTools 直接指著畫面:它會把檔案與行號交給你的 agent。

  3. 3 · 它自己檢查

    它會執行 hozu check、修好發現的問題,再告訴你完成了。

bash
npm create hozu@latest my-app -- --agent claude
cd my-app
npm install
npx hozu add feature tasks --page /tasks
npx hozu check

由 agent 檢查

你的 agent 會檢查自己的成果,在真正的瀏覽器裡。

hozu check 會在任何東西執行之前讀過整個程式。接著 hozu browse 在 Chrome 裡像真人一樣操作頁面,並回報每一步改變了什麼。你的 agent 會在告訴你完成之前執行這兩者,你也可以自己執行。

  1. hozu check

    型別、每條規則及其建議修正、每個決策的 contract,以及記錄每個部分行為的 lock。

  2. hozu browse

    填寫、點擊、送出、在頁面間移動;JavaScript 可開、可關或兩者都測,還能同時扮演 Ada 與 Bob。

  3. hold,然後 release

    讓一次儲存保持等待,讀取忙碌中的頁面。每一步都會回報重新載入、閃爍與版面位移。

在某次實測中,一張商店卡片看起來沒問題,連結卻點不到:標題的 ::after 蓋在它上面。從 0.25 起,hozu browse 會讓這次點擊失敗,並指出蓋在上面的是什麼。0.25 的變更

誰能看到

你的筆記只屬於你,Hozu 會檢查。

每個涉及訪客資料的 query 與修改,都要宣告誰可以執行。忘了宣告,應用程式就無法通過型別檢查。Hozu 會在你的程式碼執行前拒絕其他所有人,並檢查清單裡只有該訪客自己的資料列。

  1. access: 'signedIn'

    或是資料列的擁有者,或像管理員角色這樣的條件。在 query 旁邊寫一行即可。

  2. Forbidden

    其他人都會被拒絕,頁面回應 403 或請他們先登入。

  3. 以 Bob 的身分試試

    一個 hozu browse 指令就能以 Ada 與 Bob 登入,並以 Bob 的身分開啟 Ada 的頁面。

資料

你的 API,從該呼叫的地方呼叫。

每個 query 與 mutation 宣告它需要什麼,由 Hozu 決定它在哪裡執行。公開 API 先在伺服器上渲染,之後直接從瀏覽器呼叫:不多繞一手,也沒有雙倍流量。存在瀏覽器裡的 token 永遠不會傳到你的伺服器,而沒有伺服器的應用程式可以匯出到 GitHub Pages。

  1. runs: 'server'

    資料庫、機密或 session。resolver 照舊寫在 app.ts。

  2. runs: 'either'

    公開 API,或你自己開了 CORS 的 API。首次繪製時就在 HTML 裡,之後由瀏覽器呼叫。

  3. runs: 'browser'

    訪客自己的 token。伺服器上呈現載入狀態,API 呼叫在瀏覽器中進行。

後端

你的後端可以是 Go。

resolver 只是具型別宣告背後的一般後端程式碼,所以不一定要用 TypeScript。在 remote() 中列出 effect,執行 hozu gen,再實作它產生的 Go interface。誰可以呼叫、快取、tag,以及依 output schema 檢查每個回應,這些都留在 Hozu 伺服器中,因此回應錯誤的服務會直接被拒絕(fail closed)。

  1. remote()

    在 app.ts 中寫一個項目,列出服務、它的 secret 與它實作的 effect。其餘部分仍是 TypeScript。

  2. hozu gen

    依你的宣告產生 Go contract。改了宣告卻忘了重新產生時,hozu check 會指出哪些 effect 已經過期。

  3. 由 agent 實測

    一個 agent 把商店後台的訂單流程、庫存與儀表板搬到了 Go。筆記應用程式的每個 resolver 都改用 Go 後,仍通過了隱藏的瀏覽器檢查。

當 resolver 確實有吃重的工作時,Go 才划算;若只是薄薄一層,多繞一手的成本比省下的還多。TypeScript 仍是預設。

邊做邊測

在頁面旁邊執行你的 API。

npm run dev 會在每個頁面下方放一個 API 抽屜:頁面讀取的資料、它做出的修改與你的 endpoint,以及每次呼叫實際送出的請求。環境只需宣告一次,機密留在伺服器上,伺服器也能從內部呼叫你的 API。

規模

五百個 feature,同樣的頁面。

我們產生了 50 與 500 個 feature 的應用程式,並修正了那些隨應用程式而非頁面成長的部分。現在頁面只載入自己的程式碼與連結,編輯後的檢查會與型別檢查並行,多台伺服器也會讓彼此的快取保持正確。

  1. 1.9 秒檢查

    500 個 feature 時,修改一行後執行 hozu check。原本要 4.6 秒。

  2. 頁面大小不變

    不論 50 或 500 個 feature,頁面攜帶的資料都相同,而且只載入自己的程式碼:這裡是 237 bytes。

  3. 多台伺服器

    一台伺服器上的變更會清除其他伺服器的快取。記憶體用量維持在上限內。

DevTools

指著它,你的 agent 就拿到那一行。

執行 npm run dev,選擇 Select,點擊有問題的地方。說明該怎麼改,直接在頁面上試試尺寸、顏色或其他文字,然後交出去。

這份請求會指出檔案、行號,以及用 Hozu 的方式該怎麼改:只改這一個按鈕還是全部按鈕、兩處共用的訊息、需要 contract 的狀態。你的 agent 不用再找,直接開始修。

它也會回頭指給你看:每個改過的部分都會在你的頁面上加上編號框,並附上用你的話寫的說明。

給設計師

用起來就像 Figma。

DevTools 沿用設計師已經熟悉的快捷鍵、測量方式與用語:Shift+Enter 往上一層,Alt 測量,Design 面板讀起來就像 Figma 的,而且你的 token 排在最前面。

Assets 在同一頁呈現每個元件與 variant,以及你在 previews.ts 中命名的畫面。在頁面上試著修改,然後交給你的 agent。程式碼由它寫,你完全不用打開檔案。

Shift+Enter · Enter · Tab

選取外層、內層與相鄰的元素。

Alt

按住並指向:顯示 px 距離。

W × H

每個選取都會顯示尺寸。

Design 面板

Frame、Auto layout、Layer、Fill、Stroke、Effects、Text。

Variables

token 優先:red · #fb3a0e、2xl · 24px。

Comments

你的 agent 留下的說明是編號圖釘:可以回覆或標為已解決。

它抓得到什麼

看起來沒問題,實際上會壞的錯誤。

這些錯誤都能通過型別檢查與建置。Hozu 還是會擋下來,並告訴你該怎麼做。點擊、滑過或用 Tab 移到卡片上,就能看到真正的診斷訊息。

✘ HZ049

你的筆記,出現在別人的螢幕上。

✘ HZ049 cached-user-data

A user-scoped query is cached across requests.

fix: freshness: 'request'

✘ HZ054

你勾了三個方塊,應用程式只看到一個。

✘ HZ054 single-value-form-read

A multi-value field is read with ui.dom.form.

fix: ui.dom.formAll(name)

✘ HZ091

Ada 的清單裡出現了 Bob 的筆記。

✘ HZ091 rows-outside-owner

A query with owner access returned rows the visitor does not own.

fix: read only the visitor’s rows in the resolver

✘ HZ057

行為變了,卻沒有人審查。

✘ HZ057 lock-out-of-date

The lock differs from the computed lock.

fix: hozu check --update-lock, then read each now: line

元件

宣告式 UI,逐一檢查每個 class。

按鈕是 kit 裡的一個宣告,而不是會消失的 helper。選一個 variant:預覽、它的原始碼,以及 hozu render 印出的內容,全都來自同一個元件。本站也是用同一套 kit 打造的。

typescript
import { ui } from '@hozu/core'
import { z } from 'zod'
import { tv } from './tv.ts'

const styles = tv({
  base: 'inline-block border-4 px-4 py-2.5 text-sm font-extrabold uppercase tracking-wide transition-colors duration-150',
  variants: {
    intent: {
      solid: 'border-ink bg-ink text-paper hover:bg-paper hover:text-ink',
      outline: 'border-ink text-ink hover:bg-ink hover:text-paper',
      light: 'border-paper bg-paper text-ink hover:bg-transparent hover:text-paper',
      lightOutline: 'border-paper text-paper hover:bg-paper hover:text-ink',
    },
  },
  defaultVariants: { intent: 'solid' },
})
export const Button = ui.component({
  tag: 'a',
  styles,
  props: z.object({ href: z.string() }),
  children: true,
  render: ({ props, children }) => ui.a({ href: props.href, 'data-button': '' }, children),
})

hozu render site.Button --variant intent=solid

{
  "html": "<a class=\"inline-block border-4 px-4 py-2.5 text-sm font-extrabold uppercase tracking-wide transition-colors duration-150 border-ink bg-ink text-paper hover:bg-paper hover:text-ink\" href=\"#\" data-button></a>",
  "class": "inline-block border-4 px-4 py-2.5 text-sm font-extrabold uppercase tracking-wide transition-colors duration-150 border-ink bg-ink text-paper hover:bg-paper hover:text-ink",
  "owned": [
    "background-color",
    "border-color",
    "border-style",
    "border-width",
    "color",
    "display",
    "font-size",
    "font-weight",
    "letter-spacing",
    "line-height",
    "padding-block",
    "padding-inline",
    "text-transform",
    "transition-duration",
    "transition-property",
    "transition-timing-function"
  ]
}

同一個元素上有兩個 class 設定同一個屬性時,會回報為 HZ079,所以覆寫永遠不會意外生效。閱讀元件說明

頂端的 3D 榫接也是一個元件。

由 Blender 腳本建立模型,three.js 在 client component 中渲染,而執行示範的同一個 machine 會告訴它何時分開。沒有 JavaScript 時,它是一張靜態圖片。

typescript
import { ui } from '@hozu/core'
import { z } from 'zod'
import { tv } from './tv.ts'

const poster = ui.asset(new URL('../assets/joint-poster.webp', import.meta.url))
const model = ui.asset(new URL('../assets/joint.glb', import.meta.url))

export const Joint = ui.component({
  tag: 'div',
  styles: tv({ base: 'relative aspect-square w-full max-w-[34rem] select-none' }),
  props: z.object({ split: z.boolean() }),
  client: new URL('./joint.client.ts', import.meta.url),
  load: 'visible',
  render: () =>
    ui.div({}, [
      ui.img({
        src: poster,
        'data-model': model,
        width: 900,
        height: 900,
        alt: 'A tenon joint: two posts, a beam with two tenons and two red pegs',
        class: 'h-full w-full',
      }),
    ]),
})

// site/joint.client.ts
export default implement<typeof Joint>(({ el, props, signal }) => {
  // … GLTFLoader, toon materials, ink hulls, idle turn, drag …
  return {
    update(next) {
      const changed = next.split !== split
      split = next.split
      if (changed) pose(split, !reduce)
    },
    destroy() { … },
  }
})

底層原理

feature() → IR → validator → compiler → runtime

每個頁面都依其資料的宣告來規劃:誰能看到、需要多新。只有綁定 machine 的節點會送出 JavaScript;這個頁面上其餘的一切都是純 HTML。

實測

實際測量,連不完美的地方也一併呈現。

實測 0024:同一個筆記應用程式被建立並修改 28 次,分別是從指南學習 Hozu、已經熟悉 Hozu,以及使用 Nuxt。最後八次修改由一個從未看過 Hozu 的 session 撰寫。保留驗證的結果:學習期間 Nuxt 的 1.32–1.42× token,熟悉之後 1.02–1.06×(各跑兩次)。

實測 0024:29 個步驟,同一個模型、同一套隱藏檢查
Hozu 0.14Nuxt 4.5.2
所有檢查都通過的修改2926
靜默失敗(建置通過,功能卻壞了)03
每次全新修改的 token,相對 Nuxt1.32–1.42×基準
同上,熟悉 Hozu 之後1.02–1.06×基準

在那之前是實測 0021(Hozu 0.8)。成本:每次修改為 Nuxt 的 1.34–1.72× token(第 13–20 步,每個框架跑一次),對比 Hozu 0.7 為 2.64–2.73×。結果:16 次修改中有 0 次迴歸、0 次靜默失敗;對比 Hozu 0.7 的 8 次迴歸失敗,以及 5 個步驟出現靜默失敗。未達成:成本比例在整個過程中仍略微上升。

實測 0021:同一個筆記應用程式,由 agent 再修改 16 次
Hozu 0.7Hozu 0.8
迴歸失敗80
出現靜默失敗的步驟50
相對 Nuxt 的成本(幾何平均)2.64–2.73×1.34–1.72×
實測 0021 中 Hozu 0.8 與 Nuxt 每一步的成本、程式碼行數與檢查結果

原始紀錄;其中 Nuxt 第 14 步少算了,報告中說明了修正方式。閱讀實測 0021 · 閱讀實測 0024 · 所有實測

開始

動手做點東西,然後試著弄壞它。