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([]); + }); +});