diff --git a/content/blog/give-your-agent-rules-for-governable-apps/index.de.mdx b/content/blog/give-your-agent-rules-for-governable-apps/index.de.mdx
index 23ac611..0c28f1f 100644
--- a/content/blog/give-your-agent-rules-for-governable-apps/index.de.mdx
+++ b/content/blog/give-your-agent-rules-for-governable-apps/index.de.mdx
@@ -3,7 +3,7 @@ title: "Agent-Regeldateien schreiben: KI soll governbare Apps generieren"
description: "AGENTS.md, .cursor/rules und CLAUDE.md sollten nicht nur Code-Stil regeln. Beschreiben Sie Rechte, Freigaben, Audit und das Ziel-Metadatenformat, damit KI-Apps prüfbar entstehen."
author: ObjectStack Team
date: 2026-06-16T18:00:00+08:00
-updated: 2026-10-15T10:00:00+08:00
+updated: 2026-10-22T10:00:00+08:00
status: published
topic: ai-agents
audience: developer
@@ -54,7 +54,7 @@ Die Antwort ist genau jenes Schwungrad, das [der Artikel zur offenen semantische
1. **Lernbar**: offen + öffentlich + diskutiert → in die Trainingsdaten → das Modell kann es ohnehin schreiben;
2. **Beschränkbar**: Eine Regeldatei verankert den Output des Agents an diesem deklarativen Ziel;
-3. **Zur Generierungszeit validierbar**: Die Runtime validiert die generierten Metadaten – eine ungültige Definition wird direkt abgewiesen, und der Agent **bekommt das Korrektursignal auf der Stelle**. Das ist der entscheidende Unterschied, den freier Code nicht bietet: Ein Stück falsches SQL läuft womöglich trotzdem und geht erst live hoch, während falsche Metadaten gar nicht erst durch die Tür kommen und auf der Stelle zur Neufassung zurückgewiesen werden.
+3. **Zur Generierungszeit validierbar**: Die Runtime validiert die generierten Metadaten – eine ungültige Definition wird direkt abgewiesen, und der Agent **bekommt das Korrektursignal auf der Stelle**. Das ist der entscheidende Unterschied, den freier Code nicht bietet: Ein Stück falsches SQL läuft womöglich trotzdem und geht erst live hoch, während falsche Metadaten gar nicht erst durch die Tür kommen und auf der Stelle zur Neufassung zurückgewiesen werden. Dieses Gate ist eines von vier – Typed, Validated, Reviewed, Governed – und [What Makes a Business App AI-Writable?](/en/blog/what-makes-a-business-app-ai-writable/) (auf Englisch) zeigt in einem echten Lauf, wie es drei absichtliche Fehler zurückweist, mit der Meldung zu jedem.
Genau Punkt 3 macht aus „den Agent es richtig schreiben lassen" – statt auf Glück angewiesen – einen rückgekoppelten Konvergenzprozess: Der Agent schreibt falsch, wird abgewiesen, korrigiert – unmittelbar, wie bei einem Compilerfehler. Du kannst sogar noch einen Schritt weiter gehen und die governten Werkzeuge der Runtime über **MCP** nach außen freigeben, sodass der Agent zur Generierungszeit die autoritative Definition abruft und zur Laufzeit innerhalb der Grenzen damit operiert – Schreiben und Nutzen, dieselbe Governance. (Die Sache mit der governten Werkzeugschicht behandelt [der MCP-Artikel](/de/blog/mcp-governed-tool-layer/) gesondert.)
diff --git a/content/blog/give-your-agent-rules-for-governable-apps/index.es.mdx b/content/blog/give-your-agent-rules-for-governable-apps/index.es.mdx
index ba7d6a2..74e6742 100644
--- a/content/blog/give-your-agent-rules-for-governable-apps/index.es.mdx
+++ b/content/blog/give-your-agent-rules-for-governable-apps/index.es.mdx
@@ -3,7 +3,7 @@ title: "Cómo escribir reglas de agent para que la IA genere apps gobernables"
description: "AGENTS.md, .cursor/rules y CLAUDE.md no deberían cubrir solo estilo de código. Describe permisos, aprobaciones, auditoría y el formato de metadatos objetivo para que las apps generadas sean revisables."
author: ObjectStack Team
date: 2026-06-16T18:00:00+08:00
-updated: 2026-10-15T10:00:00+08:00
+updated: 2026-10-22T10:00:00+08:00
status: published
topic: ai-agents
audience: developer
@@ -54,7 +54,7 @@ La respuesta es justo ese volante que ya argumentó [el artículo sobre la capa
1. **Se puede aprender**: abierto + público + comentado → entra en el corpus de entrenamiento → el modelo ya sabe escribirlo;
2. **Se puede restringir**: un archivo de reglas ancla el resultado del agent a este objetivo declarativo;
-3. **Se puede validar en el momento de la generación**: el runtime valida los metadatos generados; una definición ilegal se rechaza de inmediato y el agent **recibe la señal de corrección en el acto**. Esta es la diferencia clave que el código de formato libre no puede dar: un trozo de SQL erróneo puede correr igualmente y solo estallar en producción, mientras que unos metadatos erróneos no pasan de la puerta y se devuelven al instante para reescribir.
+3. **Se puede validar en el momento de la generación**: el runtime valida los metadatos generados; una definición ilegal se rechaza de inmediato y el agent **recibe la señal de corrección en el acto**. Esta es la diferencia clave que el código de formato libre no puede dar: un trozo de SQL erróneo puede correr igualmente y solo estallar en producción, mientras que unos metadatos erróneos no pasan de la puerta y se devuelven al instante para reescribir. Esa puerta es una de cuatro: Typed, Validated, Reviewed, Governed; [What Makes a Business App AI-Writable?](/en/blog/what-makes-a-business-app-ai-writable/) (en inglés) la muestra rechazando tres errores deliberados en una ejecución real, con el mensaje que recibe cada uno.
Es precisamente el punto 3 lo que convierte «conseguir que el agent escriba bien» de un asunto de suerte en un proceso de convergencia con realimentación: el agent escribe mal, es rechazado, corrige, igual de inmediato que un error de compilación. Y, yendo más allá, también puedes exponer las herramientas gobernadas del runtime mediante **MCP**, de modo que el agent obtenga las definiciones autoritativas en el momento de generar y, en tiempo de ejecución, pueda operarlas dentro de los límites: escribir y usar, una misma gobernanza. (Lo de la capa de herramientas gobernada lo cuenta aparte [el artículo sobre MCP](/es/blog/mcp-governed-tool-layer/).)
diff --git a/content/blog/give-your-agent-rules-for-governable-apps/index.fr.mdx b/content/blog/give-your-agent-rules-for-governable-apps/index.fr.mdx
index af210eb..0fd1385 100644
--- a/content/blog/give-your-agent-rules-for-governable-apps/index.fr.mdx
+++ b/content/blog/give-your-agent-rules-for-governable-apps/index.fr.mdx
@@ -3,7 +3,7 @@ title: "Comment écrire des règles d'agent pour que l'IA génère des apps gouv
description: "AGENTS.md, .cursor/rules et CLAUDE.md ne devraient pas couvrir seulement le style de code. Décrivez permissions, approbations, audit et format cible pour obtenir des apps générées qui restent vérifiables."
author: ObjectStack Team
date: 2026-06-16T18:00:00+08:00
-updated: 2026-10-15T10:00:00+08:00
+updated: 2026-10-22T10:00:00+08:00
status: published
topic: ai-agents
audience: developer
@@ -54,7 +54,7 @@ La réponse, c'est précisément ce volant d'inertie démontré dans [l'article
1. **Apprenable** : ouvert + public + discuté → entre dans les corpus d'entraînement → le modèle sait déjà l'écrire ;
2. **Contraignable** : un fichier de règles ancre le résultat de l'agent sur cette cible déclarative ;
-3. **Validable à la génération** : le runtime valide les métadonnées générées — une définition invalide est rejetée d'emblée, et l'agent **reçoit un signal de correction sur-le-champ**. C'est la différence clé que ne peut pas offrir du code en format libre : un bout de SQL erroné peut très bien tourner et n'exploser qu'en production, tandis qu'un fichier de métadonnées erroné ne passe pas la porte et est renvoyé pour réécriture immédiate.
+3. **Validable à la génération** : le runtime valide les métadonnées générées — une définition invalide est rejetée d'emblée, et l'agent **reçoit un signal de correction sur-le-champ**. C'est la différence clé que ne peut pas offrir du code en format libre : un bout de SQL erroné peut très bien tourner et n'exploser qu'en production, tandis qu'un fichier de métadonnées erroné ne passe pas la porte et est renvoyé pour réécriture immédiate. Cette porte est l'une des quatre — Typed, Validated, Reviewed, Governed — et [What Makes a Business App AI-Writable?](/en/blog/what-makes-a-business-app-ai-writable/) (en anglais) la montre, lors d'une exécution réelle, rejeter trois erreurs délibérées avec le message que chacune reçoit.
C'est justement ce point 3 qui transforme « faire écrire l'agent correctement » d'un coup de chance en un processus de convergence avec rétroaction — l'agent se trompe, est rejeté, corrige, aussi instantanément qu'une erreur de compilation. Mieux encore, vous pouvez exposer les outils gouvernés du runtime via **MCP**, pour que l'agent récupère les définitions faisant autorité au moment de la génération, et puisse ensuite les exploiter dans les limites fixées au moment de l'exécution — écrire et utiliser, une seule et même gouvernance. (Cette couche d'outils gouvernée, [l'article sur MCP](/fr/blog/mcp-governed-tool-layer/) en parle séparément.)
diff --git a/content/blog/give-your-agent-rules-for-governable-apps/index.ja.mdx b/content/blog/give-your-agent-rules-for-governable-apps/index.ja.mdx
index 4f8030a..4e94478 100644
--- a/content/blog/give-your-agent-rules-for-governable-apps/index.ja.mdx
+++ b/content/blog/give-your-agent-rules-for-governable-apps/index.ja.mdx
@@ -3,7 +3,7 @@ title: "Agent ルールファイルの書き方:AI にガバナンス可能な
description: "AGENTS.md、.cursor/rules、CLAUDE.md はコードスタイルだけを管理すべきではない。権限、承認、監査、対象メタデータ形式を書けば、AI 生成アプリはレビューしやすくなる。"
author: ObjectStack Team
date: 2026-06-16T18:00:00+08:00
-updated: 2026-10-15T10:00:00+08:00
+updated: 2026-10-22T10:00:00+08:00
status: published
topic: ai-agents
audience: developer
@@ -54,7 +54,7 @@ AIでコードを書くチームなら、いまやほぼどこでもエージェ
1. **学習されうる**:オープン + 公開 + 議論されている → モデルや検索システムに学ばれやすい。
2. **制約されうる**:一つのルールファイルが、エージェントの産物をこの宣言的な目標へ錨(いかり)で固定する。
-3. **生成時に検証されうる**:ランタイムが生成されたメタデータを検証する——不正な定義はその場で拒否され、エージェントは**即座に訂正信号を受け取る**。これは自由形式のコードには出せない決定的な差だ。間違ったSQLはそのまま動いて本番で爆発しうるが、間違ったメタデータは門をくぐれず、その場で突き返されて書き直される。
+3. **生成時に検証されうる**:ランタイムが生成されたメタデータを検証する——不正な定義はその場で拒否され、エージェントは**即座に訂正信号を受け取る**。これは自由形式のコードには出せない決定的な差だ。間違ったSQLはそのまま動いて本番で爆発しうるが、間違ったメタデータは門をくぐれず、その場で突き返されて書き直される。このゲートは四つ——Typed・Validated・Reviewed・Governed——のうちの一つで、[ビジネスアプリが「AI が書ける」とはどういうことか](/ja/blog/what-makes-a-business-app-ai-writable/)は、実走で故意の三つのミスを拒否する様子を、それぞれが受け取るメッセージとともに示している。
まさにこの3点目が、「エージェントに正しく書かせる」を運任せから、フィードバックのある収束プロセスへ変える——エージェントが書き間違え、拒否され、直す。コンパイルエラーのように即時だ。さらに一歩進めて、ランタイムのガバナンスされたツールを **MCP** 経由で公開すれば、エージェントは生成時に権威ある定義を取りに行き、ランタイムでは境界内でそれを操作できる——書くことと使うことが、一つのガバナンスを共有する。(ガバナンスされたツール層という件は、[MCPの記事](/ja/blog/mcp-governed-tool-layer/) で別途語った。)
diff --git a/content/blog/give-your-agent-rules-for-governable-apps/index.ko.mdx b/content/blog/give-your-agent-rules-for-governable-apps/index.ko.mdx
index 489005a..e9c64e6 100644
--- a/content/blog/give-your-agent-rules-for-governable-apps/index.ko.mdx
+++ b/content/blog/give-your-agent-rules-for-governable-apps/index.ko.mdx
@@ -3,7 +3,7 @@ title: "Agent 규칙 파일 작성법: AI가 거버넌스 가능한 앱을 생
description: "AGENTS.md, .cursor/rules, CLAUDE.md는 코드 스타일만 다뤄서는 안 된다. 권한, 승인, 감사, 대상 메타데이터 형식을 쓰면 AI 생성 앱은 처음부터 검토하기 쉬워진다."
author: ObjectStack Team
date: 2026-06-16T18:00:00+08:00
-updated: 2026-10-15T10:00:00+08:00
+updated: 2026-10-22T10:00:00+08:00
status: published
topic: ai-agents
audience: developer
@@ -54,7 +54,7 @@ AI로 코드를 쓰는 거의 모든 팀이 이제 에이전트용 규칙 파일
1. **학습될 수 있다**: 개방 + 공개 + 활발한 논의 → 학습 코퍼스에 편입 → 모델이 애초에 쓸 줄 안다.
2. **제약될 수 있다**: 규칙 파일 하나가 에이전트의 결과물을 이 선언적 목표에 닻 내린다.
-3. **생성 시점에 검증될 수 있다**: 런타임이 생성된 메타데이터를 검증한다 — 부적합한 정의는 곧장 거부되고, 에이전트는 **그 자리에서 교정 신호를 받는다**. 이것이 자유 형식 코드는 줄 수 없는 결정적 차이다. 잘못된 SQL 한 토막은 멀쩡히 돌아가다가 출시되고 나서야 터질 수 있지만, 잘못된 메타데이터 한 벌은 문턱을 넘지 못하고 그 자리에서 되돌려 보내진다.
+3. **생성 시점에 검증될 수 있다**: 런타임이 생성된 메타데이터를 검증한다 — 부적합한 정의는 곧장 거부되고, 에이전트는 **그 자리에서 교정 신호를 받는다**. 이것이 자유 형식 코드는 줄 수 없는 결정적 차이다. 잘못된 SQL 한 토막은 멀쩡히 돌아가다가 출시되고 나서야 터질 수 있지만, 잘못된 메타데이터 한 벌은 문턱을 넘지 못하고 그 자리에서 되돌려 보내진다. 이 게이트는 네 개 — Typed, Validated, Reviewed, Governed — 중 하나이며, [What Makes a Business App AI-Writable?](/en/blog/what-makes-a-business-app-ai-writable/)(영어)는 실제 실행에서 일부러 넣은 세 가지 실수를 거부하는 모습을, 각 실수가 받는 메시지와 함께 보여 준다.
바로 이 3번이, "에이전트가 제대로 쓰게 하기"를 운에 맡기는 것에서 피드백이 있는 수렴 과정으로 바꿔놓는다 — 에이전트가 틀리고, 거부당하고, 고쳐 쓰는 것이, 컴파일 에러처럼 즉각적이다. 더 나아가, 런타임의 거버넌스되는 도구를 **MCP**로 노출해, 에이전트가 생성 시점에는 권위 있는 정의를 끌어다 쓰고 런타임에는 경계 안에서 그것을 조작하게 할 수도 있다 — 쓰기와 쓰임이, 하나의 거버넌스를 공유한다. (거버넌스되는 도구 계층이라는 이 이야기는 [MCP 글](/ko/blog/mcp-governed-tool-layer/)에서 따로 다뤘다.)
diff --git a/content/blog/give-your-agent-rules-for-governable-apps/index.mdx b/content/blog/give-your-agent-rules-for-governable-apps/index.mdx
index c6b8a02..31f7b95 100644
--- a/content/blog/give-your-agent-rules-for-governable-apps/index.mdx
+++ b/content/blog/give-your-agent-rules-for-governable-apps/index.mdx
@@ -3,7 +3,7 @@ title: "How to Write Agent Rules That Generate Governable Apps"
description: "Most agent rule files police style, not governance. Use AGENTS.md, .cursor/rules, and an open declarative target so AI-generated apps are reviewable from day one."
author: ObjectStack Team
date: 2026-06-16T18:00:00+08:00
-updated: 2026-10-15T10:00:00+08:00
+updated: 2026-10-22T10:00:00+08:00
status: published
topic: ai-agents
audience: developer
@@ -54,7 +54,7 @@ The answer is the flywheel argued in [the piece on the open semantic layer](/en/
1. **Learnable**: open + public + discussed → enters the training corpus → the model already knows how to write it;
2. **Constrainable**: a rules file anchors the agent's output to this declarative target;
-3. **Validatable at generation time**: the runtime validates the generated metadata — an illegal definition is rejected outright, and the agent **gets a correction signal on the spot.** This is the key difference free-form code can't offer: a bad piece of SQL may run fine and only blow up in production, while a bad piece of metadata never gets in the door — it's bounced back to be rewritten right then.
+3. **Validatable at generation time**: the runtime validates the generated metadata — an illegal definition is rejected outright, and the agent **gets a correction signal on the spot.** This is the key difference free-form code can't offer: a bad piece of SQL may run fine and only blow up in production, while a bad piece of metadata never gets in the door — it's bounced back to be rewritten right then. That gate is one of four — Typed, Validated, Reviewed, Governed — and [what makes a business app AI-writable](/en/blog/what-makes-a-business-app-ai-writable/) shows it refusing three deliberate mistakes in a real run, with the message each one gets.
That third point turns "have the agent write it correctly" from luck into feedback-driven convergence: the agent writes it wrong, gets rejected, fixes it, much like a compile error. Going further, you can expose the runtime's governed tools via **MCP**, so the agent pulls authoritative definitions at generation time and operates within the boundary at runtime. Writing and using share one governance layer. That governed tool layer is covered in [the MCP piece](/en/blog/mcp-governed-tool-layer/).
diff --git a/content/blog/give-your-agent-rules-for-governable-apps/index.zh-Hans.mdx b/content/blog/give-your-agent-rules-for-governable-apps/index.zh-Hans.mdx
index f49ce45..66f7cdf 100644
--- a/content/blog/give-your-agent-rules-for-governable-apps/index.zh-Hans.mdx
+++ b/content/blog/give-your-agent-rules-for-governable-apps/index.zh-Hans.mdx
@@ -3,7 +3,7 @@ title: Agent 规则文件怎么写:让 AI 生成可治理应用
description: 编码 agent 的 AGENTS.md、.cursor/rules 或 CLAUDE.md 不该只管代码风格。把权限、审批、审计和目标元数据格式写进去,AI 生成的应用才更容易被审查和签字。
author: ObjectStack Team
date: 2026-06-16T18:00:00+08:00
-updated: 2026-10-15T10:00:00+08:00
+updated: 2026-10-22T10:00:00+08:00
status: published
topic: ai-agents
audience: developer
@@ -54,7 +54,7 @@ tags:
1. **可被学到**:开放 + 公开 + 被讨论 → 更容易被模型和检索系统学到;
2. **可被约束**:一份规则文件把 agent 的产出锚定到这个声明式目标;
-3. **可在生成时校验**:运行时会校验生成的元数据——非法的定义直接被拒,agent **当场拿到纠错信号**。这是自由格式代码给不了的关键差别:一段错的 SQL 可能照样跑、上线才爆,而一份错的元数据进不了门、当场就被打回重写。
+3. **可在生成时校验**:运行时会校验生成的元数据——非法的定义直接被拒,agent **当场拿到纠错信号**。这是自由格式代码给不了的关键差别:一段错的 SQL 可能照样跑、上线才爆,而一份错的元数据进不了门、当场就被打回重写。这道门禁是四道之一——类型、验证、审查、治理——[什么样的业务应用是 AI 可编写的](/zh-Hans/blog/what-makes-a-business-app-ai-writable/)用一次真实运行展示了它拒收三个故意犯的错,以及每个错拿到的消息。
正是第 3 点,把"让 agent 写对"变成了有反馈的收敛过程——agent 写错、被拒、改对,像编译错误一样即时。更进一步,你还可以把运行时的受治理工具通过 **MCP** 暴露出来,让 agent 在生成时就调取权威定义、在运行时又能在边界内操作它——写和用,同一套治理。(受治理工具层这件事,[MCP 那篇](/zh-Hans/blog/mcp-governed-tool-layer/) 单独讲过。)
diff --git a/content/blog/give-your-agent-rules-for-governable-apps/index.zh-Hant.mdx b/content/blog/give-your-agent-rules-for-governable-apps/index.zh-Hant.mdx
index c521118..eeb2258 100644
--- a/content/blog/give-your-agent-rules-for-governable-apps/index.zh-Hant.mdx
+++ b/content/blog/give-your-agent-rules-for-governable-apps/index.zh-Hant.mdx
@@ -4,7 +4,7 @@ title: Agent 規則檔案怎麼寫:讓 AI 生成可治理應用
description: 編碼 agent 的 AGENTS.md、.cursor/rules 或 CLAUDE.md 不該只管程式碼風格。把權限、審批、審計和目標後設資料格式寫進去,AI 生成的應用才更容易被審查和簽字。
author: ObjectStack Team
date: 2026-06-16T18:00:00+08:00
-updated: 2026-10-15T10:00:00+08:00
+updated: 2026-10-22T10:00:00+08:00
status: published
topic: ai-agents
audience: developer
@@ -55,7 +55,7 @@ tags:
1. **可被學到**:開放 + 公開 + 被討論 → 更容易被模型和檢索系統學到;
2. **可被約束**:一份規則檔案把 agent 的產出錨定到這個宣告式目標;
-3. **可在生成時校驗**:執行時會校驗生成的後設資料——非法的定義直接被拒,agent **當場拿到糾錯訊號**。這是自由格式程式碼給不了的關鍵差別:一段錯的 SQL 可能照樣跑、上線才爆,而一份錯的後設資料進不了門、當場就被打回重寫。
+3. **可在生成時校驗**:執行時會校驗生成的後設資料——非法的定義直接被拒,agent **當場拿到糾錯訊號**。這是自由格式程式碼給不了的關鍵差別:一段錯的 SQL 可能照樣跑、上線才爆,而一份錯的後設資料進不了門、當場就被打回重寫。這道門禁是四道之一——型別、驗證、審查、治理——[什麼樣的業務應用是 AI 可編寫的](/zh-Hant/blog/what-makes-a-business-app-ai-writable/)用一次真實執行展示了它拒收三個故意犯的錯,以及每個錯拿到的訊息。
正是第 3 點,把"讓 agent 寫對"變成了有反饋的收斂過程——agent 寫錯、被拒、改對,像編譯錯誤一樣即時。更進一步,你還可以把執行時的受治理工具通過 **MCP** 暴露出來,讓 agent 在生成時就調取權威定義、在執行時又能在邊界內操作它——寫和用,同一套治理。(受治理工具層這件事,[MCP 那篇](/zh-Hant/blog/mcp-governed-tool-layer/) 單獨講過。)
diff --git a/content/blog/how-to-point-your-agent-at-objectstack/index.mdx b/content/blog/how-to-point-your-agent-at-objectstack/index.mdx
index 09ed8b5..0be4093 100644
--- a/content/blog/how-to-point-your-agent-at-objectstack/index.mdx
+++ b/content/blog/how-to-point-your-agent-at-objectstack/index.mdx
@@ -3,6 +3,7 @@ title: 'How to Configure Your AI Coding Agent to Write ObjectStack Apps'
description: 'Three wires point a coding agent at ObjectStack: a scaffolded rule file, an 11-skill bundle, and an in-runtime tool server. Each one verified against 17.2.0 — including the real output of the gate rejecting a wrong definition.'
author: ObjectStack Team
date: 2026-09-03T10:00:00+08:00
+updated: 2026-10-22T10:00:00+08:00
status: published
topic: ai-agents
audience: developer
@@ -322,7 +323,11 @@ npx skills add objectstack-ai/objectstack/skills --all
Then give your agent a real task and run `npm run validate` on what comes back.
The gate's answer will tell you more in ten seconds than any evaluation harness
will in a day — and if it fails, read the message it gives you before you fix
-anything. That message is the product.
+anything. That message is the product. It is also one of four gates — Typed,
+Validated, Reviewed, Governed — and the one this post's transcript shows;
+[what makes a business app AI-writable](/en/blog/what-makes-a-business-app-ai-writable/)
+runs the same gate at 17.7.0 against three deliberate mistakes, one per class the
+README names, and reads the messages as a ledger.
For the format the agent is writing into, see the
[agent developer guide](/en/agent-developer/); for the tool surface, the
diff --git a/content/blog/what-makes-a-business-app-ai-writable/cover-en.svg b/content/blog/what-makes-a-business-app-ai-writable/cover-en.svg
new file mode 100644
index 0000000..d062a42
--- /dev/null
+++ b/content/blog/what-makes-a-business-app-ai-writable/cover-en.svg
@@ -0,0 +1,72 @@
+
diff --git a/content/blog/what-makes-a-business-app-ai-writable/cover.svg b/content/blog/what-makes-a-business-app-ai-writable/cover.svg
new file mode 100644
index 0000000..85d3b37
--- /dev/null
+++ b/content/blog/what-makes-a-business-app-ai-writable/cover.svg
@@ -0,0 +1,72 @@
+
diff --git a/content/blog/what-makes-a-business-app-ai-writable/four-gates-en.svg b/content/blog/what-makes-a-business-app-ai-writable/four-gates-en.svg
new file mode 100644
index 0000000..aa78975
--- /dev/null
+++ b/content/blog/what-makes-a-business-app-ai-writable/four-gates-en.svg
@@ -0,0 +1,142 @@
+
diff --git a/content/blog/what-makes-a-business-app-ai-writable/four-gates.svg b/content/blog/what-makes-a-business-app-ai-writable/four-gates.svg
new file mode 100644
index 0000000..bbd0b36
--- /dev/null
+++ b/content/blog/what-makes-a-business-app-ai-writable/four-gates.svg
@@ -0,0 +1,142 @@
+
diff --git a/content/blog/what-makes-a-business-app-ai-writable/index.ja.mdx b/content/blog/what-makes-a-business-app-ai-writable/index.ja.mdx
new file mode 100644
index 0000000..e29c43d
--- /dev/null
+++ b/content/blog/what-makes-a-business-app-ai-writable/index.ja.mdx
@@ -0,0 +1,230 @@
+---
+title: "ビジネスアプリが「AI が書ける」とは?四つのゲート:Typed・Validated・Reviewed・Governed"
+description: "「AI が書ける」はモデルではなく対象フォーマットの性質だ。四つのゲートと一回の実走:故意の三つのミス、49 のルール、三つの位置特定済みエラー、そして修正後の通過。"
+author: ObjectStack Team
+date: 2026-10-22
+status: published
+topic: ai-agents
+audience: developer
+solutions: []
+industries: []
+cover: ./cover-en.svg
+tags:
+ - AI エージェント
+ - AI が書ける
+---
+
+ビジネスアプリが「AI が書ける」と言えるのは、エージェントが必ず犯すミスが、ユーザーに届く前に死ねる場所を持っているときだ。それはモデルの性質ではなく、対象フォーマットの性質である。ObjectStack の README はそうした場所を四つ挙げ、ゲートと呼ぶ:**型(Typed)**、**検証(Validated)**、**レビュー(Reviewed)**、**ガバナンス(Governed)**。型のエラーはエディタの中で死ぬ。検証のエラーは `os validate` で死ぬ——型チェックは通るが実行時に静かに壊れるメタデータを、この一つのコマンドが拒否する。レビューのエラーは、人がまるごと読める diff の中で死ぬ。ガバナンスのエラーは、たとえ出荷されても、ランタイムの権限と監査のフェンスの内側にとどまる。以下は ObjectStack 17.7.0 で、スキャフォールドしたサポートデスクに対して前二つのゲートを実際に走らせた記録だ。README が挙げる三つの種類のミスを一つずつ故意に仕込み、49 ルールのゲートがそれぞれを位置を特定し修正方法を添えて拒否し、直した後にクリーンに通過するまでを示す。
+
+## すでに成り立っている二つのこと
+
+ゲートの前に、反対側の言い分を最大限に立てておく。この文章の論証は、どちらかが間違っていることを必要としないからだ。
+
+**Vibe coding で作ったアプリは動く。** モデルにサポートデスクを説明すれば、午後のうちに動くものが手に入る。画面があり、データベースがあり、API があり、言語はモデルが一番得意なものだ。プロトタイプ、一回限りのツール、捨てる前提のデモなら、それが正しいトレードオフであり、以下にそれへの反論は一行もない。モデルはもうボトルネックではない。
+
+**手書きのコードはレビューでき、そのツールは成熟している。** シニアエンジニアがプルリクエストを読み、型システムがあり、テストスイートがあり、コードレビューのプロセスがある。それらは毎日、本物の欠陥を捕まえている。新規のロジックに代替はなく、ObjectStack 自身もそう言う。「コードは消えない。フック、アクション本体、CEL、制約付き JSX として、ランタイムの中へ移る」——そしてそのコードは、やはり人が読む。
+
+どちらも答えていないのは、エージェントが書き手になった瞬間に重要になる問いだ。*間違った一行はどこへ行くのか?* 自由形式のコードベースでは、間違った認可チェックは何千もの `if` のうちの一つで、レビューの対象面はリポジトリ全体で、ランタイムは権限について何も強制しない。スキャフォールドされたプロジェクトの CI ファイルは、メタデータについての一文でそれを言い切っている(ゲートのないフォーマットなら何にでも当てはまる)。ミスは「何も起こさず、最初にそれを踏んだ人に対して間違った振る舞いをするだけだ」。AI が書けるとは、この一文が成り立たなくなったということだ。
+
+## 四つのゲート、README の言葉で
+
+README のその節は「Why the mistakes don't ship」(ミスが出荷されない理由)と題され、表は丸ごと訳せるほど短い:
+
+| ゲート | 何を捕まえるか |
+| :--- | :--- |
+| **型(Typed)** | 厳格な TypeScript + Zod——形のエラーはエディタの中で、エージェントが書いた数秒後に死ぬ |
+| **検証(Validated)** | `os validate` が、型チェックは通るが実行時に静かに壊れるメタデータを拒否する:宙に浮いたバインディング、誤った CEL 述語、欠けたセキュリティ姿勢 |
+| **レビュー(Reviewed)** | あなたが Console で、小さく読める diff を承認する——生成された糊のコードの山ではなく |
+| **ガバナンス(Governed)** | ランタイムがすべての呼び出しで権限と監査を強制するので、間違ったアプリでさえフェンスの内側にとどまる |
+
+
+
+この表について三つのことが、約束が本物かどうかを決める。記録を見る前に述べておく価値がある。
+
+第一に、ゲートはコストの順に並んでいる。形のエラーはエージェントの一秒を使う。検証のエラーは一ターンを使う。レビューは人の数分を使う。ガバナンスで止まった失敗は、封じ込められたインシデントだ。README が「これが機能する理由」として述べるのは最初の二行にある。「エージェントのエラーは、位置が特定され、修正方法の示されたテキストになり、エージェント自身が読んで直せる」。
+
+第二に、二番目のゲートが存在するのは最初のゲートでは足りないからで、ドキュメントはその理由を一文で言う。「ObjectStack のメタデータはデータであって、コードパスではない——だから大半のミスは TypeScript コンパイラに**捕まらない**。`tsc` を通り、問題なくロードされ、そして**実行時に静かに壊れる**。」述語は文字列だ。行アクションは名前だ。共有モデルは任意のキーだ。三つとも、ミスを抱えたまま型チェックを通る。検証ゲートはこの種のエラーのために作られ、以下の実走が狙うのもこの種だ。
+
+第三に、後ろの二つのゲートは人とランタイムであって、ツールではない。この文章はそうでないふりをしない。ドキュメントの言うレビューゲートは Studio の「Review N changes」の diff だが、以下の実走では人が読む `git diff` だ。ガバナンスゲートはランタイムの認可チェーンと監査ログで、この実走はそれを一度も起動していない。記録が証明するのは、安価な二つのゲートが README の言う通りのものを捕まえることだ。証明できないことは末尾に書く。
+
+## サポートデスク一つ、ミス三つ、実走一回
+
+以下はすべて、再構成ではなく実際に走らせたものだ。Node 22、公開パッケージ。スキャフォールドは README 自身の最初の一歩:
+
+```bash
+$ npm create objectstack@latest support-desk
+ ╔═══════════════════════════════════╗
+ ║ ◆ Create ObjectStack v17.7.0 ║
+ ╚═══════════════════════════════════╝
+
+◆ New Environment
+────────────────────────────────────────
+ Environment: support-desk
+ Namespace: support_desk
+ Template: blank — Minimal starter — one object, REST API, ready to extend
+
+ ✓ Template files written (20)
+ → Installing dependencies...
+ → Installing AI skills for your coding agent...
+ ✓ Environment created!
+```
+
+スキャフォールドは `AGENTS.md` を書き、その中で唯一譲れない一行がゲートだ。「**Never report a metadata change as done until `npm run validate` passes.**」(`npm run validate` が通るまで、メタデータの変更を完了と報告してはならない。)CI ワークフローも書かれ、その唯一のジョブは `pnpm validate`、`pnpm lint`、`pnpm typecheck` を走らせる。ビジネスオブジェクトが一つも存在する前に、ゲートは配線されている。
+
+次に README の要件を、エージェントが書くであろう形で書く。件名、説明、優先度の選択、ステータスの選択を持つ `support_desk_ticket` オブジェクト。まだ解決していないチケットにだけ表示される **Resolve** アクション。「未解決チケット」のリストビュー。Support ナビゲーショングループ。そしてサポート担当者向けの権限セット一つ。アプリ全体で **11 ファイル、追加 127 行**。ミスが入ることになる二つのファイル:
+
+```ts
+// src/actions/resolve-ticket.action.ts
+export const ResolveTicketAction = defineAction({
+ name: 'resolve_ticket',
+ label: 'Resolve',
+ objectName: 'support_desk_ticket',
+ locations: ['record_header', 'list_item'],
+ visible: P`record.status != "resolved"`, // only on tickets not yet resolved
+ operation: 'update',
+ patch: { status: 'resolved' },
+ confirmText: 'Mark this ticket as resolved?',
+});
+```
+
+```ts
+// src/views/ticket.view.ts
+export const TicketViews = defineView({
+ list: {
+ label: 'All tickets',
+ type: 'grid',
+ data,
+ columns: ['subject', 'priority', 'status'],
+ rowActions: ['resolve_ticket'],
+ },
+ // ... form, and an "Open tickets" list view filtered on status = open
+});
+```
+
+まずクリーンな実走を。ルール数を記録に残すためだ:
+
+```text
+$ npm run validate
+◆ Validate
+────────────────────────────────────────
+ → Loading configuration...
+ → Validating against ObjectStack Protocol...
+ → Running author-time rules (49)...
+ → Checking that every required capability has a provider installable in this edition...
+ → Checking package docs (ADR-0046)...
+
+ ✓ Validation passed (327ms)
+
+ Support Desk v0.1.0
+
+ Data: 2 Objects 6 Fields
+ UI: 1 Apps 1 Views 1 Actions
+ Logic: 0 Flows
+ Security: 1 Permissions
+ Runtime: 3 plugins
+```
+
+17.7.0 で 49 のオーサリング時ルール。[このサイトが前回同じゲートを走らせた](/en/blog/how-to-point-your-agent-at-objectstack/)のは 17.2.0 で、そのときは 41 だった。ルール表は成長しており、ゲートにとってはそれが正しい方向だ。この数はツール自身が出力し、公開パッケージ `@objectstack/lint` の `AUTHORING_RULES.length` に等しい。突き合わせるべき第二の数字はない。
+
+### 三つのミス
+
+ここで README の三つの分類に、故意のミスを一つずつ。三つとも、ふつうの JavaScript で訓練されたエージェントが手を伸ばしがちなもので、三つとも型チェックを通る:
+
+```diff
+ // 1 · a bad CEL predicate: the bare identifier most template languages use
+- visible: P`record.status != "resolved"`,
++ visible: P`status != "resolved"`,
+
+ // 2 · a dangling binding: a row action that names nothing
+- rowActions: ['resolve_ticket'],
++ rowActions: ['resolve'],
+
+ // 3 · a missing security posture: the org-wide default left unsaid
+- sharingModel: 'private',
+```
+
+三つを入れたままでも `npm run typecheck` は緑だ。`tsc` は文字列の中身にも、配列の中の名前にも、任意のキーにも意見を持たない。ゲートは持つ:
+
+```text
+$ npm run validate
+◆ Validate
+────────────────────────────────────────
+ → Loading configuration...
+ → Validating against ObjectStack Protocol...
+ → Running author-time rules (49)...
+
+ ✗ Author-time rules failed (3 issues)
+ • stack · action 'resolve_ticket' visible: bare reference `status` — a formula/validation expression binds the record as the `record` namespace, not at top level, so `status` resolves to nothing and the expression silently evaluates to null. Write `record.status`.
+ source: `status != "resolved"`
+ rule: expression-invalid at stack · action 'resolve_ticket' visible
+ • view "#0" · list · rowActions: Row-action menu names action "resolve", which is defined by no action in this stack (neither `stack.actions` nor any object's `actions`). The button renders and does nothing when clicked — a dead affordance the runtime cannot dispatch. Did you mean "resolve_ticket"?
+ Define an action named "resolve" (in `stack.actions` or the object's `actions`) with the location this surface needs, remove the reference, or ignore this if the action is contributed by another installed package. Defined actions: resolve_ticket.
+ rule: action-name-undefined at views[0].list.rowActions[0]
+ • object "support_desk_ticket": custom object "support_desk_ticket" declares no sharingModel (OWD). The runtime fails CLOSED to 'private' (ADR-0090 D1), but the baseline must be an authored decision, not an accident — this is the exact shape of the leave_request incident, where an object with no sharingModel let an ordinary read/write grant read and edit every other user's records.
+ Declare sharingModel explicitly: 'private' (owner + shares; recommended default), 'public_read', 'public_read_write', or 'controlled_by_parent' (master-detail children).
+ rule: security-owd-unset at objects[1].sharingModel
+ ELIFECYCLE Command failed with exit code 1.
+```
+
+この三つのメッセージをエラーとしてではなく台帳として読んでほしい。どれも、ゲートがなければ実行時に何が起きたかを明記しているからだ。これを**ミスの台帳**と呼ぶ。ある対象フォーマットについて、エージェントが犯しうるミスの種類、それを捕まえるゲート、そして捕まえなければ実行時に現れたはずの症状を並べたリストだ。フォーマットが「AI が書ける」のは、正確にこの台帳に空の行がない範囲までである。
+
+
+
+| ミス | 出荷されていたらどうなったか | 捕まえたゲート · ルール |
+| :--- | :--- | :--- |
+| `` visible: P`status != "resolved"` `` | 述語は「静かに null に評価され」、Resolve ボタンはすべてのチケットで隠れる | 検証 · `expression-invalid` |
+| `rowActions: ['resolve']` | 「ボタンは描画されるがクリックしても何も起きない——ランタイムがディスパッチできない死んだアフォーダンス」 | 検証 · `action-name-undefined` |
+| オブジェクトに `sharingModel` がない | ランタイムは「'private' にフェイルクローズする」が、言葉にされていないベースラインは「leave_request インシデントとまったく同じ形」 | 検証 · `security-owd-unset` |
+| フィールドの `required: 'yes'` | 何も出荷されない:`tsc` が「Type 'string' is not assignable to type 'boolean \| undefined'」で拒否する | 型 · `TS2322` |
+
+四行目は型のゲートで、バリデータの記録を検証だけのものに保つために、クリーンなツリー上で別に走らせた。`required: true` を `required: 'yes'` に変えると、`npm run typecheck` はそのメッセージとともに終了コード 2 で終わる。ほかの何かが動く前に。もっとも安価でもっとも面白みのないゲートであり、それが最初に置かれている意味だ。
+
+メッセージの二つの性質が、約束のすべてを担っている。それらは**位置が特定されている**:アクションと述語のスロット、ビューと配列のインデックス、オブジェクトとキーパス。そして**修正方法が示されている**:「Write `record.status`」、「Did you mean `resolve_ticket`?」、`sharingModel` の四つの正しい値。これを読んだエージェントは一ターンでループを閉じる。[エージェントを ObjectStack 向けに設定する記事](/en/blog/how-to-point-your-agent-at-objectstack/)はこれを*修正的エラー*(corrective error)と呼び、フォーマットが持つ最もレバレッジの高いものはドキュメントではなくエラーメッセージだと論じた。この実走はそのための新たな三つのデータ点だ。
+
+三行を元に戻すと、ゲートはまた静かになる:
+
+```text
+$ npm run validate
+ → Running author-time rules (49)...
+
+ ✓ Validation passed (240ms)
+
+ Data: 2 Objects 6 Fields
+ UI: 1 Apps 1 Views 1 Actions
+ Security: 1 Permissions
+ Runtime: 3 plugins
+```
+
+実走からもう一行、ここに置くべきものがある。ゲートが拒否よりも微妙なことをしている場面だ。権限セットの最初の版は、プライベートなオブジェクトに対して読み・作成・編集を与えながら、読みがどこまで届くかを言っていなかった。クリーンな実走にはこのアドバイザリが付いていた。「`permission set "support_agent": "support_desk_ticket" is private (OWD) and this set grants allowRead without a readScope — holders see ONLY records they own (plus explicit shares).`」(権限セット「support_agent」:「support_desk_ticket」はプライベート(OWD)で、このセットは readScope なしに allowRead を与えている——保持者は自分が所有するレコード(と明示的に共有されたもの)しか見えない。)これはエラーではなく、ゲートがエラーにしないのも正しい。担当者が自分のチケットしか見えないサポートデスクは、正当な設計だ。これは、決めなければならない人の前に浮上した、決めかけの決定である。修正は `readScope: 'org'` というキー一つで、それは訂正ではなくプロダクト上の決定だった。
+
+## ゲートが捕まえないもの
+
+ゲートについての記事は、ゲートがどこで止まるかを言わなければならない。出典はこの点について珍しく率直だ。
+
+**ビジネスロジックの正しさ。** ドキュメント:「ガードレール 1–3 はメタデータが*正しい形をしている*ことを証明する。それが*あなたの意図したもの*であることは証明できない。」ゲートは `record.status` が解決することを確認した。チケットをリストから直接解決できてよいか、`resolved` が正しい終端状態か、サポート担当者が組織のすべてのチケットを見てよいかについて、ゲートは何の意見も持たない。先の記事は同じ限界を「ゲートは構造を検査する。意図ではない」と言い、49 のルールはそれを何も変えない。
+
+**レビュアーが見落とす要件。** レビューゲートは、人が diff を読むことだ。構造上それは小さい——アプリ全体で 127 行、三つのミスで三行——だが、小さいことと読まれたことは同じではない。契約社員も `support_agent` セットを持つのかを尋ねずに `readScope: 'org'` を承認したレビュアーは、決定を出荷したのであって、ゲートは止めない。メタデータは正しい形をしているからだ。アドバイザリのチャネルが役に立つのは、誰かがそれを読む範囲までである。
+
+**新規のロジック。** フックとアクション本体はコードだ。「ビジネスオントロジー」の概念ページはそれらが「ランタイムの権限と監査のフェンスの内側で実行される」と言い、フェンスは間違った本体が触れる範囲を限定する。本体を正しくはしない。それはやはりコードレビューだ。
+
+**この実走が試していないこと。** 記録はコマンドライン上の最初の二つのゲートだ。ドキュメントが述べるレビューゲート——変更が本番に出る前に人が承認する Studio の diff——はここでは `git diff` だった。ガバナンスゲート——ドキュメントが匿名拒否からフィールドレベルセキュリティまで、各層がフェイルクローズする六層として述べる認可チェーンと、すべての呼び出しの監査——は起動していない。`os validate` はアーティファクトを書かずサーバーも立ち上げない、オーサリング時のチェックだからだ。README のこのゲートに関する主張は「間違ったアプリでさえフェンスの内側にとどまる」であり、この記事はそれを実演するのではなく引用する。
+
+**そして、二つの言い分は立ったままだ。** 捨てる前提のプロトタイプに四つのゲートは要らない。良いテストと注意深いレビュアーを持つ手書きのサービスには、ツールではなく人が埋める、それ自身の台帳がある。ここでの主張は「こちらの方が良い」より狭い。エージェントが書き、企業が動かすアプリについては、台帳はフォーマットが埋めるべきだ、ということだ。エージェントは覚えていないし、レビュアーはすべてを読めないからである。
+
+## 同じゲートを、ブラウザから
+
+ターミナルを開かない人に対しても四つのゲートの話を正直に保つ事実が一つある。`os validate` は唯一の扉ではない。ドキュメントは四番目の扉を述べる。「すべてのメタデータの**書き込み**——Studio のデザイナー、`PUT /api/v1/meta/*`、MCP/AI の書き手——は `saveMetaItem` に着地し、#4463 以降、**`state: 'active'`** へ向かう書き込みは永続化の前に同じ表を走らせる。」上で 49 と出力されたのと同じ `AUTHORING_RULES` が、Studio から公開された宙に浮いたバインディングを持つダッシュボードを、同じ位置特定済みのメッセージで拒否する。検証ゲートはランタイムの性質であって、CLI の性質ではない。
+
+そして README 自身のループについての最後の一文:「同じループをホスト型で、ブラウザだけで、何もインストールせずに使いたい?それが ObjectOS — ObjectStack の上に構築された商用ランタイム環境です。」ObjectOS は、ブラウザ上でホストされインストール不要の ObjectOS Cloud としても、自社インフラでセルフマネージドの ObjectOS Enterprise としても動作します。どちらのエディションでも、オントロジーをエクスポートしてオープンソースの ObjectStack ランタイムで実行できます。
+
+## 自分の台帳を埋める
+
+上の実走は README の五分の道をたどった。唯一の変更は、ミスを故意に犯したことだ。フォーマットがこの記事の意味で「AI が書ける」かどうかを知る最速の方法である。
+
+```bash
+npm create objectstack@latest my-app && cd my-app
+# コーディングエージェントで開き、業務の要件を伝える
+npx os dev --ui
+```
+
+README の述べるサポートデスクを頼む。そして Console を見る前に diff を開き、この実走と同じことをする。述語から `record.` を落とし、ビューの中でアクション名を綴り間違え、`sharingModel` を消して、`npm run validate` を走らせる。ゲートが埋めた行と、あなたに残された行を数える。[エージェントに渡すルールファイル](/ja/blog/give-your-agent-rules-for-governable-apps/)はミスがいくつ生まれるかを決め、四つのゲートはそれがどこで止まるかを決める。あなたの台帳は、その両方の正直な尺度だ。
diff --git a/content/blog/what-makes-a-business-app-ai-writable/index.mdx b/content/blog/what-makes-a-business-app-ai-writable/index.mdx
new file mode 100644
index 0000000..11cb9d6
--- /dev/null
+++ b/content/blog/what-makes-a-business-app-ai-writable/index.mdx
@@ -0,0 +1,230 @@
+---
+title: "What Makes a Business App AI-Writable? Typed, Validated, Reviewed, Governed"
+description: "AI-writable is a property of the target, not the model. Four gates, one real run: three deliberate mistakes, 49 rules, three located errors, then a clean pass."
+author: ObjectStack Team
+date: 2026-10-22
+status: published
+topic: ai-agents
+audience: developer
+solutions: []
+industries: []
+cover: ./cover-en.svg
+tags:
+ - AI agents
+ - AI-writable
+---
+
+A business app is AI-writable when the mistakes an agent will inevitably make have somewhere to die before they reach a user. That is a property of the target format, not of the model. The ObjectStack README names four such places, and calls them gates: **Typed**, **Validated**, **Reviewed** and **Governed**. Typed errors die in the editor. Validated errors die at `os validate`, which refuses metadata that type-checks but would fail silently at runtime. Reviewed errors die in a diff a human can read whole. Governed errors stay inside the runtime's permission and audit fence even when they ship. Below is one real run of the first two gates on a scaffolded support desk at ObjectStack 17.7.0: three deliberate mistakes of the three kinds the README lists, the 49-rule gate refusing each with a located, corrective message, and a clean pass after the fix.
+
+## Two things that are already true
+
+Before the gates, two steelmen, because the argument does not need either of them to be wrong.
+
+**Vibe-coded apps work.** You can describe a support desk to a model and have a running one in an afternoon, with screens, a database and an API, in whatever language the model likes best. For a prototype, a one-off tool or a demo that will be thrown away, that is the right trade, and nothing below is an argument against it. The model is not the bottleneck any more.
+
+**Hand-written code is reviewable, and the tooling for it is mature.** A senior engineer reading a pull request, a type system, a test suite and a code review process catch real defects every day. For novel logic there is no substitute, and ObjectStack itself says so: "code does not disappear: it moves into the runtime, as hooks, action bodies, CEL, and constrained JSX", and that code is still read by a person.
+
+What neither of them answers is the question that matters once an agent is the one writing: *where does a wrong line go?* In a free-form codebase a wrong authorization check is one `if` among thousands, the review surface is the whole repository, and the runtime enforces nothing about authority on its own. The scaffolded project's own CI file puts it in one sentence, about metadata, but it is true of any format with no gate: a mistake "raises nothing, it just behaves wrongly for whoever hits it first." AI-writable means that sentence has stopped being true.
+
+## The four gates, as the README states them
+
+The README's section is called "Why the mistakes don't ship", and its table is short enough to quote whole:
+
+| Gate | Catches |
+| :--- | :--- |
+| **Typed** | Strict TypeScript + Zod — shape errors die in the editor, seconds after the agent writes them |
+| **Validated** | `os validate` rejects metadata that type-checks but would fail silently at runtime: dangling bindings, bad CEL predicates, missing security posture |
+| **Reviewed** | You approve a small readable diff in the Console — not a pile of generated glue |
+| **Governed** | The runtime enforces permissions and audit on every call, so even a wrong app stays inside the fence |
+
+
+
+Three things about this table decide whether the promise is real, and they are worth stating before the transcript.
+
+First, the gates are ordered by cost. A shape error costs the agent a second. A validation error costs it one turn. A review costs a human minutes. A governed failure costs an incident that was contained. The README's stated reason the whole thing works is the first two rows: "an agent's errors become located, corrective text it can read and fix itself".
+
+Second, the second gate exists because the first one is not enough, and the docs say why in one sentence: "ObjectStack metadata is data, not code paths — so most mistakes are **not** caught by the TypeScript compiler. They pass `tsc`, load fine, and then fail **silently at runtime**." A predicate is a string. A row action is a name. A sharing model is an optional key. All three type-check with the mistake in them. That is the class of error the Validated gate was built for, and it is the class the run below targets.
+
+Third, the last two gates are human and runtime, not tooling, and this post does not pretend otherwise. The Reviewed gate in the docs is Studio's "Review N changes" diff; in the run below it is a `git diff`, read by a person. The Governed gate is the runtime's authorization chain and audit log, which the run never starts. What the transcript proves is that the two cheap gates catch what the README says they catch. What it does not prove is covered at the end.
+
+## One support desk, three mistakes, one run
+
+Everything below was run, not reconstructed, on Node 22 with the published packages. The scaffold is the README's own first step:
+
+```bash
+$ npm create objectstack@latest support-desk
+ ╔═══════════════════════════════════╗
+ ║ ◆ Create ObjectStack v17.7.0 ║
+ ╚═══════════════════════════════════╝
+
+◆ New Environment
+────────────────────────────────────────
+ Environment: support-desk
+ Namespace: support_desk
+ Template: blank — Minimal starter — one object, REST API, ready to extend
+
+ ✓ Template files written (20)
+ → Installing dependencies...
+ → Installing AI skills for your coding agent...
+ ✓ Environment created!
+```
+
+The scaffold writes an `AGENTS.md` whose one non-negotiable line is the gate: "**Never report a metadata change as done until `npm run validate` passes.**" It also writes a CI workflow whose only job runs `pnpm validate`, `pnpm lint` and `pnpm typecheck`. The gate is wired before any business object exists.
+
+Then the README's requirement, written as the agent would write it: a `support_desk_ticket` object with a subject, a description, a priority select and a status select; a **Resolve** action that only shows on tickets that are not already resolved; an "Open tickets" list view; a Support navigation group; and one permission set for support agents. The whole app is **127 added lines across 11 files**. The two files the mistakes will live in:
+
+```ts
+// src/actions/resolve-ticket.action.ts
+export const ResolveTicketAction = defineAction({
+ name: 'resolve_ticket',
+ label: 'Resolve',
+ objectName: 'support_desk_ticket',
+ locations: ['record_header', 'list_item'],
+ visible: P`record.status != "resolved"`, // only on tickets not yet resolved
+ operation: 'update',
+ patch: { status: 'resolved' },
+ confirmText: 'Mark this ticket as resolved?',
+});
+```
+
+```ts
+// src/views/ticket.view.ts
+export const TicketViews = defineView({
+ list: {
+ label: 'All tickets',
+ type: 'grid',
+ data,
+ columns: ['subject', 'priority', 'status'],
+ rowActions: ['resolve_ticket'],
+ },
+ // ... form, and an "Open tickets" list view filtered on status = open
+});
+```
+
+The clean run first, so the number of rules is on the record:
+
+```text
+$ npm run validate
+◆ Validate
+────────────────────────────────────────
+ → Loading configuration...
+ → Validating against ObjectStack Protocol...
+ → Running author-time rules (49)...
+ → Checking that every required capability has a provider installable in this edition...
+ → Checking package docs (ADR-0046)...
+
+ ✓ Validation passed (327ms)
+
+ Support Desk v0.1.0
+
+ Data: 2 Objects 6 Fields
+ UI: 1 Apps 1 Views 1 Actions
+ Logic: 0 Flows
+ Security: 1 Permissions
+ Runtime: 3 plugins
+```
+
+Forty-nine author-time rules at 17.7.0. When [this site last ran the same gate](/en/blog/how-to-point-your-agent-at-objectstack/), at 17.2.0, it ran 41; the rule table grows, which is the right direction for a gate. The count is printed by the tool and equals `AUTHORING_RULES.length` in the published `@objectstack/lint` package, so there is no second number to reconcile.
+
+### The three mistakes
+
+Now the README's three classes, one deliberate mistake each. All three are the kind an agent trained on ordinary JavaScript reaches for, and all three type-check:
+
+```diff
+ // 1 · a bad CEL predicate: the bare identifier most template languages use
+- visible: P`record.status != "resolved"`,
++ visible: P`status != "resolved"`,
+
+ // 2 · a dangling binding: a row action that names nothing
+- rowActions: ['resolve_ticket'],
++ rowActions: ['resolve'],
+
+ // 3 · a missing security posture: the org-wide default left unsaid
+- sharingModel: 'private',
+```
+
+`npm run typecheck` is green on all three; `tsc` has no opinion on the contents of a string, the name in an array or an optional key. The gate does:
+
+```text
+$ npm run validate
+◆ Validate
+────────────────────────────────────────
+ → Loading configuration...
+ → Validating against ObjectStack Protocol...
+ → Running author-time rules (49)...
+
+ ✗ Author-time rules failed (3 issues)
+ • stack · action 'resolve_ticket' visible: bare reference `status` — a formula/validation expression binds the record as the `record` namespace, not at top level, so `status` resolves to nothing and the expression silently evaluates to null. Write `record.status`.
+ source: `status != "resolved"`
+ rule: expression-invalid at stack · action 'resolve_ticket' visible
+ • view "#0" · list · rowActions: Row-action menu names action "resolve", which is defined by no action in this stack (neither `stack.actions` nor any object's `actions`). The button renders and does nothing when clicked — a dead affordance the runtime cannot dispatch. Did you mean "resolve_ticket"?
+ Define an action named "resolve" (in `stack.actions` or the object's `actions`) with the location this surface needs, remove the reference, or ignore this if the action is contributed by another installed package. Defined actions: resolve_ticket.
+ rule: action-name-undefined at views[0].list.rowActions[0]
+ • object "support_desk_ticket": custom object "support_desk_ticket" declares no sharingModel (OWD). The runtime fails CLOSED to 'private' (ADR-0090 D1), but the baseline must be an authored decision, not an accident — this is the exact shape of the leave_request incident, where an object with no sharingModel let an ordinary read/write grant read and edit every other user's records.
+ Declare sharingModel explicitly: 'private' (owner + shares; recommended default), 'public_read', 'public_read_write', or 'controlled_by_parent' (master-detail children).
+ rule: security-owd-unset at objects[1].sharingModel
+ ELIFECYCLE Command failed with exit code 1.
+```
+
+Read the three messages as a ledger rather than as errors, because each one states what would have happened at runtime without the gate. Call it the **mistake ledger**: for a target format, the list of mistake classes an agent can make, the gate that catches each, and the runtime symptom it would otherwise have had. A format is AI-writable to exactly the extent that the ledger has no empty rows.
+
+
+
+| The mistake | Would have shipped as | Caught by · rule |
+| :--- | :--- | :--- |
+| `` visible: P`status != "resolved"` `` | The predicate "silently evaluates to null": the Resolve button hidden on every ticket | Validated · `expression-invalid` |
+| `rowActions: ['resolve']` | "The button renders and does nothing when clicked — a dead affordance the runtime cannot dispatch" | Validated · `action-name-undefined` |
+| no `sharingModel` on the object | The runtime "fails CLOSED to 'private'", but an unsaid baseline is "the exact shape of the leave_request incident" | Validated · `security-owd-unset` |
+| `required: 'yes'` on a field | Nothing: `tsc` refuses it with "Type 'string' is not assignable to type 'boolean \| undefined'" | Typed · `TS2322` |
+
+The fourth row is the Typed gate, run separately on the clean tree so the validator transcript stays about validation: change `required: true` to `required: 'yes'` and `npm run typecheck` exits 2 with that message, before anything else runs. It is the cheapest gate and the least interesting, which is the point of its position.
+
+Two properties of the messages carry the whole promise. They are **located**: an action and a predicate slot, a view and an array index, an object and a key path. And they are **corrective**: "Write `record.status`", "Did you mean `resolve_ticket`?", the four legal values of `sharingModel`. An agent reading these closes the loop in one turn. The post on [configuring an agent for ObjectStack](/en/blog/how-to-point-your-agent-at-objectstack/) calls this a *corrective error* and argues that error messages, not documentation, are the highest-leverage thing a format owns; this run is three more data points for it.
+
+Restore the three lines and the gate is quiet again:
+
+```text
+$ npm run validate
+ → Running author-time rules (49)...
+
+ ✓ Validation passed (240ms)
+
+ Data: 2 Objects 6 Fields
+ UI: 1 Apps 1 Views 1 Actions
+ Security: 1 Permissions
+ Runtime: 3 plugins
+```
+
+One more line from the run belongs here, because it is the gate doing something subtler than refusing. The first version of the permission set granted read, create and edit on a private object without saying how far a read reaches, and the clean run carried an advisory: "`permission set "support_agent": "support_desk_ticket" is private (OWD) and this set grants allowRead without a readScope — holders see ONLY records they own (plus explicit shares).`" That is not an error, and the gate is right not to make it one: a support desk where agents see only their own tickets is a legitimate design. It is a half-decision surfaced to the human who has to make it. The fix was one key, `readScope: 'org'`, and it was a product decision, not a correction.
+
+## What the gates do not catch
+
+A gate post has to say where the gates stop, and the sources are unusually direct about it.
+
+**Business-logic correctness.** The docs: "Guardrails 1–3 prove the metadata is *well-formed*. They can't prove it's *what you meant*." The gate confirmed that `record.status` resolves. It has no opinion on whether a ticket should be resolvable from the list, whether `resolved` is the right terminal state, or whether support agents should see every ticket in the organisation. The earlier post's version of the same limit is "the gate checks structure, not intent", and nothing in 49 rules changes that.
+
+**Requirements the reviewer misses.** The Reviewed gate is a human reading a diff. It is small by construction — 127 lines for the whole app, three lines for the three mistakes — but small is not the same as read. A reviewer who approves `readScope: 'org'` without asking whether contractors hold the `support_agent` set has shipped a decision, and the gate will not stop them, because the metadata is well-formed. The advisory channel helps exactly as far as someone reads it.
+
+**Novel logic.** Hooks and action bodies are code. The Business Ontology concepts page says they "run inside the runtime's permission and audit fence", and the fence bounds what a wrong body can touch; it does not make the body right. That is still code review.
+
+**What this run did not exercise.** The transcript is the first two gates on the command line. The Reviewed gate as the docs describe it, the Studio diff a human approves before a change goes live, was a `git diff` here. The Governed gate — the authorization chain the docs describe as six fail-closed layers from anonymous deny through field-level security, with audit on every call — was not started, because `os validate` is an author-time check that writes no artifact and starts no server. The README's claim for that gate is that "even a wrong app stays inside the fence"; this post quotes it rather than demonstrating it.
+
+**And the steelmen stand.** A throwaway prototype does not need four gates. A hand-written service with good tests and a careful reviewer has its own ledger, filled in by people instead of a tool. The claim here is narrower than "this is better": it is that for an app an agent writes and a business runs, the ledger should be filled in by the format, because the agent will not remember and the reviewer cannot read everything.
+
+## The same gate, from the browser
+
+One fact that keeps the four-gate story honest for people who never open a terminal: `os validate` is not the only door. The docs describe a fourth: "Every metadata **write** — Studio's designer, `PUT /api/v1/meta/*`, an MCP/AI author — lands in `saveMetaItem`, and since #4463 a write going **`state: 'active'`** runs that same table before it persists." The same `AUTHORING_RULES` that printed 49 above refuses a dashboard published from Studio with a dangling binding, with the same located message. The Validated gate is a property of the runtime, not of the CLI.
+
+And the README's own last line about the loop: "Want the same loop hosted, in the browser, nothing to install? That's ObjectOS, the commercial runtime environment built on ObjectStack." ObjectOS runs hosted in the browser with nothing to install (ObjectOS Cloud) or self-managed on your own infrastructure (ObjectOS Enterprise), and on either edition you can export your ontology and run it on the open-source ObjectStack runtime.
+
+## Fill in your own ledger
+
+The run above took the README's five-minute path. The only change is to make the mistakes on purpose, which is the fastest way to find out whether a format is AI-writable in the sense this post means.
+
+```bash
+npm create objectstack@latest my-app && cd my-app
+# open it in your coding agent and describe what the business needs
+npx os dev --ui
+```
+
+Ask for the support desk the README describes. Then, before you look at the Console, open the diff and do what the run did: drop the `record.` from a predicate, misspell an action name in a view, delete a `sharingModel`, and run `npm run validate`. Count the rows the gate fills in and the rows it leaves to you. The [rules file you give the agent](/en/blog/give-your-agent-rules-for-governable-apps/) decides how many mistakes get made; the four gates decide where they stop; your ledger is the honest measure of both.
diff --git a/content/blog/what-makes-a-business-app-ai-writable/index.zh-Hans.mdx b/content/blog/what-makes-a-business-app-ai-writable/index.zh-Hans.mdx
new file mode 100644
index 0000000..e4c5293
--- /dev/null
+++ b/content/blog/what-makes-a-business-app-ai-writable/index.zh-Hans.mdx
@@ -0,0 +1,230 @@
+---
+title: 什么样的业务应用是 AI 可编写的?四道门禁:类型、验证、审查、治理
+description: AI 可编写是目标格式的属性,不是模型的属性。四道门禁、一次真实运行:三个故意犯的错、49 条规则、三条定位到位的报错,然后一次干净通过。
+author: ObjectStack Team
+date: 2026-10-22
+status: published
+topic: ai-agents
+audience: developer
+solutions: []
+industries: []
+cover: ./cover.svg
+tags:
+ - AI 智能体
+ - AI 可编写
+---
+
+一个业务应用是「AI 可编写」的,当且仅当 Agent 必然会犯的错误在触达用户之前有地方可死。这是目标格式的属性,不是模型的属性。ObjectStack 的 README 点名了四个这样的地方,称之为门禁:**类型(Typed)**、**验证(Validated)**、**审查(Reviewed)**、**治理(Governed)**。类型错误死在编辑器里。验证错误死在 `os validate` 上——它拒收那些能通过类型检查、却会在运行时静默失败的元数据。审查错误死在一份人能整个读完的 diff 里。治理错误即使上线了,也留在运行时的权限与审计围栏之内。下面是在 ObjectStack 17.7.0 上、用脚手架生成的支持台跑的一次真实运行,覆盖前两道门禁:按 README 列出的三类各故意犯一个错,49 条规则的门禁把每一个都定位到位、附带修正方案地拒收,修好之后一次干净通过。
+
+## 两件本来就成立的事
+
+讲门禁之前,先替对立面把话说足,因为这篇文章的论证不需要它们中的任何一个是错的。
+
+**Vibe coding 做出来的应用是能用的。** 你可以向模型描述一个支持台,一个下午就拿到一个能跑的版本,有界面、有数据库、有 API,用的是模型最拿手的任何语言。做原型、做一次性工具、做用完即弃的演示,这是正确的取舍,下文没有一句是反对它的。模型早已不是瓶颈。
+
+**手写代码是可审查的,而且工具链已经成熟。** 一位资深工程师读 pull request、一套类型系统、一套测试、一个代码评审流程,每天都在抓真实的缺陷。对于新颖的逻辑没有替代品,ObjectStack 自己也这么说:「代码不会消失:它搬进了运行时,成为钩子、动作体、CEL 和受限 JSX」——而那些代码仍然要由人来读。
+
+两者都没有回答的,是一旦由 Agent 来写、真正要紧的那个问题:*写错的那一行去了哪里?* 在一个自由形式的代码库里,一处错误的鉴权检查就是成千上万个 `if` 里的一个,审查面是整个仓库,运行时自身对权限什么也不强制。脚手架生成的项目里,CI 文件用一句话(说的是元数据,但对任何没有门禁的格式都成立)概括了这件事:一个错误「什么都不抛,只是对第一个碰上它的人表现得不对」。AI 可编写,意思就是这句话不再成立。
+
+## 四道门禁,按 README 的表述
+
+README 的那一节叫「Why the mistakes don't ship」(错误为什么上不了线),表格短到可以整个译出来:
+
+| 门禁 | 拦住什么 |
+| :--- | :--- |
+| **类型(Typed)** | 严格的 TypeScript + Zod——形状错误死在编辑器里,Agent 写下它们几秒钟之后 |
+| **验证(Validated)** | `os validate` 拒收那些能通过类型检查、却会在运行时静默失败的元数据:悬空绑定、错误的 CEL 谓词、缺失的安全姿态 |
+| **审查(Reviewed)** | 你在 Console 里批准一份小而可读的 diff——不是一堆生成出来的胶水代码 |
+| **治理(Governed)** | 运行时在每一次调用上强制执行权限与审计,所以哪怕应用写错了,也留在围栏之内 |
+
+
+
+这张表里有三件事决定了这个承诺是不是真的,值得在看记录之前先说清楚。
+
+第一,门禁按代价排序。一个形状错误花 Agent 一秒钟。一个验证错误花它一轮。一次审查花一个人几分钟。一次被治理住的失败,是一起被控制住的事故。README 说整件事成立的原因,就在前两行:「Agent 的错误变成了定位到位、可修正的文字,它自己就能读、自己就能修」。
+
+第二,第二道门禁之所以存在,是因为第一道不够,文档用一句话说明了原因:「ObjectStack 的元数据是数据,不是代码路径——所以大多数错误**不会**被 TypeScript 编译器抓到。它们通过 `tsc`,正常加载,然后**在运行时静默失败**。」一个谓词是字符串。一个行动作是名字。一个共享模型是可选键。三者带着错误都能通过类型检查。这正是验证门禁为之而建的那一类错误,也是下面这次运行瞄准的那一类。
+
+第三,后两道门禁是人和运行时,不是工具,这篇文章不假装不是。文档里的审查门禁是 Studio 的「Review N changes」diff;在下面的运行里,它是一份由人读的 `git diff`。治理门禁是运行时的鉴权链和审计日志,这次运行从未启动它。记录证明的是:两道便宜的门禁抓住了 README 说它们会抓住的东西。它证明不了的,放在文末。
+
+## 一个支持台,三个错误,一次运行
+
+下面的一切都是真跑出来的,不是拼出来的:Node 22,已发布的包。脚手架就是 README 自己的第一步:
+
+```bash
+$ npm create objectstack@latest support-desk
+ ╔═══════════════════════════════════╗
+ ║ ◆ Create ObjectStack v17.7.0 ║
+ ╚═══════════════════════════════════╝
+
+◆ New Environment
+────────────────────────────────────────
+ Environment: support-desk
+ Namespace: support_desk
+ Template: blank — Minimal starter — one object, REST API, ready to extend
+
+ ✓ Template files written (20)
+ → Installing dependencies...
+ → Installing AI skills for your coding agent...
+ ✓ Environment created!
+```
+
+脚手架写下一份 `AGENTS.md`,其中唯一没有商量余地的一行就是门禁:「**Never report a metadata change as done until `npm run validate` passes.**」(在 `npm run validate` 通过之前,永远不要把一次元数据改动报告为已完成。)它还写下一个 CI 工作流,唯一的 job 跑 `pnpm validate`、`pnpm lint` 和 `pnpm typecheck`。在任何业务对象存在之前,门禁已经接好了。
+
+接着是 README 里的需求,按 Agent 会写的方式写出来:一个 `support_desk_ticket` 对象,带主题、描述、一个优先级选择和一个状态选择;一个只在尚未解决的工单上显示的 **Resolve** 动作;一个「未解决工单」列表视图;一个 Support 导航组;再加一个给支持人员的权限集。整个应用是 **11 个文件、新增 127 行**。错误将落在其中两个文件里:
+
+```ts
+// src/actions/resolve-ticket.action.ts
+export const ResolveTicketAction = defineAction({
+ name: 'resolve_ticket',
+ label: 'Resolve',
+ objectName: 'support_desk_ticket',
+ locations: ['record_header', 'list_item'],
+ visible: P`record.status != "resolved"`, // only on tickets not yet resolved
+ operation: 'update',
+ patch: { status: 'resolved' },
+ confirmText: 'Mark this ticket as resolved?',
+});
+```
+
+```ts
+// src/views/ticket.view.ts
+export const TicketViews = defineView({
+ list: {
+ label: 'All tickets',
+ type: 'grid',
+ data,
+ columns: ['subject', 'priority', 'status'],
+ rowActions: ['resolve_ticket'],
+ },
+ // ... form, and an "Open tickets" list view filtered on status = open
+});
+```
+
+先跑一次干净的,把规则数记在案:
+
+```text
+$ npm run validate
+◆ Validate
+────────────────────────────────────────
+ → Loading configuration...
+ → Validating against ObjectStack Protocol...
+ → Running author-time rules (49)...
+ → Checking that every required capability has a provider installable in this edition...
+ → Checking package docs (ADR-0046)...
+
+ ✓ Validation passed (327ms)
+
+ Support Desk v0.1.0
+
+ Data: 2 Objects 6 Fields
+ UI: 1 Apps 1 Views 1 Actions
+ Logic: 0 Flows
+ Security: 1 Permissions
+ Runtime: 3 plugins
+```
+
+17.7.0 上是 49 条作者期规则。[本站上一次跑同一道门禁](/en/blog/how-to-point-your-agent-at-objectstack/)是在 17.2.0 上,当时是 41 条;规则表在长,对一道门禁来说这是对的方向。这个数字由工具自己打印,等于已发布的 `@objectstack/lint` 包里 `AUTHORING_RULES.length`,没有第二个数字需要对账。
+
+### 三个错误
+
+现在是 README 的三类,每类故意犯一个错。三个都是在普通 JavaScript 上训练出来的 Agent 会顺手写出的那种,三个都能通过类型检查:
+
+```diff
+ // 1 · a bad CEL predicate: the bare identifier most template languages use
+- visible: P`record.status != "resolved"`,
++ visible: P`status != "resolved"`,
+
+ // 2 · a dangling binding: a row action that names nothing
+- rowActions: ['resolve_ticket'],
++ rowActions: ['resolve'],
+
+ // 3 · a missing security posture: the org-wide default left unsaid
+- sharingModel: 'private',
+```
+
+三个错都在,`npm run typecheck` 照样是绿的:`tsc` 对一个字符串的内容、一个数组里的名字、一个可选键,都没有意见。门禁有:
+
+```text
+$ npm run validate
+◆ Validate
+────────────────────────────────────────
+ → Loading configuration...
+ → Validating against ObjectStack Protocol...
+ → Running author-time rules (49)...
+
+ ✗ Author-time rules failed (3 issues)
+ • stack · action 'resolve_ticket' visible: bare reference `status` — a formula/validation expression binds the record as the `record` namespace, not at top level, so `status` resolves to nothing and the expression silently evaluates to null. Write `record.status`.
+ source: `status != "resolved"`
+ rule: expression-invalid at stack · action 'resolve_ticket' visible
+ • view "#0" · list · rowActions: Row-action menu names action "resolve", which is defined by no action in this stack (neither `stack.actions` nor any object's `actions`). The button renders and does nothing when clicked — a dead affordance the runtime cannot dispatch. Did you mean "resolve_ticket"?
+ Define an action named "resolve" (in `stack.actions` or the object's `actions`) with the location this surface needs, remove the reference, or ignore this if the action is contributed by another installed package. Defined actions: resolve_ticket.
+ rule: action-name-undefined at views[0].list.rowActions[0]
+ • object "support_desk_ticket": custom object "support_desk_ticket" declares no sharingModel (OWD). The runtime fails CLOSED to 'private' (ADR-0090 D1), but the baseline must be an authored decision, not an accident — this is the exact shape of the leave_request incident, where an object with no sharingModel let an ordinary read/write grant read and edit every other user's records.
+ Declare sharingModel explicitly: 'private' (owner + shares; recommended default), 'public_read', 'public_read_write', or 'controlled_by_parent' (master-detail children).
+ rule: security-owd-unset at objects[1].sharingModel
+ ELIFECYCLE Command failed with exit code 1.
+```
+
+把这三条消息当作一本台账来读,而不是当作报错,因为每一条都写明了没有门禁时运行时会发生什么。叫它**错误台账**:对一个目标格式,列出 Agent 可能犯的每一类错误、抓住它的那道门禁,以及否则它会在运行时表现成什么症状。一个格式有多 AI 可编写,恰好等于这本台账有多少行不是空的。
+
+
+
+| 错误 | 否则会以什么形态上线 | 被谁拦住 · 规则 |
+| :--- | :--- | :--- |
+| `` visible: P`status != "resolved"` `` | 谓词「静默求值为 null」:Resolve 按钮在每一张工单上都被藏起来 | 验证 · `expression-invalid` |
+| `rowActions: ['resolve']` | 「按钮渲染出来,点击后什么也不做——一个运行时无法分发的死 affordance」 | 验证 · `action-name-undefined` |
+| 对象上没有 `sharingModel` | 运行时「fail closed 到 'private'」,但一个没说出口的基线正是「leave_request 事故的形状」 | 验证 · `security-owd-unset` |
+| 字段上写 `required: 'yes'` | 什么也上不了线:`tsc` 以「Type 'string' is not assignable to type 'boolean \| undefined'」拒收 | 类型 · `TS2322` |
+
+第四行是类型门禁,在干净的树上单独跑,好让验证器的记录只关于验证:把 `required: true` 改成 `required: 'yes'`,`npm run typecheck` 带着那条消息以退出码 2 结束,在任何别的东西运行之前。它是最便宜、也最无趣的一道门禁,而这正是它排在第一位的意义。
+
+消息的两个属性承载了整个承诺。它们**定位到位**:一个动作和一个谓词槽位,一个视图和一个数组下标,一个对象和一条键路径。它们**带修正方案**:「Write `record.status`」、「Did you mean `resolve_ticket`?」、`sharingModel` 的四个合法值。读到这些的 Agent 一轮就能闭环。[给 Agent 配置 ObjectStack 的那篇文章](/en/blog/how-to-point-your-agent-at-objectstack/)把它叫作*修正式报错*(corrective error),并论证一个格式手里杠杆最大的东西不是文档而是报错信息;这次运行是它的又三个数据点。
+
+把那三行改回去,门禁重归安静:
+
+```text
+$ npm run validate
+ → Running author-time rules (49)...
+
+ ✓ Validation passed (240ms)
+
+ Data: 2 Objects 6 Fields
+ UI: 1 Apps 1 Views 1 Actions
+ Security: 1 Permissions
+ Runtime: 3 plugins
+```
+
+运行里还有一行属于这里,因为它是门禁在做一件比拒收更微妙的事。权限集的第一版在一个私有对象上授予了读、建、改,却没说一次读能看多远,于是那次干净的运行带着一条提示:「`permission set "support_agent": "support_desk_ticket" is private (OWD) and this set grants allowRead without a readScope — holders see ONLY records they own (plus explicit shares).`」(权限集「support_agent」:「support_desk_ticket」是私有的(OWD),而这个权限集授予了 allowRead 却没有 readScope——持有者只能看到自己拥有的记录(加上显式共享的)。)这不是报错,门禁不把它当报错也是对的:一个支持人员只看自己工单的支持台,是一种正当的设计。它是一个浮到必须做决定的那个人面前的半个决定。修法是一个键,`readScope: 'org'`,而这是一个产品决定,不是一次纠错。
+
+## 门禁抓不住什么
+
+讲门禁的文章必须说清门禁止于何处,而来源在这一点上异常直白。
+
+**业务逻辑的正确性。** 文档:「护栏 1–3 证明元数据是*良构的*。它们证明不了它是*你想要的*。」门禁确认了 `record.status` 能解析。它对一张工单该不该能从列表里直接解决、`resolved` 是不是正确的终态、支持人员该不该看到组织内的每一张工单,都没有意见。更早那篇文章对同一条边界的说法是「门禁检查的是结构,不是意图」,49 条规则改变不了这一点。
+
+**审查者漏掉的需求。** 审查门禁是一个人读一份 diff。它按构造就是小的——整个应用 127 行,三个错误三行——但小不等于读过。一个没问过承包商是否也持有 `support_agent` 权限集、就批准了 `readScope: 'org'` 的审查者,已经把一个决定放上线了,而门禁不会拦他,因为元数据是良构的。提示通道有用的程度,恰好等于有人去读它的程度。
+
+**新颖的逻辑。** 钩子和动作体是代码。「业务本体」概念页说它们「在运行时的权限与审计围栏之内运行」,围栏限定了一个写错的函数体能碰到什么;它不能让那个函数体变对。那仍然是代码评审。
+
+**这次运行没有演练的部分。** 这份记录是命令行上的前两道门禁。文档所描述的审查门禁——一次改动上线之前由人批准的 Studio diff——在这里是一份 `git diff`。治理门禁——文档描述为从匿名拒绝到字段级安全、六层各自 fail-closed 的鉴权链,加上每次调用的审计——没有启动,因为 `os validate` 是一个作者期检查,不写产物、不起服务。README 对那道门禁的主张是「哪怕应用写错了,也留在围栏之内」;这篇文章引用它,而不是演示它。
+
+**而两位对手仍然站着。** 一个用完即弃的原型不需要四道门禁。一个测试充分、审查者细心的手写服务有它自己的台账,由人而不是工具来填。这里的主张比「这更好」窄:对于一个由 Agent 来写、由企业来跑的应用,台账应该由格式来填,因为 Agent 不会记得,而审查者读不完所有东西。
+
+## 同一道门禁,从浏览器进来
+
+有一个事实让四道门禁的故事对从不打开终端的人也诚实:`os validate` 不是唯一的门。文档描述了第四扇:「每一次元数据**写入**——Studio 的设计器、`PUT /api/v1/meta/*`、一个 MCP/AI 作者——都落在 `saveMetaItem` 上,自 #4463 起,一次进入 **`state: 'active'`** 的写入会在持久化之前跑同一张表。」上面打印出 49 的那张 `AUTHORING_RULES` 表,同样会拒收一个从 Studio 发布、带悬空绑定的仪表盘,附带同样定位到位的消息。验证门禁是运行时的属性,不是 CLI 的属性。
+
+还有 README 自己关于这个循环的最后一句:「想把同一个循环托管起来、在浏览器里用、什么都不用安装?那就是 ObjectOS——建立在 ObjectStack 之上的商业运行环境。」ObjectOS 既可托管在浏览器中、无需安装(ObjectOS Cloud),也可自管部署在你自己的基础设施上(ObjectOS Enterprise);无论哪个版本,你都可以导出本体,拿到开源的 ObjectStack 运行时上运行。
+
+## 填你自己的台账
+
+上面的运行走的是 README 的五分钟路径。唯一的改动是故意犯错,这是弄清一个格式在本文的意义上是否 AI 可编写的最快办法。
+
+```bash
+npm create objectstack@latest my-app && cd my-app
+# 用你的编码 agent 打开它,说清业务需要什么
+npx os dev --ui
+```
+
+要 README 描述的那个支持台。然后,在你看 Console 之前,打开 diff,照着这次运行做:从一个谓词里删掉 `record.`,在视图里拼错一个动作名,删掉一个 `sharingModel`,跑 `npm run validate`。数一数门禁替你填了几行,又把几行留给了你。[你交给 Agent 的规则文件](/zh-Hans/blog/give-your-agent-rules-for-governable-apps/)决定会犯多少错;四道门禁决定它们止于何处;你的台账是对两者诚实的度量。
diff --git a/content/blog/what-makes-a-business-app-ai-writable/index.zh-Hant.mdx b/content/blog/what-makes-a-business-app-ai-writable/index.zh-Hant.mdx
new file mode 100644
index 0000000..07d8590
--- /dev/null
+++ b/content/blog/what-makes-a-business-app-ai-writable/index.zh-Hant.mdx
@@ -0,0 +1,231 @@
+---
+# @generated zh-Hant from zh-Hans (s2twp) — edit the zh-Hans file; delete this line to hand-maintain.
+title: 什麼樣的業務應用是 AI 可編寫的?四道門禁:型別、驗證、審查、治理
+description: AI 可編寫是目標格式的屬性,不是模型的屬性。四道門禁、一次真實執行:三個故意犯的錯、49 條規則、三條定位到位的報錯,然後一次乾淨通過。
+author: ObjectStack Team
+date: 2026-10-22
+status: published
+topic: ai-agents
+audience: developer
+solutions: []
+industries: []
+cover: ./cover.svg
+tags:
+ - AI 智慧體
+ - AI 可編寫
+---
+
+一個業務應用是「AI 可編寫」的,當且僅當 Agent 必然會犯的錯誤在觸達使用者之前有地方可死。這是目標格式的屬性,不是模型的屬性。ObjectStack 的 README 點名了四個這樣的地方,稱之為門禁:**型別(Typed)**、**驗證(Validated)**、**審查(Reviewed)**、**治理(Governed)**。型別錯誤死在編輯器裡。驗證錯誤死在 `os validate` 上——它拒收那些能通過型別檢查、卻會在執行時靜默失敗的後設資料。審查錯誤死在一份人能整個讀完的 diff 裡。治理錯誤即使上線了,也留在執行時的權限與審計圍欄之內。下面是在 ObjectStack 17.7.0 上、用腳手架生成的支援臺跑的一次真實執行,覆蓋前兩道門禁:按 README 列出的三類各故意犯一個錯,49 條規則的門禁把每一個都定位到位、附帶修正方案地拒收,修好之後一次乾淨通過。
+
+## 兩件本來就成立的事
+
+講門禁之前,先替對立面把話說足,因為這篇文章的論證不需要它們中的任何一個是錯的。
+
+**Vibe coding 做出來的應用是能用的。** 你可以向模型描述一個支援臺,一個下午就拿到一個能跑的版本,有介面、有資料庫、有 API,用的是模型最拿手的任何語言。做原型、做一次性工具、做用完即棄的演示,這是正確的取捨,下文沒有一句是反對它的。模型早已不是瓶頸。
+
+**手寫程式碼是可審查的,而且工具鏈已經成熟。** 一位資深工程師讀 pull request、一套型別系統、一套測試、一個程式碼評審流程,每天都在抓真實的缺陷。對於新穎的邏輯沒有替代品,ObjectStack 自己也這麼說:「程式碼不會消失:它搬進了執行時,成為鉤子、動作體、CEL 和受限 JSX」——而那些程式碼仍然要由人來讀。
+
+兩者都沒有回答的,是一旦由 Agent 來寫、真正要緊的那個問題:*寫錯的那一行去了哪裡?* 在一個自由形式的程式碼庫裡,一處錯誤的鑑權檢查就是成千上萬個 `if` 裡的一個,審查面是整個倉庫,執行時自身對權限什麼也不強制。腳手架生成的專案裡,CI 檔案用一句話(說的是後設資料,但對任何沒有門禁的格式都成立)概括了這件事:一個錯誤「什麼都不拋,只是對第一個碰上它的人表現得不對」。AI 可編寫,意思就是這句話不再成立。
+
+## 四道門禁,按 README 的表述
+
+README 的那一節叫「Why the mistakes don't ship」(錯誤為什麼上不了線),表格短到可以整個譯出來:
+
+| 門禁 | 攔住什麼 |
+| :--- | :--- |
+| **型別(Typed)** | 嚴格的 TypeScript + Zod——形狀錯誤死在編輯器裡,Agent 寫下它們幾秒鐘之後 |
+| **驗證(Validated)** | `os validate` 拒收那些能通過型別檢查、卻會在執行時靜默失敗的後設資料:懸空繫結、錯誤的 CEL 謂詞、缺失的安全姿態 |
+| **審查(Reviewed)** | 你在 Console 裡批准一份小而可讀的 diff——不是一堆生成出來的膠水程式碼 |
+| **治理(Governed)** | 執行時在每一次呼叫上強制執行權限與審計,所以哪怕應用寫錯了,也留在圍欄之內 |
+
+
+
+這張表裡有三件事決定了這個承諾是不是真的,值得在看記錄之前先說清楚。
+
+第一,門禁按代價排序。一個形狀錯誤花 Agent 一秒鐘。一個驗證錯誤花它一輪。一次審查花一個人幾分鐘。一次被治理住的失敗,是一起被控制住的事故。README 說整件事成立的原因,就在前兩行:「Agent 的錯誤變成了定位到位、可修正的文字,它自己就能讀、自己就能修」。
+
+第二,第二道門禁之所以存在,是因為第一道不夠,文件用一句話說明了原因:「ObjectStack 的後設資料是資料,不是程式碼路徑——所以大多數錯誤**不會**被 TypeScript 編譯器抓到。它們通過 `tsc`,正常載入,然後**在執行時靜默失敗**。」一個謂詞是字串。一個行動作是名字。一個共享模型是可選鍵。三者帶著錯誤都能通過型別檢查。這正是驗證門禁為之而建的那一類錯誤,也是下面這次執行瞄準的那一類。
+
+第三,後兩道門禁是人和執行時,不是工具,這篇文章不假裝不是。文件裡的審查門禁是 Studio 的「Review N changes」diff;在下面的執行裡,它是一份由人讀的 `git diff`。治理門禁是執行時的鑑權鏈和審計日誌,這次執行從未啟動它。記錄證明的是:兩道便宜的門禁抓住了 README 說它們會抓住的東西。它證明不了的,放在文末。
+
+## 一個支援臺,三個錯誤,一次執行
+
+下面的一切都是真跑出來的,不是拼出來的:Node 22,已釋出的包。腳手架就是 README 自己的第一步:
+
+```bash
+$ npm create objectstack@latest support-desk
+ ╔═══════════════════════════════════╗
+ ║ ◆ Create ObjectStack v17.7.0 ║
+ ╚═══════════════════════════════════╝
+
+◆ New Environment
+────────────────────────────────────────
+ Environment: support-desk
+ Namespace: support_desk
+ Template: blank — Minimal starter — one object, REST API, ready to extend
+
+ ✓ Template files written (20)
+ → Installing dependencies...
+ → Installing AI skills for your coding agent...
+ ✓ Environment created!
+```
+
+腳手架寫下一份 `AGENTS.md`,其中唯一沒有商量餘地的一行就是門禁:「**Never report a metadata change as done until `npm run validate` passes.**」(在 `npm run validate` 通過之前,永遠不要把一次後設資料改動報告為已完成。)它還寫下一個 CI 工作流,唯一的 job 跑 `pnpm validate`、`pnpm lint` 和 `pnpm typecheck`。在任何業務物件存在之前,門禁已經接好了。
+
+接著是 README 裡的需求,按 Agent 會寫的方式寫出來:一個 `support_desk_ticket` 物件,帶主題、描述、一個優先順序選擇和一個狀態選擇;一個只在尚未解決的工單上顯示的 **Resolve** 動作;一個「未解決工單」列表檢視;一個 Support 導航組;再加一個給支援人員的權限集。整個應用是 **11 個檔案、新增 127 行**。錯誤將落在其中兩個檔案裡:
+
+```ts
+// src/actions/resolve-ticket.action.ts
+export const ResolveTicketAction = defineAction({
+ name: 'resolve_ticket',
+ label: 'Resolve',
+ objectName: 'support_desk_ticket',
+ locations: ['record_header', 'list_item'],
+ visible: P`record.status != "resolved"`, // only on tickets not yet resolved
+ operation: 'update',
+ patch: { status: 'resolved' },
+ confirmText: 'Mark this ticket as resolved?',
+});
+```
+
+```ts
+// src/views/ticket.view.ts
+export const TicketViews = defineView({
+ list: {
+ label: 'All tickets',
+ type: 'grid',
+ data,
+ columns: ['subject', 'priority', 'status'],
+ rowActions: ['resolve_ticket'],
+ },
+ // ... form, and an "Open tickets" list view filtered on status = open
+});
+```
+
+先跑一次乾淨的,把規則數記在案:
+
+```text
+$ npm run validate
+◆ Validate
+────────────────────────────────────────
+ → Loading configuration...
+ → Validating against ObjectStack Protocol...
+ → Running author-time rules (49)...
+ → Checking that every required capability has a provider installable in this edition...
+ → Checking package docs (ADR-0046)...
+
+ ✓ Validation passed (327ms)
+
+ Support Desk v0.1.0
+
+ Data: 2 Objects 6 Fields
+ UI: 1 Apps 1 Views 1 Actions
+ Logic: 0 Flows
+ Security: 1 Permissions
+ Runtime: 3 plugins
+```
+
+17.7.0 上是 49 條作者期規則。[本站上一次跑同一道門禁](/en/blog/how-to-point-your-agent-at-objectstack/)是在 17.2.0 上,當時是 41 條;規則表在長,對一道門禁來說這是對的方向。這個數字由工具自己列印,等於已釋出的 `@objectstack/lint` 包裡 `AUTHORING_RULES.length`,沒有第二個數字需要對賬。
+
+### 三個錯誤
+
+現在是 README 的三類,每類故意犯一個錯。三個都是在普通 JavaScript 上訓練出來的 Agent 會順手寫出的那種,三個都能通過型別檢查:
+
+```diff
+ // 1 · a bad CEL predicate: the bare identifier most template languages use
+- visible: P`record.status != "resolved"`,
++ visible: P`status != "resolved"`,
+
+ // 2 · a dangling binding: a row action that names nothing
+- rowActions: ['resolve_ticket'],
++ rowActions: ['resolve'],
+
+ // 3 · a missing security posture: the org-wide default left unsaid
+- sharingModel: 'private',
+```
+
+三個錯都在,`npm run typecheck` 照樣是綠的:`tsc` 對一個字串的內容、一個數組裡的名字、一個可選鍵,都沒有意見。門禁有:
+
+```text
+$ npm run validate
+◆ Validate
+────────────────────────────────────────
+ → Loading configuration...
+ → Validating against ObjectStack Protocol...
+ → Running author-time rules (49)...
+
+ ✗ Author-time rules failed (3 issues)
+ • stack · action 'resolve_ticket' visible: bare reference `status` — a formula/validation expression binds the record as the `record` namespace, not at top level, so `status` resolves to nothing and the expression silently evaluates to null. Write `record.status`.
+ source: `status != "resolved"`
+ rule: expression-invalid at stack · action 'resolve_ticket' visible
+ • view "#0" · list · rowActions: Row-action menu names action "resolve", which is defined by no action in this stack (neither `stack.actions` nor any object's `actions`). The button renders and does nothing when clicked — a dead affordance the runtime cannot dispatch. Did you mean "resolve_ticket"?
+ Define an action named "resolve" (in `stack.actions` or the object's `actions`) with the location this surface needs, remove the reference, or ignore this if the action is contributed by another installed package. Defined actions: resolve_ticket.
+ rule: action-name-undefined at views[0].list.rowActions[0]
+ • object "support_desk_ticket": custom object "support_desk_ticket" declares no sharingModel (OWD). The runtime fails CLOSED to 'private' (ADR-0090 D1), but the baseline must be an authored decision, not an accident — this is the exact shape of the leave_request incident, where an object with no sharingModel let an ordinary read/write grant read and edit every other user's records.
+ Declare sharingModel explicitly: 'private' (owner + shares; recommended default), 'public_read', 'public_read_write', or 'controlled_by_parent' (master-detail children).
+ rule: security-owd-unset at objects[1].sharingModel
+ ELIFECYCLE Command failed with exit code 1.
+```
+
+把這三條訊息當作一本臺賬來讀,而不是當作報錯,因為每一條都寫明瞭沒有門禁時執行時會發生什麼。叫它**錯誤臺賬**:對一個目標格式,列出 Agent 可能犯的每一類錯誤、抓住它的那道門禁,以及否則它會在執行時表現成什麼症狀。一個格式有多 AI 可編寫,恰好等於這本臺賬有多少行不是空的。
+
+
+
+| 錯誤 | 否則會以什麼形態上線 | 被誰攔住 · 規則 |
+| :--- | :--- | :--- |
+| `` visible: P`status != "resolved"` `` | 謂詞「靜默求值為 null」:Resolve 按鈕在每一張工單上都被藏起來 | 驗證 · `expression-invalid` |
+| `rowActions: ['resolve']` | 「按鈕渲染出來,點選後什麼也不做——一個執行時無法分發的死 affordance」 | 驗證 · `action-name-undefined` |
+| 物件上沒有 `sharingModel` | 執行時「fail closed 到 'private'」,但一個沒說出口的基線正是「leave_request 事故的形狀」 | 驗證 · `security-owd-unset` |
+| 欄位上寫 `required: 'yes'` | 什麼也上不了線:`tsc` 以「Type 'string' is not assignable to type 'boolean \| undefined'」拒收 | 型別 · `TS2322` |
+
+第四行是型別門禁,在乾淨的樹上單獨跑,好讓驗證器的記錄只關於驗證:把 `required: true` 改成 `required: 'yes'`,`npm run typecheck` 帶著那條訊息以退出碼 2 結束,在任何別的東西執行之前。它是最便宜、也最無趣的一道門禁,而這正是它排在第一位的意義。
+
+訊息的兩個屬性承載了整個承諾。它們**定位到位**:一個動作和一個謂詞槽位,一個檢視和一個數組下標,一個物件和一條鍵路徑。它們**帶修正方案**:「Write `record.status`」、「Did you mean `resolve_ticket`?」、`sharingModel` 的四個合法值。讀到這些的 Agent 一輪就能閉環。[給 Agent 配置 ObjectStack 的那篇文章](/en/blog/how-to-point-your-agent-at-objectstack/)把它叫作*修正式報錯*(corrective error),並論證一個格式手裡槓桿最大的東西不是文件而是報錯資訊;這次執行是它的又三個資料點。
+
+把那三行改回去,門禁重歸安靜:
+
+```text
+$ npm run validate
+ → Running author-time rules (49)...
+
+ ✓ Validation passed (240ms)
+
+ Data: 2 Objects 6 Fields
+ UI: 1 Apps 1 Views 1 Actions
+ Security: 1 Permissions
+ Runtime: 3 plugins
+```
+
+執行裡還有一行屬於這裡,因為它是門禁在做一件比拒收更微妙的事。權限集的第一版在一個私有物件上授予了讀、建、改,卻沒說一次讀能看多遠,於是那次乾淨的執行帶著一條提示:「`permission set "support_agent": "support_desk_ticket" is private (OWD) and this set grants allowRead without a readScope — holders see ONLY records they own (plus explicit shares).`」(權限集「support_agent」:「support_desk_ticket」是私有的(OWD),而這個權限集授予了 allowRead 卻沒有 readScope——持有者只能看到自己擁有的記錄(加上顯式共享的)。)這不是報錯,門禁不把它當報錯也是對的:一個支援人員只看自己工單的支援臺,是一種正當的設計。它是一個浮到必須做決定的那個人面前的半個決定。修法是一個鍵,`readScope: 'org'`,而這是一個產品決定,不是一次糾錯。
+
+## 門禁抓不住什麼
+
+講門禁的文章必須說清門禁止於何處,而來源在這一點上異常直白。
+
+**業務邏輯的正確性。** 文件:「護欄 1–3 證明後設資料是*良構的*。它們證明不了它是*你想要的*。」門禁確認了 `record.status` 能解析。它對一張工單該不該能從列表裡直接解決、`resolved` 是不是正確的終態、支援人員該不該看到組織內的每一張工單,都沒有意見。更早那篇文章對同一條邊界的說法是「門禁檢查的是結構,不是意圖」,49 條規則改變不了這一點。
+
+**審查者漏掉的需求。** 審查門禁是一個人讀一份 diff。它按構造就是小的——整個應用 127 行,三個錯誤三行——但小不等於讀過。一個沒問過承包商是否也持有 `support_agent` 權限集、就批准了 `readScope: 'org'` 的審查者,已經把一個決定放上線了,而門禁不會攔他,因為後設資料是良構的。提示通道有用的程度,恰好等於有人去讀它的程度。
+
+**新穎的邏輯。** 鉤子和動作體是程式碼。「業務本體」概念頁說它們「在執行時的權限與審計圍欄之內執行」,圍欄限定了一個寫錯的函式體能碰到什麼;它不能讓那個函式體變對。那仍然是程式碼評審。
+
+**這次執行沒有演練的部分。** 這份記錄是命令列上的前兩道門禁。文件所描述的審查門禁——一次改動上線之前由人批准的 Studio diff——在這裡是一份 `git diff`。治理門禁——文件描述為從匿名拒絕到欄位級安全、六層各自 fail-closed 的鑑權鏈,加上每次呼叫的審計——沒有啟動,因為 `os validate` 是一個作者期檢查,不寫產物、不起服務。README 對那道門禁的主張是「哪怕應用寫錯了,也留在圍欄之內」;這篇文章引用它,而不是演示它。
+
+**而兩位對手仍然站著。** 一個用完即棄的原型不需要四道門禁。一個測試充分、審查者細心的手寫服務有它自己的臺賬,由人而不是工具來填。這裡的主張比「這更好」窄:對於一個由 Agent 來寫、由企業來跑的應用,臺賬應該由格式來填,因為 Agent 不會記得,而審查者讀不完所有東西。
+
+## 同一道門禁,從瀏覽器進來
+
+有一個事實讓四道門禁的故事對從不開啟終端的人也誠實:`os validate` 不是唯一的門。文件描述了第四扇:「每一次後設資料**寫入**——Studio 的設計器、`PUT /api/v1/meta/*`、一個 MCP/AI 作者——都落在 `saveMetaItem` 上,自 #4463 起,一次進入 **`state: 'active'`** 的寫入會在持久化之前跑同一張表。」上面打印出 49 的那張 `AUTHORING_RULES` 表,同樣會拒收一個從 Studio 釋出、帶懸空繫結的儀表盤,附帶同樣定位到位的訊息。驗證門禁是執行時的屬性,不是 CLI 的屬性。
+
+還有 README 自己關於這個迴圈的最後一句:「想把同一個迴圈託管起來、在瀏覽器裡用、什麼都不用安裝?那就是 ObjectOS——建立在 ObjectStack 之上的商業執行環境。」ObjectOS 既可託管在瀏覽器中、無需安裝(ObjectOS Cloud),也可自管部署在你自己的基礎設施上(ObjectOS Enterprise);無論哪個版本,你都可以匯出本體,拿到開源的 ObjectStack 執行時上執行。
+
+## 填你自己的臺賬
+
+上面的執行走的是 README 的五分鐘路徑。唯一的改動是故意犯錯,這是弄清一個格式在本文的意義上是否 AI 可編寫的最快辦法。
+
+```bash
+npm create objectstack@latest my-app && cd my-app
+# 用你的編碼 agent 開啟它,說清業務需要什麼
+npx os dev --ui
+```
+
+要 README 描述的那個支援臺。然後,在你看 Console 之前,開啟 diff,照著這次執行做:從一個謂詞裡刪掉 `record.`,在視圖裡拼錯一個動作名,刪掉一個 `sharingModel`,跑 `npm run validate`。數一數門禁替你填了幾行,又把幾行留給了你。[你交給 Agent 的規則檔案](/zh-Hant/blog/give-your-agent-rules-for-governable-apps/)決定會犯多少錯;四道門禁決定它們止於何處;你的臺賬是對兩者誠實的度量。
diff --git a/content/blog/what-makes-a-business-app-ai-writable/mistake-ledger-en.svg b/content/blog/what-makes-a-business-app-ai-writable/mistake-ledger-en.svg
new file mode 100644
index 0000000..c889c37
--- /dev/null
+++ b/content/blog/what-makes-a-business-app-ai-writable/mistake-ledger-en.svg
@@ -0,0 +1,71 @@
+
diff --git a/content/blog/what-makes-a-business-app-ai-writable/mistake-ledger.svg b/content/blog/what-makes-a-business-app-ai-writable/mistake-ledger.svg
new file mode 100644
index 0000000..6e91138
--- /dev/null
+++ b/content/blog/what-makes-a-business-app-ai-writable/mistake-ledger.svg
@@ -0,0 +1,71 @@
+