diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml
index 70066ee91..53129aeb8 100644
--- a/.github/workflows/nodejs.yml
+++ b/.github/workflows/nodejs.yml
@@ -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
diff --git a/package.json b/package.json
index c0c503e46..42eff8d17 100644
--- a/package.json
+++ b/package.json
@@ -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": {
diff --git a/refactor/README.md b/refactor/README.md
index b07c8bbe6..4599d4d7c 100644
--- a/refactor/README.md
+++ b/refactor/README.md
@@ -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) | 每次行为/类型/结构变化的详细记录 |
diff --git a/refactor/ai-workflow.md b/refactor/ai-workflow.md
index 67f866abc..d9441330b 100644
--- a/refactor/ai-workflow.md
+++ b/refactor/ai-workflow.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 已连接宣称发布就绪。每项复盘发现的修复仍独立建任务、验证和提交。
diff --git a/refactor/baselines/commit-audit-validation.json b/refactor/baselines/commit-audit-validation.json
new file mode 100644
index 000000000..d6d251bd2
--- /dev/null
+++ b/refactor/baselines/commit-audit-validation.json
@@ -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."
+}
diff --git a/refactor/changes/2026-09-12-ENG-AUDIT-01-commits.md b/refactor/changes/2026-09-12-ENG-AUDIT-01-commits.md
new file mode 100644
index 000000000..df67259af
--- /dev/null
+++ b/refactor/changes/2026-09-12-ENG-AUDIT-01-commits.md
@@ -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个独立完成提交。
diff --git a/refactor/commit-audit.md b/refactor/commit-audit.md
new file mode 100644
index 000000000..aa22e198f
--- /dev/null
+++ b/refactor/commit-audit.md
@@ -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发布门槛完成。
diff --git a/refactor/plan.md b/refactor/plan.md
index 0c5447b19..5ea3326e9 100644
--- a/refactor/plan.md
+++ b/refactor/plan.md
@@ -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
支持按包非交互与 JS/TS 构建 | ENG-02, BASE-05 | build/dev 入口解析、指定包参数、原交互保留 | 三种产物、Less/SVG/worker 和本地 8082 demo 正常;保持 BASE-05 的 AMD 同时写入全局行为及 i18n/legacy 入口 | H | done |
| ENG-07 | workspace
建立 tarball 消费与产物检查 | ENG-04, ENG-06 | 隔离 npm 消费 fixtures、API/声明/入口差分 | 不借 workspace 源码通过,能识别缺文件与默认导出变化 | H | done |
| ENG-08 | workspace
增加覆盖率、资源与性能报告 | ENG-03, ENG-05, BASE-06 | 覆盖率基线、资源清理断言、性能报告与阈值 | 关键生命周期分支有门槛,报告不靠无意义断言堆数量;将 BASE-06 的同环境多组配对、计时/压缩审查阈值与资源异常分开接入候选,不把历史现象冻结成正常要求 | M | done |
-| ENG-09 | workspace
建立全包依赖影响和文档检查 | ENG-07, DOC-04 | 共享核心/构建影响映射、文档及每任务完成提交的 Git 审计接入;契约-支持版本-测试 ID-命令-候选/报告-任务的覆盖索引 | 核心变化触发必需生态检查;原有 DOC-01 至 04 基线例外明确,后续 done 任务不能缺失独立 commit;计划/已执行/缺证据分开,识别缺少验证归属的公开契约 | M | todo |
+| ENG-AUDIT-01 | workspace
核对每任务完成提交的真实Git历史 | ENG-07, DOC-04 | 独立完成提交、初始DOC例外、父分支合并和证据共提交审计;CI完整历史与报告 | 真实仓库及隔离Git反例通过,done任务不允许缺独立commit,固定DOC-01~04例外可追溯;提交后验证自身 | M | done |
+| ENG-IMPACT-01 | workspace
建立全包依赖与共享变更影响映射 | ENG-07, DOC-04 | 核心/构建/类型/锁文件到受影响包和必需生态检查的映射及CI接入 | 核心变化触发必需生态检查;新增包、未知共享文件或失配依赖不能静默漏检;解释受影响路径和测试命令 | M | todo |
+| ENG-COVERAGE-01 | workspace
建立公开契约与版本测试证据覆盖索引 | BASE-08, CORE-22, ENG-07 | 契约-包-支持版本-固定测试ID-命令-候选/报告-责任任务索引和文档检查 | 计划、已执行和缺证据状态分开,所有公开契约都有验证归属;无效路径、版本依据或报告对应不明确会被识别 | M | todo |
+| ENG-09 | workspace
建立全包依赖影响和文档检查 | 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
建立历史失败分级和测试可靠性规则 | ENG-03, ENG-04, ENG-05, ENG-07, BASE-07 | 历史失败 ID/环境/旧版复现/负责修复任务、逐模块门槛、受控等待与 trace/retry 规则 | 不靠全局忽略或无理由 skip 隐藏问题,新增回归阻止交付,设备缺口和偶发失败单独可见;以 risks.json 为统一差异索引;关闭必须有 resolutionEvidence/rationale,已复现、源码事实、未验证分开,登记不等于豁免 | M | done |
| ENG-11 | workspace
增加不改变产物的模块构建分析 | 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)
diff --git a/refactor/progress.md b/refactor/progress.md
index 9541d1e37..17ce09017 100644
--- a/refactor/progress.md
+++ b/refactor/progress.md
@@ -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回滚与晚到/重入回调。
diff --git a/refactor/scripts/commit-audit.mjs b/refactor/scripts/commit-audit.mjs
new file mode 100644
index 000000000..aefe42d26
--- /dev/null
+++ b/refactor/scripts/commit-audit.mjs
@@ -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}`)
+}
diff --git a/refactor/scripts/commit-audit.test.mjs b/refactor/scripts/commit-audit.test.mjs
new file mode 100644
index 000000000..515ff3638
--- /dev/null
+++ b/refactor/scripts/commit-audit.test.mjs
@@ -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)
+})
diff --git a/refactor/tasks.json b/refactor/tasks.json
index 5bd9bed14..2af3dac18 100644
--- a/refactor/tasks.json
+++ b/refactor/tasks.json
@@ -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",