搜索/打造不丢失用户数据、不运行未批准代码的命令行工具

打造不丢失用户数据、不运行未批准代码的命令行工具

让AI开发或修改已有用户的CLI工具时使用。保持参数与配置兼容,宁可报错停止也不破坏用户工作,项目钩子只在批准后运行,所有子进程都经过统一的追踪路径,并先显示本地结果再加载网络数据。

要使用这个AI规则
## 工具成长时,保持配置文件和命令行参数兼容 一旦工具拥有了真实用户,就需要在简洁设计与兼容性之间做权衡。重新设计本身并不能成为破坏用户使用体验的理由。 ### 每一次对外部接口的破坏都要有正当理由 修改对外接口需要带来具体的、用户能感知到的改进,而不仅仅是代码清理。相比于悄无声息地破坏兼容性,更应优先给出弃用警告和迁移路径。 ### 明确需要保护的接口范围 明确哪些接口属于契约(配置文件格式、命令行标志与参数),哪些可以自由变动(内部API、日志位置、面向人类阅读的输出格式)。对前者要严格守护,对后者保持灵活,这样重构的成本才能保持在低位。 ### 界定支持窗口 明确声明所支持的最低运行时或工具链版本,并按照可预测的维护节奏来推进版本升级,而不是随意为之。 ## 每个概念只用一个词,在文档、帮助和注释中统一 为每个概念选定一个术语,并在文档、帮助文本和代码注释中始终如一地使用它。用词的漂移会在不知不觉中让用户和维护者都感到困惑。 ### 把术语表写下来 维护一份简短的清单,固定每个词的含义,并列出应当避免使用的近义词。要明确区分容易混淆的概念,例如原始检出与次要检出的区别,或者操作的目标与操作所运行的工作区之间的区别。 ### 绝不给关键术语叠加含义 如果一个词已经用来指代某个概念,就不要把它挪用去指代另一个概念;即使在某个句子里重复使用读起来很自然,也应该为第二个概念另选一个词。 ## 改变行为时同步修改帮助文本,并用测试检查文档是否一致 每一次行为变更都应该在同一次修改中一并更新文档。可以用这个标准来检验:帮助文本描述的内容,是否仍然和代码的实际行为一致? ### 每个事实只保留一个权威来源 对于自动生成的文档,应修改其权威来源(嵌入在代码中的帮助文本和命令定义),然后重新生成渲染后的镜像文件,绝不要手工编辑生成出来的文件。如果某个目录里同时存放着生成的镜像文件和手工撰写的页面,在编辑之前要先确认这个文件到底扮演哪种角色。 ### 用测试来强制保持同步 一个对比渲染输出与源文件的文档同步测试,可以机械化地捕捉到二者之间的偏差。修改帮助文本也会改变渲染出来的快照,因此要在同一次改动中重新生成并检查这些快照,以保持测试通过。 ## 未经同意不破坏用户文件,宁可报错停止 把用户数据当作神圣不可侵犯的东西来对待。一个失败但保住了用户工作成果的命令,胜过一个悄悄销毁了成果却"成功"的命令。 ### 宁可失败,也不要悄悄丢失数据 如果某个操作可能会销毁未跟踪的文件、未提交的更改或其他用户数据,就应该报错并中止,而不是继续执行。 ### 销毁操作必须经过明确同意 诸如强制删除数据或覆盖之类的破坏性操作,只有在用户明确要求时才能执行。绝不能把删除或覆盖作为某个不相关操作的附带效果;应该把清理设计成一个由用户自行选择的独立步骤。 ### 优先选择会失败的那个版本,留意"检查到使用"之间的空隙 当一个操作存在两种形式时,应选择在发生冲突时会报错的那一种,而不是强行执行的那一种。当安全检查与它所保护的动作之间存在时间差时,要保持谨慎:状态可能在这段时间窗口内发生变化,因此在清理过程中不要强行执行。 ## 所有子进程都经过同一个记录日志和耗时的包装器 让每一个外部命令都通过同一个执行封装器发出,而不是直接调用进程API。统一路径能顺带带来一致的调试日志和计时信息,并且只需要在一个地方就能修改行为。 ### 一个封装器,一个发出者 把追踪语法只保留在一个组件里,并让它成为底层日志调用的唯一写入者,这样一来,一个子进程要么经过这道关卡被追踪到,要么完全不产生任何记录。 ### 新的启动点需要主动接入 有少数启动进程的场景无法套用这个封装器,需要手动构造追踪记录:在启动前开始记录,成功时完成记录,出错时标记失败。要把这个守卫值设计成"必须使用"的类型,如果未处理就被丢弃,会触发断言失败,这样一来,遗漏的完成记录会导致测试失败,而不是悄无声息地失去追踪。分离出去的后台子进程和交互式辅助进程是文档中明确记录的不受追踪的例外情况。 ### 清理继承来的探测型环境变量 有些环境变量会在子进程查询自己的工作目录之前就被解析,因此一个继承来的值可能会悄悄覆盖掉你所选定的目录。任何把用户命令重新定位到工具所选目录中执行的启动点,都必须清除这些变量;而那些在用户自身上下文中运行的子进程,则保留继承来的上下文,只是将其转换为绝对路径。 ## 依据退出码和JSON输出判断,而不是消息文本 相比解析面向人类阅读的消息文本,更应优先使用退出码、机器可读的状态和结构化格式——因为文本消息一旦遇到语言环境、版本或措辞的变化就会失效。 ### 选择结构化的信息通道 读取面向脚本的(porcelain)或JSON格式的输出以及数字状态标志,而不是默认的、面向人类阅读的输出。如果一个命令把含义编码进了退出码,就应该依据退出码来分支判断,而不是依据消息文本。 ### 记录下无法避免的脆弱之处 当不存在结构化的替代方案、必须解析文本时,要在解析代码的位置就地标注这种脆弱性,让下一个阅读代码的人知道这是一个已知的风险点,而不是一份稳定的契约。 ## 先用本地数据显示,再逐步加载网络信息 响应速度本身就是一项功能:先用本地数据完成绘制,再让较慢的详细信息随后陆续到达。 ### 绝不能让网络阻塞首帧渲染 只有在用户的请求确实需要时才访问网络。每个命令都应该先用本地数据渲染出第一版输出,再逐步以流式方式补充来自网络的详细信息;一个必须等到远程调用返回之后才能绘制出首帧的命令,就是一种失败模式,在全新安装的环境下表现尤其糟糕。像shell提示符这样的同步热路径,完全不应该触及网络,哪怕是以渐进的方式。 ### 实时远程数据优先放在受用户操作约束的场景中获取 逐行以流式方式输出命令结果,而不是先缓冲起来。交互式选择器是承载网络请求最宽容的场所:它能立即完成绘制,如果用户在一个较慢的调用返回之前就已经采取了操作,这个请求就会被直接放弃。而一个"运行到完成"型的命令,必须等到每一个调用都返回之后才能结束,所以应该在选择器中以渐进的方式获取实时远程数据。 ## 用户中断时,停止正在运行命令的所有循环 当一个子进程因信号(中断或终止)而终止时,前台执行路径上的每一个循环都必须中止,而不是继续推进到下一次迭代。 ### 读取结构化的信号通道 应该通过结构化的信号字段来检测由信号引发的退出,而绝不能通过嗅探大于等于128的退出码或者解析错误文本来判断。要向外传播一个规范化的"被中断退出"结果,并终止循环。 ### 在任何吞掉错误的分支之前先做检查 要在任何失败处理策略运行之前先做中断检查,这样"警告并继续"这类策略就不会把中断信号吞掉。任何新写的、对子进程进行循环处理的代码,都要在每一次迭代中执行这项检查;否则针对一个多步骤操作发出的一次中断,会被直接无视,继续执行完剩下的所有步骤。 ## 项目定义的钩子和别名只在批准后运行 项目自定义命令(生命周期钩子、别名、从项目配置中提取出来的可执行体)本质上是随代码仓库一同分发的任意代码,而用户可能刚刚才克隆了这个仓库。这些命令必须先经过审批系统的放行,才能被执行。 ### 这道关卡就是整个安全边界 绝不要构建出一条能够绕过这道关卡、直接运行项目自定义命令的代码路径。"既然我们已经验证过这个操作了,那就顺便也运行它的钩子吧"——这种走捷径的想法,会让每一个涉及项目配置的命令都变成远程代码执行的入口;这道关卡是"克隆一个仓库"和"某个钩子运行了一段从网上直接下载执行的脚本"之间唯一的屏障。 ### 一次性、不可变地选定已批准的命令集合 在做出决策的那个节点,把命令集合固化成一份不可变的、已获批准的计划,并且只让执行器消费这一份数据,不让它持有任何可以重新推导出命令集合的信息,这样一来,重新选中一个未获批准的命令就会变成编译错误,而不是需要靠代码评审才能发现的问题。在无法弹出提示的上下文中,只能以只读方式查询审批状态,并且只运行其中已经获批的那部分子集。 ## 覆盖率检查失败时,未经用户同意不合并 绝不要在覆盖率关卡未通过、且未获得用户明确批准的情况下合并改动,即便平台把这道关卡标记为"非必需"也不例外,因为它在实际操作中依然具有阻塞效力。 ### 弄清楚这个信号是如何上报的 覆盖率工具可能会以检查运行(check run)而不是提交状态(commit status)的形式发布结果,这需要通过不同的API去读取;一次综合状态查询完全有可能漏掉某个检查运行,而且结果可能要在任务完成后几分钟才会到达。在断定一次改动已经通过之前,要用正确的API去轮询确认。 ### 补上缺口,而不是放任它过关 一旦覆盖率检查失败,就要去调查并补上缺口:要么补充测试,要么删除未被使用的代码,包括那些完全可以直接落入通用路径处理、因而没有存在必要的特例处理分支。如果你认为这是一次误报,应该在合并之前询问用户,而不是自行单方面做决定。运行覆盖率检查时要使用与CI流水线相同的功能开关组合,这样就不能以"某个功能当时是关闭的"为借口来搪塞一次失败。 ## 复用现有依赖,修复警告而不是屏蔽 在自己动手实现一个工具函数之前,先检查一下现有的依赖库。一个久经考验的库——用于路径归一化、shell转义、着色或模板变量检测——胜过一个处理不好边界情况的临时实现。 ### 不要用表面功夫掩盖警告 绝不要靠一刀切的方式去压制警告、让它消失。要么删除掉那段死代码,要么留下一个带日期、有说明的标记,把它和具体的后续工作关联起来。 ### 就地记录复杂系统 对于一个不那么直观的系统(比如状态机、缓存状态、跨模块协调、微妙的失效逻辑),要配备一份模块级别的说明文档,涵盖其目的、关键决策、契约和不变量,并保持这份文档随时更新。 ### 让测试代码远离库代码 不要给库里的类型添加仅供测试使用的便捷方法;测试应该调用真实的API,或者自己定义辅助函数。对于多行字面量,优先使用真正嵌入的换行符,而不是那种会悄悄剥离空白字符的续行转义符。 ## 给错误附上上下文,不把它变成崩溃 每一个错误都要携带上下文信息。对于预期中的失败状态(比如工作区不干净、分支缺失、状态无效),要使用明确的业务逻辑错误;对于I/O和外部命令的失败,则要用上下文包装。 ### 绝不能吞掉一次失败 在一个返回错误类型的函数中,绝不要使用那种"值不存在就panic"或"unwrap风格"的调用方式,把一个本可恢复的失败变成程序崩溃;而应该用错误传播操作符向外传播,或者抛出、返回一个明确的错误。每一个被包装过的错误,都应该让读到它的人知道究竟是哪个操作失败了,以及失败的原因是什么。 ## 迁移旧的配置键,不悄悄丢弃 把所有配置兼容性处理都集中放在一个层里:在解析器看到它们之前,先把已弃用的模式改写成规范形式。绝不要悄悄丢弃一个旧的配置键,因为那本质上是一次悄无声息的行为变更;应该改为迁移它。 ### 用同一个判定函数来做检测和迁移 让一个幂等函数同时完成两件事:既改写掉已弃用的模式,也报告它究竟改动了什么,这样检测和迁移就共享同一个判定逻辑,不会出现偏差。检测时把这些相同的函数应用到一份草稿副本上,这样警告只会在一次真正落地的更新会改变文件内容时才会触发;并用一个测试把这种等价关系固定下来。 ### 为每条规则的执行时机分类 针对每一条规则,决定改写应该在什么时机生效:每次加载时都执行、只在一次明确的更新操作中执行、静默执行不发出警告,还是作为一个"待生效默认值"、留到后续版本才采用。对于那些无法被安全改写的值(格式错误,或者目标键已经被占用),保持原样不动,让解析器自身的类型错误去承载相应的提示信息。 ## 用访问器名称的前缀表明开销,并决定缓存放在哪里 把意图编码进访问器的名字里,这样调用者仅凭前缀就能预判出调用成本和可能的失败方式。 ### 让前缀承载契约 采用一套一致的命名方案:一个不带前缀的名词,表示返回一个值、没有副作用、可能会被缓存;以set为前缀的名字,表示会写入状态、可能报错;以require为前缀的名字,表示在值不存在时会报错;以fetch为前缀的名字,表示会执行网络I/O;以load为前缀的名字,表示会执行文件I/O。如果一个不带前缀的名词本身读起来就已经足够清晰,就要避免再多此一举地加上多余的get前缀。 ### 决定缓存状态应该存放在哪里 通过一个基于引用计数、克隆时会共享同一份数据的缓存,来共享只读的缓存值。根据成本和访问热度来选择存放层级:成本低、访问频繁的值采用内存中的"取或创建"方式;成本高的值持久化到磁盘;两者兼具的值,则在持久化的后端之上再加一层内存前端。要记录清楚哪些内容会被缓存、哪些不会,以及在发生变更之后刷新缓存所依据的不变量。