# spec:增强健壮性 + 重构代码 + 重组结构 + 微软规范落地 Status: accepted Date: 2026-09-26 本文件是这次改造的**唯一验收锚点**。每一项改动都要能被这里的某一条门槛判成通过或失败;判不了的就不要做。 ## 1. 四件事 1. **增强健壮性** —— 修掉实测确认的缺陷,并把「支持 5.1」从纸面承诺变成可验证的事实。 2. **重构代码** —— 消除重复实现、死代码与闭包依赖,让两个入口只做编排。 3. **重组结构** —— 让文件系统与职责对齐,并落一份可读的模块导出面。 4. **按微软规范格式化代码与文档** —— 落到可重复执行的工具上,而不是一次性手工活。 ## 2. 验收标准 | # | 门槛 | 改造前 | 目标 | | --- | --- | --- | --- | | 1 | Pester 套件 | 175 通过 / 0 失败 | 全绿(覆盖不得减少) | | 2 | 零依赖套件(瘦身后为冒烟) | 101 通过 / 0 失败 | 全绿 | | 3 | 全仓 `Parser::ParseFile`(5.1 与 7) | 5.1 上 6/6 文件失败 | **两版都零错误** | | 4 | `tests/Run-E2E.ps1`(36 项) | 只在 7.x 上跑过 | **7 与 5.1 都全绿** | | 5 | PSScriptAnalyzer 默认规则集 | 未安装 | `-Severity Warning,Error` 下 0 条 | | 6 | PSScriptAnalyzer 格式化规则集 | 无 | 0 条 | | 7 | 只读冒烟 | 未系统跑过 | `Backup.ps1 -DryRun` 与 `Restore.ps1 -VerifyOnly` 在真实清单上跑通,**一个字节都不写** | | 8 | markdownlint(可选档,不进必绿门槛) | 无 | 配置就位,0 error | | 9 | 术语表与决策记录 | 无 | `CONTEXT.md` + `docs/adr/` 6 条 | 第 3、4 条是新增的:改造前这两条根本不存在,而 README 却承诺了 5.1。 ## 3. 已确认的决策 ### 3.1 过程 - **D1** 基线与分支:先提交当前工作区为基线,再从基线开 `refactor/ms-conventions`。 - **D2** 过程产物:本文件即 spec;不为四条工作流各开 ticket。 - **D3** 行为边界:重构类改动保持行为等价;健壮性类改动可改变**边界行为**,但对外契约只增不改(参数名、退出码语义、配置字段、清单语法、归档布局一律向后兼容)。 ### 3.2 结构 - **D4** 库代码形态:`BakNRet/` 目录承载模块 —— `BakNRet.psd1` + `BakNRet.psm1`(薄加载器,按显式顺序点源)+ `Public/` + `Private/`,**一函数一文件,文件名 = 函数名**。 - **D5** 入口与配置原地不动:`Backup.ps1`、`Restore.ps1`、`BackupList.txt`、`SoftwareCatalog.psd1`、`BackupConfig.psd1` 全部留在仓库根。 - **D6** `src/` 不用。实测证据:纯 PowerShell 且无构建步骤的仓库里,用 `src/` 的为 0。 - **D7** 公共面:`.psd1` 写**显式** `FunctionsToExport` 白名单(官方要求,且 `PSUseToExportFieldsInManifest` 恒开不可关);私有件靠 `Private/` + 不导出双保险。 - **D8** 测试拿私有函数:用 Pester 的 `InModuleScope`(已实测在 5.1 与 7 上都可用)。**不**让测试点源 `Private/*.ps1` —— 那会把模块级状态建在测试脚本自己的作用域里。 - **D9** 入口沉到只做编排:`Backup.ps1` / `Restore.ps1` 里的逻辑(压缩→校验→原子替换、manifest 条目读写、解压三分支、孤档审计、7z 校验、工具定位)全部下沉进模块,**分两个提交**:先纯搬家,再参数化。 - **D10** 闭包依赖参数化:`Save-ItemRecord` 不再直接改脚本级 `$manifest`;`Invoke-BackupItem` 不再读脚本级 `$tool` / `$password`;`Test-ItemSelected` 不再读 `$Only` / `$Skip`。 - **D11** 模块级状态收敛:5 个 `$script:` 变量集中到 `Private/State.ps1`,**访问器只覆盖"写"**,并补一个测试用的状态重置函数。 ### 3.3 命名 - **D12** 公共面函数补 `BakNRet` 前缀;私有面保留短名。动词已 63/63 全部是批准动词,动词不动。 - **D13** `Set-Alias` 过渡垫片不留。 - **D14** 产品名统一写作 `BakNRet`(见 `CONTEXT.md`)。 ### 3.4 声明与编码 - **D15** 可拼接性不变量(三条,写成测试):每个私有文件不写自己的 `param()` / `using` / `Set-StrictMode` / `Export-ModuleMember`;私有文件不引用 `$PSScriptRoot` 或相对路径;加载顺序只在加载器里出现一次。 - **D16** `tools/Build-BakNRetModule.ps1`:按加载器声明的同序拼接各文件,产出 `dist/BakNRet.psm1`(gitignore);不引入 ModuleBuilder/Sampler。 - **D17** 源文件一律 **UTF-8 with BOM**;`.editorconfig` 用 `charset = utf-8-bom` 把这条锁住。 - **D18** `.gitattributes` 用 `* text=auto eol=lf` 加二进制规则(照 PSScriptAnalyzer 的写法)。 - **D19** 版本声明:`#Requires -Version 5.1`;**绝不**写 `#Requires -PSEdition`。`.psd1` 写 `CompatiblePSEditions = @('Desktop','Core')`。 - **D20** `Set-StrictMode -Version 3.0`(不用 `Latest`,双版本要的是确定性),先在库模块内开,跑绿后再考虑入口。 ### 3.5 静态分析与文档 - **D21** PSScriptAnalyzer 装进 `.tools/`(不动机器上的全局模块);`PSScriptAnalyzerSettings.psd1` 放仓库根;`tools/Invoke-Analyzer.ps1` 是唯一入口;**独立门禁脚本,不塞进 Pester 用例**。 - **D22** 全局 `ExcludeRules` 排除 `PSAvoidUsingWriteHost`(彩色控制台输出是本工具的刻意设计)。 - **D23** `PSUseSingularNouns` 用 `NounAllowList` 放行 `SecurityRecords` / `MapKeys` / `ArchiveTopLevelNames`。 - **D24** `Backup.ps1` 补 `SupportsShouldProcess`(新增 `-WhatIf` / `-Confirm`,属只增不改)。 - **D25** 启用 `PSAvoidLongLines`,上限 120,全仓折断。 - **D26** 注释帮助:公共面补齐 `.SYNOPSIS` / `.DESCRIPTION` / `.PARAMETER` / `.OUTPUTS` / `.EXAMPLE`;私有面只要求前两个。 - **D27** 文档拆分:README 瘦身成「是什么 + 快速开始 + 命令速查 + 指向 `docs/`」;11 篇主题进 `docs/`;「相对旧版修了什么」抽成 `CHANGELOG.md`;**`known-limits` 留在 README**。 - **D28** 6 条 ADR,不拆更细。 - **D29** markdownlint 用 `default: true`,行长放宽到 240(照 PowerShell 仓库的实践),只作可选检查。 - **D30** 测试入口:根级 `test.ps1` 作为唯一入口(`-Suite` 与 `-PSVersion` 参数);`tests/` 保留 Pester 的 `*.Tests.ps1` 命名,端到端降级为 `tests/E2E.ps1` 与 `tests/Drill.ps1`。 ### 3.6 明确不做 - 不搬三个配置文件(D5)。 - 不把库发布到 PSGallery,不引入 ModuleBuilder / Sampler(D16)。 - 不留旧函数名的 `Set-Alias` 垫片(D13)。 - 不为 manifest 写一次性迁移脚本 —— 它是运行产物,下次运行自然老化。 - 不补一批特征化测试再动手 —— 现有 276 个断言足够,新增断言按「修一个补一个」加。 - 不自动搬 `baknret.key`。搬密钥是使用者的动作,本改造只负责停止往仓库里放默认路径。 ## 4. 执行顺序 每一步独立提交,每一步都跑一遍全套验证。 1. `chore` 基线提交(含 `.gitignore` 加 `*.key`) 2. `fix` 源文件改存 UTF-8 with BOM + 立起 5.1 验收门槛 3. `fix` 暂存目录泄漏 —— 全仓唯一会把 junction 留在真实数据上的缺陷 4. `docs` `CONTEXT.md` 术语表 5. `refactor` `Common.psm1` → `BakNRet/{Public,Private}`,纯搬家 + 拼接校验 6. `refactor` 入口逻辑下沉 + 闭包依赖参数化 7. `refactor` 改名 + 模块清单显式导出面 8. `fix` 其余 P0(口令进日志 / manifest 原子写 / 空间守卫 / 并发保护) 9. `style` 格式化规则集 + 机械重排,再逐条修静态分析告警 10. `docs` README 拆分 + `CHANGELOG.md` + 6 条 ADR + `test.ps1` / `Invoke-Analyzer.ps1` + 门禁 顺序的三条依据:**先让 5.1 能解析**(否则后续所有验证都少一半);**先修唯一会伤数据的那一项**;**把可证明无损的结构搬家放在行为修复之前**(搬家用拼接校验证明零变化,之后每个修复都是小文件里的几行 diff,各自独立可回滚)。 ## 5. 风险与回滚 | 风险 | 控制 | | --- | --- | | BOM 改造后 5.1 暴露新问题 | 单独一个提交,改完立刻跑 5.1 验收;有问题就在这一个提交上回退 | | 结构搬家引入行为变化 | 用「同序拼接 == 模块内容」逐文件校验;先纯搬家、函数体一行不改 | | 改名牵动 276 个断言 | 纯改名单独提交,跑绿后再做其它;不留别名垫片,避免两个名字并存 | | 机械重排污染后续 review | 排在行为修复之后,单独提交,用 `git diff -w` 复核 | | 真实归档数据受影响 | 全程不碰 `Backups/`;只读冒烟只用 `-DryRun` / `-VerifyOnly` |