Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion .github/workflows/tests-unit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: 'Tests (Unit)'
on:
pull_request:
branches: [main, master]
types: [opened, synchronize, ready_for_review]
types: [opened, synchronize, ready_for_review, labeled, unlabeled]
paths:
- '.github/workflows/tests-unit.yml'
- 'package.json'
Expand Down Expand Up @@ -35,6 +35,7 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
fetch-depth: 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unconditional full git fetch penalizes all PR runs

Low Severity

The fetch-depth: 0 checkout applies unconditionally to every workflow run, fetching the entire git history even though only the conditional "Preserve English bytes" step (gated by chinese-copy-only label) needs deep history. The vast majority of PR runs—triggered by opened, synchronize, or ready_for_review—only run typecheck and unit tests, which need only a shallow clone. Combined with the new labeled/unlabeled activity types causing extra workflow triggers for any label change on matching PRs, this increases CI time and cost across all runs.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3cc309d. Configure here.

- name: Setup Bun
id: setup-bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
Expand All @@ -59,3 +60,9 @@ jobs:
run: bun run typecheck
- name: Unit tests
run: bun run test:unit
- name: Preserve English bytes in explicitly Chinese-only changes
if: ${{ contains(github.event.pull_request.labels.*.name, 'chinese-copy-only') }}
env:
ZH_GUARD_BASE_SHA: ${{ github.event.pull_request.base.sha }}
ZH_GUARD_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: bun run --cwd packages/app guard:zh-copy:chinese-only
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ The site ships a hand-authored Simplified Chinese sibling for every indexable pa

> **Claude reviews every changed user-visible Chinese string, but the Chinese maintainer makes the final decision.** Load `review-zh-copy` before opening or reviewing any PR that touches user-visible Chinese text, including refactors whose filenames do not contain `zh`. The skill evaluates semantic fidelity and natural Chinese as separate gates and follows [docs/chinese-copy.md](./docs/chinese-copy.md). Its findings never block another contributor's merge. Do not mention `@edwingao28` for a clean review, routine coverage, or an ordinary wording or naturalness suggestion. Mention `@edwingao28` only for a high-confidence semantic or factual error, changed attribution or speaker voice, or unresolved high-impact ambiguity, and ask for confirmation. The Chinese maintainer makes the final editorial decision.

> **Chinese copy has a narrow mechanical CI guard.** `packages/app/src/lib/zh-copy.test.ts` checks objective regressions: the standalone English common noun `Chip` in Chinese text (with explicit identifier and unit exemptions), prose-like English `<strong>` labels that bypass an available `zh:` dictionary, and redundant English-first double translations such as `warmup 预热`. It does not globally ban `预热`, `随机种子`, or `卸载`, and deliberately does **not** enforce pronouns, preferred contextual terminology, punctuation style, sentence structure, fluency, tone, or register; those require page context and human review. Every mechanical rule must use the same matcher for the source scan and a hand-checked case in `packages/app/src/lib/zh-copy-mechanical-regressions.jsonl`, including an exemption case whenever the rule has one.
> **Chinese copy has a narrow objective CI guard.** `packages/app/src/lib/zh-copy.test.ts`, `blog-content.test.ts`, `i18n.test.ts`, and `zh-objective-guard.test.ts` check route and Blog sibling parity, explicit `en`/`zh` dictionary shape, protected Blog code/math/Figure/link/JSON-LD structure, and high-confidence regressions such as the standalone English common noun `Chip`, prose-like English `<strong>` labels that bypass an available `zh:` dictionary, redundant English-first translations such as `warmup 预热`, and unambiguous malformed punctuation. A PR labeled `chinese-copy-only` additionally compares English Blog bytes and raw `en` subtrees against the merge base. Exact temporary exceptions, reasons, and removal conditions live in `packages/app/src/lib/zh-objective-guard-exceptions.json`. Every rule must use the same parser or matcher for the real source scan and its mutation fixtures, with a failing mutation, accepted fix, and an accepted exception mutation when applicable. The guard deliberately does **not** enforce fluency, clause order, sentence structure, register, contextual pronouns, marketing tone, quotation voice, English-token ratios, or contextual terminology; those require page context and human review. See `docs/chinese-copy.md` for the complete boundary and exception policy.

## Chart Interpolation — TS and Python Helpers MUST Stay in Sync

Expand Down
2 changes: 2 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

120 changes: 120 additions & 0 deletions docs/chinese-copy.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,123 @@ ground truth.
维护者。CI 不全局禁用 `预热`、`随机种子` 或 `卸载`,因为这些写法在解释与引用中可能
正确;`warmup 预热` 这类与上下文无关的重复表达则可由 CI 拦截。#819 中经过单独验证的
机械案例可以进入 #820 的确定性 CI fixtures,但编辑性改写不得自动视为标准答案。

