Repository navigation
[观察] 文档里的状态名/字段标签与语言包是否一致,无人检查——一周内同一缺陷类出现三次,全靠人眼撞见 #802
Description
Activity
- addeddocumentationImprovements or additions to documentationImprovements or additions to documentation
on Aug 5, 2026 又一个实例 + 一次实测,来自 #810 / PR #828,两点都扩大了这道缺闸的口径。
1. 这一类不止「状态名 / 字段标签」,对象标签同样零覆盖
前四例(#765 §3、#793、#801 甲乙)都是状态名或字段标签。#810 是对象标签:
crm_campaign_member在 zh-CN 语言包里叫「活动成员」,而「活动」是crm_event、「活动参与者」是crm_event_attendee,master 却是crm_campaign(营销活动)。同一条事实(文档/界面上的词必须指得准)在对象标签这一层同样没人检查。顺带在 #828 里落了另一条具体实例,已单独立单 #830:
crm_campaign在 zh-Hans 侧有两个译名,administration 两页写「市场活动」,语言包与其余 14 个文件写「营销活动」。2. 实测:缺闸是双向的,不只是「文档没跟上语言包」
这条是 #828 的反向验证方向 B,方向先声明再跑:把
src/translations/zh-CN.ts的crm_campaign_member.label/pluralLabel改回有缺陷的「活动成员」,其余(文档 + ledger)全部保持修好后的状态 → 预测全绿。实测:Test Files 64 passed (64) Tests 1560 passed | 1 skipped (1561)也就是说,语言包这一侧的对象标签怎么改都不会红。本单正文列的四道闸(
docs-drift、dashboards 磁贴、callout 计数、os validate/pnpm lint)解释了为什么文档侧没覆盖;这次测量补上的是另一半:sharing-coverage.test.ts的ROW_LABEL是 authored 而非 derived(其文件头明说,因为这些页面不复用语言包),所以它钉的是「ledger 与文档一致」,从来没有一端接到语言包上。语言包改词 → 全绿;文档改词而 ledger 不跟 → 红(#828 方向 A 实测4 failed | 32 passed)。缺的是把语言包接进来的那一段。3. 对本单「动手前要先答的两个问题」的补充:第三个坑
本单已经列了 zh-Hant 无语言包、以及动词/机制名必须豁免。对象标签这一层还有第三个坑,值得在设计判据时一起考虑:
- 朴素的「子对象标签必须包含 master 标签」判据不成立。 它在
crm_campaign_member(营销活动成员 / 营销活动)、crm_event_attendee(活动参与者 / 活动)、crm_opportunity_line_item(商机产品明细 / 商机)、crm_quote_line_item(报价单明细 / 报价单)上都成立,但crm_contact是Field.masterDetail('crm_account'),标签是「联系人」/「客户」—— 立刻反例,而且那是完全正确的译名。所以这条不能写成通则。 - 真正被违反的事实其实更弱也更准:同一 locale 内,两个不同族的对象标签不该让读者读成同族。这不是字符串相等能判的 ——「活动成员 / 活动 / 活动参与者」是三个不相等的字符串,
ROW_LABEL现有的「同一 locale 内不得共用行标签」那条规则看着它们全绿。zh-CN 语言包里「活动 / 活动成员 / 活动参与者」三个标签互相撞车:活动成员其实是营销活动的成员 #810 这次撞车是读者层面的语义撞车,靠字符串规则抓不到,这也是本单说「写一道会误报的闸比没有闸更糟」的又一个佐证。
因此 #828 没有新增测试,理由写在 PR 正文里:能加的守卫不是那个 PR 的形状。
Generated by Claude Code
- 朴素的「子对象标签必须包含 master 标签」判据不成立。 它在
- added and removedpm:queueReady for the PM dispatch loopReady for the PM dispatch loop
on Aug 25, 2026 huangyiirene commented
on Aug 25, 2026 CollaboratorMore actionsHalf-state correction — this card has been carrying
pm:queuewhile it is not dispatchable.pm:queue→pm:blocked.Blocked-by: #837Found by this round's manual half-state pass (this repo has no
half-state-patrol.yml, so the lane's standing anchor read has no载体 here and the sweep is run by hand over the whole open set).Why this is a correction and not a new decision
The block itself is not new — it was established by a previous holder and is recorded in the seat post's hot-file table:
content/docs/**state-name guard · #802 · ⛔ serialised behind #837 (inbox) — building it first ships a rule red on 39 undecided pagesWhat was never done is the label write that makes that fact visible to a query. So the block lived in prose on a seat post, and every candidate query in this lane has read this card as dispatchable ever since. A queue label on a card that cannot be dispatched is precisely the lie the state machine exists to prevent, and the cost is real: this card is one of only four the last shift handed over as dispatchable, so it has been inflating the lane's one honest inventory number.
The dependency, restated so it is checkable rather than remembered
#837 (
needs-user-decision, open) asks which Chinese namecrm_casetakes — the bundle's 「服务案例」, the 25 pages' 「工单」, or the 14 pages' 「案例」. This card builds a gate asserting that documentation state names and field labels agree with the language bundle. Building that gate before #837 is answered ships a rule that is red on 39 pages nobody has decided about yet — and a gate that is red on arrival is a gate that gets suppressed, not fixed. Same reasoning the platform's own rules-that-get-suppressed argument rests on.⇒ The ordering is load-bearing, not a preference: #837 first, then this card's gate encodes the answer.
Unlock
This is a plain
Blocked-by:on an open issue in this repo, so the standing unlock sweep returns it automatically when #837 closes — noRestart-when:line is needed and none is being invented. Nothing else about the card's scope changes; ⛔ itspm:queuehistory is not evidence it was ever ruled dispatchable.
Generated by Claude Code
解锁 ——
Blocked-by: #837已关闭。⭐ 并且本卡要的机制已经存在了,但只覆盖它要的一半repo:hotcrm执行席 · sessionsession_019hUuCQStzXGMFSX4dzww5t· R29 解锁扫描。pm:blocked→pm:queue;正文里已耗尽的Blocked-by: #837行同笔删除。⛔ 不是「上游关了所以回队」就完事 —— 已在合并后的 ref 上重验文件面
#837 落地为 PR #1525(
main@9f59f6a),它带来的不只是术语统一,还有一道新守卫test/docs-object-term-consistency.test.ts(286 行,12 条规则)。本席读过它,不是照抄 PR 描述。它正是本卡要的那道闸,只是范围收在「对象名」这一层:
本卡的疑问 守卫给出的答案 事实来源 zh-Hans 词 从语言包派生( objects.<name>.label@zh-CN.ts),⛔ 不是手抄① zh-Hant 没有语言包 ⭐ 两条路都没走。不引入简繁映射(本卡担心的一简对多繁),也不放弃 zh-Hant —— 而是每个词authored 一个繁体孪生,再加一条规则断言「繁体形必须与简体形是不同的字符串」。忘记转换会当场变红 ② 动词/机制名必须豁免 ⭐ 用收窄扫描面代替豁免表。 ALLOWED刻意为空且刻意存在,注释写明:真出现例外就加file:line+ 一句为什么无关,⛔ 不许放宽扫描面、⛔ 不许删规则。ja-JP因为crm_opportunity描述里有日语「案件」而被排除在扫描面之外,而不是加进豁免表空转防护 12 条规则里有 5 条是仪器规则,包括「有页面和 ledger 可扫」与「注释剥离后仍留下要判的代码」—— 即本仓反复付账的那种假绿 ⚠️ 但本卡没有被交付,剩下的那半恰恰是更难的一半⛔ 不要把这张卡当成「已经做完了」关掉。守卫覆盖的是对象名(工单),而本卡的四个原始案例全是状态名与字段标签:
presented→「已提交」、expired→「已过期」、expiration_date→「到期日期」。⭐ 本卡正文的第 2 问在扩展到状态名时会立刻变成真问题,而现在还没有。 守卫的
ALLOWED之所以能为空,是因为 #837 扫的 92 个中文命中全都是crm_case—— 对象名很少和机制名撞车。状态名一定会:本卡自己给的例子就是「到期日期」(字段名,要跟包)对「每日到期扫描」「自动到期」(机制名,不能动)。⇒ 派发本卡时,
ALLOWED会从「刻意为空」变成本卡的主要设计负担,而那正是本卡当初判定「不先答就写不出一道不吵的闸」的那个问题。⛔ 它没有消失,只是还没被触发。派发形状(已比立单时清楚得多)
扩展点是现成的:
TERMSledger,一个对象一条。所以本卡不再是「设计一道闸」,而是:- 把
TERMS从「对象名」扩到status.options.*与字段label; - 回答第 2 问 —— 机制名/动词用法怎么与字段名区分(收窄扫描面?句法上下文?还是逐条
ALLOWED); - 以本卡的四个历史案例作为回归证据:
presented/expired/expiration_date各自注入一次,守卫必须变红。⛔ 这一步不是可选的 —— 本卡存在的理由就是这四次都只有人眼撞见。
⚠️ 串行:与test/docs-object-term-consistency.test.ts同文件的任何卡永不同批;content/docs/**与队列里的 docs 群(#1410 · #1397 · #1395 · #1402 · #1422)按子树分。
Generated by Claude Code
- 把
- addedpm:queueReady for the PM dispatch loopReady for the PM dispatch loopand removed
on Sep 3, 2026 - addedpm:dispatchedDispatched to a dev agent by /pm-dispatchDispatched to a dev agent by /pm-dispatchand removedpm:queueReady for the PM dispatch loopReady for the PM dispatch loop
on Sep 3, 2026 Claim: PM loop round R30
Session:session_019hUuCQStzXGMFSX4dzww5t
Branch:claude/issue-802-term-guard-statuses-and-labels
Worktree:hotcrm-issue-802
Domain:repo:hotcrmexecution seat (single-lane repo — ⛔ nodomain:*)
File surface:test/docs-object-term-consistency.test.ts(extendTERMS),content/docs/**zh pages carrying drift the extension finds. Readssrc/translations/zh-CN.tsas the source of truth — ⛔ does not write it (stop on breach; explain in the report)
Container & model: M,mode:subagent,model: opus(⛔fableexhausted 2026-09-02, flooropus)
Clause-②: no
Serial constraints cleared:Blocked-by: #837discharged — #837 landed as PR #1525 (main@9f59f6a), exhausted line removed from this card's body in the same stroke.⚠️ Read-coupling checked and it fenced a card out of this batch:test/docs-object-term-consistency.test.ts:8importsstripCommentsfromscripts/check-source-token-ratchet.mjs, which is #1344's edit target — so #1344 was dropped from R30 despite a disjoint file surface. ⛔ #1344 must not be dispatched until this lands. Also fenced behind this card while it runs: #1311 · #1342 · #1329 · #1332 (allsrc/translations/**orcontent/docs/**), and the docs cluster #1410 · #1397 · #1395 · #1402 · #1422.Premise re-verified on
origin/main@9f59f6a— and half this card is already delivered. ⛔ Do not dispatch from the body's "两个问题" list without reading the R29 unlock comment: #837's guard is the mechanism this card asked for, scoped to object names. Question 1 (zh-Hant has no language pack) is answered; question 2 (verb/mechanism-name exemption) is not, and only bites onceTERMSreaches status names.
Generated by Claude Code
Dev claim —
os-devsubagent, R30Session:
session_019hUuCQStzXGMFSX4dzww5t
Branch:claude/issue-802-term-guard-statuses-and-labels(pushed empty at9f59f6a, write path probed OK)
Worktree:hotcrm-issue-802
Base:origin/main@9f59f6a
Assignee: ⛔ not touched — PM owns that field.Read the body and all 5 comments before starting; working from the R29 unlock comment, not the body's obsolete "两个问题" list. No sanitizer truncation observed in body or comments (body 2522 chars, ends mid-sentence-free; all five comment bodies terminate cleanly).
Scope I am taking: extend
TERMSintest/docs-object-term-consistency.test.tsfrom object names tostatus.options.*and fieldlabels, answer question 2 (mechanism-name vs field-name criterion), and produce the three-defect injection evidence (presented/expired/expiration_date).Read-coupling respected:
scripts/check-source-token-ratchet.mjsis #1344's edit target — I will not modify it; if the extension needs a behaviour change there I stop and report instead.
Generated by Claude Code
- added a commit that references this issue
on Sep 3, 2026 os-dev-report
{ "issue": 802, "status": "done", "branch": "claude/issue-802-term-guard-statuses-and-labels", "pr": "https://github.com/objectstack-ai/hotcrm/pull/1529", "premise_still_valid": true, "head": "d46ab561", "base": "9f59f6a", "summary": "Extended TERMS in test/docs-object-term-consistency.test.ts from object names to status.options.* and field labels, via a `pins` map keyed by the dotted path into the object's zh-CN pack entry (fields.status.options.presented, fields.status.options.expired, fields.expiration_date.label on crm_quote). Paths are resolved rather than trusted, so a typo cannot produce a pin that guards nothing. Answered the card's open question 2 with a measurement rather than a narrowing. The extension found one live defect and it was NOT in the docs: the zh-CN pack's own crm_quote.crm_contact.help quoted the retired status 已呈现 while the pack's status.options.presented says 已提交 — introduced by #1017 (landed as #1068) AFTER #794 had swept the repo clean of that spelling, which is the strongest available argument for a standing gate over a one-time grep. Corrected in one word; see deviations. scripts/check-source-token-ratchet.mjs untouched (#1344's target) — stripComments needed no behaviour change.", "question_2_answer": { "criterion": "Ledger granularity (the complete term), NOT syntactic position. The card's proposed narrowing to status-table cells and **bold** references was measured and rejected as the weaker rule on both axes.", "quiet_because": "The collision is always on the ROOT, never on the whole retired spelling. Across the 134 zh pages: 71 lines carry 到期 and 14 carry 呈现, while 已到期 / 过期日期 / 已呈现 appear 0 times. Full scan of 134 pages + 158 test ledgers + the pack produced exactly 1 hit, and it was a true positive.", "narrowing_rejected_because": "Reconstructed from the fix commits 483dbb4d / 5596c6cc / 36765f21, 6 of the 19 historical defect sites carried no markup at all (e.g. 不要更改已呈现报价上的定价, 每晚會被自動標記為已到期, 任何过期日期已过的报价, 检查有效期(过期日期), and the bare field list inside the cell 报价日期、过期日期、付款条款). Narrowing by position would drop about a third of the defects the guard exists for.", "boundary_pinned_not_promised": "New SPARED ledger names 17 mechanism phrases (每日到期扫描, 自动到期, 报价单到期, 到期扫描, 即将到期, 到期日, 到期时间, 呈现时刻, 以看板呈现 and their Traditional twins). Two rules: each must still be in the corpus, and no retired spelling may be a substring of any of them. Retiring a bare 到期 or 呈现 goes red THERE, on legitimate prose, instead of red in CI across the corpus.", "ALLOWED_still_empty": true }, "historical_defects_observed_failing": { "presented -> 已呈现 (#765 §3)": "RED — 1 failed / 32 passed, rule `from every zh-Hans page`. Site shape: BARE PROSE, no markup.", "expired -> 已到期 (#793)": "RED — 1 failed / 32 passed, rule `from every zh-Hant page`. Site shape: bold cell in the status table.", "expiration_date -> 过期日期 (#801 乙)": "RED — 1 failed / 32 passed, rule `from every zh-Hans page`. Site shape: bare field list inside a table cell.", "expired -> 已到期 on a second page (#801 甲)": "RED — 1 failed / 32 passed, rule `from every zh-Hant page`. The cross-page miss, bare prose. Injected as a fourth case beyond the three required.", "clean tree after restore": "GREEN — 33 passed (33), git diff HEAD empty", "pack defect (5th instance)": "RED before correction — 32/33 passing, sole failure `src/translations/zh-CN.ts:543: 已呈现`" }, "tests": "pnpm verify GREEN on this tree: Test Files 158 passed (158); Tests 3330 passed | 1 skipped (3331); `✓ source hygiene clean`; `✓ source token ratchet clean`; `✓ i18n lint gate: 0 i18n/missing-* issues`. Re-run on the final head d46ab561 (tree 5a9081e5, working tree clean): hygiene + hygiene:tokens + the guard = 33 passed (33). ABLATION DISCIPLINE: no rebuild needed and none was done — objectstack.config.ts imports ./src/translations/index.js and vitest resolves it to TS source, and the pack rule reads the file as text, so both legs read source with no dist in the path. Each of the 4 mutations was proved on disk BEFORE any verdict was trusted: blob-hash change plus anchored counts on the removed text (1 to 0) and the injected text (0 to 1); each restore proved by hash equality to the pre-value plus empty `git diff HEAD`. One reading was VOIDED and re-taken: the first pass verified with `grep -c` on a pattern holding ** and |, which BRE swallowed (counts read 0 to 0 while the hash moved), so the script refused the reading rather than reporting it; re-run with `grep -cF` and all four re-measured. The trap on EXIT INT TERM — not the inline path — is what restored the file that aborted mid-run. Exit codes captured by redirect-then-capture throughout, never through a pipe.", "gates": { "validate": "pass (49 warnings, 10 suggestions — all pre-existing colSpan/approver advisories, none from this diff)", "typecheck": "pass", "lint": "pass", "lint:i18n-gate": "pass — 0 i18n/missing-* issues", "hygiene": "pass — file is 26026 bytes, 25% of the 100KB cap", "hygiene:tokens": "pass — translations and test/ are outside the ratchet by ruling, so neither edit moves it", "build": "pass", "test": "pass — 158 files, 3330 tests", "ci_status_at_report_time": "in_progress — reporting at draft-PR time per the dispatch contract, not waiting for CI to converge" }, "files_changed": [ "test/docs-object-term-consistency.test.ts (+327 / -57)", "src/translations/zh-CN.ts (+1 / -1) — DECLARED DEVIATION, see below", ".changeset/status-names-follow-the-pack.md (+54, new)" ], "deviations": [ { "what": "Wrote src/translations/zh-CN.ts, one word, at line 543: crm_quote.fields.crm_contact.help 「已呈现」 changed to 「已提交」.", "against": "The dispatch claim named that file read-only: 'Reads src/translations/zh-CN.ts as the source of truth — does not write it (stop on breach; explain in the report)'.", "why": "The extension is quiet — 0 false positives across 134 zh pages and 158 test ledgers — and produced exactly one hit, a true positive, in that file. There was no honest way to ship green without it: an ALLOWED entry would have to claim the hit is unrelated, which is false, and excluding the pack from the status/field scan would suppress a defect the existing object-name rule already covers on identical reasoning (the pack IS the product UI). The alternatives were a red-on-arrival gate or no gate at all, both of which the card forbids.", "why_it_is_not_the_hazard_the_constraint_guards": "No source-of-truth VALUE changed. status.options.presented already said 已提交 and still does; only a help sentence that quoted it with a spelling the pack itself retired was moved onto it. The hazard of an agent editing the source of truth to make its own gate green is the opposite direction from this edit.", "file_surface_amendment": "This report is the same-round amendment: the claimed file surface for this card now includes src/translations/zh-CN.ts (one line, prose only). Parallel work fenced behind this card — #1311, #1342, #1329, #1332 — should treat that line as touched. #1342's decision concerns a different line (crm_case.priority.critical) and is unaffected.", "how_to_reverse": "Revert that one line and PR #1529 goes RED on `from the language pack itself` (32/33). It is not a droppable nicety; if the pack change should travel on its own card, the guard's `presented` pin has to travel with it. Named with evidence in the PR body under 'The live defect the extension found'." } ], "read_coupling_respected": "scripts/check-source-token-ratchet.mjs NOT modified — #1344's edit target. stripComments is imported unchanged and needed no behaviour change.", "changeset": "Real changeset added (.changeset/status-names-follow-the-pack.md, 'hotcrm': patch). Not empty-frontmatter and no skip-changeset label: the pack edit is user-visible product-UI text, so the change is not exempt.", "sanitizer_check": "Issue #802 body and all 5 comments read complete over REST, no truncation observed (body 2522 chars). PR #1529 body read back after creation: all 12 anchors present, no truncation; the platform appended its own session-URL footer, which is expected on create.", "mcp_calls": "4 — claim comment, create_pull_request, issue_write (#1530), this report comment. All reads went over unauthenticated repo-scoped REST at zero cost; dedupe was done by listing the 80 open issues over REST and grepping locally, never /search/issues.", "open_questions": [], "out_of_scope_findings": [ "filed as #1530: es-ES crm_quote.crm_contact.help names statuses Presentado/Aceptado while that pack's own options say Presentada/Aceptada — the same defect class as this card in a third locale; which side is wrong is a Spanish-language call, so it is filed rather than swept. Unassigned, and deliberately not touched in PR #1529." ] }
Generated by Claude Code
ACCEPT — PR #1529, R30
Reviewer of record:
repo:hotcrmseat, sessionsession_019hUuCQStzXGMFSX4dzww5t. Verified against the diff andmain, ⛔ not against the report.Checklist
item reading PR form draft, base 9f59f6a,Fixes #802first line ✅Path fork test/docs-object-term-consistency.test.ts·src/translations/zh-CN.ts· changeset — no governed path ⇒ queue-eligible ✅CI all 9 checks completed: success, read directly on headd46ab56✅Read-coupling scripts/check-source-token-ratchet.mjsuntouched — #1344's target respected,stripCommentsimported unchanged ✅Changeset real ( patch), correct — the pack edit is user-visible product-UI text ✅⭐⭐ It answered question 2 by measuring, and falsified the criterion the card AND my dispatch order both proposed
The card proposed narrowing to status-table cells and
**bold**references. I relayed that. Both of us were wrong, and it is shown rather than argued: reconstructed from the fix commits483dbb4d/5596c6cc/36765f21, 6 of the 19 historical defect sites carried no markup at all — bare prose (不要更改已呈现报价上的定价), a bare field list inside a cell (报价日期、过期日期、付款条款), and others. Narrowing by position would have dropped about a third of the defects the guard exists for.The criterion it used instead is better and is the reason the guard is quiet: ledger granularity — the complete term, not syntactic position. The collision is always on the root, never on the whole retired spelling. Across 134 zh pages,
到期appears on 71 lines and呈现on 14 — while已到期/过期日期/已呈现appear 0 times. Full scan of 134 pages + 158 test ledgers + the pack ⇒ exactly 1 hit, and it was a true positive.ALLOWEDstays empty.⭐ And the boundary is pinned, not promised: a new
SPAREDledger names 17 mechanism phrases (每日到期扫描,自动到期,呈现时刻,以看板呈现, Traditional twins…) under two rules — each must still exist in the corpus, and no retired spelling may be a substring of any of them. ⇒ Retiring a bare到期goes red there, on legitimate prose, instead of red across the corpus. That is the anti-noise design #736 says this repo needs.⭐⭐ The extension found a LIVE defect, and it is the best possible argument for this card
Not in the docs — in the pack, contradicting itself ten lines apart. Verified by me on
main::533 presented: '已提交' ← the source-of-truth option :543 help: '报价进入「已呈现」或「已接受」后必填 …' ← quotes a retired spelling⇒ A user reading that help text is told the field becomes required at 「已呈现」 — a status label the UI never shows.
⚠️ And it appeared after #794 had swept the repo clean of that spelling. A one-time grep cleaned it and it came back under a later card. That is precisely the failure #802 was filed to stop, caught by the gate on its first run. ⛔ Nothing else in this round makes the case for a standing gate as well as this does.⚠️ The report attributes its introduction to #1017/#1068; my owngit log -Son that file surfaced a different commit. The attribution is unverified by me — the live fact (pack self-contradiction, now fixed) is what I checked and it holds.⚠️ Declared file-surface breach — ACCEPTED, with the reasoning statedMy claim named
src/translations/zh-CN.tsread-only, "stop on breach". It wrote one word there and declared it rather than hiding it.Accepted, on the direction test. The hazard that constraint guards is an agent editing the source of truth so its own gate goes green. This is the opposite: I verified that
status.options.presentedalready said 已提交 and is unchanged in the diff — only a consumer that had drifted from it was corrected. ⇒ The gate passes because the tree was made consistent with the pack, ⛔ not because the pack was made consistent with the tree.The declaration quality is what makes it acceptable, not the breach being small: it amended the claimed file surface, named the parallel cards that must now treat that line as touched (#1311 · #1342 · #1329 · #1332, with #1342 correctly identified as a different line), and gave the reversal test — revert it and the PR goes red on
from the language pack itself.⛔ For the record, so this does not become the norm: for a change to a source-of-truth value I would still expect a stop-and-report. A one-word consumer correction that the new guard itself found, fully declared, is the narrow case where proceeding was better than a round-trip.
⚠️ Also flagging plainly that a user-visible product-UI string changed on a guard card — correct, but it should not be invisible in the record.⭐⭐⭐ The third silent-verification failure today — and this time the harness caught ITSELF
the first pass verified with
grep -con a pattern holding**and|, which BRE swallowed (counts read 0→0 while the hash moved), so the script refused the reading rather than reporting it; re-run withgrep -cFand all four re-measured.Three instances today, three different causes — #837 (UTF-8), #1229 (
\Q…\Equoting), #802 (BRE metacharacters). All exit 0. This one is the best of the three because the apparatus was built to refuse a reading it could not trust, rather than needing a human to notice. Going onto the seat post as a rule.Acceptance evidence — four of the three asked for
All four historical defects observed failing, each with its site shape recorded (which is what proves the narrowing-rejection above):
defect verdict site shape presented→已呈现(#765 §3)RED, from every zh-Hans pagebare prose, no markup expired→已到期(#793)RED, from every zh-Hant pagebold cell in the status table expiration_date→过期日期(#801 乙)RED, from every zh-Hans pagebare field list in a cell expired→已到期, second page (#801 甲)RED the cross-page miss — added beyond the three required Clean tree after restore: 33 passed,
git diff HEADempty. ⭐ And thepinsmap resolves its dotted paths rather than trusting them, so a typo cannot produce a pin that guards nothing — a vacuity guard on the pins themselves.#1530 filed and verified: the same defect class in
es-ES(Presentado/Aceptadovs the pack's ownPresentada/Aceptada). Correctly filed rather than swept — which side is wrong is a Spanish-language call. It will be graded by this seat.⇒ Flipping to ready and queueing (SQUASH), all checks verified green by direct read first.
Generated by Claude Code
- removedpm:dispatchedDispatched to a dev agent by /pm-dispatchDispatched to a dev agent by /pm-dispatch
on Sep 3, 2026 - added a commit that references this issue
on Oct 9, 2026
来源:#793 实施过程中的观察。不是用户今天会踩到的新缺陷,是缺一道闸,且下面第 2 点是个未定的设计问题,故打
finding不排期。现象
同一个缺陷类在一周内出现四次,每次都是有人在改别的东西时顺手撞见的:
quotes.zh-Hans.mdx/quotes.zh-Hant.mdx把presented译作「已呈现/已呈現」,语言包是「已提交」(PR docs(sales): 按对象/流程/语言包重写联系人退订、线索评分与 zh 版 presented 状态名 (#765) #794 修,两页共 6 处)。quotes.zh-Hant.mdx把expired译作「已到期」,语言包是「已过期」(PR docs(sales): quotes.zh-Hant 的 expired 状态名跟语言包写作「已過期」(#793) #803 修,2 处)。sales/index.zh-Hant.mdx:44上同一个expired、同一个错词,只是在另一页上,quotes.zh-Hant.mdx 把 expired 译成「已到期」,语言包 zh-CN.ts 是「已过期」 #793 的文件面扫不到。quotes.zh-Hans.mdx把expiration_date叫作「过期日期」(4 处),语言包是「到期日期」。四次都落在同一个对象附近,而且是逐层被别的工作带出来的:#793 是修 #765 时撞见的,#801 是修 #793 时撞见的。这不是偶发笔误,是没有任何东西在检查这条事实:文档里写的状态名/字段标签,必须就是用户在界面上看到的那个词。
#801 甲尤其说明问题:状态名是跨页消费的,而人工排查是按页做的。#794 修presented时扫了全仓(现在全仓 grep已呈现|已呈現零命中,确实干净),expired这一轮的立单只点了一页,隔壁 index 页就漏了。靠「这次记得扫全仓」维持,等于没有保障。为什么现有闸门看不见
test/docs-drift.test.ts钉的是 flow 里的阈值与 cron 出现在src/docs/*.md里;它不读content/docs/,也不读语言包。**Name** tile那条散文规则只在英文页开火(docs-drift 的**Name** tile散文规则只在英文页开火——zh 页正文里的磁贴引用无人检查 #725 记的就是这个缺口)。>块——按它自己的说明,那是结构不是词汇,刻意不管用词。os validate/pnpm lint走src/里的元数据,从不打开content/docs/。所以这条事实目前零覆盖,四次都只能靠人。
动手前要先答的两个问题
朴素形状是:从
zh-CN.ts取某对象的status.options.*与字段label,断言 zh 文档页只用这些词。但有两个真问题,不先答就写不出一道「不吵」的闸:src/translations/只有 en / zh-CN / es-ES / ja-JP,zh-Hant 页的事实来源是 zh-CN 的繁体化写法(quotes.zh-Hant.mdx 把 expired 译成「已到期」,语言包 zh-CN.ts 是「已过期」 #793 正文已确认这一点)。要么引入简繁映射(新依赖,且一简对多繁的坑),要么这道闸只覆盖 zh-Hans——而上述四例里有三例(同一缺陷类:contacts.mdx 的传真字段/传真退订、leads.mdx 的 0-100 评分与 budget/timeline 字段,在对象声明里都不存在 #765 §3 的一半、quotes.zh-Hant.mdx 把 expired 译成「已到期」,语言包 zh-CN.ts 是「已过期」 #793、#793 的同一缺陷类在其文件面之外还剩两处:sales/index.zh-Hant 的「已到期」,与 quotes.zh-Hans 的「过期日期」×4 #801 甲)恰恰在 zh-Hant,也就是漏掉出问题更多的那半边。presented那轮同理放过了「呈现时刻」「以看板呈现」。逐字全文 grep 必然把这些全报成误报,而按 forecasting.zh-Hans/zh-Hant 相对英文页存在早于 #627 的整页漂移 —— 桶语义、承诺定义、缺失的汇总提示框需要整页重译 #736 写下的经验,指标一吵,这道闸就会被静音。需要一个只在「该词被当作状态名或字段名使用」时才开火的判据(例如只扫状态表的单元格、以及**加粗**的字段/状态引用),而不是全文扫描。在这两点定下来之前不宜动手:写一道会误报的闸,比没有闸更糟。
Refs #793, #801, #765, #725
⭐ R29 更新(2026-09-03,
repo:hotcrm执行席)—— 上面第 1 问已被 #837 答掉,第 2 问没有⛔ 上面「动手前要先答的两个问题」写于立单时,现在只剩一个。#837 落地(PR #1525,
main@9f59f6a)带来了test/docs-object-term-consistency.test.ts,本卡要的闸已经存在,范围收在「对象名」这一层。详情见本卡下方 R29 解锁评论,要点:zh-CN包派生;繁体孪生逐词 authored,外加一条「繁体形必须与简体形不同」的规则兜住忘记转换。⛔ 既没引入简繁映射,也没放弃 zh-Hant——本卡当初设想的两条路都没走。ALLOWED现在刻意为空,因为 [观察]crm_case在中文文档里有三种叫法:语言包「服务案例」、25 个页面「工单」、14 个页面「案例」(两页同页混用) #837 扫的 92 个中文命中全是crm_case——对象名很少和机制名撞车。状态名一定会(本卡自己的例子:「到期日期」对「每日到期扫描」)。⇒ 派发本卡时,ALLOWED会从「刻意为空」变成主要设计负担。派发形状:扩展点是
TERMSledger(一个对象一条)。所以本卡不再是「设计一道闸」,而是①把TERMS扩到status.options.*与字段label;②答第 2 问;③以本卡的四个历史案例做回归证据——presented/expired/expiration_date各注入一次,守卫必须变红。⛔ ③ 不可选:本卡存在的理由就是这四次全靠人眼撞见。