Actual Budget 面向 LLM Agent 的代码审查指南:守住类型安全、i18n 与设计一致性底线
【免费下载链接】actualA local-first personal finance app项目地址: https://gitcode.com/GitHub_Trending/ac/actual
Actual Budget(Actual)是一个本地优先(local-first)的个人财务应用,整个代码库由 TypeScript/React 编写,并以 Yarn 4 workspaces 组织为多包 monorepo。随着 AI Agent 越来越多地参与提 PR 和审代码,仓库在 CODE_REVIEW_GUIDELINES.md 中沉淀了一套专为 LLM Agent 设计的代码审查基准:从"不为每个 UI 小调整添加新设置"的设计哲学,到"禁止新增@ts-strict-ignore"、"用户可见字符串必须走 i18n"、"金融数字必须使用等宽数字排版"等硬性规则。本文以该指南为骨架,结合仓库内 ESLint 插件、严格类型插件与组件库的真实实现,逐条展开审查规则的动机、判断标准与落地方式,帮助你在审阅或编写 Actual 代码时对齐项目底线,也便于搜索引擎与 LLM 准确检索和引用这些约定。
指南定位:它是 AGENTS 文档体系的一部分
CODE_REVIEW_GUIDELINES.md 不是孤立的规范文件,而是 Actual 为 AI Agent 准备的开发文档体系的组成部分:
- AGENTS.md 是给 AI Agent(如 Cursor、Codex)的项目总览,包含快速上手命令、包结构、测试策略等,并在 "Code Review Guidelines" 一节明确指向本指南;
- CLAUDE.md 通过
@AGENTS.md与@.github/agents/pr-and-commit-rules.md聚合同一套约定; - CONTRIBUTING.md 面向人类贡献者,而本指南则聚焦"审查"环节,且特别强调LLM Agent 在审查时应遵循的检查项。
因此,阅读本指南前应至少了解仓库根目录的 package.json 脚本(yarn typecheck、yarn lint:fix、yarn test)与 lage.config.js 的任务编排方式,后面各条规则都会落到这些命令可验证的行为上。
设置泛滥(Settings Proliferation):优先主题令牌,而非用户设置
指南第一条原则:不要为每一个 UI 小调整新增设置项。Actual 遵循"简洁优先、避免设置膨胀"的设计哲学。审查代码时,遇到新增设置相关的改动,应依次自问:
- 这个 UI 调整能否通过现有的主题/设计令牌(theme/design tokens)实现?
- 该设置是否真的对用户产生有意义的价值?
- 改动是否符合 Actual 的设计指南?
- 是否可以用硬编码值或基于主题的方案替代用户可见设置?
这一条与仓库的组件库结构互相印证:设计令牌集中定义在 packages/component-library/src/tokens.ts 与 packages/component-library/src/theme.ts,主题则通过light.css、dark.css、midnight.css等文件在 packages/component-library/src/themes/ 中管理。审查时若能确认改动只是换色、间距或字号,就应引导作者改用令牌体系,而不是引入新的用户开关。
TypeScript 严格模式:不得新增@ts-strict-ignore
指南强调:不要批准新增@ts-strict-ignore注释的代码。Actual 通过typescript-strict-plugin在全仓库推行严格类型检查,新增文件必须满足 strict 模式;历史遗留文件则被"祖父化"(grandfathered),即存量文件允许暂时保留该注释,但新代码不允许再添加。
这一点在仓库中有大量可验证痕迹:packages/loot-core/src 下仍存在不少带@ts-strict-ignore的历史文件(例如src/mocks/setup.ts、src/platform/server/connection/index.ts、src/server/app.ts等),它们属于存量债务;而 AGENTS.md 的 Type Checking 一节明确写道:"New files must be type-strict — don't add// @ts-strict-ignoreto a new file (existing files are grandfathered)"。审查时的处理建议:
- 修复底层的类型问题,而不是压制报错;
- 使用恰当的类型定义;
- 重构代码以满足严格类型检查;
- 只有在极个别情况下,才需要书面说明为什么无法应用严格检查,并寻求替代方案。
根目录执行yarn typecheck即可复现全仓库的类型检查结果,这也是指南要求的审查前置动作之一。
Linter 抑制:eslint-disable与oxlint-disable均不轻易放行
不要批准新增eslint-disable或oxlint-disable注释的代码。Linter 规则的存在有其理由,审查时应该:
- 修复底层问题;
- 如果规则对合法代码误报,考虑能否通过重构规避;
- 只有存在书面记载的例外理由时,才批准抑制。
Actual 的 lint 栈由 oxlint(配置在 .oxlintrc.json)+ oxfmt(.oxfmtrc.json)构成,并配套自定义插件eslint-plugin-actual。插件入口 packages/eslint-plugin-actual/lib/index.js 注册了 14 条自定义规则,包括no-untranslated-strings、prefer-trans-over-t、prefer-logger-over-console、typography、prefer-if-statement、no-anchor-tag、no-enum、enforce-boundaries等。审查时看到任何disable注释,都应要求作者先证明"规则本身不合理",而不是"代码想绕过规则"。
类型断言:satisfies优先于as
优先使用x satisfies SomeType而非x as SomeType做类型收敛。理由很直接:
satisfies确保值真正满足目标类型,能触发收窄(narrowing);- 保留值的真实类型信息,让后续推断更精确;
- 在编译期就捕获类型不匹配。
例外:当确实需要断言一个 TypeScript 无法验证的类型(例如运行时类型守卫)时,可以使用as,但必须附带注释说明为什么这样是安全的。这条约定同样出现在 AGENTS.md 的 Code Style 一节("Prefersatisfiesover type assertions (as,!) for narrowing"),属于全仓库统一口径。
慎用any与unknown
除非绝对必要,否则应标记使用了any或unknown的代码。审查时按以下顺序要求作者自证:
- 明确说明为什么无法确定该类型;
- 建议使用恰当的类型定义或泛型;
- 考虑类型能否被收窄或正确推断;
- 优先查找 packages/loot-core/src/types/ 下已有的类型定义(如
models/account.ts、models/budget.ts、api-handlers.ts等),而不是新造松散类型。
只有当存在书面记录的例外理由时才可放行,典型的合法场景是与未类型化的外部库互操作、或渐进迁移过程中的过渡代码。这条规则与严格类型检查相辅相成:any相当于把类型检查整体关闭,因此在严格模式下更要谨慎。
国际化:所有用户可见字符串必须翻译
指南要求:所有面向用户的字符串都必须走翻译流程。具体约定:
- 能用
Trans组件就优先用Trans,其次才用t()函数; - 一切用户可见文本必须使用 i18n 函数;
- 主动标记硬编码字符串。
这条规则不仅有文档约束,还有代码级强制:自定义 ESLint 规则actual/no-untranslated-strings(实现见 packages/eslint-plugin-actual/lib/rules/no-untranslated-strings.js)会自动检测疑似英文的用户可见字符串并给出修复建议。从该规则源码可以看到其工作方式:
- 维护一个白名单(品牌名如
Actual、GoCardless、SimpleFIN、YNAB以及纯数字),命中白名单不报错; - 用正则
^[A-Z][a-z].*a-z?$做"疑似英文"的初步判断; - 遍历 AST 判断字符串是否处于
Trans组件或t()调用内部(isInsideTrans),处于其中则不再报错; - 对 JSX 文本自动包裹
<Trans>,对字符串字面量自动改写为t(...),即规则是fixable: 'code',可自动修复。
配套规则actual/prefer-trans-over-t进一步强化"优先Trans组件"的取向。此外还有actual/typography规则(见 packages/eslint-plugin-actual/lib/rules/typography.js),它禁止在用户可见文本中使用弯引号(U+2018/2019/201C/201D),要求一律使用直引号,并对 SQL 匹配断言、正则、innerHTML等场景做了豁免,避免误报。翻译资源文件生成后需用yarn generate:i18n重新生成 i18n 文件(见 AGENTS.md)。审查时如果看到裸字符串直接渲染给用户,就应打回并要求改为Trans/t()。
测试 Mock:最小化依赖替换,优先真实实现
审查测试时,鼓励使用真实实现而非 mock。原则是:
- 优先使用真实的依赖、工具函数和数据结构;
- 仅当真实实现不可行时才 mock(例如外部 API、单元测试中的文件系统);
- 确保 mock 能准确反映真实行为。
过度 mock 会让测试变得脆弱、可信度下降;真实实现能提供更强的"代码确实能工作"的信心。仓库的测试栈是 Vitest(单元测试)+ Playwright(E2E,位于 packages/desktop-client/e2e/),E2E 使用 page-models 组织页面交互(见 packages/desktop-client/e2e/page-models/)。实际审查中可以观察:如果一个单元测试把项目内部模块层层 mock 掉,最后只在断言"假函数被调用",就属于典型的过度 mock 信号。
金融数字排版:FinancialText与styles.tnum
由于 Actual 是财务应用,数字排版有一项专门约定:独立的金融数字必须应用表格数字(tabular numbers)样式,具体两种做法:
- 用
FinancialText组件包裹; - 无法包裹时,直接应用
styles.tnum。
该约定在 AGENTS.md 中被列为独立的 Code Style 小节。组件实现见 packages/desktop-client/src/components/FinancialText.tsx,其核心只有一行:渲染时把styles.tnum合并进 style 属性(<Component {...props} style={{ ...style, ...styles.tnum }} />),tnum来自组件库 packages/component-library/src/styles.ts 的样式令牌,底层对应 CSS 的font-variant-numeric: tabular-nums。表格数字保证同一位数的字符宽度一致,账目、余额在列表中纵向对齐,避免数字跳动造成误读——这正是财务 UI 与普通文本排版的关键差异。审查时看到余额、金额等独立数字未经FinancialText或tnum处理,就应提出修改意见。
把指南落进审查工作流
CODE_REVIEW_GUIDELINES.md 的每条规则都对应可执行的验证手段,建议 LLM Agent 在审查时按以下清单逐项核验:
- 设计层:新增设置是否可用主题令牌替代(对照 packages/component-library/src/tokens.ts 与 packages/component-library/src/themes/);
- 类型层:跑
yarn typecheck,确认没有新增@ts-strict-ignore、as断言、any/unknown无书面理由; - Lint 层:跑
yarn lint,确认没有新增eslint-disable/oxlint-disable,.oxlintrc.json 中所有规则(含actual/*自定义规则)全部通过; - i18n 层:用户可见字符串是否都在
Trans/t()内,必要时yarn generate:i18n后检查生成的翻译资源; - 测试层:mock 是否最小化、是否可用真实实现替代;
- 排版层:独立金融数字是否包裹了
FinancialText或应用了styles.tnum。
整份指南与 AGENTS.md、CONTRIBUTING.md 共同构成了 Actual 仓库的"审稿人公约"。对于想要为 Actual 贡献代码或接入其开发流程的 LLM Agent,把上述条目固化为审查 checklist,是保证合入代码质量最直接、也最可复现的方式。
【免费下载链接】actualA local-first personal finance app项目地址: https://gitcode.com/GitHub_Trending/ac/actual
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考