## Objective CI contract / 客观 CI 检查范围

The objective guard covers only invariants that can be decided without reading a sentence for
style. Its source scan and mutation fixtures call the same parsers and matchers. Every rule must
include a failing mutation and an accepted correction; a rule with an exception must also include
an accepted exception mutation.

客观检查只覆盖无需判断文风即可确定的约束。源码扫描与 mutation fixture 必须调用同一套
解析器和 matcher。每条规则都要同时提供会失败的 mutation 和可通过的修正;存在例外时,
还必须提供可通过的例外 mutation。

- App Router parity discovers every real `page.tsx`, removes route groups, and compares English
and `/zh` route patterns in both directions. Dynamic, noindex, hidden, and feature-gated pages are
included. API handlers, `feed.xml`, `llms*.txt`, sitemap handlers, and per-post OG renderers are
not page routes and therefore are not sibling candidates.
- Blog pairing is bidirectional and includes scheduled posts. Chinese posts continue to reuse the
English OG image. Paired content preserves fenced code, math blocks, `Figure` sources, protected
inline code, flags, identifiers, normalized unit multisets, link destinations, and JSON-LD
syntax/shape. Backtick and tilde fences of any legal length are recognized, as are inline-code
delimiters longer than one backtick. Markdown links and static MDX/JSX `href`/`src` props share
the same matcher; bare destinations with balanced parentheses, angle-bracket destinations,
reference definitions, and static string/template-literal props are all covered. Chinese
internal links may add `/zh` (including `/blog` → `/zh/blog`),
same-page heading fragments may be localized, and link order may change when paragraphs are
reorganized. JSX attribute expressions use a JSX-aware JavaScript tokenizer, so division,
regular-expression literals, comments, nested template interpolation, nested JSX or fragments,
comparisons, and arrow operators before a static link prop do not truncate the tag. Protected-token
counts are bidirectional: deleting or adding a flag, identifier, or unit occurrence fails. Protected
forms include throughput, FLOP,
bit-bandwidth, power-per-GPU, slash or hyphen GPU/chip-hour forms, and numeric cost-rate units used
by the real corpus. JSON-LD wrappers may contain legal whitespace and newlines, including in fenced
examples without leaking into prose scans. Their prose values may be translated; keys, types,
array shape, and the exact object/array paths of URLs, numbers, booleans, dates, flags, and
identifiers remain protected. GPU/chip-hour occurrence counts remain protected even when they
appear inside otherwise translatable JSON-LD prose.
- Every containing object with an explicit `en` or `zh` object literal must carry the other locale,
and the two objects must have the same statically declared nested key shape. Identifier and
quoted locale keys are treated alike. Computed keys and spread contents cannot be proven locally
and remain outside this structural check; their explicit assignment and shorthand sibling keys
are still checked. Temporary dictionary exceptions are consumed once, so one digest cannot hide
two identical object violations.
- A PR labeled `chinese-copy-only` runs the merge-base-aware English-byte check. English Blog MDX
is protected as a whole, while mixed TypeScript/TSX/JSON files protect raw `en` subtrees. The
repository-root pathspec works from the workflow's `packages/app` working directory, and
rename-aware base/head mapping preserves unchanged moves while still checking an edited renamed
file. Additions, copies, modifications, renames, deletions, and Git type changes are included.
The objective-guard exception manifest is locale-neutral plumbing rather than UI copy. The check
intentionally permits that plumbing and tests; remove the label when English copy is intentionally
part of the change.
- High-confidence copy rules remain limited to the documented `Chip` common-noun case,
prose-like hardcoded English labels beside a Chinese dictionary, redundant English-first
loanword translations, and unambiguous malformed punctuation such as whitespace before Chinese
terminal punctuation. TS/TSX string literals and JSX visible text, and MDX attributes and visible
prose, are scanned independently so one clean segment cannot hide another bad one. Units,
identifiers, and acronym/unit labels keep their documented English forms.

