mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-11 13:56:15 -08:00
ci(refactor): [ENG-AUDIT-01] verify dedicated task completion commits
This commit is contained in:
1 parent
4f324a8438
commit
e0aacc5d6a
12 files changed
+450
-6
No files matched your search
@@ -37,6 +37,7 @@ jobs:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version-file: .node-version
|
||||
@@ -69,6 +70,7 @@ jobs:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version-file: .node-version
|
||||
@@ -116,6 +118,7 @@ jobs:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version-file: .node-version
|
||||
|
||||
+3
-2
@@ -47,7 +47,7 @@
|
||||
"check:plan": "node refactor/scripts/plan.mjs --check",
|
||||
"test:node": "yarn test:unit && node --test test/toolchain.test.js test/build-docs.test.js test/package-check.test.js test/declarations.test.js test/editor-types.test.js test/coverage.test.js test/performance-report.test.js test/media-gate.test.js",
|
||||
"test:baseline": "node --test refactor/scripts/*.test.mjs",
|
||||
"ci:check": "yarn check:toolchain --strict && yarn check:plan && yarn lint && yarn check:types && yarn typecheck && yarn test",
|
||||
"ci:check": "yarn check:toolchain --strict && yarn check:commits --report && yarn check:plan && yarn lint && yarn check:types && yarn typecheck && yarn test",
|
||||
"ci:build": "yarn build:types && yarn build all && yarn build:i18n && yarn build:ts && yarn build:docs && yarn test:imports",
|
||||
"test:imports": "node --test test/esm.test.js test/i18n.test.js test/ssr.test.js",
|
||||
"typecheck": "node scripts/typecheck.mjs",
|
||||
@@ -65,7 +65,8 @@
|
||||
"test:dash-types-package": "node refactor/scripts/dash-package-types.mjs",
|
||||
"test:ads-types-package": "node refactor/scripts/ads-package-types.mjs",
|
||||
"test:ads-native-visibility": "node refactor/scripts/ads-native-visibility.mjs",
|
||||
"test:vast": "node --test test/vast.test.js test/vast-lifecycle.test.js"
|
||||
"test:vast": "node --test test/vast.test.js test/vast-lifecycle.test.js",
|
||||
"check:commits": "node refactor/scripts/commit-audit.mjs"
|
||||
},
|
||||
"browserslist": "last 1 Chrome version",
|
||||
"devDependencies": {
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
| [已实现的 CI 入口](ci-setup.md) | Yarn 检查/构建、只读 lint、Pages 隔离及远端待验收状态 |
|
||||
| [GitHub CI/CD](github-ci-cd.md) | PR/兼容矩阵、构建报告、Pages、npm 发布及远端准入验证 |
|
||||
| [AI 协作流程](ai-workflow.md) | AI 接续工作、任务边界、验证、记录和交接模板 |
|
||||
| [每任务提交审计](commit-audit.md) | 实际Git状态迁移、独立提交、初始例外、分支合并和CI报告 |
|
||||
| [架构决策](decisions.md) | 已选方向、待验证方案及被拒绝方案 |
|
||||
| [进度与证据](progress.md) | 本次会话结果、阻塞、下一步;不重复维护每个任务状态 |
|
||||
| [变更记录模板](changes/TEMPLATE.md) | 每次行为/类型/结构变化的详细记录 |
|
||||
|
||||
@@ -46,6 +46,10 @@ AI 用于源码分析、任务拆分、实现、测试和差异审查。现代
|
||||
- 本地 commit 已获授权;push、merge、tag 和公开发布仍按用户明确授权执行。
|
||||
- 新规则生效前已完成但未提交的 DOC-01 至 DOC-04 文档,作为 DOC-05 的初始文档基线一次入库,记录真实情况,不伪造之前存在的提交。
|
||||
|
||||
执行`yarn check:commits --report`核对真实Git完成历史,详见[提交审计](commit-audit.md)。
|
||||
任务仍doing时跑适用检查;标done后立即独立提交,再运行审计验证自身。未提交的done
|
||||
状态会被拒绝,这不是可跳过的CI错误。审计与现有历史源码测试要求完整Git历史。
|
||||
|
||||
## 后续任务提示模板
|
||||
|
||||
重构实现完成后继续执行 [多轮复盘与 npm 准入](release-reviews.md)。用户计划分多次任务复盘,后续 AI 从 REVIEW 任务与持久报告接续;不以迁移完成或 Chrome 已连接宣称发布就绪。每项复盘发现的修复仍独立建任务、验证和提交。
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"task": "ENG-AUDIT-01",
|
||||
"status": "done",
|
||||
"sourceCommit": "4f324a84",
|
||||
"checks": [
|
||||
{
|
||||
"command": "node --test refactor/scripts/commit-audit.test.mjs",
|
||||
"result": "pass",
|
||||
"tests": 4,
|
||||
"scope": "Real isolated Git repositories, negative cases and a real merge",
|
||||
"log": {
|
||||
"file": "refactor/.cache/eng-audit-tests-first.log",
|
||||
"sha256": "95a8c9fa5388f828a37a35c97667bb641107ddd027073ce027a6126e986c2761"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "yarn check:commits --report",
|
||||
"result": "pass before task completion",
|
||||
"doneTasks": 80,
|
||||
"dedicatedCommits": 76,
|
||||
"bootstrapBundledTasks": 4,
|
||||
"head": "4f324a84389186be7acd099ab074c4b21011ee81",
|
||||
"report": {
|
||||
"file": "refactor/.cache/eng-audit-before.json",
|
||||
"sha256": "8dd298e4efd1b8ee21c06f245e3c73fe24d21fcfa77bbd83e2beea0d447c2a22"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "actionlint 1.7.12",
|
||||
"result": "pass",
|
||||
"workflows": 2
|
||||
},
|
||||
{
|
||||
"command": "yarn ci:check",
|
||||
"result": "pass",
|
||||
"tests": {
|
||||
"unit": 832,
|
||||
"engineering": 14,
|
||||
"baseline": 58,
|
||||
"total": 904
|
||||
},
|
||||
"log": {
|
||||
"file": "refactor/.cache/eng-audit-ci.log",
|
||||
"sha256": "73d92161c74c921a6755298efd18b49b1b68caf693c0c31f8aba040e050ebf92"
|
||||
}
|
||||
}
|
||||
],
|
||||
"limitations": [
|
||||
"Local Git and workflow static validation only; no remote GitHub run",
|
||||
"Commit/evidence provenance is not semantic test or implementation acceptance",
|
||||
"ENG-09 impact and coverage subtasks remain open"
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"file": "refactor/scripts/commit-audit.mjs",
|
||||
"sha256LF": "3b2076e428042017df2eb12b02c827a81059704f7435d86279e20f3e986f3d57"
|
||||
},
|
||||
{
|
||||
"file": "refactor/scripts/commit-audit.test.mjs",
|
||||
"sha256LF": "74d807947473ccafb783a9127a8fac480c3051b422a5e2edb9e1509ac02cd49c"
|
||||
},
|
||||
{
|
||||
"file": "package.json",
|
||||
"sha256LF": "94f3162e4ed876f5d4dcfbb4a93948a87b46314cf09d67ceaf9c3a8fd8960624"
|
||||
},
|
||||
{
|
||||
"file": ".github/workflows/nodejs.yml",
|
||||
"sha256LF": "a88eee053a57333c4234b88d93535ee8de1d805bf884ce2ad168e3617a1becc1"
|
||||
}
|
||||
],
|
||||
"postCommitVerification": "Run yarn check:commits --report immediately after this task commit; its actual HEAD and completion must appear in the generated report. The report is not embedded in its own commit."
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
# ENG-AUDIT-01 独立完成提交的Git审计
|
||||
|
||||
起点4f324a84。把ENG-09拆出可独立验收的Git审计、依赖影响和契约覆盖三个子任务;
|
||||
本步实现Git审计,其他两项及父任务保持todo。VAST初始化选择仍待用户确认,没有
|
||||
更改其默认行为或将VAST-03标完成。
|
||||
|
||||
`commit-audit.mjs`核对真实任务状态迁移、父分支/merge、准确ID、plan和当时的本地
|
||||
证据文件,不从done标志或提交标题推断工作已提交。初始80个done任务对应76个
|
||||
独立完成提交,DOC-01~04只在固定DOC-05提交中合并。历史标题符号差异保留,
|
||||
任务ID仍准确可追溯;没有重写、amend或伪造过去的提交。
|
||||
|
||||
新命令`yarn check:commits --report`接入`ci:check`,报告由既有CI日志artifact收集。
|
||||
三个GitHub checkout补`fetch-depth: 0`;除审计外,已有测试也会读取历史源码Git对象。
|
||||
没有新增依赖或更改Yarn锁文件。详细行为与本地提交前后顺序见
|
||||
[维护说明](../commit-audit.md)。
|
||||
|
||||
使用隔离的真实Git仓库验证反例和分支合并,不能用mock成功输出替代历史验证。
|
||||
实际仓库审计、测试、lint与工作流静态检查结果见
|
||||
[验证记录](../baselines/commit-audit-validation.json)。远端GitHub运行仍由CI验收任务
|
||||
处理,本次没有push,不能把本地通过称为远端通过。
|
||||
|
||||
完成后独立本地提交`ENG-AUDIT-01`,提交后重新运行审计验证本任务自身。回退本步
|
||||
撤销脚本/测试/命令/checkout设置及文档台账,生产包API和已有历史均不变。
|
||||
|
||||
最终本地CI904项通过(832单元、14工程、58基线);脚本lint、plan及两个工作流的
|
||||
actionlint检查通过。提交后审计将验证新增的第81个done任务及第77个独立完成提交。
|
||||
@@ -0,0 +1,46 @@
|
||||
# 每任务提交审计
|
||||
|
||||
`ENG-AUDIT-01`负责ENG-09的Git审计子项。运行`yarn check:commits`,或增加`--report`
|
||||
将本次HEAD及完成记录写入`refactor/.cache/ci/commit-audit.json`。检查已接入`ci:check`,
|
||||
GitHub日志artifact收集该报告。CI三个checkout使用完整历史:审计以及既有历史源码
|
||||
兼容测试均依赖旧Git对象,浅克隆不能提供它们。
|
||||
|
||||
## 实际核对内容
|
||||
|
||||
脚本从可达Git历史读取每次tasks.json变化及其父提交,而不是从提交标题推断完成。
|
||||
一项从非done转为done,必须在同一个提交包含:准确任务ID、tasks.json、生成plan.md、
|
||||
存在于该提交树中的本地证据,以及至少一份该任务证据文件的实际变更。
|
||||
|
||||
普通完成提交只能完成一个任务。历史中的checkpoint不会被当作完成提交。合并时,
|
||||
父分支已经done的任务沿用该分支的原始完成提交,不要求merge标题冒充任务完成。
|
||||
当前工作区新增done但尚未提交会失败,已完成历史任务被从台账删除也会失败。
|
||||
脚本核实起点与bootstrap提交可达;浅仓库明确报错,不悄悄跳过历史验证。
|
||||
|
||||
唯一合并完成例外固定为完整SHA `570600d2f6ffd580a890e0b87dfa9843bf8dd8d0`:
|
||||
DOC-05携带先前已完成的DOC-01~04;要求该提交的新增done集合精确匹配这五项。
|
||||
见 [原始说明](changes/2026-09-10-DOC-05-task-commits.md)。不能把例外按ID无限复用。
|
||||
|
||||
早期历史标题存在`test(BASE-04): ...`和`... ENG-04 ...`等格式。审计接受准确的任务
|
||||
编号token,拒绝把TEST-010冒充TEST-01;这保留真实的独立提交,不是额外合并例外。
|
||||
新增提交仍遵循AI工作流的`type(scope): [TASK-ID] description`格式。
|
||||
|
||||
## 本地实施顺序
|
||||
|
||||
1. 任务保持doing时运行适用测试、`ci:check`与审计;已提交的其他done任务必须可追溯。
|
||||
2. 验收完成后将本任务标done,生成plan并暂存具体成果,立即独立提交。
|
||||
3. 提交后再次运行`yarn check:commits --report`确认本任务的真实完成记录,并检查Git状态。
|
||||
|
||||
第2步尚未提交时审计会报告未提交完成,这是预期保护。没有“临时豁免”参数,也不能
|
||||
在CI中通过环境变量绕过。审计不代替源码、测试结果或发布审查:证据文件共存不证明
|
||||
其中断言有效,因此报告明确限制。它也不凭历史标题判断某个实现是否有足够测试。
|
||||
|
||||
## 验证和维护
|
||||
|
||||
`refactor/scripts/commit-audit.test.mjs`建立临时真实Git仓库,覆盖独立完成、固定
|
||||
bootstrap例外、多个任务混在一提交、错误ID、缺plan/证据、复用未变化旧证据、路径
|
||||
逃逸、未提交done、删除历史任务、浅仓库以及实际feature分支合并。临时仓库使用
|
||||
专用作者,不修改用户Git配置;清理只针对已验证位于测试cache内的目录。
|
||||
|
||||
更改任务状态或提交政策时同时维护这些反例;禁止仅更新计数或忽略失败使审计变绿。
|
||||
`ENG-IMPACT-01`继续处理影响范围,`ENG-COVERAGE-01`继续契约覆盖索引;本子项完成
|
||||
不代表ENG-09整体、远端GitHub执行或npm发布门槛完成。
|
||||
+7
-3
@@ -2,9 +2,9 @@
|
||||
|
||||
> 由 tasks.json 生成。请修改数据后运行 `node refactor/scripts/plan.mjs --write`,不要手改本表。
|
||||
|
||||
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 217 项,范围 22 个包及工作区/示例。
|
||||
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 220 项,范围 22 个包及工作区/示例。
|
||||
|
||||
状态:todo 131 / doing 6 / blocked 0 / done 80 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
状态:todo 133 / doing 6 / blocked 0 / done 81 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
|
||||
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
|
||||
|
||||
@@ -82,7 +82,10 @@
|
||||
| ENG-06 | workspace<br>支持按包非交互与 JS/TS 构建 | ENG-02, BASE-05 | build/dev 入口解析、指定包参数、原交互保留 | 三种产物、Less/SVG/worker 和本地 8082 demo 正常;保持 BASE-05 的 AMD 同时写入全局行为及 i18n/legacy 入口 | H | done |
|
||||
| ENG-07 | workspace<br>建立 tarball 消费与产物检查 | ENG-04, ENG-06 | 隔离 npm 消费 fixtures、API/声明/入口差分 | 不借 workspace 源码通过,能识别缺文件与默认导出变化 | H | done |
|
||||
| ENG-08 | workspace<br>增加覆盖率、资源与性能报告 | ENG-03, ENG-05, BASE-06 | 覆盖率基线、资源清理断言、性能报告与阈值 | 关键生命周期分支有门槛,报告不靠无意义断言堆数量;将 BASE-06 的同环境多组配对、计时/压缩审查阈值与资源异常分开接入候选,不把历史现象冻结成正常要求 | M | done |
|
||||
| ENG-09 | workspace<br>建立全包依赖影响和文档检查 | ENG-07, DOC-04 | 共享核心/构建影响映射、文档及每任务完成提交的 Git 审计接入;契约-支持版本-测试 ID-命令-候选/报告-任务的覆盖索引 | 核心变化触发必需生态检查;原有 DOC-01 至 04 基线例外明确,后续 done 任务不能缺失独立 commit;计划/已执行/缺证据分开,识别缺少验证归属的公开契约 | M | todo |
|
||||
| ENG-AUDIT-01 | workspace<br>核对每任务完成提交的真实Git历史 | ENG-07, DOC-04 | 独立完成提交、初始DOC例外、父分支合并和证据共提交审计;CI完整历史与报告 | 真实仓库及隔离Git反例通过,done任务不允许缺独立commit,固定DOC-01~04例外可追溯;提交后验证自身 | M | done |
|
||||
| ENG-IMPACT-01 | workspace<br>建立全包依赖与共享变更影响映射 | ENG-07, DOC-04 | 核心/构建/类型/锁文件到受影响包和必需生态检查的映射及CI接入 | 核心变化触发必需生态检查;新增包、未知共享文件或失配依赖不能静默漏检;解释受影响路径和测试命令 | M | todo |
|
||||
| ENG-COVERAGE-01 | workspace<br>建立公开契约与版本测试证据覆盖索引 | BASE-08, CORE-22, ENG-07 | 契约-包-支持版本-固定测试ID-命令-候选/报告-责任任务索引和文档检查 | 计划、已执行和缺证据状态分开,所有公开契约都有验证归属;无效路径、版本依据或报告对应不明确会被识别 | M | todo |
|
||||
| ENG-09 | workspace<br>建立全包依赖影响和文档检查 | ENG-07, DOC-04, ENG-AUDIT-01, ENG-IMPACT-01, ENG-COVERAGE-01 | 共享核心/构建影响映射、文档及每任务完成提交的 Git 审计接入;契约-支持版本-测试 ID-命令-候选/报告-任务的覆盖索引 | 核心变化触发必需生态检查;原有 DOC-01 至 04 基线例外明确,后续 done 任务不能缺失独立 commit;计划/已执行/缺证据分开,识别缺少验证归属的公开契约 | M | todo |
|
||||
| ENG-10 | workspace<br>建立历史失败分级和测试可靠性规则 | ENG-03, ENG-04, ENG-05, ENG-07, BASE-07 | 历史失败 ID/环境/旧版复现/负责修复任务、逐模块门槛、受控等待与 trace/retry 规则 | 不靠全局忽略或无理由 skip 隐藏问题,新增回归阻止交付,设备缺口和偶发失败单独可见;以 risks.json 为统一差异索引;关闭必须有 resolutionEvidence/rationale,已复现、源码事实、未验证分开,登记不等于豁免 | M | done |
|
||||
| ENG-11 | workspace<br>增加不改变产物的模块构建分析 | ENG-03, ENG-08 | 正式构建的可选模块归因报告及源码/产物指纹 | 相同构建开关前后三格式字节一致,报告不进入分发包;区分 Rollup 渲染字节与压缩体积,并保留真实工作区/发布来源 | L | done |
|
||||
|
||||
@@ -437,6 +440,7 @@
|
||||
- ENG-06: [记录](changes/2026-09-10-ENG-06-build-development.md) [记录](build-development.md) [记录](baselines/build-validation.json)
|
||||
- ENG-07: [记录](changes/2026-09-10-ENG-07-package-consumers.md) [记录](baselines/package-validation.json)
|
||||
- ENG-08: [记录](coverage-performance.md) [记录](baselines/quality-validation.json) [记录](changes/2026-09-11-ENG-08-quality-reports.md)
|
||||
- ENG-AUDIT-01: [记录](commit-audit.md) [记录](changes/2026-09-12-ENG-AUDIT-01-commits.md) [记录](baselines/commit-audit-validation.json)
|
||||
- ENG-10: [记录](changes/2026-09-10-ENG-10-test-reliability.md) [记录](test-reliability.md)
|
||||
- ENG-11: [记录](build-analysis.md) [记录](baselines/bundle-attribution.json) [记录](changes/2026-09-11-ENG-11-build-analysis.md)
|
||||
- PILOT-01: [记录](changes/2026-09-10-PILOT-01-chapter.md) [记录](baselines/pilot-validation.json)
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
# 进度与证据
|
||||
|
||||
## 最新完成:ENG-AUDIT-01 每任务提交审计
|
||||
|
||||
新增实际Git历史审计并接入ci:check,核对状态迁移、独立提交、证据/plan共提交和合并分支。
|
||||
原80个done对应76个独立完成提交,另4项为固定DOC-05初始例外;保留真实历史标题格式。
|
||||
真实Git反例及完整本地CI904项通过,GitHub三个checkout改为完整历史,actionlint通过。
|
||||
提交后再审计本任务自身。ENG-09拆成Git审计、影响映射、契约覆盖三个子项,后二者
|
||||
和父任务仍未完成;没有远端运行或推送。当前220项:81 done、6 doing、133 todo。
|
||||
VAST初始化选择仍待确认;其余实施继续。见 [本步记录](changes/2026-09-12-ENG-AUDIT-01-commits.md)
|
||||
和 [验证证据](baselines/commit-audit-validation.json)。
|
||||
|
||||
## 当前实施:VAST-03 TS拆分与生命周期修复
|
||||
|
||||
原入口拆成5个严格TS模块,处理核心终止、显式广告重建、SDK/DOM回滚与晚到/重入回调。
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const root = fileURLToPath(new URL('../../', import.meta.url))
|
||||
export const commitPolicy = {
|
||||
bootstrapCommit: '570600d2f6ffd580a890e0b87dfa9843bf8dd8d0',
|
||||
bootstrapTask: 'DOC-05',
|
||||
bundledTasks: ['DOC-01', 'DOC-02', 'DOC-03', 'DOC-04'],
|
||||
}
|
||||
|
||||
export function auditCommits(directory = root, policy = commitPolicy) {
|
||||
const git = args => execFileSync('git', args, { cwd: directory, encoding: 'utf8', maxBuffer: 32 * 1024 * 1024, stdio: ['ignore', 'pipe', 'pipe'] }).trimEnd()
|
||||
assert.equal(git(['rev-parse', '--is-shallow-repository']), 'false', 'Commit audit requires full Git history; use checkout fetch-depth: 0')
|
||||
const head = git(['rev-parse', 'HEAD'])
|
||||
const current = JSON.parse(fs.readFileSync(path.join(directory, 'refactor/tasks.json'), 'utf8'))
|
||||
git(['merge-base', '--is-ancestor', current.baselineCommit, head])
|
||||
git(['merge-base', '--is-ancestor', policy.bootstrapCommit, head])
|
||||
const snapshots = new Map()
|
||||
function snapshot(commit) {
|
||||
if (!snapshots.has(commit)) {
|
||||
const entry = git(['ls-tree', commit, '--', 'refactor/tasks.json'])
|
||||
const tasks = entry ? JSON.parse(git(['show', `${commit}:refactor/tasks.json`])).tasks : []
|
||||
snapshots.set(commit, new Map(tasks.map(task => [task.id, task])))
|
||||
}
|
||||
return snapshots.get(commit)
|
||||
}
|
||||
const history = git(['log', '--full-history', '--reverse', '--format=%H%x00%P%x00%s', head, '--', 'refactor/tasks.json']).split('\n').filter(Boolean)
|
||||
const completions = []
|
||||
for (const line of history) {
|
||||
const [commit, parentText, subject] = line.split('\0')
|
||||
const parents = parentText.split(' ').filter(Boolean).map(snapshot)
|
||||
const newlyDone = [...snapshot(commit).values()].filter(task => task.status === 'done' && parents.every(parent => parent.get(task.id)?.status !== 'done'))
|
||||
if (!newlyDone.length)
|
||||
continue
|
||||
const bootstrap = commit === policy.bootstrapCommit
|
||||
const exceptions = bootstrap ? policy.bundledTasks : []
|
||||
const dedicated = newlyDone.filter(task => !exceptions.includes(task.id))
|
||||
assert.equal(dedicated.length, 1, `${commit}: one completion commit must complete exactly one task; found ${dedicated.map(task => task.id).join(', ')}`)
|
||||
if (bootstrap) {
|
||||
assert.equal(dedicated[0].id, policy.bootstrapTask, 'Bootstrap exception is restricted to DOC-05')
|
||||
assert.deepEqual(newlyDone.map(task => task.id).sort(), [...policy.bundledTasks, policy.bootstrapTask].sort(), 'Bootstrap exception task set changed')
|
||||
}
|
||||
const id = dedicated[0].id
|
||||
assert(/^[A-Z]+(?:-[A-Z]+)*-\d{2}$/.test(id), 'Invalid task ID')
|
||||
// Older subjects use parentheses or plain tokens; punctuation does not erase a real task commit.
|
||||
assert(new RegExp(`(?:^|[^A-Z0-9-])${id}(?:$|[^A-Z0-9-])`).test(subject), `${commit}: completion subject does not identify exact task ${id}`)
|
||||
const changes = git(['diff-tree', '--root', '-m', '--no-commit-id', '--name-only', '-r', '-z', commit]).split('\0').filter(Boolean)
|
||||
assert(changes.includes('refactor/tasks.json'), `${commit}: missing task-state change`)
|
||||
assert(changes.includes('refactor/plan.md'), `${commit}: missing generated plan change`)
|
||||
const tree = new Set(git(['ls-tree', '-r', '--name-only', '-z', commit, '--', 'refactor']).split('\0').filter(Boolean))
|
||||
for (const task of newlyDone) {
|
||||
assert(task.evidence?.length, `${commit}: ${task.id} has no evidence`)
|
||||
const local = task.evidence.filter(file => !/^https?:\/\//.test(file)).map((file) => {
|
||||
const name = file.split('#')[0]
|
||||
const normalized = path.posix.normalize(name)
|
||||
assert(name && !name.includes('\\') && !path.posix.isAbsolute(name) && !/^[A-Z]:/i.test(name) && normalized !== '..' && !normalized.startsWith('../'), `${commit}: evidence escapes refactor`)
|
||||
return `refactor/${normalized}`
|
||||
})
|
||||
assert(local.length, `${commit}: ${task.id} needs durable local completion evidence`)
|
||||
for (const file of local) assert(tree.has(file), `${commit}: ${task.id} evidence missing at completion: ${file}`)
|
||||
if (!exceptions.includes(task.id))
|
||||
assert(local.some(file => changes.includes(file)), `${commit}: ${task.id} has no evidence updated in its completion commit`)
|
||||
completions.push({ task: task.id, commit, subject, bundledWith: exceptions.includes(task.id) ? policy.bootstrapTask : null, evidence: local, changedEvidence: local.filter(file => changes.includes(file)) })
|
||||
}
|
||||
}
|
||||
const currentTasks = new Map(current.tasks.map(task => [task.id, task]))
|
||||
const committed = snapshot(head)
|
||||
for (const completion of completions)
|
||||
assert(currentTasks.has(completion.task), `Historical completed task was removed: ${completion.task}`)
|
||||
for (const task of current.tasks.filter(task => task.status === 'done')) {
|
||||
assert.equal(committed.get(task.id)?.status, 'done', `Uncommitted completion: ${task.id}; commit this task before the final audit`)
|
||||
assert(completions.some(completion => completion.task === task.id), `No dedicated completion found for ${task.id}`)
|
||||
}
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
head,
|
||||
baselineCommit: current.baselineCommit,
|
||||
policy,
|
||||
doneTasks: current.tasks.filter(task => task.status === 'done').length,
|
||||
dedicatedCommits: new Set(completions.filter(item => !item.bundledWith).map(item => item.commit)).size,
|
||||
completions,
|
||||
limitation: 'Verifies actual completion transitions, task identity and durable evidence co-committed with the plan; does not prove the semantic quality of implementation or tests.',
|
||||
}
|
||||
}
|
||||
|
||||
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||
assert(process.argv.slice(2).every(arg => arg === '--report'), 'Use --report or no arguments')
|
||||
const report = auditCommits()
|
||||
if (process.argv.includes('--report')) {
|
||||
const directory = path.join(root, 'refactor/.cache/ci')
|
||||
fs.mkdirSync(directory, { recursive: true })
|
||||
fs.writeFileSync(path.join(directory, 'commit-audit.json'), `${JSON.stringify(report, null, 2)}\n`)
|
||||
}
|
||||
console.log(`Commit audit: ${report.doneTasks} done tasks, ${report.dedicatedCommits} dedicated completion commits, ${report.policy.bundledTasks.length} documented bootstrap tasks; HEAD ${report.head}`)
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Exercise real isolated Git histories, not mocked command output.
|
||||
import test from 'node:test'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { auditCommits } from './commit-audit.mjs'
|
||||
|
||||
const cache = fileURLToPath(new URL('../.cache/', import.meta.url))
|
||||
function repository(t) {
|
||||
fs.mkdirSync(cache, { recursive: true })
|
||||
const directory = fs.mkdtempSync(path.join(cache, 'commit-audit-test-'))
|
||||
t.after(() => {
|
||||
const relative = path.relative(cache, directory)
|
||||
assert(relative.startsWith('commit-audit-test-') && !relative.includes(path.sep))
|
||||
fs.rmSync(directory, { recursive: true, force: true })
|
||||
})
|
||||
const git = args => execFileSync('git', args, {
|
||||
cwd: directory,
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
env: { ...process.env, GIT_AUTHOR_NAME: 'Audit Fixture', GIT_AUTHOR_EMAIL: 'audit@example.test', GIT_COMMITTER_NAME: 'Audit Fixture', GIT_COMMITTER_EMAIL: 'audit@example.test', GIT_CONFIG_NOSYSTEM: '1', GIT_CONFIG_GLOBAL: process.platform === 'win32' ? 'NUL' : '/dev/null' },
|
||||
}).trim()
|
||||
const write = (file, text) => {
|
||||
fs.mkdirSync(path.dirname(path.join(directory, file)), { recursive: true })
|
||||
fs.writeFileSync(path.join(directory, file), text)
|
||||
}
|
||||
const commit = (subject) => {
|
||||
git(['add', '.'])
|
||||
git(['-c', 'core.hooksPath=', '-c', 'commit.gpgSign=false', 'commit', '-m', subject])
|
||||
return git(['rev-parse', 'HEAD'])
|
||||
}
|
||||
git(['init', '-b', 'main'])
|
||||
write('README.md', 'Test-only Git repository\n')
|
||||
const baselineCommit = commit('Baseline')
|
||||
const bundledTasks = ['DOC-01', 'DOC-02', 'DOC-03', 'DOC-04']
|
||||
const data = { baselineCommit, tasks: [...bundledTasks, 'DOC-05'].map(id => ({ id, status: 'done', evidence: ['changes/bootstrap.md'] })) }
|
||||
for (const id of ['TEST-01', 'TEST-02']) data.tasks.push({ id, status: 'doing', evidence: [] })
|
||||
const save = (plan = true) => {
|
||||
write('refactor/tasks.json', JSON.stringify(data))
|
||||
if (plan)
|
||||
write('refactor/plan.md', data.tasks.map(task => `${task.id}: ${task.status}`).join('\n'))
|
||||
}
|
||||
write('refactor/changes/bootstrap.md', 'Initial documentation and the per-task rule\n')
|
||||
save()
|
||||
const bootstrapCommit = commit('docs: [DOC-05] establish initial documentation')
|
||||
const policy = { bootstrapCommit, bootstrapTask: 'DOC-05', bundledTasks }
|
||||
const finish = (ids = ['TEST-01'], options = {}) => {
|
||||
for (const id of ids) {
|
||||
const task = data.tasks.find(task => task.id === id)
|
||||
task.status = 'done'
|
||||
task.evidence = options.evidence || [`changes/${id}.md`]
|
||||
if (options.writeEvidence !== false)
|
||||
write(`refactor/changes/${id}.md`, `${id}: tested completion\n`)
|
||||
}
|
||||
save(options.plan !== false)
|
||||
if (options.commit !== false)
|
||||
return commit(options.subject || `test: [${ids[0]}] completed work`)
|
||||
}
|
||||
return { directory, git, write, commit, save, finish, data, policy, audit: () => auditCommits(directory, policy) }
|
||||
}
|
||||
|
||||
test('Commit audit proves dedicated transitions and only the pinned bootstrap bundling', (t) => {
|
||||
const repo = repository(t)
|
||||
const first = repo.finish()
|
||||
const second = repo.finish(['TEST-02'], { subject: 'test(TEST-02): historical punctuation remains traceable' })
|
||||
const report = repo.audit()
|
||||
assert.equal(report.doneTasks, 7)
|
||||
assert.equal(report.dedicatedCommits, 3)
|
||||
assert.deepEqual(report.completions.filter(item => item.bundledWith).map(item => item.task), repo.policy.bundledTasks)
|
||||
assert.equal(report.completions.find(item => item.task === 'TEST-01').commit, first)
|
||||
assert.equal(report.completions.find(item => item.task === 'TEST-02').commit, second)
|
||||
assert.throws(() => auditCommits(repo.directory, { ...repo.policy, bundledTasks: [...repo.policy.bundledTasks, 'TEST-01'] }), /Bootstrap exception task set changed/)
|
||||
})
|
||||
|
||||
test('Commit audit rejects combined task completion, misleading subjects and missing co-committed evidence', (t) => {
|
||||
for (const [ids, options, expected] of [
|
||||
[['TEST-01', 'TEST-02'], {}, /exactly one task/],
|
||||
[['TEST-01'], { subject: 'test: [TEST-010] not the completed task' }, /exact task TEST-01/],
|
||||
[['TEST-01'], { plan: false }, /missing generated plan change/],
|
||||
[['TEST-01'], { writeEvidence: false }, /evidence missing at completion/],
|
||||
[['TEST-01'], { evidence: ['changes/bootstrap.md'], writeEvidence: false }, /no evidence updated/],
|
||||
[['TEST-01'], { evidence: ['../README.md'] }, /evidence escapes refactor/],
|
||||
]) {
|
||||
const repo = repository(t)
|
||||
repo.finish(ids, options)
|
||||
assert.throws(repo.audit, expected)
|
||||
}
|
||||
})
|
||||
|
||||
test('Commit audit rejects uncommitted done state and shallow or removed completion history', (t) => {
|
||||
const repo = repository(t)
|
||||
repo.finish(['TEST-01'], { commit: false })
|
||||
assert.throws(repo.audit, /Uncommitted completion: TEST-01/)
|
||||
repo.commit('test: [TEST-01] finish with proof')
|
||||
repo.data.tasks = repo.data.tasks.filter(task => task.id !== 'TEST-01')
|
||||
repo.save()
|
||||
assert.throws(repo.audit, /Historical completed task was removed/)
|
||||
repo.write('.git/shallow', `${repo.git(['rev-parse', 'HEAD'])}\n`)
|
||||
assert.throws(repo.audit, /requires full Git history/)
|
||||
})
|
||||
|
||||
test('Commit audit finds feature-branch completion through a real merge without calling the merge a second completion', (t) => {
|
||||
const repo = repository(t)
|
||||
repo.git(['checkout', '-b', 'feature'])
|
||||
const completed = repo.finish()
|
||||
repo.git(['checkout', 'main'])
|
||||
repo.write('notes.md', 'Unrelated main branch work\n')
|
||||
repo.commit('docs: independent work')
|
||||
repo.git(['-c', 'core.hooksPath=', '-c', 'commit.gpgSign=false', 'merge', '--no-ff', 'feature', '-m', 'Merge feature branch'])
|
||||
const report = repo.audit()
|
||||
assert.equal(report.doneTasks, 6)
|
||||
assert.equal(report.dedicatedCommits, 2)
|
||||
assert.equal(report.completions.filter(item => item.task === 'TEST-01').length, 1)
|
||||
assert.equal(report.completions.find(item => item.task === 'TEST-01').commit, completed)
|
||||
})
|
||||
+60
-1
@@ -604,6 +604,62 @@
|
||||
"changes/2026-09-11-ENG-08-quality-reports.md"
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "2 工程保障",
|
||||
"scope": [
|
||||
"workspace"
|
||||
],
|
||||
"risk": "M",
|
||||
"id": "ENG-AUDIT-01",
|
||||
"title": "核对每任务完成提交的真实Git历史",
|
||||
"status": "done",
|
||||
"dependsOn": [
|
||||
"ENG-07",
|
||||
"DOC-04"
|
||||
],
|
||||
"deliverable": "独立完成提交、初始DOC例外、父分支合并和证据共提交审计;CI完整历史与报告",
|
||||
"acceptance": "真实仓库及隔离Git反例通过,done任务不允许缺独立commit,固定DOC-01~04例外可追溯;提交后验证自身",
|
||||
"evidence": [
|
||||
"commit-audit.md",
|
||||
"changes/2026-09-12-ENG-AUDIT-01-commits.md",
|
||||
"baselines/commit-audit-validation.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "2 工程保障",
|
||||
"scope": [
|
||||
"workspace"
|
||||
],
|
||||
"risk": "M",
|
||||
"id": "ENG-IMPACT-01",
|
||||
"title": "建立全包依赖与共享变更影响映射",
|
||||
"status": "todo",
|
||||
"dependsOn": [
|
||||
"ENG-07",
|
||||
"DOC-04"
|
||||
],
|
||||
"deliverable": "核心/构建/类型/锁文件到受影响包和必需生态检查的映射及CI接入",
|
||||
"acceptance": "核心变化触发必需生态检查;新增包、未知共享文件或失配依赖不能静默漏检;解释受影响路径和测试命令",
|
||||
"evidence": []
|
||||
},
|
||||
{
|
||||
"phase": "2 工程保障",
|
||||
"scope": [
|
||||
"workspace"
|
||||
],
|
||||
"risk": "M",
|
||||
"id": "ENG-COVERAGE-01",
|
||||
"title": "建立公开契约与版本测试证据覆盖索引",
|
||||
"status": "todo",
|
||||
"dependsOn": [
|
||||
"BASE-08",
|
||||
"CORE-22",
|
||||
"ENG-07"
|
||||
],
|
||||
"deliverable": "契约-包-支持版本-固定测试ID-命令-候选/报告-责任任务索引和文档检查",
|
||||
"acceptance": "计划、已执行和缺证据状态分开,所有公开契约都有验证归属;无效路径、版本依据或报告对应不明确会被识别",
|
||||
"evidence": []
|
||||
},
|
||||
{
|
||||
"id": "ENG-09",
|
||||
"phase": "2 工程保障",
|
||||
@@ -613,7 +669,10 @@
|
||||
],
|
||||
"dependsOn": [
|
||||
"ENG-07",
|
||||
"DOC-04"
|
||||
"DOC-04",
|
||||
"ENG-AUDIT-01",
|
||||
"ENG-IMPACT-01",
|
||||
"ENG-COVERAGE-01"
|
||||
],
|
||||
"status": "todo",
|
||||
"risk": "M",
|
||||
|
||||
Reference in new issue
Block a user