diff --git a/.changeset/copilot-persona-depersonalisation.md b/.changeset/copilot-persona-depersonalisation.md
new file mode 100644
index 00000000..b300bcc7
--- /dev/null
+++ b/.changeset/copilot-persona-depersonalisation.md
@@ -0,0 +1,56 @@
+---
+'hotcrm': patch
+---
+
+Stop naming a retired persona in the product docs, and pin the version the
+release page prints.
+
+`#512` removed the two app-authored agents and ADR-0063 §2 made the surface
+skills-only; `#589` / PR `#611` rewrote `content/docs/ai-copilot/*` to match. The
+rest of the tree did not follow: 29 product pages across all three locales still
+called the assistant "the Sales Copilot" / "the Service Copilot" in running
+prose — 65 occurrences. None of them declared a `sales_copilot` agent, so every
+gate stayed green: `os validate` and `pnpm lint` walk authored metadata and never
+open a paragraph.
+
+Per the maintainer's ruling on `#612`, those pages now say **AI assistant**
+(zh-Hans / zh-Hant: **AI 助手**), with each sentence's functional meaning left
+alone — only the name changes. The wording follows the architecture the pages
+have to describe: AI capability is implemented by agents in
+`objectstack-ai/cloud`, and HotCRM contributes domain skills that attach to the
+platform assistant (`ask`), so no page implies an app-owned agent any more. Two
+zh index pages were also carrying a `## Sales Copilot` / `## Service Copilot`
+heading whose English counterpart had already become *Sales AI skills* /
+*Service AI skills*; they now match. `content/docs/whats-new.mdx` was the mirror
+case — both translations already said "向助手询问" and the English page alone was
+still personifying.
+
+Twelve pages keep the names on purpose and are exempt by name, each with its
+reason recorded: the nine `ai-copilot/*` retirement callouts PR `#611` wrote, and
+the three `whats-new` v1.0 release records, which describe what that release
+actually shipped.
+
+`test/docs-drift.test.ts` now enforces both halves so a third cleanup round is
+not needed:
+
+- **Persona rule** — no page under `content/docs/**` may write either name.
+ The scan normalises soft wraps and blockquote continuation markers before
+ matching, because two of the live occurrences were split across lines
+ (`the Sales\n> Copilot`, `ask the Sales\nCopilot`) and a line-oriented grep —
+ how the original inventory was taken — reads neither. Whitespace between two
+ CJK characters is stripped as well, since `ai-copilot/index.zh-Hant.mdx` wraps
+ 「服務 Copilot」 between 服 and 務. Three vacuity guards keep it honest: the walk
+ must find a real tree, every exemption must still cover a live occurrence, and
+ a probe test asserts the detector reads all six spellings in every wrap shape —
+ without that last one, a detector that had stopped matching would report a
+ clean tree and read exactly like success.
+- **Version rule** — `docs/RELEASE_STRATEGY.md` had printed `1.0.5` since v1
+ while the manifest declared `2.2.2`, a whole major behind on the one page a
+ releaser trusts for the current version. It now reads `2.2.2`, and the value
+ is extracted from `objectstack.config.ts` `manifest.version` and asserted
+ against `docs/RELEASE_STRATEGY.md`, `docs/STATUS.md`, `docs/ARCHITECTURE.md`
+ and `README.md`, plus a `package.json` parity check — the alignment
+ `RELEASE_STRATEGY.md`'s own *Version Sources* section already asks for.
+
+Docs, prose and tests only: no metadata, no `src/` behaviour, no dependency
+changed. Fixes `#612`.
diff --git a/content/docs/index.mdx b/content/docs/index.mdx
index 95d4bd06..75a264c1 100644
--- a/content/docs/index.mdx
+++ b/content/docs/index.mdx
@@ -6,8 +6,8 @@ description: The AI-Native enterprise CRM that rewrites itself when your busines
# Welcome to HotCRM
> **HotCRM is the CRM that rewrites itself when business changes.**
-> Add a field in the admin console. Ten seconds later, the Sales
-> Copilot uses it in its next answer. No retraining, no redeploy.
+> Add a field in the admin console. Ten seconds later, the AI
+> assistant uses it in its next answer. No retraining, no redeploy.
> [See what's new in v1.0 →](/docs/whats-new)
**HotCRM** is a full-featured enterprise CRM with AI as a structural
@@ -17,7 +17,7 @@ feature, not a bolted-on widget. It helps your team:
- 🎧 **Service** — log customer cases, route them by priority, meet your SLA, and escalate when needed.
- 📣 **Marketing** — plan campaigns, track who responded, and measure ROI.
- 💰 **Revenue** — manage products, prices, contracts and discount approvals.
-- 🤖 **AI Copilots** — let the Sales Copilot qualify leads for you and the Service Copilot triage incoming cases.
+- 🤖 **AI Copilot** — let the AI assistant qualify leads for you and triage incoming cases.
- 📊 **Analytics** — see dashboards and reports for executives, sales managers and support leads.
This documentation is organised by **what you want to do**. Pick the section that matches your role.
@@ -41,7 +41,7 @@ This documentation is organised by **what you want to do**. Pick the section tha
Product catalog, pricing, contract lifecycle, renewals and discount approvals.
- How the Sales and Service Copilots help you day-to-day.
+ How the sales and service skills help you day-to-day.
Standard dashboards, reports, and the numbers behind them.
@@ -65,7 +65,7 @@ This documentation is organised by **what you want to do**. Pick the section tha
| **Service** | Cases (with SLA and escalation), Tasks, knowledge base for the copilot |
| **Marketing** | Campaigns (8 types), Campaign Members (lead/contact enrollment), ROI tracking |
| **Revenue** | Products, Contracts (5 types), Discount Approvals |
-| **AI** | Sales Copilot, Service Copilot, 6 skills (live data, lead qualification, case triage, email drafting, revenue forecasting, customer 360) |
+| **AI** | 6 skills on the platform assistant (live data, lead qualification, case triage, email drafting, revenue forecasting, customer 360) |
| **Analytics** | 4 dashboards (CRM / Sales / Service / Executive), 10+ standard reports |
| **Security** | 6 user profiles, role hierarchy, sharing rules, multi-step approvals |
diff --git a/content/docs/index.zh-Hans.mdx b/content/docs/index.zh-Hans.mdx
index 4d6999e0..9d6ceceb 100644
--- a/content/docs/index.zh-Hans.mdx
+++ b/content/docs/index.zh-Hans.mdx
@@ -6,7 +6,7 @@ description: AI 原生的企业级 CRM——业务变化时它会自我重写。
# 欢迎使用 HotCRM
> **HotCRM 是一款会在业务变化时自我重写的 CRM。**
-> 在管理后台新增一个字段,十秒后销售 Copilot 就会在下一次回答中用上它。
+> 在管理后台新增一个字段,十秒后 AI 助手就会在下一次回答中用上它。
> 无需重新训练,无需重新部署。
> [查看 v1.0 新特性 →](/zh-Hans/docs/whats-new)
@@ -16,7 +16,7 @@ description: AI 原生的企业级 CRM——业务变化时它会自我重写。
- 🎧 **服务** — 记录客户工单、按优先级路由、满足 SLA,并在需要时升级。
- 📣 **市场** — 规划活动、追踪响应人群、衡量 ROI。
- 💰 **营收** — 管理产品、价格、合同与折扣审批。
-- 🤖 **AI Copilot** — 让销售 Copilot 替你做线索资格判定,服务 Copilot 为来件工单分流。
+- 🤖 **AI Copilot** — 让 AI 助手替你做线索资格判定,并为来件工单分流。
- 📊 **分析** — 为高管、销售经理与支持主管提供仪表盘与报表。
本文档按**你想做什么**来组织。请选择与你角色匹配的板块。
@@ -40,7 +40,7 @@ description: AI 原生的企业级 CRM——业务变化时它会自我重写。
产品目录、定价、合同生命周期、续约与折扣审批。
- 销售与服务 Copilot 如何在日常中助你一臂之力。
+ 销售与服务技能如何在日常中助你一臂之力。
标准仪表盘、报表,以及背后的数据。
@@ -64,7 +64,7 @@ description: AI 原生的企业级 CRM——业务变化时它会自我重写。
| **服务** | 工单(含 SLA 与升级)、任务、面向 Copilot 的知识库 |
| **市场** | 营销活动(8 种类型)、营销活动成员(线索/联系人纳入)、ROI 追踪 |
| **营收** | 产品、合同(5 种类型)、折扣审批 |
-| **AI** | 销售 Copilot、服务 Copilot、6 项技能(实时数据、线索资格判定、工单分流、邮件起草、营收预测、客户 360) |
+| **AI** | 平台助手上的 6 项技能(实时数据、线索资格判定、工单分流、邮件起草、营收预测、客户 360) |
| **分析** | 4 个仪表盘(CRM / 销售 / 服务 / 高管)、10+ 标准报表 |
| **安全** | 6 种用户权限档案、角色层级、共享规则、多步审批 |
diff --git a/content/docs/index.zh-Hant.mdx b/content/docs/index.zh-Hant.mdx
index dd6f871f..20a210fc 100644
--- a/content/docs/index.zh-Hant.mdx
+++ b/content/docs/index.zh-Hant.mdx
@@ -6,7 +6,7 @@ description: AI 原生的企業級 CRM——業務變化時它會自我重寫。
# 歡迎使用 HotCRM
> **HotCRM 是一款會在業務變化時自我重寫的 CRM。**
-> 在管理後台新增一個欄位,十秒後銷售 Copilot 就會在下一次回答中用上它。
+> 在管理後台新增一個欄位,十秒後 AI 助手就會在下一次回答中用上它。
> 無需重新訓練,無需重新部署。
> [查看 v1.0 新特性 →](/zh-Hant/docs/whats-new)
@@ -16,7 +16,7 @@ description: AI 原生的企業級 CRM——業務變化時它會自我重寫。
- 🎧 **服務** — 記錄客戶工單、依優先順序路由、達成 SLA,並在需要時升級。
- 📣 **行銷** — 規劃活動、追蹤回應人群、衡量 ROI。
- 💰 **營收** — 管理產品、價格、合約與折扣審批。
-- 🤖 **AI Copilot** — 讓銷售 Copilot 替你做潛客資格判定,服務 Copilot 為來件工單分流。
+- 🤖 **AI Copilot** — 讓 AI 助手替你做潛客資格判定,並為來件工單分流。
- 📊 **分析** — 為高階主管、銷售經理與支援主管提供儀表板與報表。
本文件依**你想做什麼**來組織。請選擇與你角色相符的區塊。
@@ -40,7 +40,7 @@ description: AI 原生的企業級 CRM——業務變化時它會自我重寫。
產品目錄、定價、合約生命週期、續約與折扣審批。
- 銷售與服務 Copilot 如何在日常中助你一臂之力。
+ 銷售與服務技能如何在日常中助你一臂之力。
標準儀表板、報表,以及背後的資料。
@@ -64,7 +64,7 @@ description: AI 原生的企業級 CRM——業務變化時它會自我重寫。
| **服務** | 工單(含 SLA 與升級)、任務、面向 Copilot 的知識庫 |
| **行銷** | 行銷活動(8 種類型)、行銷活動成員(潛客/聯絡人納入)、ROI 追蹤 |
| **營收** | 產品、合約(5 種類型)、折扣審批 |
-| **AI** | 銷售 Copilot、服務 Copilot、6 項技能(即時資料、潛客資格判定、工單分流、郵件草擬、營收預測、客戶 360) |
+| **AI** | 平台助手上的 6 項技能(即時資料、潛客資格判定、工單分流、郵件草擬、營收預測、客戶 360) |
| **分析** | 4 個儀表板(CRM / 銷售 / 服務 / 高階主管)、10+ 標準報表 |
| **安全** | 6 種使用者權限設定檔、角色層級、共用規則、多步審批 |
diff --git a/content/docs/revenue/products.mdx b/content/docs/revenue/products.mdx
index a5b7e57b..2e7b3fbd 100644
--- a/content/docs/revenue/products.mdx
+++ b/content/docs/revenue/products.mdx
@@ -110,7 +110,7 @@ The product catalog is **public read-only** for the whole organisation. Sales re
## Tips for product managers
-- ✅ **Keep the datasheet up to date** — the Sales Copilot uses it to draft customer-facing content.
+- ✅ **Keep the datasheet up to date** — the AI assistant uses it to draft customer-facing content.
- ✅ **Use the Product Image** — it dramatically improves the look of customer-facing quotes.
- ✅ **Mark old products as inactive**, don't delete them — past quotes and contracts still reference them.
diff --git a/content/docs/revenue/products.zh-Hans.mdx b/content/docs/revenue/products.zh-Hans.mdx
index 91aa92d1..e275c657 100644
--- a/content/docs/revenue/products.zh-Hans.mdx
+++ b/content/docs/revenue/products.zh-Hans.mdx
@@ -110,7 +110,7 @@ description: 你所销售内容的目录——编码、标价、计费类型和
## 给产品经理的提示
-- ✅ **保持规格表最新**——Sales Copilot 用它来起草面向客户的内容。
+- ✅ **保持规格表最新**——AI 助手用它来起草面向客户的内容。
- ✅ **使用产品图片**——它能显著改善面向客户的报价的外观。
- ✅ **将旧产品标记为非活跃**,不要删除它们——过往的报价和合同仍引用它们。
diff --git a/content/docs/revenue/products.zh-Hant.mdx b/content/docs/revenue/products.zh-Hant.mdx
index fe87a1a7..8356bd28 100644
--- a/content/docs/revenue/products.zh-Hant.mdx
+++ b/content/docs/revenue/products.zh-Hant.mdx
@@ -110,7 +110,7 @@ description: 你所銷售內容的目錄——編碼、標價、計費類型和
## 給產品經理的提示
-- ✅ **保持規格表最新**——Sales Copilot 用它來起草面向客戶的內容。
+- ✅ **保持規格表最新**——AI 助手用它來起草面向客戶的內容。
- ✅ **使用產品圖片**——它能顯著改善面向客戶的報價的外觀。
- ✅ **將舊產品標記為非活躍**,不要刪除它們——過往的報價和合約仍引用它們。
diff --git a/content/docs/sales/contacts.mdx b/content/docs/sales/contacts.mdx
index 27816178..dcb4447a 100644
--- a/content/docs/sales/contacts.mdx
+++ b/content/docs/sales/contacts.mdx
@@ -81,7 +81,7 @@ There is no owner-filtered contact view, and none is needed for access: contacts
## How the AI Copilot uses contacts
-When you ask the Sales Copilot to draft an email, it uses:
+When you ask the AI assistant to draft an email, it uses:
- The contact's first name and title (for personalisation).
- The account name and industry (for context).
diff --git a/content/docs/sales/contacts.zh-Hans.mdx b/content/docs/sales/contacts.zh-Hans.mdx
index cd1cfe95..9c924083 100644
--- a/content/docs/sales/contacts.zh-Hans.mdx
+++ b/content/docs/sales/contacts.zh-Hans.mdx
@@ -81,7 +81,7 @@ CEO
## AI Copilot 如何使用联系人
-当你让 Sales Copilot 起草一封邮件时,它会使用:
+当你让 AI 助手起草一封邮件时,它会使用:
- 联系人的名和职位(用于个性化)。
- 客户名称和行业(用于上下文)。
diff --git a/content/docs/sales/contacts.zh-Hant.mdx b/content/docs/sales/contacts.zh-Hant.mdx
index 47c620b1..5f7c6a77 100644
--- a/content/docs/sales/contacts.zh-Hant.mdx
+++ b/content/docs/sales/contacts.zh-Hant.mdx
@@ -81,7 +81,7 @@ CEO
## AI Copilot 如何使用聯絡人
-當你讓 Sales Copilot 起草一封郵件時,它會使用:
+當你讓 AI 助手起草一封郵件時,它會使用:
- 聯絡人的名和職位(用於個人化)。
- 客戶名稱和產業(用於上下文)。
diff --git a/content/docs/sales/index.zh-Hans.mdx b/content/docs/sales/index.zh-Hans.mdx
index 735b212e..e5a11410 100644
--- a/content/docs/sales/index.zh-Hans.mdx
+++ b/content/docs/sales/index.zh-Hans.mdx
@@ -44,7 +44,7 @@ Lead ──► Account + Contact ──► Opportunity ──► Quote
- **报价过期**——超过有效期的报价每晚会被自动标记为已过期。
- **折扣审批**——金额超过 $500K 的商机会自动路由给你的销售经理和总监进行审批。
-## Sales Copilot
+## 销售 AI 技能
[销售技能](/zh-Hans/docs/ai-copilot/sales-copilot) 已内置于平台助手。当你打开一条线索、客户或商机时,它可以:
diff --git a/content/docs/sales/index.zh-Hant.mdx b/content/docs/sales/index.zh-Hant.mdx
index 3576e0b8..a91f1557 100644
--- a/content/docs/sales/index.zh-Hant.mdx
+++ b/content/docs/sales/index.zh-Hant.mdx
@@ -44,7 +44,7 @@ Lead ──► Account + Contact ──► Opportunity ──► Quote
- **報價到期**——超過有效期的報價每晚會被自動標記為已過期。
- **折扣審批**——金額超過 $500K 的商機會自動路由給你的銷售經理和總監進行審批。
-## Sales Copilot
+## 銷售 AI 技能
[銷售技能](/zh-Hant/docs/ai-copilot/sales-copilot) 已內建於平台助手。當你打開一筆潛在客戶、客戶或商機時,它可以:
diff --git a/content/docs/sales/leads.mdx b/content/docs/sales/leads.mdx
index 1b49cdf9..318700c5 100644
--- a/content/docs/sales/leads.mdx
+++ b/content/docs/sales/leads.mdx
@@ -126,7 +126,7 @@ After conversion, the lead becomes read-only — all future activity happens on
## How the AI Copilot helps
-When you open a lead, the Sales Copilot is available in the side panel. Try asking:
+When you open a lead, the AI assistant is available in the side panel. Try asking:
- *"Qualify this lead"* — runs BANT, gives you a score and a one-line justification per dimension.
- *"Is this a hot lead?"* — quick yes/no plus reasoning.
diff --git a/content/docs/sales/leads.zh-Hans.mdx b/content/docs/sales/leads.zh-Hans.mdx
index 0a17cce4..2b126c4f 100644
--- a/content/docs/sales/leads.zh-Hans.mdx
+++ b/content/docs/sales/leads.zh-Hans.mdx
@@ -126,7 +126,7 @@ description: 在入站咨询成为真正交易之前对其进行捕获、评分
## AI Copilot 如何提供帮助
-当你打开一条线索时,Sales Copilot 会出现在侧边面板。试着提问:
+当你打开一条线索时,AI 助手会出现在侧边面板。试着提问:
- *"Qualify this lead"*——运行 BANT,给你一个评分以及每个维度的一行理由。
- *"Is this a hot lead?"*——快速是/否加上推理。
diff --git a/content/docs/sales/leads.zh-Hant.mdx b/content/docs/sales/leads.zh-Hant.mdx
index 7313b73b..2b4b6978 100644
--- a/content/docs/sales/leads.zh-Hant.mdx
+++ b/content/docs/sales/leads.zh-Hant.mdx
@@ -126,7 +126,7 @@ description: 在入站詢問成為真正交易之前對其進行擷取、評分
## AI Copilot 如何提供協助
-當你打開一筆潛在客戶時,Sales Copilot 會出現在側邊面板。試著提問:
+當你打開一筆潛在客戶時,AI 助手會出現在側邊面板。試著提問:
- *"Qualify this lead"*——執行 BANT,給你一個評分以及每個維度的一行理由。
- *"Is this a hot lead?"*——快速是/否加上推理。
diff --git a/content/docs/sales/pipeline-management.mdx b/content/docs/sales/pipeline-management.mdx
index 0c2f474d..0631891f 100644
--- a/content/docs/sales/pipeline-management.mdx
+++ b/content/docs/sales/pipeline-management.mdx
@@ -63,9 +63,9 @@ A few derived fields surface deal risk so you can act on them in your weekly rev
| **Days in current stage** > 30 | Deal is stagnant — push or close it. |
| **Close date in the past** AND stage still open | Forecast is wrong; reset the date or accept the loss. |
| **Amount > $500K** with no submitted approval | Discount approval is required before this can close. |
-| **Competitors set** with no win plan | The Sales Copilot can suggest a strategy. |
+| **Competitors set** with no win plan | The AI assistant can suggest a strategy. |
-The Sales Copilot surfaces at-risk deals when you ask *"what's slipping?"* or *"how does my pipeline look?"*
+The AI assistant surfaces at-risk deals when you ask *"what's slipping?"* or *"how does my pipeline look?"*
## Pipeline reports worth bookmarking
@@ -86,7 +86,7 @@ See [Analytics › Reports](/docs/analytics/reports) for details.
| Cadence | What to do |
| --- | --- |
| **Daily** | Open the **Pipeline** board. Find stalled cards (in-stage > 14 days). Push or close. |
-| **Weekly** | Run *Pipeline Coverage*. Ask the Sales Copilot: *"what's at risk?"* |
+| **Weekly** | Run *Pipeline Coverage*. Ask the AI assistant: *"what's at risk?"* |
| **Monthly** | Run *Opportunity Funnel by Owner → Stage*. Coach reps with leaky stages 1:1. |
| **Quarter-end** | Run *Won Opportunities by Owner* for commissions. Archive lost deals with a *loss reason*. |
diff --git a/content/docs/sales/pipeline-management.zh-Hans.mdx b/content/docs/sales/pipeline-management.zh-Hans.mdx
index 146f95f0..b6065373 100644
--- a/content/docs/sales/pipeline-management.zh-Hans.mdx
+++ b/content/docs/sales/pipeline-management.zh-Hans.mdx
@@ -63,9 +63,9 @@ description: 销售管道的日常运作方式——推进交易、自动概率
| **当前阶段停留天数** > 30 | 交易停滞——推进或关闭它。 |
| **成交日期已过** 且阶段仍开放 | 预测有误;重设日期或接受失单。 |
| **金额 > $500K** 但未提交审批 | 在此交易可成交之前需要折扣审批。 |
-| **已设置竞争对手** 但无制胜计划 | Sales Copilot 可以建议一个策略。 |
+| **已设置竞争对手** 但无制胜计划 | AI 助手可以建议一个策略。 |
-当你提问 *"what's slipping?"* 或 *"how does my pipeline look?"* 时,Sales Copilot 会呈现有风险的交易。
+当你提问 *"what's slipping?"* 或 *"how does my pipeline look?"* 时,AI 助手会呈现有风险的交易。
## 值得收藏的销售管道报告
@@ -86,7 +86,7 @@ description: 销售管道的日常运作方式——推进交易、自动概率
| 节奏 | 该做什么 |
| --- | --- |
| **每日** | 打开**销售管道**这块板。找出停滞的卡片(阶段内 > 14 天)。推进或关闭。 |
-| **每周** | 运行 *Pipeline Coverage*。向 Sales Copilot 提问:*"what's at risk?"* |
+| **每周** | 运行 *Pipeline Coverage*。向 AI 助手提问:*"what's at risk?"* |
| **每月** | 运行 *Opportunity Funnel by Owner → Stage*。对漏斗漏水的销售代表进行一对一辅导。 |
| **季末** | 运行 *Won Opportunities by Owner* 以计算提成。归档*失败*的交易并附上*丢单原因*。 |
diff --git a/content/docs/sales/pipeline-management.zh-Hant.mdx b/content/docs/sales/pipeline-management.zh-Hant.mdx
index 4ac6d2c3..a34459e9 100644
--- a/content/docs/sales/pipeline-management.zh-Hant.mdx
+++ b/content/docs/sales/pipeline-management.zh-Hant.mdx
@@ -63,9 +63,9 @@ description: 銷售管道的日常運作方式——推進交易、自動機率
| **當前階段停留天數** > 30 | 交易停滯——推進或關閉它。 |
| **成交日期已過** 且階段仍開放 | 預測有誤;重設日期或接受失單。 |
| **金額 > $500K** 但未提交審批 | 在此交易可成交之前需要折扣審批。 |
-| **已設定競爭對手** 但無制勝計劃 | Sales Copilot 可以建議一個策略。 |
+| **已設定競爭對手** 但無制勝計劃 | AI 助手可以建議一個策略。 |
-當你提問 *"what's slipping?"* 或 *"how does my pipeline look?"* 時,Sales Copilot 會呈現有風險的交易。
+當你提問 *"what's slipping?"* 或 *"how does my pipeline look?"* 時,AI 助手會呈現有風險的交易。
## 值得收藏的銷售管道報告
@@ -86,7 +86,7 @@ description: 銷售管道的日常運作方式——推進交易、自動機率
| 節奏 | 該做什麼 |
| --- | --- |
| **每日** | 打開**銷售管道**這塊板。找出停滯的卡片(階段內 > 14 天)。推進或關閉。 |
-| **每週** | 執行 *Pipeline Coverage*。向 Sales Copilot 提問:*"what's at risk?"* |
+| **每週** | 執行 *Pipeline Coverage*。向 AI 助手提問:*"what's at risk?"* |
| **每月** | 執行 *Opportunity Funnel by Owner → Stage*。對漏斗漏水的銷售代表進行一對一輔導。 |
| **季末** | 執行 *Won Opportunities by Owner* 以計算提成。歸檔*失敗*的交易並附上*丟單原因*。 |
diff --git a/content/docs/sales/quotes.mdx b/content/docs/sales/quotes.mdx
index 15e253f0..62049a4e 100644
--- a/content/docs/sales/quotes.mdx
+++ b/content/docs/sales/quotes.mdx
@@ -99,7 +99,7 @@ The rep then reviews the draft, attaches a PDF (or uses the built-in templating)
## Sending a quote to the customer
-From the quote detail page, click **Send to Customer**. This sets the status to *Presented* and emails the PDF using a quote-cover-email template. The Sales Copilot can draft the cover email for you — *"Draft a cover note for this quote"*.
+From the quote detail page, click **Send to Customer**. This sets the status to *Presented* and emails the PDF using a quote-cover-email template. The AI assistant can draft the cover email for you — *"Draft a cover note for this quote"*.
## Mark as Accepted / Rejected
diff --git a/content/docs/sales/quotes.zh-Hans.mdx b/content/docs/sales/quotes.zh-Hans.mdx
index 29c802e6..9a1adabf 100644
--- a/content/docs/sales/quotes.zh-Hans.mdx
+++ b/content/docs/sales/quotes.zh-Hans.mdx
@@ -99,7 +99,7 @@ Total Price = Subtotal − Discount + Tax + Shipping & Handling
## 将报价发送给客户
-在报价详情页,点击**发送给客户**。这会将状态设为*已提交*,并使用报价封面邮件模板将 PDF 通过邮件发送。Sales Copilot 可以为你起草封面邮件——*"Draft a cover note for this quote"*。
+在报价详情页,点击**发送给客户**。这会将状态设为*已提交*,并使用报价封面邮件模板将 PDF 通过邮件发送。AI 助手可以为你起草封面邮件——*"Draft a cover note for this quote"*。
## 标记为已接受 / 已拒绝
diff --git a/content/docs/sales/quotes.zh-Hant.mdx b/content/docs/sales/quotes.zh-Hant.mdx
index ada68a6b..b8be9fe4 100644
--- a/content/docs/sales/quotes.zh-Hant.mdx
+++ b/content/docs/sales/quotes.zh-Hant.mdx
@@ -99,7 +99,7 @@ Total Price = Subtotal − Discount + Tax + Shipping & Handling
## 將報價發送給客戶
-在報價詳情頁,點擊**發送給客戶**。這會將狀態設為*已提交*,並使用報價封面郵件範本將 PDF 透過郵件發送。Sales Copilot 可以為你起草封面郵件——*"Draft a cover note for this quote"*。
+在報價詳情頁,點擊**發送給客戶**。這會將狀態設為*已提交*,並使用報價封面郵件範本將 PDF 透過郵件發送。AI 助手可以為你起草封面郵件——*"Draft a cover note for this quote"*。
## 標記為已接受 / 已拒絕
diff --git a/content/docs/service/index.mdx b/content/docs/service/index.mdx
index d7cadb63..6e2322b2 100644
--- a/content/docs/service/index.mdx
+++ b/content/docs/service/index.mdx
@@ -13,7 +13,7 @@ The Service module helps your support team **receive customer issues, prioritise
| --- | --- |
| [**Cases**](/docs/service/cases) | The ticket record — subject, priority, status, resolution. |
| [**SLA & Escalation**](/docs/service/sla-and-escalation) | Automatic deadlines, the escalation process, and how the system surfaces breaches. |
-| [**Knowledge Base**](/docs/service/knowledge-base) | The support knowledge the AI Service Copilot reads from. |
+| [**Knowledge Base**](/docs/service/knowledge-base) | The support knowledge the AI assistant reads from. |
## A typical case life
@@ -23,7 +23,7 @@ New ──► In Progress ──► Waiting on Customer ──► Resolved ─
╲ Escalated ────────────╱
```
-1. **Case is created** (via email, web form, phone log or the **Service Copilot** triaging an inbound message).
+1. **Case is created** (via email, web form, phone log or the **AI assistant** triaging an inbound message).
2. **Priority is assigned** — Low, Medium, High or Critical. *Critical* alerts the **case owner** immediately, not a support manager: the alert's recipient list is `{caseRecord.owner_id}` alone.
3. **SLA target is calculated** — every case gets a deadline based on priority.
4. **Agent works the case**, moving status as they go.
@@ -32,7 +32,7 @@ New ──► In Progress ──► Waiting on Customer ──► Resolved ─
## What the system does for you
-- **Auto-prioritise** with the Case Triage skill (the Service Copilot reads the case and recommends a priority).
+- **Auto-prioritise** with the Case Triage skill (the AI assistant reads the case and recommends a priority).
- **Auto-escalate** Critical cases — the flow flags them *Escalated* and alerts the owner; it never touches `owner_id`, so nothing is handed to a senior agent. A High case is never escalated automatically, whatever the account is: raising one is the manual **Escalate Case** button.
- **Notify on critical** — inbox message + email to the **case owner** whenever a case is set to Critical. That is the Case Escalation flow's notify node, whose recipient list is `{caseRecord.owner_id}` alone; there is no `support_manager@example.com` recipient anywhere in this app.
- **Notify on escalation** — no email goes to an escalation team: `escalation_team@example.com` exists nowhere in this app. What the *Escalated* status fires is the `case_status_side_effects` hook, which opens an **Urgent follow-up task due tomorrow for the account owner**.
diff --git a/content/docs/service/index.zh-Hans.mdx b/content/docs/service/index.zh-Hans.mdx
index b5608462..afeb0392 100644
--- a/content/docs/service/index.zh-Hans.mdx
+++ b/content/docs/service/index.zh-Hans.mdx
@@ -13,7 +13,7 @@ description: 接收客户问题,按优先级路由,达成 SLA 目标,并
| --- | --- |
| [**工单**](/zh-Hans/docs/service/cases) | 工单记录——主题、优先级、状态、解决方案。 |
| [**SLA 与升级**](/zh-Hans/docs/service/sla-and-escalation) | 自动截止时间、升级流程,以及系统如何呈现违约。 |
-| [**知识库**](/zh-Hans/docs/service/knowledge-base) | AI 服务 Copilot 读取的支持知识。 |
+| [**知识库**](/zh-Hans/docs/service/knowledge-base) | AI 助手读取的支持知识。 |
## 一个典型工单的生命周期
@@ -23,7 +23,7 @@ New ──► In Progress ──► Waiting on Customer ──► Resolved ─
╲ Escalated ────────────╱
```
-1. **创建工单**(通过电子邮件、网页表单、电话记录,或由 **Service Copilot** 对来访消息进行分流)。
+1. **创建工单**(通过电子邮件、网页表单、电话记录,或由 **AI 助手**对来访消息进行分流)。
2. **分配优先级**——Low、Medium、High 或 Critical。*Critical* 会立即提醒**工单负责人**,而不是支持经理:这条提醒的收件人列表只有 `{caseRecord.owner_id}` 一项。
3. **计算 SLA 目标**——每个工单都会根据优先级获得一个截止时间。
4. **客服处理工单**,随进度推进状态。
@@ -32,7 +32,7 @@ New ──► In Progress ──► Waiting on Customer ──► Resolved ─
## 系统为你做的事
-- **自动确定优先级**——通过工单分流技能(Service Copilot 阅读工单并推荐优先级)。
+- **自动确定优先级**——通过工单分流技能(AI 助手阅读工单并推荐优先级)。
- **自动升级**——只针对 Critical 工单:流程把它们标记为 *Escalated* 并提醒负责人,从不改写 `owner_id`,因此没有任何工单被交到资深客服手里。High 工单永远不会被自动升级,无论账户是什么:要升级它,靠的是手动的 **Escalate Case** 按钮。
- **紧急时通知**——每当工单被设为 Critical 时,站内消息 + 邮件只发给**工单负责人**。这就是工单升级流程的 notify 节点,其收件人列表只有 `{caseRecord.owner_id}` 一项;本应用中不存在 `support_manager@example.com` 这个收件人。
- **升级时通知**——不会给任何升级团队发邮件:`escalation_team@example.com` 在本应用中根本不存在。状态转为 *Escalated* 真正触发的,是 `case_status_side_effects` 钩子开出的那条**次日到期、优先级为紧急、归账户负责人的跟进任务**。
@@ -40,7 +40,7 @@ New ──► In Progress ──► Waiting on Customer ──► Resolved ─
- **标记 SLA 违约**——一旦 SLA 目标时间过去,工单会被标记为 **SLA 已违约**并升级,其负责人收到站内消息 + 邮件提醒。没有红色横幅:本应用没有任何横幅机制。
- **阻止错误关闭**——未填写解决方案就不能将工单标记为已关闭。
-## Service Copilot
+## 服务 AI 技能
[服务技能](/zh-Hans/docs/ai-copilot/service-copilot) 内置于每个工单中。你可以问助手:
diff --git a/content/docs/service/index.zh-Hant.mdx b/content/docs/service/index.zh-Hant.mdx
index 821ec94b..8e057228 100644
--- a/content/docs/service/index.zh-Hant.mdx
+++ b/content/docs/service/index.zh-Hant.mdx
@@ -13,7 +13,7 @@ description: 接收客戶問題,按優先順序路由,達成 SLA 目標,
| --- | --- |
| [**工單**](/zh-Hant/docs/service/cases) | 工單記錄——主題、優先順序、狀態、解決方案。 |
| [**SLA 與升級**](/zh-Hant/docs/service/sla-and-escalation) | 自動截止時間、升級流程,以及系統如何呈現違約。 |
-| [**知識庫**](/zh-Hant/docs/service/knowledge-base) | AI 服務 Copilot 讀取的支援知識。 |
+| [**知識庫**](/zh-Hant/docs/service/knowledge-base) | AI 助手讀取的支援知識。 |
## 一個典型工單的生命週期
@@ -23,7 +23,7 @@ New ──► In Progress ──► Waiting on Customer ──► Resolved ─
╲ Escalated ────────────╱
```
-1. **建立工單**(透過電子郵件、網頁表單、電話記錄,或由 **Service Copilot** 對來訪訊息進行分流)。
+1. **建立工單**(透過電子郵件、網頁表單、電話記錄,或由 **AI 助手**對來訪訊息進行分流)。
2. **分配優先順序**——Low、Medium、High 或 Critical。*Critical* 會立即提醒**工單負責人**,而不是支援經理:這條提醒的收件人列表只有 `{caseRecord.owner_id}` 一項。
3. **計算 SLA 目標**——每個工單都會根據優先順序獲得一個截止時間。
4. **客服處理工單**,隨進度推進狀態。
@@ -32,7 +32,7 @@ New ──► In Progress ──► Waiting on Customer ──► Resolved ─
## 系統為你做的事
-- **自動確定優先順序**——透過工單分流技能(Service Copilot 閱讀工單並推薦優先順序)。
+- **自動確定優先順序**——透過工單分流技能(AI 助手閱讀工單並推薦優先順序)。
- **自動升級**——只針對 Critical 工單:流程把它們標記為 *Escalated* 並提醒負責人,從不改寫 `owner_id`,因此沒有任何工單被交到資深客服手裡。High 工單永遠不會被自動升級,無論帳戶是什麼:要升級它,靠的是手動的 **Escalate Case** 按鈕。
- **緊急時通知**——每當工單被設為 Critical 時,站內訊息 + 郵件只發給**工單負責人**。這就是工單升級流程的 notify 節點,其收件人列表只有 `{caseRecord.owner_id}` 一項;本應用中不存在 `support_manager@example.com` 這個收件人。
- **升級時通知**——不會給任何升級團隊發郵件:`escalation_team@example.com` 在本應用中根本不存在。狀態轉為 *Escalated* 真正觸發的,是 `case_status_side_effects` 鉤子開出的那條**次日到期、優先順序為緊急、歸帳戶負責人的跟進任務**。
@@ -40,7 +40,7 @@ New ──► In Progress ──► Waiting on Customer ──► Resolved ─
- **標記 SLA 違約**——一旦 SLA 目標時間過去,工單會被標記為 **SLA 已違約**並升級,其負責人收到站內訊息 + 郵件提醒。沒有紅色橫幅:本應用沒有任何橫幅機制。
- **阻止錯誤關閉**——未填寫解決方案就不能將工單標記為已關閉。
-## Service Copilot
+## 服務 AI 技能
[服務技能](/zh-Hant/docs/ai-copilot/service-copilot) 內建於每個工單中。你可以問助手:
diff --git a/content/docs/service/knowledge-base.mdx b/content/docs/service/knowledge-base.mdx
index e103b562..d68f942f 100644
--- a/content/docs/service/knowledge-base.mdx
+++ b/content/docs/service/knowledge-base.mdx
@@ -1,11 +1,11 @@
---
title: Knowledge Base
-description: The support knowledge the AI Service Copilot reads from, so agents can answer customers consistently and faster.
+description: The support knowledge the AI assistant reads from, so agents can answer customers consistently and faster.
---
# Knowledge Base
-The knowledge base is the **library of support content** the AI Service Copilot reads from when it drafts responses, suggests resolutions, or triages cases. A well-maintained knowledge base is what makes the Copilot useful — it's the difference between vague AI fluff and accurate, on-brand customer answers.
+The knowledge base is the **library of support content** the AI assistant reads from when it drafts responses, suggests resolutions, or triages cases. A well-maintained knowledge base is what makes the Copilot useful — it's the difference between vague AI fluff and accurate, on-brand customer answers.
## What's in the knowledge base
@@ -36,7 +36,7 @@ The **Knowledge** list ships with four tabs:
The object's declared search targets are the **Title**, **Summary** and **Article Number**, so those are the fields global search and the lookup picker read — the words a colleague would search for belong there rather than buried in the body.
-## How the Service Copilot uses it
+## How the AI assistant uses it
One skill reads articles: **Customer 360°**. Ask it for a customer's history and, alongside the account, contacts, open deals and cases, it queries **published** articles matched on the **category or tags** of the cases it just read, and cites each one by number (`KA-0007`).
diff --git a/content/docs/service/knowledge-base.zh-Hans.mdx b/content/docs/service/knowledge-base.zh-Hans.mdx
index 9e9e1a48..20a101a5 100644
--- a/content/docs/service/knowledge-base.zh-Hans.mdx
+++ b/content/docs/service/knowledge-base.zh-Hans.mdx
@@ -1,11 +1,11 @@
---
title: 知识库
-description: AI 服务 Copilot 读取的支持知识,让客服能够更一致、更快速地回答客户。
+description: AI 助手读取的支持知识,让客服能够更一致、更快速地回答客户。
---
# 知识库
-知识库是 AI 服务 Copilot 在起草回复、建议解决方案或分流工单时所读取的**支持内容库**。一个维护良好的知识库正是让 Copilot 变得有用的关键——它决定了 AI 是输出含糊空洞的废话,还是准确、契合品牌的客户答复。
+知识库是 AI 助手在起草回复、建议解决方案或分流工单时所读取的**支持内容库**。一个维护良好的知识库正是让 Copilot 变得有用的关键——它决定了 AI 是输出含糊空洞的废话,还是准确、契合品牌的客户答复。
## 知识库里有什么
@@ -36,7 +36,7 @@ description: AI 服务 Copilot 读取的支持知识,让客服能够更一致
对象声明的检索字段是**标题**、**摘要**和**文章编号**,全局搜索与查找选择器读的正是这三个字段——同事会拿来搜索的那些词,应该写在这里,而不是埋在正文里。
-## Service Copilot 如何使用它
+## AI 助手如何使用它
读文章的技能只有一个:**Customer 360°**。向它要一位客户的全貌,它在客户、联系人、进行中商机和工单之外,还会按刚读到的那些工单的**分类或标签**去查询**已发布**的文章,并按编号(`KA-0007`)逐条标注出处。
diff --git a/content/docs/service/knowledge-base.zh-Hant.mdx b/content/docs/service/knowledge-base.zh-Hant.mdx
index 4dd9aa18..e7c34b88 100644
--- a/content/docs/service/knowledge-base.zh-Hant.mdx
+++ b/content/docs/service/knowledge-base.zh-Hant.mdx
@@ -1,11 +1,11 @@
---
title: 知識庫
-description: AI 服務 Copilot 讀取的支援知識,讓客服能夠更一致、更快速地回答客戶。
+description: AI 助手讀取的支援知識,讓客服能夠更一致、更快速地回答客戶。
---
# 知識庫
-知識庫是 AI 服務 Copilot 在起草回覆、建議解決方案或分流工單時所讀取的**支援內容庫**。一個維護良好的知識庫正是讓 Copilot 變得有用的關鍵——它決定了 AI 是輸出含糊空洞的廢話,還是準確、契合品牌的客戶答覆。
+知識庫是 AI 助手在起草回覆、建議解決方案或分流工單時所讀取的**支援內容庫**。一個維護良好的知識庫正是讓 Copilot 變得有用的關鍵——它決定了 AI 是輸出含糊空洞的廢話,還是準確、契合品牌的客戶答覆。
## 知識庫裡有什麼
@@ -36,7 +36,7 @@ description: AI 服務 Copilot 讀取的支援知識,讓客服能夠更一致
物件宣告的檢索欄位是**標題**、**摘要**和**文章編號**,全域搜尋與查找選擇器讀的正是這三個欄位——同事會拿來搜尋的那些詞,應該寫在這裡,而不是埋在正文裡。
-## Service Copilot 如何使用它
+## AI 助手如何使用它
讀文章的技能只有一個:**Customer 360°**。向它要一位客戶的全貌,它在客戶、聯絡人、進行中商機和工單之外,還會按剛讀到的那些工單的**分類或標籤**去查詢**已發布**的文章,並按編號(`KA-0007`)逐條標注出處。
diff --git a/content/docs/service/sla-and-escalation.mdx b/content/docs/service/sla-and-escalation.mdx
index 24ac48a8..6389449e 100644
--- a/content/docs/service/sla-and-escalation.mdx
+++ b/content/docs/service/sla-and-escalation.mdx
@@ -110,7 +110,7 @@ By default cases are **private**. But **critical open cases** are automatically
## How the AI Copilot uses SLA
-The [Service Copilot's Case Triage skill](/docs/ai-copilot/skills#case-triage) considers SLA when recommending a priority:
+The [Case Triage skill](/docs/ai-copilot/skills#case-triage) considers SLA when recommending a priority:
- A customer-tier account with a vague subject still gets **High** because of the customer relationship.
- A prospect with *"production is down"* in the description gets **Critical** despite not being a paying customer.
diff --git a/content/docs/service/sla-and-escalation.zh-Hans.mdx b/content/docs/service/sla-and-escalation.zh-Hans.mdx
index c5b56a35..50efe04d 100644
--- a/content/docs/service/sla-and-escalation.zh-Hans.mdx
+++ b/content/docs/service/sla-and-escalation.zh-Hans.mdx
@@ -110,7 +110,7 @@ Critical 的 4 小时是系统唯一会盯的目标,而且它写在钩子里
## AI Copilot 如何使用 SLA
-[Service Copilot 的工单分流技能](/zh-Hans/docs/ai-copilot/skills)在推荐优先级时会考虑 SLA:
+[工单分流技能](/zh-Hans/docs/ai-copilot/skills)在推荐优先级时会考虑 SLA:
- 一个客户等级的账户即使主题含糊,仍会因客户关系而获得 **High**。
- 一个描述中含 *"production is down"* 的潜在客户会获得 **Critical**,尽管它不是付费客户。
diff --git a/content/docs/service/sla-and-escalation.zh-Hant.mdx b/content/docs/service/sla-and-escalation.zh-Hant.mdx
index 006f2eb8..128e639c 100644
--- a/content/docs/service/sla-and-escalation.zh-Hant.mdx
+++ b/content/docs/service/sla-and-escalation.zh-Hant.mdx
@@ -110,7 +110,7 @@ Critical 的 4 小時是系統唯一會盯的目標,而且它寫在鉤子裡
## AI Copilot 如何使用 SLA
-[Service Copilot 的工單分流技能](/zh-Hant/docs/ai-copilot/skills)在推薦優先順序時會考慮 SLA:
+[工單分流技能](/zh-Hant/docs/ai-copilot/skills)在推薦優先順序時會考慮 SLA:
- 一個客戶等級的帳戶即使主題含糊,仍會因客戶關係而獲得 **High**。
- 一個描述中含 *"production is down"* 的潛在客戶會獲得 **Critical**,儘管它不是付費客戶。
diff --git a/content/docs/whats-new.mdx b/content/docs/whats-new.mdx
index 8c11fb2e..9545c120 100644
--- a/content/docs/whats-new.mdx
+++ b/content/docs/whats-new.mdx
@@ -14,8 +14,8 @@ release make it different from every CRM you've used before.
### ✨ Wow #1 — Live schema, live AI
-Add a field in the admin console. Ten seconds later, ask the Sales
-Copilot a question about it. It already knows. No retraining, no
+Add a field in the admin console. Ten seconds later, ask the AI
+assistant a question about it. It already knows. No retraining, no
restart, no redeploy — the platform re-reads metadata on every
agent turn and the Copilot's `describe_object` tool returns your
new field as soon as it's saved.
diff --git a/docs/RELEASE_STRATEGY.md b/docs/RELEASE_STRATEGY.md
index 20ee4e34..d02d2a67 100644
--- a/docs/RELEASE_STRATEGY.md
+++ b/docs/RELEASE_STRATEGY.md
@@ -11,7 +11,7 @@ HotCRM releases as one ObjectStack app package:
| npm package name | `hotcrm` |
| ObjectStack manifest id | `app.objectstack.hotcrm` |
| Namespace | `crm` |
-| Current version | `1.0.5` |
+| Current version | `2.2.2` |
| Publish artifact | output from `pnpm build` |
The active repository is not released as separate scoped npm packages.
diff --git a/test/docs-drift.test.ts b/test/docs-drift.test.ts
index 020fc75b..2c370912 100644
--- a/test/docs-drift.test.ts
+++ b/test/docs-drift.test.ts
@@ -884,3 +884,315 @@ describe('the action example teaches a selection key the platform can deliver (#
).toBe(true);
});
});
+
+/**
+ * Persona drift — the product pages must not re-personify the retired copilots
+ * (#612).
+ *
+ * #512 removed the two app-authored agents and ADR-0063 §2 made the surface
+ * skills-only; #589 / PR #611 rewrote `content/docs/ai-copilot/*` accordingly.
+ * What #611 could not reach was the rest of the tree. At the time this guard was
+ * written the docs carried 79 occurrences of the two names across 39 pages; 14
+ * of those, on the 12 pages in HISTORICAL below, are retirement history and
+ * belong there. The other 65, on 29 product pages in all three locales, were
+ * live prose still calling the assistant "the Sales Copilot" / "the Service
+ * Copilot". None asserted a `sales_copilot` agent (the check above already
+ * covers fenced samples), so every gate this repo runs was green: `os validate`
+ * and `pnpm lint` walk authored metadata and never open a paragraph.
+ *
+ * Maintainer ruling (2026-08-04, on #612): the personas are retired as PRODUCT
+ * VOCABULARY too. The prose says "AI assistant" (zh: 「AI 助手」), because the
+ * architecture it must describe is "AI capability is implemented by agents in
+ * objectstack-ai/cloud; HotCRM contributes domain skills" — a page that names an
+ * app-owned persona is describing an entity this app does not contain.
+ *
+ * Two things about the scan are load-bearing rather than style:
+ *
+ * - **Soft wraps are normalised first.** `content/docs/index.mdx` wrote "the
+ * Sales\n> Copilot" across a blockquote line break, and `whats-new.mdx` wrote
+ * "ask the Sales\nCopilot" across a plain one. A line-oriented grep — the
+ * obvious way to write this, and the way the issue's own inventory was taken —
+ * reads neither. Both were live prose, and the second had already been fixed
+ * in both zh translations ("向助手询问"), so the English page was the only one
+ * still personifying: exactly the drift this rule exists to stop, and exactly
+ * the drift a naive scan would have certified as absent.
+ * - **CJK wraps are tightened after that.** `ai-copilot/index.zh-Hant.mdx`
+ * breaks 「服務 Copilot」 between 服 and 務, which no amount of space-joining
+ * repairs — join the lines with a space and the phrase reads 服 務 Copilot.
+ * Whitespace between two CJK characters is a typesetting artifact, never a
+ * word boundary, so it is removed before matching.
+ *
+ * ## Reverse verification (#612)
+ *
+ * Predicted direction: **red before the rewrite, green after** — an ordinary
+ * forbidden-string rule over pages that plainly contained the string. Measured
+ * on the pre-rewrite tree: 29 non-exempt pages reported, 65 occurrences. After:
+ * 0 reported, with 14 occurrences surviving inside HISTORICAL. The rule is not
+ * vacuous either — see the three guards below, of which the probe test is the
+ * one that matters, since a regex that had stopped matching would otherwise
+ * report a clean tree and read exactly like success.
+ */
+describe('product docs do not name a retired copilot persona (#612)', () => {
+ /**
+ * Pages allowed to write a persona name, each with the reason it may.
+ *
+ * A map rather than a list: an exemption with no stated reason is how a
+ * targeted whitelist turns into a blanket one. Both reasons here are the same
+ * kind — the page is talking ABOUT the retirement, in the past tense, which is
+ * the one context where naming the thing is the point.
+ */
+ const HISTORICAL: Record = {
+ 'content/docs/ai-copilot/index.mdx': "#611's \"No app-owned agents\" retirement callout",
+ 'content/docs/ai-copilot/index.zh-Hans.mdx': "#611's retirement callout (zh-Hans)",
+ 'content/docs/ai-copilot/index.zh-Hant.mdx': "#611's retirement callout (zh-Hant)",
+ 'content/docs/ai-copilot/sales-copilot.mdx': "#611's \"Where the personas went\" callout",
+ 'content/docs/ai-copilot/sales-copilot.zh-Hans.mdx': "#611's persona callout (zh-Hans)",
+ 'content/docs/ai-copilot/sales-copilot.zh-Hant.mdx': "#611's persona callout (zh-Hant)",
+ 'content/docs/ai-copilot/service-copilot.mdx': "#611's \"Where the personas went\" callout",
+ 'content/docs/ai-copilot/service-copilot.zh-Hans.mdx': "#611's persona callout (zh-Hans)",
+ 'content/docs/ai-copilot/service-copilot.zh-Hant.mdx': "#611's persona callout (zh-Hant)",
+ 'content/docs/whats-new.mdx': 'the v1.0 release record — what that release actually shipped',
+ 'content/docs/whats-new.zh-Hans.mdx': 'the v1.0 release record (zh-Hans)',
+ 'content/docs/whats-new.zh-Hant.mdx': 'the v1.0 release record (zh-Hant)',
+ };
+
+ /** Soft wraps and blockquote continuation markers collapse to one space. */
+ const unwrap = (text: string): string => text.replace(/[ \t]*\n[ \t]*>?[ \t]*/g, ' ');
+
+ /**
+ * Whitespace BETWEEN two CJK characters is typesetting, not a word boundary
+ * (see the header) — `服 務` is one word that a line break split.
+ */
+ const CJK = '\\u4e00-\\u9fff';
+ const tighten = (text: string): string =>
+ text.replace(new RegExp(`([${CJK}])[ \\t]+(?=[${CJK}])`, 'g'), '$1');
+
+ const normalise = (text: string): string => tighten(unwrap(text));
+
+ /**
+ * The persona spellings, one regex each so the failure names the spelling it
+ * found. The separator is an OPTIONAL single space: after `normalise()` a
+ * wrapped phrase is space-joined, and Chinese typography writes 「服务Copilot」
+ * with no space at all.
+ */
+ const PERSONAS: { label: string; re: RegExp }[] = [
+ { label: 'Sales Copilot', re: /Sales ?Copilot/ },
+ { label: 'Service Copilot', re: /Service ?Copilot/ },
+ { label: '销售 Copilot', re: /销售 ?Copilot/ },
+ { label: '服务 Copilot', re: /服务 ?Copilot/ },
+ { label: '銷售 Copilot', re: /銷售 ?Copilot/ },
+ { label: '服務 Copilot', re: /服務 ?Copilot/ },
+ ];
+
+ /** Depth-first walk of `content/docs`, REPO_ROOT-relative. */
+ const walkDocs = (dir: string): string[] => {
+ const root = join(REPO_ROOT, dir);
+ if (!existsSync(root)) return [];
+ return readdirSync(root, { withFileTypes: true }).flatMap((entry) => {
+ const rel = join(dir, entry.name);
+ return entry.isDirectory() ? walkDocs(rel) : rel.endsWith('.mdx') ? [rel] : [];
+ });
+ };
+
+ const PAGES = walkDocs('content/docs').map((file) => ({
+ file,
+ normalised: normalise(readFileSync(join(REPO_ROOT, file), 'utf8')),
+ }));
+
+ /** The persona spellings `file` still writes, after normalisation. */
+ const personasIn = (normalised: string): string[] =>
+ PERSONAS.filter((p) => p.re.test(normalised)).map((p) => p.label);
+
+ it('the scan reads a real docs tree', () => {
+ // Vacuity guard #1: a walk that returned nothing would report a clean tree.
+ expect(
+ PAGES.length,
+ 'no .mdx pages found under content/docs — this guard has gone vacuous',
+ ).toBeGreaterThan(50);
+ });
+
+ it('the detector reads every persona spelling, wrapped and unwrapped', () => {
+ // Vacuity guard #2, and the one that actually protects this rule. Every
+ // check below reports "nothing found" when the tree is clean AND when the
+ // regexes have stopped matching; only a positive probe tells those apart.
+ // The three wrap shapes are the ones that really occur in this tree: a
+ // blockquote continuation (`index.mdx`), a plain soft wrap
+ // (`whats-new.mdx`), and a CJK word split mid-token
+ // (`ai-copilot/index.zh-Hant.mdx`).
+ const probes: { text: string; expected: string }[] = [
+ { text: 'ask the Sales Copilot about it', expected: 'Sales Copilot' },
+ { text: 'the AI Service Copilot reads from', expected: 'Service Copilot' },
+ { text: '让销售 Copilot 替你判定', expected: '销售 Copilot' },
+ { text: '由服务Copilot 分流', expected: '服务 Copilot' },
+ // blockquote continuation — `content/docs/index.mdx` wrote exactly this
+ { text: '> Ten seconds later, the Sales\n> Copilot uses it', expected: 'Sales Copilot' },
+ // plain soft wrap — `content/docs/whats-new.mdx` wrote exactly this
+ { text: 'ten seconds later, ask the Sales\nCopilot a question', expected: 'Sales Copilot' },
+ // CJK word split — `ai-copilot/index.zh-Hant.mdx` wraps 服務 this way
+ { text: '> 兩個自己的智能體——「銷售 Copilot」和「服\n> 務 Copilot」', expected: '服務 Copilot' },
+ ];
+ const unread = probes
+ .filter((p) => !personasIn(normalise(p.text)).includes(p.expected))
+ .map((p) => `${JSON.stringify(p.text)} → expected ${p.expected}, read ${JSON.stringify(personasIn(normalise(p.text)))}`);
+ // Collected rather than asserted per probe, so a narrowed detector names
+ // every spelling it stopped reading in one run instead of only the first.
+ expect(
+ unread,
+ `the persona detector no longer reads:\n ${unread.join('\n ')}\n` +
+ 'A spelling this scan cannot see is a page nobody is checking — and the rule below ' +
+ 'would go green over it, which is indistinguishable from the tree being clean.',
+ ).toEqual([]);
+ });
+
+ it('every exempt page still writes a persona, so no exemption is dead', () => {
+ // Vacuity guard #3, pointed the other way: an exemption that no longer
+ // covers anything is a standing licence for the next author to re-introduce
+ // the persona on that page, granted by nobody, reviewed by nobody.
+ const dead = Object.keys(HISTORICAL).filter((file) => {
+ const page = PAGES.find((p) => p.file === file);
+ return !page || personasIn(page.normalised).length === 0;
+ });
+ expect(
+ dead,
+ `HISTORICAL exempts pages that no longer name a persona (or no longer exist):\n ${dead.join('\n ')}\n` +
+ 'Delete the entry. An exemption is granted to a specific piece of retirement history, ' +
+ 'not to a filename in perpetuity.',
+ ).toEqual([]);
+ });
+
+ it('no other page names "Sales Copilot" or "Service Copilot"', () => {
+ const offenders = PAGES.filter((p) => !(p.file in HISTORICAL))
+ .map((p) => ({ file: p.file, found: personasIn(p.normalised) }))
+ .filter((p) => p.found.length > 0)
+ .map((p) => `${p.file}: ${p.found.join(', ')}`);
+ expect(
+ offenders,
+ `pages naming a retired copilot persona:\n ${offenders.join('\n ')}\n` +
+ 'Say "AI assistant" (zh: 「AI 助手」) instead, and keep the sentence\'s functional ' +
+ 'meaning — only the name changes. HotCRM ships SKILLS; the assistant they attach to is ' +
+ 'the platform\'s (`ask`), implemented by an agent in objectstack-ai/cloud. Naming an ' +
+ 'app-owned persona describes an entity this app does not contain (#512, ADR-0063 §2). ' +
+ 'A page genuinely writing retirement HISTORY belongs in HISTORICAL, with its reason.',
+ ).toEqual([]);
+ });
+});
+
+/**
+ * Version drift — the docs must print the version the app declares (#612).
+ *
+ * The same three digits are hand-copied into at least four maintainer docs, and
+ * `docs/RELEASE_STRATEGY.md` had been sitting on `1.0.5` since v1 while the
+ * manifest said `2.2.2` — a whole major behind, on the page whose entire job is
+ * to tell a releaser what the current version IS. #589 caught the same defect in
+ * `docs/ARCHITECTURE.md` and #611 fixed that one copy; nothing generalised, so
+ * the next copy went on lying.
+ *
+ * `manifest.version` in `objectstack.config.ts` is the single source of truth
+ * here — it is what `pnpm build` stamps into the artifact — and it is READ FROM
+ * THE CONFIG rather than regexed out of it, the same derivation the flow rules
+ * at the top of this file use for thresholds.
+ *
+ * Two rules, because the docs make two different claims:
+ *
+ * - **contains**: each listed doc prints the version somewhere. Catches a doc
+ * that quietly drops the number.
+ * - **table rows**: a `| Current version | \`x.y.z\` |` row states the version
+ * as a FACT, so it must state THE version. This is the rule that was red on
+ * `RELEASE_STRATEGY.md` before this change (`1.0.5` vs `2.2.2`) and is the
+ * precise shape of the defect #612 filed.
+ *
+ * Reverse verification: predicted and measured **red before, green after** —
+ * pre-fix the row rule reported `docs/RELEASE_STRATEGY.md:14 states 1.0.5, the
+ * manifest declares 2.2.2`, and nothing else in the set was drifted.
+ */
+describe('the docs print the version the manifest declares (#612)', () => {
+ const VERSION: string = ((stack as any).manifest ?? {}).version;
+
+ /** Docs that state the CURRENT version as a fact a reader may act on. */
+ const VERSION_DOCS = [
+ 'docs/RELEASE_STRATEGY.md',
+ 'docs/STATUS.md',
+ 'docs/ARCHITECTURE.md',
+ 'README.md',
+ ];
+
+ /**
+ * `| Current version | \`2.2.2\` |` — first cell is the LABEL, second carries
+ * a backticked semver.
+ *
+ * Keyed on the first cell, not on "a row mentioning version":
+ * `ARCHITECTURE.md` has a row whose text contains "conversion", and
+ * `RELEASE_STRATEGY.md` has a `| Change type | Version impact |` header with
+ * no version in it at all. Both are matched by the obvious regex and neither
+ * states a version.
+ */
+ const VERSION_ROW = /^\|\s*(?:current\s+)?version\s*\|\s*`([^`]+)`\s*\|/gim;
+
+ const rowsIn = (file: string): { file: string; stated: string }[] => {
+ const text = readFileSync(join(REPO_ROOT, file), 'utf8');
+ return [...text.matchAll(VERSION_ROW)].map((m) => ({ file, stated: m[1].trim() }));
+ };
+
+ const ALL_ROWS = VERSION_DOCS.flatMap(rowsIn);
+
+ it('the manifest declares a version this rule can compare against', () => {
+ // Vacuity guard #1: a config whose shape moved would leave VERSION
+ // undefined, and `includes(undefined)` throws rather than passing — but the
+ // row rule below would go green over zero rows. Pin the source directly.
+ expect(
+ VERSION,
+ 'objectstack.config.ts declares no manifest.version — this whole guard is vacuous',
+ ).toMatch(/^\d+\.\d+\.\d+/);
+ });
+
+ it('package.json agrees with the manifest', () => {
+ // The two halves RELEASE_STRATEGY.md's own "Version Sources" section tells a
+ // releaser to keep aligned. Cheap to check, and a mismatch here would make
+ // every doc below correct against one source and wrong against the other.
+ const pkg = JSON.parse(readFileSync(join(REPO_ROOT, 'package.json'), 'utf8')) as { version: string };
+ expect(
+ pkg.version,
+ `package.json is ${pkg.version} but objectstack.config.ts declares ${VERSION}. ` +
+ 'These ship as one artifact; align them (docs/RELEASE_STRATEGY.md §Version Sources).',
+ ).toBe(VERSION);
+ });
+
+ it('every doc that states a version in a table states the declared one', () => {
+ // Vacuity guard #2: the extraction finding nothing looks exactly like every
+ // row agreeing. Both docs that carry such a row are in the list, so the
+ // floor is 2.
+ expect(
+ ALL_ROWS.length,
+ 'no `| Current version | `x.y.z` |` row parsed in any of ' +
+ `${VERSION_DOCS.join(', ')} — either the tables were reformatted (teach VERSION_ROW ` +
+ 'the new shape) or the rows are gone (drop this rule rather than leaving it green ' +
+ 'over nothing).',
+ ).toBeGreaterThanOrEqual(2);
+ const drifted = ALL_ROWS.filter((r) => r.stated !== VERSION).map(
+ (r) => `${r.file} states ${r.stated}, the manifest declares ${VERSION}`,
+ );
+ expect(
+ drifted,
+ `version rows that do not match objectstack.config.ts:\n ${drifted.join('\n ')}\n` +
+ 'Update the doc — the manifest is the source of truth, and a release page printing a ' +
+ 'stale version is the one page a releaser trusts.',
+ ).toEqual([]);
+ });
+
+ it('every doc that names the current version prints the declared one', () => {
+ // Bounded so `2.2.2` cannot be satisfied by `12.2.2` or `2.2.22`.
+ const boundedVersion = new RegExp(
+ `(? !boundedVersion.test(readFileSync(join(REPO_ROOT, file), 'utf8')),
+ );
+ expect(
+ silent,
+ `docs that no longer print the declared version (${VERSION}):\n ${silent.join('\n ')}\n` +
+ 'Each of these tells a reader which version they are looking at. If one legitimately ' +
+ 'stopped making that claim, drop it from VERSION_DOCS rather than leaving the check ' +
+ 'green over a page that says nothing.',
+ ).toEqual([]);
+ });
+});