ci(refactor): [ENG-IMPACT-01] map workspace changes to required ecosystem checks

This commit is contained in:
Harvey Zhao committed 2026-09-12 10:14:54 +08:00
1 parent e0aacc5d6a
commit 6abac891ce
14 files changed
+1070 -9

No files matched your search

+6 -3
View File
@@ -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:commits --report && yarn check:plan && yarn lint && yarn check:types && yarn typecheck && yarn test",
"ci:check": "yarn check:toolchain --strict && yarn check:commits --report && yarn check:impact --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",
@@ -66,7 +66,8 @@
"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",
"check:commits": "node refactor/scripts/commit-audit.mjs"
"check:commits": "node refactor/scripts/commit-audit.mjs",
"check:impact": "node refactor/scripts/impact.mjs"
},
"browserslist": "last 1 Chrome version",
"devDependencies": {
@@ -87,13 +88,15 @@
"lerna": "8.2.4",
"less": "4.5.1",
"prompts": "2.4.2",
"semver": "7.7.4",
"servor": "4.0.2",
"svgo": "4.1.0",
"terser": "5.51.2",
"typescript": "5.9.3",
"typescript-compat": "npm:typescript@4.3.5",
"typescript-runtime-compat": "npm:typescript@5.1.6",
"vite": "7.3.6"
"vite": "7.3.6",
"yaml": "2.8.2"
},
"resolutions": {
"dts-bundle-generator/typescript": "5.9.3"
+1
View File
@@ -34,6 +34,7 @@
| [GitHub CI/CD](github-ci-cd.md) | PR/兼容矩阵、构建报告、Pages、npm 发布及远端准入验证 |
| [AI 协作流程](ai-workflow.md) | AI 接续工作、任务边界、验证、记录和交接模板 |
| [每任务提交审计](commit-audit.md) | 实际Git状态迁移、独立提交、初始例外、分支合并和CI报告 |
| [全包影响映射](impact-analysis.md) | 变更范围、依赖/验证关系、必需CI命令及尚缺的包验证 |
| [架构决策](decisions.md) | 已选方向、待验证方案及被拒绝方案 |
| [进度与证据](progress.md) | 本次会话结果、阻塞、下一步;不重复维护每个任务状态 |
| [变更记录模板](changes/TEMPLATE.md) | 每次行为/类型/结构变化的详细记录 |
+116
View File
@@ -0,0 +1,116 @@
{
"schemaVersion": 1,
"task": "ENG-IMPACT-01",
"status": "done",
"sourceCommit": "e0aacc5d6a75097a50fab2e8fdee46a2277455e6",
"checks": [
{
"command": "node --test refactor/scripts/impact.test.mjs",
"result": "expected regression reproduced before fix",
"scope": "Shell conditional wrapper bypassed the original workflow matcher",
"log": {
"file": "refactor/.cache/eng-impact-shell-before.log",
"sha256": "5851b82f25d8f5161cdb5a92ca18e30202afb6c7cbe83827228d6caf0f6789f9"
}
},
{
"command": "node --test refactor/scripts/impact.test.mjs",
"result": "pass after fix",
"tests": 7,
"scope": "Actual 22-package model, isolated dependency/policy negative cases, real Git ranges and workflow shell/condition negatives",
"log": {
"file": "refactor/.cache/eng-impact-shell-after.log",
"sha256": "48dcf18eb1b0d696165d862f2acf4ecc5bdd0c4421972436f5c871c059446a45"
}
},
{
"command": "node node_modules/eslint/bin/eslint.js refactor/scripts/impact-model.mjs refactor/scripts/impact.mjs refactor/scripts/impact.test.mjs --no-fix",
"result": "pass",
"log": {
"file": "refactor/.cache/eng-impact-lint.log",
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
}
},
{
"command": "actionlint 1.7.12",
"result": "pass",
"workflows": 2,
"log": {
"file": "refactor/.cache/eng-impact-actionlint.log",
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
}
},
{
"command": "yarn ci:check",
"result": "pass",
"tests": {
"unit": 832,
"engineering": 14,
"baseline": 65,
"total": 911
},
"log": {
"file": "refactor/.cache/eng-impact-ci-final.log",
"sha256": "04a7f49a111ed565e38e6b6e5e1ebd55487506231f88c9f5db221e62f253cd3b"
}
},
{
"command": "yarn check:impact --report (inside final ci:check)",
"result": "pass",
"packages": 22,
"affectedPackages": 22,
"installedConsumerGaps": 19,
"unresolvedDynamicImports": 0,
"report": {
"file": "refactor/.cache/eng-impact-candidate-report.json",
"sha256": "7db3db5f7d6642066d17e9eefa506787b00c74fce7b0e2118ad94a829adcde53"
},
"provenance": "Pre-completion candidate report; metadata/docs changed afterward, implementation inputs recorded below"
}
],
"inputs": [
{
"file": "refactor/scripts/impact-model.mjs",
"sha256LF": "d2dc168d2d358694be7e203ab3970deb1f42bff275e89ede88adbe870a962e4c"
},
{
"file": "refactor/scripts/impact.mjs",
"sha256LF": "26cb7432fad82f3e9f1e454c911e15692610b6ae3052d9e2e2ec4cbc86957c36"
},
{
"file": "refactor/scripts/impact.test.mjs",
"sha256LF": "3a7c82c0b8d47fba7c39b83e12ceba8b3c02c648689def89d73be44c57b31855"
},
{
"file": "refactor/impact-policy.json",
"sha256LF": "9e3c0ac40f26d19b9c37783df28ea3adf1ada8ae084401c028867ec45ed47f97"
},
{
"file": "package.json",
"sha256LF": "367015c0d3caa704eb8e6be179c517d7c1f529e5a956338145e366cff43b1955"
},
{
"file": "yarn.lock",
"sha256LF": "1a6642a9728e77faf26dce43a849b56ba24573b44f08096a07fa36593eecc8de"
},
{
"file": ".github/workflows/nodejs.yml",
"sha256LF": "a88eee053a57333c4234b88d93535ee8de1d805bf884ce2ad168e3617a1becc1"
},
{
"file": "scripts/package-consumer.mjs",
"sha256LF": "3c85b3d664d5cf20e593a938cc756017f55fff5a2df5304054dbaa2f65131044"
},
{
"file": "scripts/coverage-policy.json",
"sha256LF": "db3f2eacc44d61465031af6e618c8863ac4e9f23ef889865a9202919246ca403"
}
],
"limitations": [
"Local validation only; remote GitHub execution remains CI-04",
"Workflow validation accepts a restricted shell structure, not arbitrary shell semantics",
"Impact analysis does not prove package/device/vendor acceptance; 19 library packages lack the unified installed consumer suite",
"Existing generated docs eslint unused-disable warning and Yarn url.parse deprecation remain; no production files changed by this task",
"ENG-COVERAGE-01 and parent ENG-09 remain incomplete"
]
}
@@ -0,0 +1,29 @@
# ENG-IMPACT-01 全包依赖影响与CI检查映射
起点e0aacc5d。核对22包manifest、源码/声明导入、共享构建及示例,新增显式验证
关系与静态依赖图;核心到全生态、danmuku到mask的共享DOM、库到站点分别说明来源。
说明与实际命令见 [影响分析](../impact-analysis.md)。
新增`yarn check:impact --report`接入ci:check,报告进入已有CI日志收集目录。
真实YAML校验确保六类现有必需命令不会被路径过滤、job/step条件或允许失败静默
略过;新增包、错误workspace版本范围和无法解析的源码也会明确失败。
7项测试组覆盖真实项目、可控包配置反例及真实Git。Git范围包括暂存/未暂存/未跟踪/
删除和改名两端;PR/push读取结构化SHA,缺少可用事件base时记录并检查全仓库。
未知共享路径或计算型import保守扩大范围,没有把未知当作无影响。
报告如实标出统一安装消费者只有core/chapter、其余19个库包尚缺完整安装消费。
这不替代已有Ads/DASH专属类型命令,也不把影响分析当作所有插件或真实设备已通过。
安装范围从实际consumer脚本提取,后续新增验证包时需同步报告策略。
yaml2.8.2/semver7.7.4固定为直接开发依赖,用于可靠解析workflow和版本范围;
初次PowerShell搜索中的引号/目录通配符错误已改为兼容的rg参数,没有改变生产代码。
检查结果与输入指纹见 [验证记录](../baselines/impact-validation.json)。
复查补入shell条件包装及`|| true`反例,先复现校验器漏检,再收紧必需命令首行和shell
格式校验;同时覆盖YAML布尔false条件及checkout条件。问题已修复并保留失败/通过日志,
没有只登记后放行。最终7项影响测试组通过;完整CI结果在验证记录中归档。
本任务完成独立本地提交后进行提交审计;无push/tag/publish。回退本步移除影响
脚本/测试、策略、直接开发依赖及CI命令接入;原有CI任务仍可运行。ENG-COVERAGE-01
和父任务ENG-09继续,VAST初始化选择仍待用户确认。
+1
View File
@@ -17,6 +17,7 @@
| `yarn test:baseline` | 固定发布包完整性及本地 HTTP 基线测试;首次可能下载已固定归档到缓存 |
| `yarn ci:check` | 严格 Node/Yarn/锁检查、计划、只读 lint、类型、Node 和基线测试;允许写忽略缓存,不修改源码 |
| `yarn ci:build` | 21 库包、i18n、编辑器声明和文档站构建,以及构建后包导入 smoke;会生成 dist 和 docs 内容 |
| `yarn check:impact --report` | 读取实际依赖/验证关系和Git变更,核对workflow必需命令,写CI影响报告;已接入ci:check,见[影响映射](impact-analysis.md) |
| `yarn build:all` | 保留旧入口,执行 ci:build 后只读 lint |
scripts/build-docs.js 保留原 npm run build 子命令兼容入口,现在传播失败退出码;包管理/锁维护继续使用 Yarn。MOD-02 可再统一旧内部脚本。build-ts.js 仅对刚生成的声明执行 ESLint layout 格式修正,使只读 lint 在构建后仍能通过;不是对生产源码执行自动修复。
+71
View File
@@ -0,0 +1,71 @@
# 全包变更影响映射
ENG-IMPACT-01是ENG-09的影响分析子项。`yarn check:impact --report`已接入`ci:check`,
报告写入`refactor/.cache/ci/impact.json`,由现有CI日志artifact收集。它解释哪些包和
检查受影响,不通过路径过滤减少当前CI任务。
## 模型与校验
`impact-policy.json`显式登记22个包、核心的全部生态验证消费者、站点消费者、跨插件
关系、示例及共享文件。包新增/删除/改名必须同步维护映射,不能默默忽略。
`impact-model.mjs`读取当前manifest并用固定semver解析内部依赖和npm别名的版本
范围;不匹配当前workspace版本时失败。再用当前TypeScript解析src/types/public的
静态import/export/require/import-type及相对跨包引用。未知workspace导入或解析
失败明确报错;计算型动态import使变更保守扩大到全包,并列出需审查的源文件。
关系分开记录:
- manifest及静态导入是实际可观察依赖,沿消费者方向传播。
- 核心到全部21个生态包是**验证契约**,不是声称所有包都有运行时import。
核心API/DOM/类型/媒体行为改变需要全生态检查。
- 库包到站点是文档、示例及编辑器声明消费关系。
- danmuku到danmuku-mask来自共享`$danmuku` DOM,源码和组合示例作为证据;
不能只靠package.json发现它。
普通插件源码改动影响自身和站点;修改danmuku还影响mask;核心、共享脚本/类型、
锁文件和根工具配置影响全包。compiled/uncompiled路径映射回源码所属包。
示例的初始所有权来自冻结包清单,SITE-01仍负责遗留示例/历史thumbnail来源的完整
复盘;未分类路径始终扩大到全包,报告`reviewRequired`,不把未知路径当作无影响。
## Git范围
本地默认包含HEAD之后的暂存、未暂存、删除和未跟踪文件;忽略目录遵循Git规则。
`yarn check:impact --base <commit/ref> --report`另外包含该基准与HEAD的merge-base
之后变化。禁用rename折叠,两端路径都会出现,因此跨包移动不会漏掉原包。
GitHub PR读取事件里的base SHA,push读取before SHA;初次push的全零SHA、手动/
复用入口缺少基准、事件基准对象不可用时检查整个跟踪文件集合,并明确记录fallback。
显式传错`--base`报错,不自行猜测。事件内容只用作结构化数据,不拼接shell命令。
报告保存HEAD、实际base/merge-base、是否有工作区变化、文件/策略/manifest/锁指纹。
## CI门槛与能力边界
六项必需命令不能从策略中漏掉:`ci:check`、`ci:build`、`test:package`、
`test:browser`、`test:coverage`、`test:performance`。固定yaml2.8.2解析实际workflow,
检查对应job/步骤无条件执行、没有continue-on-error、没有路径过滤,且checkout有
完整历史。命令被移走、改成echo或增加条件会失败。命令名还必须存在于根scripts。
必需命令必须是步骤首个非空、非注释行,只接受独立命令或当前显式bash的固定tee日志
格式;其他shell包装语法需审查后扩展。条件包装、`|| true`、非bash的tee和YAML布尔
`if: false`均有拒绝反例;checkout本身也不能带条件或允许失败。这是受限结构校验,
不是通用shell解释器,也不证明脚本内部的测试语义。
现有CI继续运行全部这些检查;影响报告没有输出允许跳过任务的开关。报告同时区分:
- `test:package`当前实际只有core/chapter;脚本读取其names数组核对策略,变更范围
后必须更新声明。其余19个库包完整安装消费仍缺证据,不能因识别为affected而过关。
- 覆盖率范围从实际coverage-policy读取;浏览器/性能命令运行已有用例,不代表所有
包、真实设备、远程SDK已验收。
- ENG-COVERAGE-01继续把契约、支持版本、测试ID、候选/报告及任务逐项连接;
本映射不是发布准入或对测试语义有效性的证明。
## 文件与验证
- `impact-model.mjs`:包模型、关系传播、策略和workflow约束。
- `impact.mjs`:Git范围/事件数据、报告及CLI。
- `impact.test.mjs`:实际22包关系、隔离包配置的反例、真实Git改名/删除/暂存/未跟踪、
PR/push及范围fallback、workflow漏检反例。
新增直接开发依赖yaml2.8.2和semver7.7.4,分别用于解析真实YAML和可靠的版本范围
校验,均固定为当前工具树已有版本。依赖和锁由Yarn维护;没有新增运行时包依赖。
运行`yarn test:baseline`或定向Node测试,再执行`yarn ci:check`。远端GitHub实际运行
仍由CI-04验收,本次本地检查不等于远端成功。
+309
View File
@@ -0,0 +1,309 @@
{
"schemaVersion": 1,
"packages": [
{
"name": "artplayer",
"kind": "core"
},
{
"name": "artplayer-plugin-ads",
"kind": "plugin"
},
{
"name": "artplayer-plugin-ambilight",
"kind": "plugin"
},
{
"name": "artplayer-plugin-asr",
"kind": "plugin"
},
{
"name": "artplayer-plugin-audio-track",
"kind": "plugin"
},
{
"name": "artplayer-plugin-auto-thumbnail",
"kind": "plugin"
},
{
"name": "artplayer-plugin-chapter",
"kind": "plugin"
},
{
"name": "artplayer-plugin-chromecast",
"kind": "plugin"
},
{
"name": "artplayer-plugin-danmuku",
"kind": "plugin"
},
{
"name": "artplayer-plugin-danmuku-mask",
"kind": "plugin"
},
{
"name": "artplayer-plugin-dash-control",
"kind": "plugin"
},
{
"name": "artplayer-plugin-document-pip",
"kind": "plugin"
},
{
"name": "artplayer-plugin-hls-control",
"kind": "plugin"
},
{
"name": "artplayer-plugin-jassub",
"kind": "plugin"
},
{
"name": "artplayer-plugin-multiple-subtitles",
"kind": "plugin"
},
{
"name": "artplayer-plugin-vast",
"kind": "plugin"
},
{
"name": "artplayer-plugin-vtt-thumbnail",
"kind": "plugin"
},
{
"name": "artplayer-proxy-canvas",
"kind": "proxy"
},
{
"name": "artplayer-proxy-mediabunny",
"kind": "proxy"
},
{
"name": "artplayer-tool-iframe",
"kind": "tool"
},
{
"name": "artplayer-tool-thumbnail",
"kind": "tool"
},
{
"name": "artplayer-vitepress",
"kind": "docs"
}
],
"core": "artplayer",
"site": "artplayer-vitepress",
"coreValidationConsumers": [
"artplayer-plugin-ads",
"artplayer-plugin-ambilight",
"artplayer-plugin-asr",
"artplayer-plugin-audio-track",
"artplayer-plugin-auto-thumbnail",
"artplayer-plugin-chapter",
"artplayer-plugin-chromecast",
"artplayer-plugin-danmuku",
"artplayer-plugin-danmuku-mask",
"artplayer-plugin-dash-control",
"artplayer-plugin-document-pip",
"artplayer-plugin-hls-control",
"artplayer-plugin-jassub",
"artplayer-plugin-multiple-subtitles",
"artplayer-plugin-vast",
"artplayer-plugin-vtt-thumbnail",
"artplayer-proxy-canvas",
"artplayer-proxy-mediabunny",
"artplayer-tool-iframe",
"artplayer-tool-thumbnail",
"artplayer-vitepress"
],
"coreEvidence": "refactor/compatibility.md",
"siteEvidence": "scripts/build-ts.js",
"edges": [
{
"dependency": "artplayer-plugin-danmuku",
"consumer": "artplayer-plugin-danmuku-mask",
"reason": "Mask renders into the $danmuku element supplied by the danmuku plugin",
"evidence": [
"packages/artplayer-plugin-danmuku-mask/src/index.js",
"docs/assets/example/danmuku.mask.js"
]
}
],
"examples": [
{
"file": "docs/assets/example/index.js",
"owner": "artplayer"
},
{
"file": "docs/assets/example/mobile.js",
"owner": "artplayer"
},
{
"file": "docs/assets/example/setting.test.js",
"owner": "artplayer"
},
{
"file": "docs/assets/example/hls.js",
"owner": "artplayer"
},
{
"file": "docs/assets/example/dash.js",
"owner": "artplayer"
},
{
"file": "docs/assets/example/flv.js",
"owner": "artplayer"
},
{
"file": "docs/assets/example/mpegts.js",
"owner": "artplayer"
},
{
"file": "docs/assets/example/webtorrent.js",
"owner": "artplayer"
},
{
"file": "docs/assets/example/thumbnail.js",
"owner": "artplayer"
},
{
"file": "docs/assets/example/ads.js",
"owner": "artplayer-plugin-ads"
},
{
"file": "docs/assets/example/ambilight.js",
"owner": "artplayer-plugin-ambilight"
},
{
"file": "docs/assets/example/asr.js",
"owner": "artplayer-plugin-asr"
},
{
"file": "docs/assets/example/audio.track.js",
"owner": "artplayer-plugin-audio-track"
},
{
"file": "docs/assets/example/auto.thumbnail.js",
"owner": "artplayer-plugin-auto-thumbnail"
},
{
"file": "docs/assets/example/chapter.js",
"owner": "artplayer-plugin-chapter"
},
{
"file": "docs/assets/example/chromecast.js",
"owner": "artplayer-plugin-chromecast"
},
{
"file": "docs/assets/example/danmuku.js",
"owner": "artplayer-plugin-danmuku"
},
{
"file": "docs/assets/example/danmuku.mask.js",
"owner": "artplayer-plugin-danmuku-mask"
},
{
"file": "docs/assets/example/dash.control.js",
"owner": "artplayer-plugin-dash-control"
},
{
"file": "docs/assets/example/document.pip.js",
"owner": "artplayer-plugin-document-pip"
},
{
"file": "docs/assets/example/hls.control.js",
"owner": "artplayer-plugin-hls-control"
},
{
"file": "docs/assets/example/jassub.js",
"owner": "artplayer-plugin-jassub"
},
{
"file": "docs/assets/example/multiple.subtitles.js",
"owner": "artplayer-plugin-multiple-subtitles"
},
{
"file": "docs/assets/example/vast.js",
"owner": "artplayer-plugin-vast"
},
{
"file": "docs/assets/example/vtt.thumbnail.js",
"owner": "artplayer-plugin-vtt-thumbnail"
},
{
"file": "docs/assets/example/canvas.js",
"owner": "artplayer-proxy-canvas"
},
{
"file": "docs/assets/example/mediabunny.js",
"owner": "artplayer-proxy-mediabunny"
},
{
"file": "docs/assets/example/iframe.js",
"owner": "artplayer-tool-iframe"
},
{
"file": "docs/assets/example/tool.thumbnail.js",
"owner": "artplayer-tool-thumbnail"
}
],
"sharedPrefixes": [
"scripts/",
"types/",
"test/",
".github/",
"refactor/scripts/",
"refactor/fixtures/",
"refactor/baselines/"
],
"sharedFiles": [
"package.json",
"yarn.lock",
".node-version",
".yarnrc",
".yarnrc.yml",
"tsconfig.json",
"tsconfig.base.json",
"eslint.config.js",
"playwright.config.js",
"playwright.performance.config.js",
"svgo.config.json",
".editorconfig",
"refactor/impact-policy.json"
],
"gates": [
{
"job": "checks",
"command": "yarn ci:check",
"scope": "Toolchain, task/commit/impact contracts, lint, current strict projects and existing Node/baseline suites"
},
{
"job": "checks",
"command": "yarn ci:build",
"scope": "All library builds, i18n, editor declarations, docs site and existing import suites"
},
{
"job": "browser-smoke",
"command": "yarn test:package",
"scope": "Installed core/chapter consumers only; other package installation acceptance remains pending"
},
{
"job": "browser-smoke",
"command": "yarn test:browser",
"scope": "All existing three-engine browser suites; does not imply every package/device has a suite"
},
{
"job": "browser-smoke",
"command": "yarn test:performance",
"scope": "Existing installed performance and resource-cleanup scenarios"
},
{
"job": "coverage",
"command": "yarn test:coverage",
"scope": "Current coverage-policy packages and critical module thresholds only"
}
],
"installedConsumerPackages": [
"artplayer",
"artplayer-plugin-chapter"
]
}
+3 -2
View File
@@ -4,7 +4,7 @@
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 220 项,范围 22 个包及工作区/示例。
状态:todo 133 / doing 6 / blocked 0 / done 81 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
状态:todo 132 / doing 6 / blocked 0 / done 82 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
@@ -83,7 +83,7 @@
| 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-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-IMPACT-01 | workspace<br>建立全包依赖与共享变更影响映射 | ENG-07, DOC-04 | 核心/构建/类型/锁文件到受影响包和必需生态检查的映射及CI接入 | 核心变化触发必需生态检查;新增包、未知共享文件或失配依赖不能静默漏检;解释受影响路径和测试命令 | M | done |
| 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 |
@@ -441,6 +441,7 @@
- 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-IMPACT-01: [记录](impact-analysis.md) [记录](changes/2026-09-12-ENG-IMPACT-01-mapping.md) [记录](baselines/impact-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)
+11
View File
@@ -1,5 +1,16 @@
# 进度与证据
## 最新完成:ENG-IMPACT-01 全包影响与必需检查映射
实际22包依赖/验证关系、共享构建/类型/锁文件及示例映射已接入ci:check;Git变更包含
改名两端及所有本地状态,未知路径保守扩大到全包。六项必需CI命令继续执行,并校验
真实workflow的条件、失败传播和完整历史;shell漏检已用反例复现并修复。
7组专项测试和完整CI911项通过,专项lint及actionlint通过。报告明确统一安装消费者
当前只有core/chapter,其余19个库包不能据此算验收完成。ENG-COVERAGE-01和父任务
ENG-09继续,VAST初始化选择仍待确认。当前220项:82 done、6 doing、132 todo。
本任务独立本地提交后审计自身;未推送或发布。见 [实施记录](changes/2026-09-12-ENG-IMPACT-01-mapping.md)
和 [验证证据](baselines/impact-validation.json)。
## 最新完成:ENG-AUDIT-01 每任务提交审计
新增实际Git历史审计并接入ci:check,核对状态迁移、独立提交、证据/plan共提交和合并分支。
+229
View File
@@ -0,0 +1,229 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import path from 'node:path'
import semver from 'semver'
import ts from 'typescript'
import YAML from 'yaml'
export function repositoryPath(file) {
assert(typeof file === 'string' && file.length && !file.includes('\\') && !file.includes('\0') && !path.posix.isAbsolute(file) && !/^[A-Z]:/i.test(file), `Invalid repository path: ${file}`)
const normalized = path.posix.normalize(file)
assert(normalized !== '..' && !normalized.startsWith('../'), `Path escapes repository: ${file}`)
return normalized
}
export function analyzeImpact(model, changedFiles) {
const names = model.packages.map(pkg => pkg.name)
const affected = new Set()
const reasons = []
const unknownPaths = []
function include(name, reason) {
if (!affected.has(name)) {
affected.add(name)
reasons.push({ package: name, ...reason })
}
}
const files = [...new Set(changedFiles.map(repositoryPath))].sort()
const all = (file, rule) => names.forEach(name => include(name, { file, rule }))
for (const file of files) {
if (file.startsWith('packages/')) {
const owner = file.split('/')[1]
assert(names.includes(owner), `Changed package is not mapped: ${owner}`)
include(owner, { file, rule: 'package-owner' })
}
else if (model.policy.sharedFiles.includes(file) || model.policy.sharedPrefixes.some(prefix => file.startsWith(prefix))) {
all(file, 'shared-engineering')
}
else {
const generated = names.find(name => file.startsWith(`docs/uncompiled/${name}/`) || file === `docs/compiled/${name}.js` || file === `docs/compiled/${name}.legacy.js` || file === `docs/compiled/${name}.mjs`)
const examples = model.policy.examples.filter(example => example.file === file)
if (generated) {
include(generated, { file, rule: 'generated-package-artifact' })
}
else if (examples.length) {
examples.forEach(example => include(example.owner, { file, rule: 'mapped-example' }))
}
else {
unknownPaths.push(file)
all(file, 'unclassified-conservative-fallback')
}
}
}
if (files.length && model.dynamicImports.length)
all(model.dynamicImports[0], 'unresolved-dynamic-import-conservative-fallback')
let previousSize = -1
while (affected.size !== previousSize) {
previousSize = affected.size
for (const edge of model.edges) {
if (affected.has(edge.dependency))
include(edge.consumer, { rule: edge.kind, via: edge.dependency, evidence: edge.evidence, explanation: edge.reason })
}
}
const affectedPackages = [...affected].sort()
return {
files,
affectedPackages,
reasons,
unknownPaths,
reviewRequired: Boolean(unknownPaths.length || model.dynamicImports.length),
requiredChecks: model.policy.gates,
requiredCIJobs: [...new Set(model.policy.gates.map(gate => gate.job))].sort(),
installedConsumerGaps: affectedPackages.filter(name => name !== model.policy.site && !model.policy.installedConsumerPackages.includes(name)),
limitation: 'CI gates remain unconditional. This dependency report selects no tests for omission and certifies neither missing package suites nor device/vendor acceptance.',
}
}
function importsIn(file, text) {
const source = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true)
assert.equal(source.parseDiagnostics.length, 0, `Cannot reliably scan malformed dependency source: ${file}`)
const imports = []
const dynamic = []
const add = (node) => {
if (node && ts.isStringLiteralLike(node))
imports.push(node.text)
else
dynamic.push(file)
}
function visit(node) {
if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) {
if (node.moduleSpecifier)
add(node.moduleSpecifier)
}
else if (ts.isImportTypeNode(node) && ts.isLiteralTypeNode(node.argument)) {
add(node.argument.literal)
}
else if (ts.isCallExpression(node) && (node.expression.kind === ts.SyntaxKind.ImportKeyword || (ts.isIdentifier(node.expression) && node.expression.text === 'require'))) {
add(node.arguments[0])
}
ts.forEachChild(node, visit)
}
visit(source)
return { imports, dynamic }
}
export function readImpactModel(directory) {
const read = file => JSON.parse(fs.readFileSync(path.join(directory, file), 'utf8'))
const policy = read('refactor/impact-policy.json')
assert.equal(policy.schemaVersion, 1)
const gateCommands = policy.gates.map(gate => gate.command)
assert.equal(new Set(gateCommands).size, gateCommands.length, 'Duplicate impact gate')
for (const command of ['yarn ci:check', 'yarn ci:build', 'yarn test:package', 'yarn test:browser', 'yarn test:coverage', 'yarn test:performance'])
assert(gateCommands.includes(command), `Missing mandatory ecosystem gate: ${command}`)
const names = policy.packages.map(pkg => pkg.name).sort()
assert.equal(new Set(names).size, names.length, 'Duplicate mapped package')
const actual = fs.readdirSync(path.join(directory, 'packages')).filter(name => fs.existsSync(path.join(directory, 'packages', name, 'package.json'))).sort()
assert.deepEqual(actual, names, 'Workspace package inventory changed; update impact ownership explicitly')
assert(names.includes(policy.core) && names.includes(policy.site), 'Missing core/site mapping')
assert.deepEqual([...policy.coreValidationConsumers].sort(), names.filter(name => name !== policy.core), 'Core changes must trigger every ecosystem validation consumer')
const packages = names.map((name) => {
const manifest = read(`packages/${name}/package.json`)
assert.equal(manifest.name, name, 'Package directory and manifest name disagree')
return { name, version: manifest.version, manifest }
})
const edges = []
const dynamicImports = []
const exists = (file) => {
repositoryPath(file)
assert(fs.existsSync(path.join(directory, file)), `Impact evidence is missing: ${file}`)
}
const edge = (dependency, consumer, kind, evidence, reason) => {
assert(names.includes(dependency) && names.includes(consumer), 'Unknown dependency or consumer in impact graph')
evidence.forEach(exists)
if (dependency !== consumer)
edges.push({ dependency, consumer, kind, evidence, reason })
}
for (const consumer of policy.coreValidationConsumers)
edge(policy.core, consumer, 'core-validation-contract', [policy.coreEvidence], 'Core API/DOM/type and media contracts require ecosystem validation, even without manifest dependencies')
for (const name of names.filter(name => name !== policy.site))
edge(name, policy.site, 'site-consumer-contract', [policy.siteEvidence], 'Library examples, generated declarations and docs consume the package')
for (const item of policy.edges)
edge(item.dependency, item.consumer, 'explicit-integration-contract', item.evidence, item.reason)
for (const example of policy.examples) {
assert(names.includes(example.owner), 'Example owner is not mapped')
exists(example.file)
}
for (const pkg of packages) {
for (const field of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) {
for (const [declaredName, value] of Object.entries(pkg.manifest[field] || {})) {
assert.equal(typeof value, 'string', `Invalid dependency declaration: ${pkg.name}/${declaredName}`)
const alias = /^npm:(.+)@([^@]+)$/.exec(value)
const name = alias?.[1] || declaredName
const range = alias?.[2] || value
if (!names.includes(name)) {
assert(!/^artplayer(?:-|$)/.test(name), `Unknown workspace dependency: ${name}`)
continue
}
const target = packages.find(candidate => candidate.name === name)
assert(semver.validRange(range) && semver.satisfies(target.version, range), `Workspace dependency range mismatch: ${pkg.name} ${field} ${name}@${range}, current ${target.version}`)
edge(name, pkg.name, `manifest-${field}`, [`packages/${pkg.name}/package.json`], `${declaredName}@${value}`)
}
}
const walk = (relative) => {
const absolute = path.join(directory, relative)
if (!fs.existsSync(absolute))
return
for (const entry of fs.readdirSync(absolute, { withFileTypes: true })) {
const file = `${relative}/${entry.name}`
assert(!entry.isSymbolicLink(), `Review redirected source before dependency scanning: ${file}`)
if (entry.isDirectory()) {
walk(file)
}
else if (/\.[cm]?[jt]sx?$/.test(file)) {
const found = importsIn(file, fs.readFileSync(path.join(directory, file), 'utf8'))
dynamicImports.push(...found.dynamic)
for (const specifier of found.imports) {
const resolved = specifier.startsWith('.') ? path.posix.normalize(path.posix.join(path.posix.dirname(file), specifier)) : specifier
const dependency = names.find(name => resolved === name || resolved.startsWith(`${name}/`) || resolved.startsWith(`packages/${name}/`))
if (dependency)
edge(dependency, pkg.name, 'static-source-import', [file], specifier)
else
assert(!/^artplayer(?:-|$)/.test(specifier), `Unknown workspace import ${specifier} in ${file}`)
}
}
}
}
for (const folder of ['src', 'types', 'public']) walk(`packages/${pkg.name}/${folder}`)
}
const scripts = read('package.json').scripts
for (const gate of policy.gates) {
assert(/^yarn [a-z][\w:-]*$/.test(gate.command), 'Impact gates require an explicit repository script')
assert(Object.hasOwn(scripts, gate.command.slice(5)), `Missing required command: ${gate.command}`)
}
const consumerSource = ts.createSourceFile('package-consumer.mjs', fs.readFileSync(path.join(directory, 'scripts/package-consumer.mjs'), 'utf8'), ts.ScriptTarget.Latest, true)
const consumerNames = consumerSource.statements.filter(ts.isVariableStatement).flatMap(statement => [...statement.declarationList.declarations]).filter(node => ts.isIdentifier(node.name) && node.name.text === 'names')
assert(consumerNames.length === 1 && consumerNames[0].initializer && ts.isArrayLiteralExpression(consumerNames[0].initializer), 'Review changed installed consumer scope extraction')
const installed = consumerNames[0].initializer.elements.map((element) => {
assert(ts.isStringLiteralLike(element), 'Installed consumer scope must be statically reviewable')
return element.text
})
assert.deepEqual(installed.sort(), [...policy.installedConsumerPackages].sort(), 'Installed consumer coverage changed; update its reported scope')
const coveragePackages = read('scripts/coverage-policy.json').packages
assert(coveragePackages.every(name => names.includes(name)), 'Unknown coverage package')
return { policy, packages, edges, dynamicImports: [...new Set(dynamicImports)].sort(), coveragePackages }
}
export function validateImpactWorkflow(text, model) {
const workflow = YAML.parse(text, { uniqueKeys: true })
assert(workflow.on && Object.hasOwn(workflow.on, 'pull_request') && Object.hasOwn(workflow.on, 'push'), 'Impact checks require both pull_request and push triggers')
for (const event of ['pull_request', 'push']) {
const config = workflow.on[event]
assert(!config?.paths && !config?.['paths-ignore'], 'Path filters may skip required ecosystem gates')
}
for (const gate of model.policy.gates) {
const job = workflow.jobs?.[gate.job]
assert(job && !Object.hasOwn(job, 'if') && !job['continue-on-error'], `Required impact job must not be conditional or allowed to fail: ${gate.job}`)
const steps = job.steps || []
const command = gate.command.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
assert(steps.some((step) => {
if (Object.hasOwn(step, 'if') || step['continue-on-error'] || typeof step.run !== 'string')
return false
const shell = step.shell || job.defaults?.run?.shell || workflow.defaults?.run?.shell
const defaultBash = !shell && /^(?:ubuntu|macos)-/.test(job['runs-on'])
if (shell !== 'bash' && !defaultBash)
return false
const first = step.run.split(/\r?\n/).map(line => line.trim()).find(line => line && !line.startsWith('#'))
return first === gate.command || (shell === 'bash' && new RegExp(`^${command} 2>&1 \\| tee refactor/\\.cache/ci/[a-z-]+\\.log$`).test(first))
}), `Required unconditional command missing from ${gate.job}: ${gate.command}`)
assert(steps.some(step => step.uses?.startsWith('actions/checkout@') && !Object.hasOwn(step, 'if') && !step['continue-on-error'] && step.with?.['fetch-depth'] === 0), `Impact and compatibility checks require full history: ${gate.job}`)
}
}
+102
View File
@@ -0,0 +1,102 @@
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'
import { analyzeImpact, readImpactModel, validateImpactWorkflow } from './impact-model.mjs'
import { hash } from './releases.mjs'
const root = fileURLToPath(new URL('../../', import.meta.url))
export function changedFiles(directory, options = {}) {
const git = args => execFileSync('git', args, { cwd: directory, encoding: 'utf8', maxBuffer: 32 * 1024 * 1024, stdio: ['ignore', 'pipe', 'pipe'] }).trimEnd()
const head = git(['rev-parse', 'HEAD'])
const split = text => text.split('\0').filter(Boolean)
const local = [...split(git(['diff', '--name-only', '--no-renames', '-z', 'HEAD', '--'])), ...split(git(['ls-files', '--others', '--exclude-standard', '-z']))]
let requested = options.base
let origin = requested ? 'explicit-base' : 'worktree'
if (!requested && options.eventName) {
const event = options.eventPath ? JSON.parse(fs.readFileSync(options.eventPath, 'utf8')) : {}
requested = options.eventName === 'pull_request' ? event.pull_request?.base?.sha : options.eventName === 'push' ? event.before : null
if (requested && /^0+$/.test(requested))
requested = null
if (requested)
assert(/^[a-f\d]{40}$/.test(requested), 'Invalid GitHub event base SHA')
origin = requested ? `github-${options.eventName}` : 'full-repository-event-fallback'
}
let base = null
let mergeBase = null
let committed = []
if (requested) {
try {
base = git(['rev-parse', '--verify', '--end-of-options', `${requested}^{commit}`])
mergeBase = git(['merge-base', base, head])
}
catch (error) {
if (options.base)
throw error
origin = 'full-repository-missing-event-base'
}
if (mergeBase)
committed = split(git(['diff', '--name-only', '--no-renames', '-z', mergeBase, head, '--']))
}
if (origin.startsWith('full-repository-'))
committed = split(git(['ls-files', '-z']))
return { head, requestedBase: requested || null, base, mergeBase, origin, worktreeChanged: local.length > 0, files: [...new Set([...committed, ...local])].sort() }
}
export function createImpactReport(directory = root, options = {}) {
const model = readImpactModel(directory)
validateImpactWorkflow(fs.readFileSync(path.join(directory, '.github/workflows/nodejs.yml'), 'utf8'), model)
const range = changedFiles(directory, options)
const impact = analyzeImpact(model, range.files)
const metadata = ['refactor/impact-policy.json', 'package.json', 'yarn.lock', ...model.packages.map(pkg => `packages/${pkg.name}/package.json`)]
const inputs = [...new Set([...metadata, ...range.files])].map((file) => {
const absolute = path.join(directory, file)
if (!fs.existsSync(absolute))
return { file, state: 'deleted-or-missing' }
if (fs.lstatSync(absolute).isSymbolicLink())
return { file, state: 'symlink', sha256: hash(fs.readlinkSync(absolute)) }
return { file, state: 'file', sha256: hash(fs.readFileSync(absolute)) }
})
return {
schemaVersion: 1,
range,
...impact,
packages: model.packages.map(pkg => ({ name: pkg.name, version: pkg.version })),
edges: model.edges,
unresolvedDynamicImports: model.dynamicImports,
coveragePackages: model.coveragePackages,
installedConsumerPackages: model.policy.installedConsumerPackages,
inputs,
}
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
let base
let report = false
const args = process.argv.slice(2)
for (let index = 0; index < args.length; index++) {
if (args[index] === '--report') {
report = true
}
else if (args[index] === '--base') {
base = args[++index]
assert(base && !base.startsWith('--'), 'Missing --base commit/ref')
}
else {
assert.fail('Use --report and/or --base <commit/ref>')
}
}
const result = createImpactReport(root, { base, eventName: process.env.GITHUB_EVENT_NAME, eventPath: process.env.GITHUB_EVENT_PATH })
if (report) {
const directory = path.join(root, 'refactor/.cache/ci')
fs.mkdirSync(directory, { recursive: true })
fs.writeFileSync(path.join(directory, 'impact.json'), `${JSON.stringify(result, null, 2)}\n`)
}
console.log(`Impact: ${result.files.length} changed paths, ${result.affectedPackages.length}/${result.packages.length} packages; ${result.range.origin}; required jobs ${result.requiredCIJobs.join(', ')}`)
if (result.reviewRequired)
console.warn(`Impact review: ${result.unknownPaths.length} unclassified paths and ${result.unresolvedDynamicImports.length} dynamic-import files conservatively require the full ecosystem`)
console.log(`Installed-consumer gaps among affected packages: ${result.installedConsumerGaps.length}; impact mapping is not release acceptance`)
}
+184
View File
@@ -0,0 +1,184 @@
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 -- Dependency and real Git change-selection contracts use the baseline runner.
import test from 'node:test'
import { fileURLToPath } from 'node:url'
import YAML from 'yaml'
import { analyzeImpact, readImpactModel, validateImpactWorkflow } from './impact-model.mjs'
import { changedFiles } from './impact.mjs'
const root = fileURLToPath(new URL('../../', import.meta.url))
const actual = readImpactModel(root)
const cache = path.join(root, 'refactor/.cache')
function fixture(t) {
const directory = fs.mkdtempSync(path.join(cache, 'impact-test-'))
t.after(() => {
const relative = path.relative(cache, directory)
assert(relative.startsWith('impact-test-') && !relative.includes(path.sep))
fs.rmSync(directory, { recursive: true, force: true })
})
const write = (file, value) => {
fs.mkdirSync(path.dirname(path.join(directory, file)), { recursive: true })
fs.writeFileSync(path.join(directory, file), typeof value === 'string' ? value : JSON.stringify(value))
}
const names = ['artplayer', 'artplayer-plugin-a', 'artplayer-plugin-b', 'artplayer-vitepress']
const policy = { ...structuredClone(actual.policy), packages: names.map(name => ({ name })), coreValidationConsumers: names.slice(1), edges: [], examples: [], installedConsumerPackages: ['artplayer'] }
write('refactor/impact-policy.json', policy)
write('refactor/compatibility.md', 'Explicit core validation contract')
write('scripts/build-ts.js', '// Shared docs declaration consumer')
write('scripts/package-consumer.mjs', 'export const names = ["artplayer"]')
write('scripts/coverage-policy.json', { packages: ['artplayer'] })
write('package.json', { scripts: Object.fromEntries(policy.gates.map(gate => [gate.command.slice(5), 'echo fixture'])) })
for (const name of names) {
write(`packages/${name}/package.json`, { name, version: '1.0.0' })
write(`packages/${name}/src/index.ts`, 'export default function example() {}')
}
const git = args => execFileSync('git', args, { cwd: directory, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, GIT_AUTHOR_NAME: 'Impact Fixture', GIT_AUTHOR_EMAIL: 'impact@example.test', GIT_COMMITTER_NAME: 'Impact Fixture', GIT_COMMITTER_EMAIL: 'impact@example.test', GIT_CONFIG_NOSYSTEM: '1', GIT_CONFIG_GLOBAL: process.platform === 'win32' ? 'NUL' : '/dev/null' } }).trim()
const commit = (message) => {
git(['add', '.'])
git(['-c', 'core.hooksPath=', '-c', 'commit.gpgSign=false', 'commit', '-m', message])
return git(['rev-parse', 'HEAD'])
}
return { directory, names, policy, write, git, commit, model: () => readImpactModel(directory) }
}
test('Actual impact graph covers core ecosystem and real plugin/DOM/example consumers without inventing installed coverage', () => {
assert.equal(actual.packages.length, 22)
const core = analyzeImpact(actual, ['packages/artplayer/src/index.ts'])
assert.equal(core.affectedPackages.length, 22)
assert.equal(core.installedConsumerGaps.length, 19)
const ads = analyzeImpact(actual, ['packages/artplayer-plugin-ads/src/session.ts'])
assert.deepEqual(ads.affectedPackages, ['artplayer-plugin-ads', 'artplayer-vitepress'])
const danmuku = analyzeImpact(actual, ['packages/artplayer-plugin-danmuku/src/danmuku.js'])
assert(danmuku.affectedPackages.includes('artplayer-plugin-danmuku-mask'))
assert(danmuku.reasons.some(reason => reason.rule === 'explicit-integration-contract'))
for (const file of ['docs/compiled/artplayer-plugin-ads.legacy.js', 'docs/uncompiled/artplayer-plugin-ads/index.js', 'docs/assets/example/ads.js'])
assert.deepEqual(analyzeImpact(actual, [file]).affectedPackages, ads.affectedPackages)
validateImpactWorkflow(fs.readFileSync(path.join(root, '.github/workflows/nodejs.yml'), 'utf8'), actual)
})
test('Static imports and manifest aliases propagate transitively; contradictory ranges and unknown packages fail', (t) => {
const repo = fixture(t)
repo.write('packages/artplayer-plugin-a/src/index.ts', 'export { default } from "../../artplayer-plugin-b/src/index"')
repo.write('packages/artplayer-plugin-b/package.json', { name: 'artplayer-plugin-b', version: '1.0.0', dependencies: { alias: 'npm:artplayer@^1.0.0' } })
const model = repo.model()
assert(model.edges.some(edge => edge.consumer === 'artplayer-plugin-b' && edge.dependency === 'artplayer' && edge.kind === 'manifest-dependencies'))
assert.deepEqual(analyzeImpact(model, ['packages/artplayer-plugin-b/src/index.ts']).affectedPackages, ['artplayer-plugin-a', 'artplayer-plugin-b', 'artplayer-vitepress'])
repo.write('packages/artplayer-plugin-b/package.json', { name: 'artplayer-plugin-b', version: '1.0.0', peerDependencies: { artplayer: '^2.0.0' } })
assert.throws(repo.model, /range mismatch/)
repo.write('packages/artplayer-plugin-b/package.json', { name: 'artplayer-plugin-b', version: '1.0.0' })
repo.write('packages/artplayer-plugin-a/src/index.ts', 'import unknown from "artplayer-plugin-missing"')
assert.throws(repo.model, /Unknown workspace import/)
repo.write('packages/artplayer-plugin-new/package.json', { name: 'artplayer-plugin-new', version: '1.0.0' })
assert.throws(repo.model, /inventory changed/)
})
test('Policy cannot omit core consumers or claim stale installed-package coverage', (t) => {
const missingGate = fixture(t)
missingGate.policy.gates = []
missingGate.write('refactor/impact-policy.json', missingGate.policy)
assert.throws(missingGate.model, /Missing mandatory ecosystem gate/)
const repo = fixture(t)
repo.policy.coreValidationConsumers.pop()
repo.write('refactor/impact-policy.json', repo.policy)
assert.throws(repo.model, /every ecosystem validation consumer/)
repo.policy.coreValidationConsumers = repo.names.slice(1)
repo.write('refactor/impact-policy.json', repo.policy)
repo.write('scripts/package-consumer.mjs', 'export const names = ["artplayer", "artplayer-plugin-a"]')
assert.throws(repo.model, /consumer coverage changed/)
repo.write('scripts/package-consumer.mjs', 'export const names = computeNames()')
assert.throws(repo.model, /scope extraction/)
})
test('Shared/unknown files and computed imports conservatively expand; unsafe or unmapped package paths are rejected', (t) => {
for (const file of ['yarn.lock', 'scripts/new-build-stage.mjs', 'types/new-boundary.d.ts'])
assert.equal(analyzeImpact(actual, [file]).affectedPackages.length, 22)
const unknown = analyzeImpact(actual, ['new-shared-config.toml'])
assert.equal(unknown.affectedPackages.length, 22)
assert.equal(unknown.reviewRequired, true)
for (const file of ['../outside', '/absolute', 'C:/absolute', 'packages\\artplayer\\src\\index.ts'])
assert.throws(() => analyzeImpact(actual, [file]), /path|Path/)
assert.throws(() => analyzeImpact(actual, ['packages/unmapped/src/index.js']), /not mapped/)
const repo = fixture(t)
repo.write('packages/artplayer-plugin-a/src/index.ts', 'export const load = (name: string) => import(name)')
const model = repo.model()
assert.deepEqual(model.dynamicImports, ['packages/artplayer-plugin-a/src/index.ts'])
assert.equal(analyzeImpact(model, ['packages/artplayer-plugin-b/src/index.ts']).affectedPackages.length, 4)
repo.write('packages/artplayer-plugin-a/src/index.ts', 'export { from')
assert.throws(repo.model, /malformed dependency source/)
})
test('CI workflow cannot silently skip or soften required impact gates', () => {
const source = fs.readFileSync(path.join(root, '.github/workflows/nodejs.yml'), 'utf8')
for (const edit of [
(workflow) => { workflow.on.pull_request = { paths: ['packages/artplayer/**'] } },
(workflow) => { workflow.jobs.coverage.if = 'false' },
(workflow) => { workflow.jobs.coverage.if = false },
(workflow) => { workflow.jobs.coverage['continue-on-error'] = true },
(workflow) => { workflow.jobs.checks.steps.find(step => step.run?.startsWith('yarn ci:check')).if = 'false' },
(workflow) => { workflow.jobs.checks.steps.find(step => step.run?.startsWith('yarn ci:check')).run = 'echo yarn ci:check' },
(workflow) => { workflow.jobs.checks.steps.find(step => step.run?.startsWith('yarn ci:check')).run = 'if false; then\nyarn ci:check\nfi' },
(workflow) => { workflow.jobs.checks.steps.find(step => step.run?.startsWith('yarn ci:check')).run = 'yarn ci:check || true' },
(workflow) => { workflow.jobs.checks.defaults.run.shell = 'sh' },
(workflow) => { workflow.jobs.checks.steps.find(step => step.run?.startsWith('yarn ci:check')).if = false },
(workflow) => { workflow.jobs['browser-smoke'].steps.find(step => step.uses?.startsWith('actions/checkout@')).if = false },
(workflow) => { workflow.jobs['browser-smoke'].steps.find(step => step.uses?.startsWith('actions/checkout@')).with['fetch-depth'] = 1 },
]) {
const workflow = YAML.parse(source)
edit(workflow)
assert.throws(() => validateImpactWorkflow(YAML.stringify(workflow), actual), /Path filters|Required|full history/)
}
})
test('Real Git selection includes both sides of renames, staged/unstaged/untracked paths and deletions', (t) => {
const repo = fixture(t)
repo.git(['init', '-b', 'main'])
repo.write('packages/artplayer-plugin-a/src/old.ts', 'old content')
repo.write('packages/artplayer-plugin-a/src/deleted.ts', 'delete later')
const base = repo.commit('base')
repo.git(['mv', 'packages/artplayer-plugin-a/src/old.ts', 'packages/artplayer-plugin-b/src/renamed.ts'])
repo.commit('rename across packages')
repo.write('packages/artplayer-plugin-a/src/staged.ts', 'staged')
repo.git(['add', 'packages/artplayer-plugin-a/src/staged.ts'])
repo.write('packages/artplayer-plugin-a/src/space name.ts', 'untracked')
repo.write('packages/artplayer-plugin-b/src/index.ts', 'unstaged edit')
fs.unlinkSync(path.join(repo.directory, 'packages/artplayer-plugin-a/src/deleted.ts'))
const range = changedFiles(repo.directory, { base })
assert.equal(range.origin, 'explicit-base')
assert.equal(range.mergeBase, base)
assert.deepEqual(range.files, [
'packages/artplayer-plugin-a/src/deleted.ts',
'packages/artplayer-plugin-a/src/old.ts',
'packages/artplayer-plugin-a/src/space name.ts',
'packages/artplayer-plugin-a/src/staged.ts',
'packages/artplayer-plugin-b/src/index.ts',
'packages/artplayer-plugin-b/src/renamed.ts',
])
assert.equal(range.worktreeChanged, true)
assert(!changedFiles(repo.directory).files.includes('packages/artplayer-plugin-a/src/old.ts'))
assert.throws(() => changedFiles(repo.directory, { base: 'missing-ref' }))
})
test('GitHub event base is read as data; absent, zero and unavailable event bases require full repository checks', (t) => {
const repo = fixture(t)
repo.git(['init', '-b', 'main'])
const base = repo.commit('base')
repo.write('packages/artplayer-plugin-a/src/index.ts', 'changed source')
repo.commit('change')
const eventPath = path.join(repo.directory, '.git/event.json')
const event = data => fs.writeFileSync(eventPath, JSON.stringify(data))
event({ pull_request: { base: { sha: base } } })
assert.deepEqual(changedFiles(repo.directory, { eventName: 'pull_request', eventPath }).files, ['packages/artplayer-plugin-a/src/index.ts'])
for (const before of ['0'.repeat(40), 'a'.repeat(40), undefined]) {
event({ before })
const range = changedFiles(repo.directory, { eventName: 'push', eventPath })
assert(range.origin.startsWith('full-repository-'))
assert(range.files.includes('packages/artplayer-plugin-b/src/index.ts'))
}
event({ before: '--not-a-sha' })
assert.throws(() => changedFiles(repo.directory, { eventName: 'push', eventPath }), /Invalid GitHub event base/)
})
+6 -2
View File
@@ -633,14 +633,18 @@
"risk": "M",
"id": "ENG-IMPACT-01",
"title": "建立全包依赖与共享变更影响映射",
"status": "todo",
"status": "done",
"dependsOn": [
"ENG-07",
"DOC-04"
],
"deliverable": "核心/构建/类型/锁文件到受影响包和必需生态检查的映射及CI接入",
"acceptance": "核心变化触发必需生态检查;新增包、未知共享文件或失配依赖不能静默漏检;解释受影响路径和测试命令",
"evidence": []
"evidence": [
"impact-analysis.md",
"changes/2026-09-12-ENG-IMPACT-01-mapping.md",
"baselines/impact-validation.json"
]
},
{
"phase": "2 工程保障",
+2 -2
View File
@@ -7126,7 +7126,7 @@ seedrandom@^3.0.5:
resolved "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz"
integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==
semver@^7.0.0, semver@^7.1.1, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.3, semver@^7.5.4, semver@^7.6.3, semver@^7.7.2, semver@^7.7.3:
semver@7.7.4, semver@^7.0.0, semver@^7.1.1, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.3, semver@^7.5.4, semver@^7.6.3, semver@^7.7.2, semver@^7.7.3:
version "7.7.4"
resolved "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz"
integrity sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==
@@ -8153,7 +8153,7 @@ yaml-eslint-parser@^2.0.0:
eslint-visitor-keys "^5.0.0"
yaml "^2.0.0"
yaml@^2.0.0, yaml@^2.6.0, yaml@^2.8.2:
yaml@2.8.2, yaml@^2.0.0, yaml@^2.6.0, yaml@^2.8.2:
version "2.8.2"
resolved "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz"
integrity sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==