Skip to content

docs(administration): write the state-machine status vocabulary and the last two behavioural claims to source (#921, #940) - #950

Merged
yinlianghui merged 3 commits into
mainfrom
claude/issue-921-940-state-machines-truth
Aug 6, 2026
Merged

yinlianghui merged 3 commits into
mainfrom
claude/issue-921-940-state-machines-truth

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #921
Fixes #940

两单并单落地。文档 only,src/ 零改动,content/docs/releases/ 未触碰。基线 origin/main = 11f62429(PR #939 之后),行号全部在 fresh main 上重定位。

一、#921 状态词以 src/ 为准

lead —— Working / Disqualified 不是状态

真值是 lead.object.ts:191-197 的 new / contacted / qualified / unqualified / converted。页面用的 Working、Disqualified 只是 src/mappings/lead_import.mapping.ts 的导入别名('Working': 'contacted'、'Disqualified': 'unqualified',同表还有 Open / In Progress / Nurturing / Closed - Not Converted),产品从不显示这两个词;而真实存在的 Contacted / Unqualified 在这份「状态机权威页」上一次都没出现过。

替换清单(英文页行号,三语同址):

位置 原 现
:8 导语 New → Working → Qualified,且「绝不会倒退回 New」 New → Contacted → Qualified → Converted(「绝不倒退回 New」一并删:unqualified: ['new'] 就是一条回到 New 的边)
:14 列表项 New → Working → Qualified → Converted / Disqualified New → Contacted → Qualified → Converted,并写明 Unqualified 从任何开放状态可进、唯一出路是重新打开回 New
:28-47 ASCII 图 Working / Disqualified 两个框 按 transitions 重画(主路线 + 两条 Disqualify 边 + Unqualified 的 re-open 边),三语共用同一张对齐过的 ASCII
:49-58 转换表 6 行,含 3 条 Working 边、Disqualified 终态 改成「每个 from 一行、列出全部 to」的形状,逐字照抄 lead_status_progression;Trigger 列一并重写(原列名的 Begin Working / Mark Qualified / Disqualify 三个按钮在 src/actions/lead.actions.ts 里并不存在,那里只有 convert_lead / schedule_followup / create_campaign)
:60 Disallowed 「禁止 New 直接到 Converted」 方向反了,按实况改写为「这是有意允许的」
:118 Copilot 举例 「status 是 Working 时才建议」 整节重写,见下

:60 的依据:lead.object.ts:516-519 的注释明写 Convert 动作可见性放宽到 new/contacted/qualified,ConvertLeadAction.visible 是 record.is_converted == false && status != "unqualified" && status != "converted",而 lead-conversion.flow.ts:242 直接盖 status: 'converted' —— 这条捷径必须在表里,否则每一次从未跟进过的线索转化都会记一条名不副实的警告。原文把一条设计上刻意允许的边写成了「禁止」的典型。

opportunity —— 没有 Closed Without Decision,漏了 Needs Analysis

真值是 _picklists.ts:111-119 的 7 个阶段。「7 个阶段」这个数字是对的(换掉幻影、补回 Needs Analysis 之后仍是 7),成员错了一个。:65-70 的图与 :72-77 的规则块按 opportunity_stage_progression 重写。

顺带被同一处失真拖累的两条(写在同一个 Rules 块里、且我新写的表会与它们直接矛盾,故一并改,非扩围):

  • :76「可以移动到三个已关闭阶段之一」—— 只改数字仍是假:closed_won 只从 proposal / negotiation 可达,closed_lost 才是五个开放阶段都可达。
  • :75「可以自由向后移动」/ :77「除非重新打开 —— 仅限管理员」—— 转换表里没有任何一条通往更早阶段的边,closed_won / closed_lost 都是 [](无 reopen 边)。:77 的锁定描述同时过窄:opportunity.hook.ts:78-116 的冻结拒绝的是用户对已关单记录的一切编辑,只放行 description / next_step / notes,且是抛错而非警告。

setup 三页

setup.mdx:88-94 的阶段/赢率表命中 Closed Without Decision。删掉这一行后暴露出:缺失的 Needs Analysis 把它下面每一行的赢率都错位了一格 —— 页面的 Proposal 40% / Negotiation 60% 实际是 opportunity.hook.ts:29-37 里 needs_analysis: 40 / proposal: 60 的值,真值是 60% / 80%。整表改为照抄 STAGE_PROBABILITY,7 行仍对应「7 阶段默认管道」。

(setup.mdx:96「请在 设置 → 商机 → 阶段 中调整」与 PR #939 在 state-machines :108 处理掉的「设置面不存在」同型,但属行为性主张、不在本单裁定面 —— 未动,另行归档见文末。)

二、#940 两条行为性主张

三、Copilot 节(:114-122)的取舍

选了「重写为中性表述」而非删节。 理由:删掉整节会让「状态机和 Copilot 有没有关系」这个问题从页面上消失,而它恰恰是读者会问的;本仓能证的事实(零支撑)本身就有信息量,值得留在页面上。

写进去的只有本仓能证的:grep -rniE "state.?machine|transition|legalNext" src/skills/ src/actions/ 零命中,六个 *.skill.ts 与全部 *.actions.ts 都没提转换表,技能提示词里也没有任何合法后继清单。

没有写进去的:平台侧 agent 装载对象元数据时会不会读 validations[] 里的 state_machine 并据此收窄建议 —— 本仓证不了,所以明确写成「这是运行时的问题,本页对它不作任何方向的断言」,并提示读者在自己的部署上验证。不硬判平台侧真伪、不虚构,与 #939 dev 的「证不了」结论一致。

四、验证

#919 守卫单跑(test/status-state-machines.test.ts)

 Test Files  1 passed (1)
      Tests  39 passed (39)
   Duration  2.13s

方向如实报:predicted GREEN → observed GREEN,且这是守卫的盲区而非确认。 该守卫(describe('the admin docs roster matches the ledger'))只读首个 ## 小节的 - 列表项,并在其中做对象名子串匹配(DOC_LABEL:Leads / 潜在客户 / 潛在客戶 …),外加一条「列表项数量 > 0」的前置条件。我改的是列表项内部的状态词,对象标签与 - 前缀原样保留,所以守卫在结构上不可能因这次改动变红 —— 它既不会红,也不构成「状态词写对了」的证据。真正的证据是正文里逐条标注的 src/ 出处。

六道门(flock -w 7200 /tmp/os-heavy-verify.lock 内串行,NODE_OPTIONS=--max-old-space-size=4096)

门 退出码 关键行
pnpm validate 0 13 warning,全部为既有的 approval-approvers-may-resolve-empty / crm_campaign_member 分组告警,与本 PR 无关
pnpm typecheck 0 tsc --noEmit 无输出
pnpm lint 0 13 warning(s), 14 suggestion(s) (1077ms),同为既有
pnpm hygiene 0 ✓ no raw control bytes in first-party files / ✓ source hygiene clean
pnpm build 0 Artifact: dist/objectstack.json (1921.4 KB),17 Objects 344 Fields
pnpm test -- --maxWorkers=2 0 Test Files 70 passed (70) / Tests 1602 passed | 1 skipped (1603)

控制字节 push 前自扫(超出 hygiene 门的扫描面):grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f]' 覆盖 6 个改动文件 + changeset,退出码 1(无命中)。未起 dev server。

回退检查

PR #939 的落地行(:3 description、:8 后半、:86-96「What a state machine buys you」、:108「没有设置面」、:110-114「用户看到什么」、:135-136「给用户的提示」、:142 Related)逐条 grep 确认三语全部 OK,零回退。git diff 删除行清单核对:删掉的正好是目标行集,无一条 #939 文本被移除。

三张 ASCII 图(英/简/繁)用脚本核过列位,三份完全一致;简繁图内标签保持 ASCII((terminal) 而非全角),避免等宽下错位。zh 页未新增任何内链,故无锚点问题。

五、范围声明


Generated by Claude Code

…he last two behavioural claims to source (#921, #940)

The state-machines page used lead statuses and opportunity stages that do not
exist. `Working` and `Disqualified` are entries in the lead import alias table
(src/mappings/lead_import.mapping.ts), not statuses; the real vocabulary is
new/contacted/qualified/unqualified/converted, and Contacted and Unqualified had
never appeared on the page. `Closed Without Decision` exists nowhere in src, and
the real Needs Analysis stage was missing. Both diagrams, both transition tables
and the lede are now copied from the state_machine rules.

Two route claims were backwards: New -> Converted is in the lead table
deliberately (Convert Lead is offered on any open lead and lead_conversion
stamps status:'converted'), and Unqualified is not terminal (it re-opens to
New). The opportunity section advertised free backwards movement and a reopen
path the table declares nowhere.

#940: close_date and amount are unconditionally required, not stage-gated;
approval starts at $100K with $500K as the Sales Director tier. The won/lost
reason claim was already correct and is untouched. The AI Copilot section is
replaced with what src can show - no skill or action references a transition
table - leaving the platform-side behaviour explicitly unclaimed.

administration/setup carried the same phantom stage, and removing it exposed
that the missing Needs Analysis had shifted every probability down one row.

All three locales. Documentation only; src unchanged.
@vercel

vercel Bot commented Aug 6, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
hotcrm Ignored Ignored Aug 6, 2026 1:01pm

Request Review

…claims

Self-review of the previous commit caught two of my own sentences claiming
more than src supports:

- The disqualification-reason requirement is a separate error-severity
  validations[] rule named disqualification_reason_required, not a
  requiredWhen predicate on the field. (The enforcement itself is real -
  severity is 'error' - only the mechanism was mis-named.)
- 'These four reject the save' was too broad: opportunity_approval is a
  record_change flow that runs after the update and locks the record, not a
  save-time check. Only the three required/requiredWhen claims reject a save.

All three locales.

Copy link
Copy Markdown
Collaborator Author

追加一个提交(9c13a26b)—— 自查抓到我自己新写的两句比 src/ 说得多,与两单原有的失实无关,但同属「把页面写到源码」的同一条标准,故当场改掉而不是留给评审:

  1. 取消资格原因的机制名错了。 我写成「该字段的 requiredWhen」,实际是 lead.object.ts:452-464 一条独立的 validations[] 规则 disqualification_reason_required(type: 'script')。约束本身是真的(severity: 'error',确实拒绝保存,注释还记了它补的正是「字段 description 承诺了但没人执行」这个洞),错的只是机制名 —— 而这一页的全部价值就在于机制名准确,所以照改。

  2. 「这四条确实会拒绝保存」太宽。 前三条(close_date / amount 的 required、win_reason / loss_reason 的 requiredWhen)成立;第四条不成立 —— opportunity_approval 是 record_change 流程,triggerType: 'record-after-update',在更新之后才跑,然后靠 lockRecord: true 锁住记录,它从不拒绝那次保存。已拆开写清。

改后复跑:test/status-state-machines.test.ts 仍 39 passed (39);pnpm hygiene ✓ source hygiene clean;控制字节自扫三张页面退出码 1(无命中)。三语同步。


Generated by Claude Code

…three locales

The Closed Lost line in the diagram was an English sentence ('any of the five
open stages'), which reads as untranslated text on the two Chinese pages while
CJK replacements would break monospace alignment. Replaced with the stage
labels themselves, which are already English in these diagrams by design and
carry the same fact.
@yinlianghui
yinlianghui marked this pull request as ready for review August 6, 2026 13:05
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 6, 2026
Merged via the queue into main with commit b7791ca Aug 6, 2026
9 checks passed
yinlianghui added a commit to yinlianghui/hotcrm that referenced this pull request Aug 10, 2026
…d the whole cubes page to source (objectstack-ai#962, objectstack-ai#965) (objectstack-ai#975)

objectstack-ai#962: src/reports/ publishes ten reports; the Sales / Revenue / Marketing
sections named thirteen that exist nowhere, while the six real ones -
Accounts by Industry and Type, Customer Churn Signals and the four in
opportunity.report.ts - had never appeared on the page. Two of the thirteen
were real names on the wrong thing: Pipeline by Stage is the funnel tile
shared by three dashboards and the chart title of the real Opportunities by
Stage report, and Stale Opportunities is the Stale list view, which applies
no 14-day cut (it ranks open deals by stage_entry_date; the 14 days belong
to opportunity_stagnation). Each remaining name is judged against the
semantic layer: Forecast vs Actual, Win/Loss Analysis, Big Deals Won and the
renewal pipeline are a custom report away; Sales Cycle Length, Discount
Approval Activity and the three contract reports are not, because no dataset
reads crm_contract or the line items and days_in_stage is a post-query
formula. Marketing has no campaign dataset at all. The subscription example,
the permissions bullet and the manager tips no longer name absent reports.

objectstack-ai#965: the page was built on four cubes that do not exist. This app declares
no cube - every semantic definition is a defineDataset() under src/datasets/
and the analytics service compiles each into its cube (ADR-0021;
objectstack.config.ts registers no analyticsCubes). The page now lists the
nine real datasets with their real dimensions and measures, folds Sales and
Pipeline into the single opportunity_metrics both described, and names per
item what is unreachable and why - weighted pipeline (Expected Revenue is a
record field, no measure), discount (a line-item percent), product/family,
account tier and size, team and region (positions and Billing Country, i.e.
sharing machinery), sub-month grains, snapshot dates. Marketing keeps no
claim it cannot support: the campaign records carry the spend, counters and
the ROI % / Response Rate % formulas, and nothing aggregates them.

Platform-side sections are left explicitly unclaimed rather than declared
false, per the objectstack-ai#950 precedent: the cube UI, the refresh cadence and the cube
access log are runtime questions this repo cannot answer. What is stated is
the app-side half - no skill names a dataset or cube, and Live Data Access
answers through the platform's object tools, not a compiled cube.

All three locales. Documentation only; nothing under src/ changed.


Claude-Session: https://claude.ai/code/session_01VHrPAGEgFDoHjphqYG4BMa

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment