mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-08 10:56:15 -08:00
ci(refactor): [ENG-IMPACT-01] map workspace changes to required ecosystem checks
This commit is contained in:
1 parent
e0aacc5d6a
commit
6abac891ce
14 files changed
+1070
-9
No files matched your search
+6
-3
@@ -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"
|
||||
|
||||
@@ -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) | 每次行为/类型/结构变化的详细记录 |
|
||||
|
||||
@@ -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初始化选择仍待用户确认。
|
||||
@@ -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 在构建后仍能通过;不是对生产源码执行自动修复。
|
||||
|
||||
@@ -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验收,本次本地检查不等于远端成功。
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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共提交和合并分支。
|
||||
|
||||
@@ -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}`)
|
||||
}
|
||||
}
|
||||
@@ -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`)
|
||||
}
|
||||
@@ -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
@@ -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 工程保障",
|
||||
|
||||
@@ -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==
|
||||
|
||||
Reference in new issue
Block a user