- App Router 路由检查会发现所有真实的 `page.tsx`,去掉 route group 后双向比对英文与
`/zh` 路由。动态、noindex、隐藏和 feature-gated 页面都在范围内。API handler、
`feed.xml`、`llms*.txt`、sitemap handler 和文章 OG renderer 不是页面路由,因此不要求
`/zh` sibling。
- Blog 文章双向配对,定时发布的文章也不例外;中文文章继续复用英文 OG 图。中英文配对
必须保留 fenced code、数学公式块、`Figure` 图片来源、受保护的 inline code、flag、
identifier、规范化后的单位多重集、链接目标,以及 JSON-LD 的语法和结构。检查器同时识别
任意合法长度的反引号或波浪线 fence,以及由多个反引号界定的 inline code;Markdown 链接
与 MDX/JSX 中静态的 `href`/`src` 属性共用同一 matcher;带平衡括号的普通 destination、
尖括号 destination、reference definition,以及静态字符串或 template literal 属性都在
检查范围内。中文站内链接可以添加 `/zh`
(包括 `/blog` → `/zh/blog`);页内标题锚点可以随中文标题本地化;段落重组时链接顺序可以
变化。JSX 属性表达式会使用支持 JSX 的 JavaScript tokenizer,因此静态链接属性前的除法、
正则表达式 literal、注释、嵌套 template interpolation、嵌套 JSX 或 fragment、比较和箭头
运算符都不会截断标签。受保护 token 会双向核对出现次数,增删任何 flag、identifier 或单位
都会失败;
实际语料中的吞吐量、FLOP、bit 带宽、每 GPU 功耗、斜杠或连字符形式的 GPU/chip-hour,
以及数值成本率等单位都会覆盖。JSON-LD wrapper 可以包含合法空白与换行;即使它位于 fenced
示例中,也不会泄漏到正文扫描。JSON-LD 中的说明性文案可以翻译,但 key、type、数组结构,
以及 URL、数字、boolean、日期、flag 和 identifier 所在的对象/数组路径必须保持不变;即使
GPU/chip-hour 出现在其他内容可翻译的 JSON-LD 文案中,其出现次数仍受保护。
- 只要一个 containing object 中显式声明了 `en` 或 `zh` object literal,就必须同时声明另一
个 locale,且两者的静态嵌套 key 结构一致;identifier key 与带引号的 locale key 采用相同
规则。computed key 和 spread 的内容无法在本地可靠推断,不纳入该结构检查;同一对象中的
显式 assignment 与 shorthand sibling key 仍会检查。临时字典例外按次消费,单个 digest
不能同时掩盖两个相同的对象错误。
- PR 添加 `chinese-copy-only` label 后,会运行基于 merge base 的英文逐字节检查。英文 Blog
MDX 整体受保护;混合语言 TypeScript、TSX 和 JSON 文件则保护原始 `en` subtree。路径检查
固定从仓库根目录解析,因此工作流在 `packages/app` 下执行也不会漏扫;rename-aware 的
base/head 映射允许内容不变的移动,同时仍会拦截重命名文件中的英文改动;新增、复制、修改、
重命名、删除和 Git type change 都会纳入。客观检查的例外 manifest 属于 locale-neutral
plumbing,而不是 UI 英文文案。测试和这类 plumbing 可以修改;如果 PR 本来就要改英文文案,
应移除该 label。
- 高置信度文案规则仅限于已记录的 `Chip` 普通名词、已有中文字典旁绕过本地化的英文说明
标签、English-first 重复翻译,以及中文句末标点前多余空格等无歧义错误。单位和 identifier
继续保留文档规定的英文写法;缩写和单位标签也明确豁免。TS/TSX 的字符串 literal 与 JSX
可见文本、MDX 属性与可见正文会分别扫描,避免一个正常片段掩盖另一个片段中的错误。

Temporary baseline exceptions live in
`packages/app/src/lib/zh-objective-guard-exceptions.json`. Every row must record a reason and an
explicit removal condition. Blog inline-code exceptions match exact English and Chinese spans;
each file's protected-token exception set is pinned to the SHA-256 digest of the complete paired
English/Chinese source, so a waiver cannot migrate to another occurrence after either post changes.
an empty side records one exact addition/removal rather than a wildcard, and duplicate occurrences
require duplicate rows. Dictionary exceptions fingerprint the exact mismatched key set. A stale,
broadened, or newly different exception fails its non-vacuity test.

临时 baseline 例外统一记录在
`packages/app/src/lib/zh-objective-guard-exceptions.json`。每条记录都必须说明原因和明确的
删除条件。Blog inline-code 例外精确匹配中英文两侧文本;每个文件的 protected-token 例外集合
还会绑定中英文完整源码对的 SHA-256 digest,因此任一文章变化后,豁免都不能迁移到其他位置。
空字符串一侧表示一次明确的新增或删除,并非通配符;重复出现的差异必须逐条记录。字典例外
对完整的 key 差异集合计算 fingerprint。
失效、扩大范围或内容发生变化的例外都会在非空验证中失败。

The deterministic guard never scores fluency, clause order, sentence structure, register,
contextual pronouns, marketing tone, or quotation voice. It also does not use English-token ratios
or a closed list to decide whether an established ML infrastructure term should remain English.
Those decisions stay with `review-zh-copy` and the Chinese maintainer.

确定性检查永远不评价流畅度、分句顺序、句法、语域、上下文相关的第二人称、营销语气或引用
者口吻,也不会通过英文 token 占比或封闭术语表来判断 ML 基础设施术语是否应保留英文。
这些判断继续由 `review-zh-copy` 和中文维护者完成。
5 changes: 4 additions & 1 deletion packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
"clean:all": "rimraf .next out cypress/videos cypress/screenshots coverage",
"cache:invalidate": "bun --env-file=../../.env scripts/invalidate-cache.ts",
"cache:warmup": "bun --env-file=../../.env scripts/warmup-cache.ts",
"capture:fixtures": "bun scripts/capture-cypress-fixtures.ts"
"capture:fixtures": "bun scripts/capture-cypress-fixtures.ts",
"guard:zh-copy:chinese-only": "bun scripts/check-zh-chinese-only.ts --chinese-only"
},
"dependencies": {
"@chenglou/pretext": "^0.0.8",
Expand Down Expand Up @@ -90,6 +91,8 @@
"@types/react-dom": "19.2.5",
"@types/three": "^0.185.4",
"@vitest/coverage-v8": "^4.1.11",
"acorn": "^8.16.0",
"acorn-jsx": "^5.3.2",
"axe-core": "^4.13.0",
"cypress": "15.21.1",
"cypress-axe": "^1.7.0",
Expand Down
109 changes: 109 additions & 0 deletions packages/app/scripts/check-zh-chinese-only.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
#!/usr/bin/env bun
import { execFileSync } from 'node:child_process';

import { compareEnglishSurfaces } from '../src/lib/zh-objective-guard';

interface Options {
readonly chineseOnly: boolean;
readonly base: string;
readonly head: string;
}

function optionValue(args: readonly string[], name: string): string | undefined {
const index = args.indexOf(name);
return index === -1 ? undefined : args[index + 1];
}

function parseOptions(args: readonly string[]): Options {
const chineseOnly = args.includes('--chinese-only');
const base = optionValue(args, '--base') ?? process.env.ZH_GUARD_BASE_SHA ?? '';
const head = optionValue(args, '--head') ?? process.env.ZH_GUARD_HEAD_SHA ?? '';
if (!chineseOnly || !base || !head) {
throw new Error(
'Usage: check-zh-chinese-only.ts --chinese-only --base <base-sha> --head <head-sha>',
);
}
return { chineseOnly, base, head };
}

function git(args: readonly string[]): string {
return execFileSync('git', args, { encoding: 'utf8' }).trim();
}

function sourceAt(revision: string, file: string): string {
if (!file) return '';
try {
return execFileSync('git', ['show', `${revision}:${file}`], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore'],
});
} catch {
return '';
}
}

interface ChangedFile {
readonly baseFile: string;
readonly headFile: string;
}

function changedFiles(base: string, head: string): ChangedFile[] {
const raw = execFileSync(
'git',
[
'diff',
'--name-status',
'-z',
'--find-renames',
'--diff-filter=ACMRTD',
`${base}...${head}`,
'--',
':(top)packages/app',
],
{ encoding: 'utf8' },
);
const fields = raw.split('\0');
if (fields.at(-1) === '') fields.pop();
const changed: ChangedFile[] = [];
for (let index = 0; index < fields.length;) {
const status = fields[index++];
if (status.startsWith('R') || status.startsWith('C')) {
changed.push({ baseFile: fields[index++], headFile: fields[index++] });
continue;
}
const file = fields[index++];
changed.push({
baseFile: status === 'A' ? '' : file,
headFile: status === 'D' ? '' : file,
});
}
return changed;
}

const options = parseOptions(process.argv.slice(2));
const mergeBase = git(['merge-base', options.base, options.head]);
const files = changedFiles(mergeBase, options.head);
const violations = files.flatMap(({ baseFile, headFile }) => {
const baseSource = sourceAt(mergeBase, baseFile);
const headSource = sourceAt(options.head, headFile);
const preferredFile = headFile || baseFile;
const preferred = compareEnglishSurfaces(preferredFile, baseSource, headSource);
if (preferred.length > 0 || baseFile === headFile || !baseFile) return preferred;
return compareEnglishSurfaces(baseFile, baseSource, headSource);
});

if (violations.length > 0) {
console.error(
[
'Chinese-only mode found changed English bytes:',
...violations.map((violation) => ` ${violation.file}: ${violation.detail}`),
'',
'Remove the chinese-copy-only label when the PR intentionally changes English copy.',
].join('\n'),
);
process.exitCode = 1;
} else {
console.log(
`Chinese-only English-byte guard passed for ${files.length} changed app file(s) against merge base ${mergeBase}.`,
);
}
Loading
Loading