ci: [CI-01] verify identical tarballs on three Node runtimes

This commit is contained in:
Harvey Zhao committed 2026-09-13 13:21:17 +08:00
1 parent 585e11c53c
commit 38355ae4cf
21 files changed
+839 -27

No files matched your search

+24
View File
@@ -107,6 +107,30 @@ jobs:
run: |
yarn test:package 2>&1 | tee refactor/.cache/ci/package.log
node --input-type=module -e 'import fs from "node:fs"; const { output } = JSON.parse(fs.readFileSync("refactor/.cache/packages/latest.json")); fs.appendFileSync(process.env.GITHUB_ENV, `ARTPLAYER_BROWSER_ARTIFACTS=${output}/browser-artifacts.json\n`);'
- name: Select Node 20 consumer runtime
id: consumer-node-20
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 20.19.0
package-manager-cache: false
- name: Verify Node 20 installed consumers
run: node scripts/package-runtime.mjs --expected-node 20.19.0 2>&1 | tee refactor/.cache/ci/consumer-node-20.log
- name: Select Node 22 consumer runtime
id: consumer-node-22
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22.12.0
package-manager-cache: false
- name: Verify Node 22 installed consumers
run: node scripts/package-runtime.mjs --expected-node 22.12.0 2>&1 | tee refactor/.cache/ci/consumer-node-22.log
- name: Restore canonical runtime for browser tools
id: restore-canonical-node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: .node-version
package-manager-cache: false
- name: Verify canonical installed consumers
run: node scripts/package-runtime.mjs --canonical 2>&1 | tee refactor/.cache/ci/consumer-node-canonical.log
- name: Run all three engines
run: yarn test:browser 2>&1 | tee refactor/.cache/ci/browser.log
- name: Verify iframe cached history and interrupted navigation
+3 -2
View File
@@ -45,7 +45,7 @@
"check:toolchain": "node scripts/check-toolchain.mjs",
"lint:fix": "eslint \"packages/*/{src,public,types,package.json}\" \"scripts/*.{js,mjs}\" \"test/*\" \"docs/assets/ts/*\" \"types/*.d.ts\" \"playwright*.config.js\" --fix",
"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/ci-summary.test.js",
"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/ci-summary.test.js test/package-runtime.test.js",
"test:baseline": "node --test refactor/scripts/*.test.mjs",
"ci:check": "yarn check:toolchain --strict && yarn check:commits --report && yarn check:impact --report && yarn check:ci && yarn test:contracts && yarn check:contracts --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",
@@ -92,7 +92,8 @@
"test:multiple-subtitles-types-package": "node refactor/scripts/multiple-subtitles-package-types.mjs",
"test:jassub": "node --test test/jassub.test.js refactor/scripts/jassub-contract.test.mjs",
"check:ci": "node refactor/scripts/ci-workflow.mjs",
"test:ci": "node --test test/ci-summary.test.js refactor/scripts/ci-workflow.test.mjs refactor/scripts/impact.test.mjs"
"test:ci": "node --test test/ci-summary.test.js refactor/scripts/ci-workflow.test.mjs refactor/scripts/impact.test.mjs test/package-runtime.test.js",
"test:package:runtime": "node scripts/package-runtime.mjs --canonical"
},
"browserslist": "last 1 Chrome version",
"devDependencies": {
@@ -0,0 +1,441 @@
{
"schemaVersion": 1,
"task": "CI-01",
"date": "2026-09-13",
"baseCommit": "585e11c53c03b4b12933b21dcfe8c4a117299b2b",
"candidateScope": "Accompanying CI-01 working tree; production package code unchanged",
"nodeBinaries": [
{
"node": "20.19.0",
"platform": "win-x64",
"sha256": "6e3a39787e667d50487f7335c85636c2823a53e636d73c2c841d45da4e57906c",
"source": "https://nodejs.org/download/release/v20.19.0/SHASUMS256.txt"
},
{
"node": "22.12.0",
"platform": "win-x64",
"sha256": "b3b117a08ee61efee09e6fd523ab33c0c018da1b570bde08e4fd914dc1170ed6",
"source": "https://nodejs.org/download/release/v22.12.0/SHASUMS256.txt"
}
],
"build": {
"node": "24.21.0",
"yarn": "1.22.22",
"legacyTypeModes": 5,
"preciseTypeModes": 8,
"knownTypeBlockers": 0,
"knownRuntimeBlockers": 1
},
"runs": [
{
"node": "20.19.0",
"buildNode": "24.21.0",
"passed": true,
"candidateChecks": [
"DIST.cjs",
"DIST.cjs-legacy",
"DIST.esm",
"DIST.esm-default-only",
"DIST.esm-legacy",
"DIST.runtime-cjs-identity",
"DIST.runtime-esm-identity",
"DIST.runtime-legacy-identity",
"DIST.runtime-shared-statics",
"DIST.runtime-types-only",
"DIST.global-core",
"DIST.amd-core",
"DIST.global-core-legacy",
"DIST.amd-core-legacy",
"DIST.global-chapter",
"DIST.amd-chapter",
"DIST.global-chapter-legacy",
"DIST.amd-chapter-legacy",
"SSR.import-template",
"SSR.constructor-browser-only",
"SSR.custom-ua-umd-legacy",
"SSR.custom-ua-esm",
"DIST.i18n-cjs-esm-global",
"DIST.exports-boundary",
"DIST.direct-esm-file",
"EVENT.lazy-registry",
"EVENT.symbol-number-channels",
"EVENT.context-removal",
"EVENT.once-throw-consumed",
"EVENT.chain-context-arguments",
"EVENT.once-reentry",
"EVENT.dispatch-snapshot",
"EVENT.off-original-once",
"EVENT.throw-propagation",
"SSR.defaults-missing-navigator-known-failure",
"API.defaults-controlled-language"
],
"publishedChecks": [
"DIST.cjs",
"DIST.cjs-legacy",
"DIST.esm",
"DIST.esm-default-only",
"DIST.esm-legacy",
"DIST.global-core",
"DIST.amd-core",
"DIST.global-core-legacy",
"DIST.amd-core-legacy",
"DIST.global-chapter",
"DIST.amd-chapter",
"DIST.global-chapter-legacy",
"DIST.amd-chapter-legacy",
"SSR.import-template",
"SSR.constructor-browser-only",
"SSR.custom-ua-umd-legacy",
"SSR.custom-ua-esm",
"DIST.i18n-cjs-esm-global",
"DIST.exports-boundary",
"DIST.direct-esm-file",
"EVENT.lazy-registry",
"EVENT.symbol-number-channels",
"EVENT.context-removal",
"EVENT.once-throw-consumed",
"EVENT.chain-context-arguments",
"EVENT.once-reentry",
"EVENT.dispatch-snapshot",
"EVENT.off-original-once",
"EVENT.throw-propagation",
"SSR.defaults-missing-navigator-known-failure",
"API.defaults-controlled-language"
],
"unresolved": {
"name": "ReferenceError",
"message": "navigator is not defined",
"historical": false,
"resolved": false
},
"installedPackages": [
{
"name": "artplayer",
"version": "5.4.1",
"sha256": "6298aa247ca273fa5fb80297fc49d4434609e611e24a7f24cb37752c6768c214"
},
{
"name": "artplayer-plugin-chapter",
"version": "1.1.0",
"sha256": "53eb3c1bbcc046a28ae499cfb19731eb66a16453bf44eae73e03693bbb510793"
}
],
"report": {
"file": "refactor/.cache/packages/run-oT7iFE/runtime-node-20.19.0.json",
"sha256": "19f69281782a34a7829b47bdfbebeaa22a2bcbaef9e1b982bdd615247b136286"
},
"installLog": {
"file": "refactor/.cache/packages/run-oT7iFE/runtime-node-20.19.0-install.log",
"sha256": "048cba88c2a0a9c14e1e195ac88af9c4c359b6058672d542c49cdb2c6e4f1e6d"
}
},
{
"node": "22.12.0",
"buildNode": "24.21.0",
"passed": true,
"candidateChecks": [
"DIST.cjs",
"DIST.cjs-legacy",
"DIST.esm",
"DIST.esm-default-only",
"DIST.esm-legacy",
"DIST.runtime-cjs-identity",
"DIST.runtime-esm-identity",
"DIST.runtime-legacy-identity",
"DIST.runtime-shared-statics",
"DIST.runtime-types-only",
"DIST.global-core",
"DIST.amd-core",
"DIST.global-core-legacy",
"DIST.amd-core-legacy",
"DIST.global-chapter",
"DIST.amd-chapter",
"DIST.global-chapter-legacy",
"DIST.amd-chapter-legacy",
"SSR.import-template",
"SSR.constructor-browser-only",
"SSR.custom-ua-umd-legacy",
"SSR.custom-ua-esm",
"DIST.i18n-cjs-esm-global",
"DIST.exports-boundary",
"DIST.direct-esm-file",
"EVENT.lazy-registry",
"EVENT.symbol-number-channels",
"EVENT.context-removal",
"EVENT.once-throw-consumed",
"EVENT.chain-context-arguments",
"EVENT.once-reentry",
"EVENT.dispatch-snapshot",
"EVENT.off-original-once",
"EVENT.throw-propagation",
"SSR.defaults-missing-navigator-known-failure",
"API.defaults-controlled-language"
],
"publishedChecks": [
"DIST.cjs",
"DIST.cjs-legacy",
"DIST.esm",
"DIST.esm-default-only",
"DIST.esm-legacy",
"DIST.global-core",
"DIST.amd-core",
"DIST.global-core-legacy",
"DIST.amd-core-legacy",
"DIST.global-chapter",
"DIST.amd-chapter",
"DIST.global-chapter-legacy",
"DIST.amd-chapter-legacy",
"SSR.import-template",
"SSR.constructor-browser-only",
"SSR.custom-ua-umd-legacy",
"SSR.custom-ua-esm",
"DIST.i18n-cjs-esm-global",
"DIST.exports-boundary",
"DIST.direct-esm-file",
"EVENT.lazy-registry",
"EVENT.symbol-number-channels",
"EVENT.context-removal",
"EVENT.once-throw-consumed",
"EVENT.chain-context-arguments",
"EVENT.once-reentry",
"EVENT.dispatch-snapshot",
"EVENT.off-original-once",
"EVENT.throw-propagation",
"SSR.defaults-missing-navigator-known-failure",
"API.defaults-controlled-language"
],
"unresolved": {
"name": "ReferenceError",
"message": "navigator is not defined",
"historical": false,
"resolved": false
},
"installedPackages": [
{
"name": "artplayer",
"version": "5.4.1",
"sha256": "6298aa247ca273fa5fb80297fc49d4434609e611e24a7f24cb37752c6768c214"
},
{
"name": "artplayer-plugin-chapter",
"version": "1.1.0",
"sha256": "53eb3c1bbcc046a28ae499cfb19731eb66a16453bf44eae73e03693bbb510793"
}
],
"report": {
"file": "refactor/.cache/packages/run-oT7iFE/runtime-node-22.12.0.json",
"sha256": "6014c69d26444d0c1884aa2be5de801351d4b4f120682ecaef1d5d440ff67084"
},
"installLog": {
"file": "refactor/.cache/packages/run-oT7iFE/runtime-node-22.12.0-install.log",
"sha256": "0ad1239965a5caa605c43986d81a6efa3778fdc4d5752534edd8b21f1059fe62"
}
},
{
"node": "24.21.0",
"buildNode": "24.21.0",
"passed": true,
"candidateChecks": [
"DIST.cjs",
"DIST.cjs-legacy",
"DIST.esm",
"DIST.esm-default-only",
"DIST.esm-legacy",
"DIST.runtime-cjs-identity",
"DIST.runtime-esm-identity",
"DIST.runtime-legacy-identity",
"DIST.runtime-shared-statics",
"DIST.runtime-types-only",
"DIST.global-core",
"DIST.amd-core",
"DIST.global-core-legacy",
"DIST.amd-core-legacy",
"DIST.global-chapter",
"DIST.amd-chapter",
"DIST.global-chapter-legacy",
"DIST.amd-chapter-legacy",
"SSR.import-template",
"SSR.constructor-browser-only",
"SSR.custom-ua-umd-legacy",
"SSR.custom-ua-esm",
"DIST.i18n-cjs-esm-global",
"DIST.exports-boundary",
"DIST.direct-esm-file",
"EVENT.lazy-registry",
"EVENT.symbol-number-channels",
"EVENT.context-removal",
"EVENT.once-throw-consumed",
"EVENT.chain-context-arguments",
"EVENT.once-reentry",
"EVENT.dispatch-snapshot",
"EVENT.off-original-once",
"EVENT.throw-propagation",
"SSR.defaults-missing-navigator-known-failure",
"API.defaults-controlled-language"
],
"publishedChecks": [
"DIST.cjs",
"DIST.cjs-legacy",
"DIST.esm",
"DIST.esm-default-only",
"DIST.esm-legacy",
"DIST.global-core",
"DIST.amd-core",
"DIST.global-core-legacy",
"DIST.amd-core-legacy",
"DIST.global-chapter",
"DIST.amd-chapter",
"DIST.global-chapter-legacy",
"DIST.amd-chapter-legacy",
"SSR.import-template",
"SSR.constructor-browser-only",
"SSR.custom-ua-umd-legacy",
"SSR.custom-ua-esm",
"DIST.i18n-cjs-esm-global",
"DIST.exports-boundary",
"DIST.direct-esm-file",
"EVENT.lazy-registry",
"EVENT.symbol-number-channels",
"EVENT.context-removal",
"EVENT.once-throw-consumed",
"EVENT.chain-context-arguments",
"EVENT.once-reentry",
"EVENT.dispatch-snapshot",
"EVENT.off-original-once",
"EVENT.throw-propagation",
"SSR.defaults-missing-navigator-known-failure",
"API.defaults-controlled-language"
],
"unresolved": {
"name": "ReferenceError",
"message": "navigator is not defined",
"historical": false,
"resolved": false
},
"installedPackages": [
{
"name": "artplayer",
"version": "5.4.1",
"sha256": "6298aa247ca273fa5fb80297fc49d4434609e611e24a7f24cb37752c6768c214"
},
{
"name": "artplayer-plugin-chapter",
"version": "1.1.0",
"sha256": "53eb3c1bbcc046a28ae499cfb19731eb66a16453bf44eae73e03693bbb510793"
}
],
"report": {
"file": "refactor/.cache/packages/run-oT7iFE/runtime-node-24.21.0.json",
"sha256": "8ca41aa19f023561500a9956d8c34a873cf438e062214dfe76310218ccf7b3fd"
},
"installLog": {
"file": "refactor/.cache/packages/run-oT7iFE/runtime-node-24.21.0-install.log",
"sha256": "973f6f01784e113ec2a4da62c6df6dcb32c99b548f23549cc3bc0eec5280f462"
}
}
],
"firstFailure": {
"error": "ReferenceError: navigator is not defined",
"result": "reproduced in published and candidate; CORE-25 open",
"log": {
"file": "refactor/.cache/ci01-node20-runtime.log",
"sha256": "2b2074af5e5afaab44530a047656a53d5736b19fb2dfe40dd54af724570ae60f"
}
},
"strictRelease": {
"command": "yarn test:package:release",
"expectedExitCode": 1,
"actualExitCode": 1,
"reason": "Known runtime blockers remain",
"log": {
"file": "refactor/.cache/ci01-node-release-negative.log",
"sha256": "6ff96a023493ea284411b725734b2cedf7eac3e4b0602ba458dcb14f09515319"
}
},
"localCI": {
"state": "passed",
"command": "yarn ci:check",
"total": 2429,
"unit": 2031,
"engineering": 27,
"baseline": 371,
"seconds": 240.99,
"targeted": 44,
"metadata": 9,
"actionlint": "1.7.12 passed",
"explicitLint": "passed"
},
"remoteActions": null,
"releaseReady": false,
"limitations": [
"Only core/chapter actual package fixture",
"Three Windows Node runtimes; hosted OS runs not verified",
"Historical and candidate missing-navigator failure is an explicit unresolved observation",
"Tooling minimum clean installs and older consumer Node scope remain unverified"
],
"logs": [
{
"file": "refactor/.cache/ci01-node-ci.log",
"sha256": "b753192c80948138e6ef3c063cf68e9a293496c0601340d24c4122d009975071"
},
{
"file": "refactor/.cache/ci01-node-test-ci.log",
"sha256": "c0f509f45e12f0fcf71939c520be4770673cb504e7c44622060ce111efd43279"
},
{
"file": "refactor/.cache/ci01-node-metadata.log",
"sha256": "7ed48f9eab2ac84ff388255dc87c7e28bcaec5125a5fee1651b9326946901e15"
},
{
"file": "refactor/.cache/ci01-node-actionlint.log",
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"file": "refactor/.cache/ci01-node-lint-final.log",
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
}
],
"inputs": [
{
"file": "scripts/package-runtime.mjs",
"sha256": "ecbe917118e67e4c53965002912bf4bded821f6bd6d00db1f3f1a56e62cab732"
},
{
"file": "scripts/package-consumer.mjs",
"sha256": "3ab9a6ea73981f777ff7db8a7cb0e54c61b5be43c1bcbc05013423afd1645a25"
},
{
"file": "scripts/package-check.mjs",
"sha256": "8b4a65f7f65083104cb0dba6325e622f221574c213f59656367c3c71b610571b"
},
{
"file": "test/package/runtime.cjs",
"sha256": "cf70882e29817dacfde9323b54890c3d0ae23eb2536c98ce90d573963de9721f"
},
{
"file": "test/package-runtime.test.js",
"sha256": "633e15b760c2ea8b9a0cb3eee49fde353f77a8dda7655595f9bded13e48dc4cb"
},
{
"file": "test/package-check.test.js",
"sha256": "e4f41015c81331ef085a158ec0330e82786402aa4f7e0308b7e3b1a95f2a6ff5"
},
{
"file": "refactor/scripts/ci-workflow.mjs",
"sha256": "588ad0e032c3e9a16620202a21bd293edfb042954411f58d7352d6fee38a631c"
},
{
"file": "refactor/scripts/ci-workflow.test.mjs",
"sha256": "6cac9f185739e9ea7753f7220ddcfa4b708a9699c029b998e2e257f6a04dcdb0"
},
{
"file": ".github/workflows/nodejs.yml",
"sha256": "f3f8476f4a35a80845350f83beaba15fdb70690273f9b56e4614b5e048d002bd"
},
{
"file": "package.json",
"sha256": "4b68d39f4e8fb3c1634ab11bde8686c48aa277e2bf2775a4bf3472f2d1030aca"
}
],
"inputByteScope": "Local working-tree bytes; Git checkout line endings may differ"
}
@@ -0,0 +1,64 @@
# CI-01 同一 tarball 的 Node 消费者矩阵
基于 585e11c53c03b4b12933b21dcfe8c4a117299b2b,标准构建 Node 为 24.21.0。
新增 Node 20.19.0、22.12.0、24.21.0 的实际安装运行,CI-01 保持 doing。
这些是私有根包 engines 的两个分支边界与标准环境,不是发布包声明的最低 Node;
目前各发布包没有 engines.node,不能据此抬高消费者门槛或宣称更早 Node 不受支持。
## 实现与维护
package-check 仍在标准 Node 完成构建、Yarn pack、隔离安装、五个旧类型模式和八个
精确类型模式。报告增加真实源码 SHA、Yarn 版本/执行路径与发布基线成员指纹,保留
已校验的 published-artifacts。候选输出保持原路径、内容与 browser-artifacts 格式。
scripts/package-runtime.mjs 只依赖 Node 内置模块,接管原 package-consumer 的运行时
夹具装配;原导出继续转发,类型编译器留在 package-consumer。新增入口在选择的精确
Node 中,读取同一 package-check 报告、校验 HEAD 和 tarball 摘要,使用记录的 Yarn
1.22.22 在仓库外临时目录 offline/frozen 安装同一 tarball,核对锁不变、逐文件字节、
非 workspace 链接,再执行 require/ESM、legacy/runtime/global/AMD、SSR、语言和
Emitter 契约。真实 child Node 版本写入结果;不会悄悄回退到构建 Node。
发布基线也在该 Node 执行。默认值和 API 形状在同一环境比较;历史行为与候选分别报告。
每次消费生成 runtime-node-<version>.json、安装日志,失败时记录错误及 stdout/stderr,
finally 删除经过根路径校验的临时目录。配置/来源预检的早期失败由 workflow tee 日志
保存;尚未创建运行报告时不会伪造成功报告。旧输出无新增来源字段需重新 test:package。
三个系统的 browser-smoke 在打包后依次切到固定 Node 20/22、运行消费者,最后恢复
.node-version 并重装消费;浏览器工具继续使用标准 Node。工作流校验保护版本、执行
顺序、无条件步骤和恢复动作。每个 Node 复用同一构建,不重新生成候选。
新入口 yarn test:package:runtime 在标准环境运行;其他版本显式执行
node scripts/package-runtime.mjs --expected-node <exact version>。需要先运行 yarn test:package。
新测试加入 test:node 与 test:ci;现有构建/类型/包消费入口保留。没有新增 npm 依赖或锁变化。
## 实际发现:CORE-25
Node 20.19.0 的首轮在实际发布 5.4.0 的静态 option getter 触发
ReferenceError: navigator is not defined。当前候选默认值中的 navigator?.language
同样不能保护未声明的全局变量。较新 Node 提供 navigator,原来的测试因此漏检。
现在夹具在每个 Node 明确删除 navigator,精确记录旧/候选都抛出这个错误;然后在
相同受控 en-US navigator 下比较默认值,并恢复原全局描述符。这仅是区分两个测试
场景,不是修复生产代码,也不代表真实浏览器语言验收。新增 CORE-DEFAULTS-SSR-01
风险和 CORE-25 修复任务,REVIEW-01 依赖该任务。候选 knownRuntimeBlockers=1,
严格 test:package:release 因该问题失败;常规兼容测试中的历史失败观察不等于发布就绪。
## 验证与后续
具体版本、二进制/产物摘要、三个运行结果、初始失败、严格发布反例和最终本地 CI
见 [证据](../baselines/node-consumer-validation.json)。新自动化反例覆盖文件篡改、
路径穿越、workspace 链接、错误 Node、过期源码、遗漏消费者与错误恢复运行时。
本批没有播放器生产代码或公开类型改动,不需要生成或提交库产物;真实打包在忽略
缓存的独立快照中执行。没有新浏览器播放或远端 Actions 执行。每个系统的实际运行、
更早消费者范围、最低工具链干净安装、全插件安装矩阵及类型/设备全组合仍需补证。
下一步独立修复 CORE-25,并重跑这三个 Node 和浏览器语言默认值;CI-01 不标完成。
回退本批恢复原消费者装配和 workflow,保留 CORE-25 的缺陷记录与待办。
Node 本地二进制仅下载到忽略缓存,执行前按官方发布摘要核验:
[20.19.0](https://nodejs.org/download/release/v20.19.0/SHASUMS256.txt)、
[22.12.0](https://nodejs.org/download/release/v22.12.0/SHASUMS256.txt)。旧版本用于隔离兼容测试,
没有替换标准 Node 或更改包管理器。提交主题包含 [CI-01];没有推送、部署或发布。
最终本地验证:完整 ci:check 2429 项(2031 单元、27 工程、371 基线),
定向 test:ci 44 项、元数据 9 项、actionlint 1.7.12 与定向只读 ESLint 全部通过。
严格发布反例按预期 exit 1;并未清除 CORE-25。
+9 -4
View File
@@ -19,7 +19,8 @@
| `yarn check:contracts --report` | 校验12类契约/22包归属、版本及报告对应;--write更新静态表,详见 [维护说明](contract-coverage.md) |
| `yarn ci:check` | 严格 Node/Yarn/锁检查、计划、只读 lint、类型、Node 和基线测试;允许写忽略缓存,不修改源码 |
| `yarn check:ci` | 只读校验实际工作流的完整系统矩阵、安装、缓存、报告和最终检查;已接入 ci:check |
| `yarn test:ci` | CI 汇总真实退出码、工作流破坏反例与全包影响分析,共 37 项 |
| `yarn test:ci` | CI 汇总退出码、工作流/影响分析反例与隔离运行时校验,共 44 项 |
| `yarn test:package:runtime` | 标准 Node 重装同一已检查 tarball;须先运行 test:package,其他 Node 使用显式 --expected-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 |
@@ -38,10 +39,12 @@ Actions 固定完整 SHA,Node 来自 .node-version,Yarn 固定 1.22.22;安
| --- | --- | --- | --- |
| checks | Linux、Windows | ci:check、ci:build;Linux 额外执行 actionlint | 45 分钟 |
| coverage | Linux、Windows | 既有源码映射与生命周期覆盖检查 | 15 分钟 |
| browser-smoke | Linux、Windows、macOS | 实际 core/chapter 安装产物、Chromium/Firefox/WebKit、iframe history、性能 | 60 分钟 |
| browser-smoke | Linux、Windows、macOS | 同一 core/chapter tarball 的 Node 20/22/24 消费,三浏览器、iframe history、性能 | 60 分钟 |
| CI result | Linux | 汇总以上三个作业组,所有结果必须为 success | 5 分钟 |
Node 均使用 .node-version 的 24.21.0。TS 5.9.3、4.3.5 和迁移运行时兼容 5.1.6
构建与浏览器工具使用 .node-version 的 24.21.0。安装运行时消费分别使用固定 20.19.0、
22.12.0 和标准 Node,不重新构建候选。它们是根工具 engines 的边界测试点,发布包没有
声明 Node 下限;更早 Node 和最低工具链干净安装仍需验证。TS 5.9.3、4.3.5 和迁移运行时兼容 5.1.6
继续由既有类型脚本按各自适用范围运行,不代表最低 Node 或所有包/TS 组合已验收。
矩阵不设置 fail-fast,单个系统失败后仍尽量收集其他系统证据;显式 bash 保留 tee
上游命令的失败退出码。影响报告继续扩大核心/共享变更到全生态,当前不缩减必需作业。
@@ -75,7 +78,9 @@ summary.md 和 GitHub Job Summary,不依赖先前下载的 artifact 来判断
必须针对实际故障,不以清缓存代替修复锁或构建问题。
本地验证与指纹见 [CI-01 记录](changes/2026-09-13-CI-01-matrix-summary.md)。
最低 Node 消费/工具环境、全生态安装矩阵和有证据的影响调度仍待 CI-01;CI-04 负责
三个 Node 的 Windows 安装证据及新发现见 [消费者记录](changes/2026-09-13-CI-01-node-consumers.md)。
CORE-25 的无 navigator 默认选项缺陷尚未修复,严格 test:package:release 会因此失败。
更早 Node 消费/最低工具环境、全生态安装矩阵和有证据的影响调度仍待 CI-01;CI-04 负责
各系统远端运行、冷热缓存、失败/取消演练及 required check 设置。没有新增远端通过证据。
## Pages 隔离与启用条件
+6
View File
@@ -52,6 +52,12 @@ ENG-PERF-01/02 的后续核心性能工作归 MOD-03,并在 REVIEW-01 重新
## 仍需全项目完成的范围
CI-01 后续 Node 20 安装消费发现 CORE-25:静态 option 在没有 navigator 时抛出
ReferenceError,发布 5.4.0 和当前候选均复现。较新 Node 的全局 navigator 掩盖了它。
已新增独立修复任务与 REVIEW-01 前置,并使严格 package release 检查阻断。
本核心阶段的历史完成记录不代表这个后续发现已修复,见
[Node 消费者记录](changes/2026-09-13-CI-01-node-consumers.md)。
- 各 PKG-*-01/05/06:完整发布基线、支持范围旧核心/新插件、新核心/旧插件、SDK/组合/分发;
chapter 的本轮四组合不能代替所有包,也不能代替它自己的最终进度/质量/全屏组合任务。
- EX-01/02/03 与 SITE:React/Vue、全部 demo/在线编辑器及远端资源;核心 Monaco 场景不是全站验收。
+7 -5
View File
@@ -2,9 +2,9 @@
> 由 tasks.json 生成。请修改数据后运行 `node refactor/scripts/plan.mjs --write`,不要手改本表。
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 223 项,范围 22 个包及工作区/示例。
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 224 项,范围 22 个包及工作区/示例。
状态:todo 86 / doing 16 / blocked 0 / done 121 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
状态:todo 87 / doing 16 / blocked 0 / done 121 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
@@ -14,7 +14,7 @@
| 包 | 基线版本 | 任务 |
| --- | --- | --- |
| artplayer | 5.4.1 | CORE-01, CORE-02, CORE-03, CORE-04, CORE-05, CORE-06, CORE-07, CORE-08, CORE-09, CORE-10, CORE-11, CORE-12, CORE-13, CORE-14, CORE-15, CORE-16, CORE-17, CORE-18, CORE-19, CORE-20, CORE-21, CORE-23, CORE-22, CORE-24 |
| artplayer | 5.4.1 | CORE-01, CORE-02, CORE-03, CORE-04, CORE-05, CORE-06, CORE-07, CORE-08, CORE-09, CORE-10, CORE-11, CORE-12, CORE-13, CORE-14, CORE-15, CORE-16, CORE-17, CORE-18, CORE-19, CORE-20, CORE-21, CORE-23, CORE-22, CORE-24, CORE-25 |
| artplayer-plugin-ads | 2.1.0 | PKG-ADS-01, PKG-ADS-02, PKG-ADS-03, PKG-ADS-04, PKG-ADS-05, PKG-ADS-06 |
| artplayer-plugin-ambilight | 1.1.0 | PKG-AMBILIGHT-01, PKG-AMBILIGHT-02, PKG-AMBILIGHT-03, PKG-AMBILIGHT-04, PKG-AMBILIGHT-PROXY-01, PKG-AMBILIGHT-05, PKG-AMBILIGHT-06, PKG-FACTORY-01 |
| artplayer-plugin-asr | 2.1.0 | PKG-ASR-01, PKG-ASR-02, PKG-ASR-03, PKG-ASR-04, PKG-ASR-05, PKG-ASR-06 |
@@ -132,6 +132,7 @@
| CORE-23 | artplayer<br>检查并补齐键盘、焦点与可访问名称 | CORE-13, CORE-14, CORE-17, ENG-05, BASE-04 | 主要控件/设置/模式退出的键盘与焦点回归、名称和字幕可用性检查及必要兼容修正 | 保持旧快捷键和 DOM/CSS 钩子;真实浏览器验证,不以静态属性检查代替交互;处理 BASE-DOM-01 的主要控件 Tab 不可达,保留既有名称与快捷键 | H | done |
| CORE-22 | artplayer<br>核心阶段完整验收 | CORE-21, CORE-23, PILOT-01, ENG-08, ENG-10, ENG-11 | 核心与旧插件的可自动化完整回归、资源/性能及明确外部验证缺口 | 核心自动化和公开差异处置通过;真实环境缺口链接包集成/REL 门槛,阶段完成不代表可公开发布 | H | done |
| CORE-24 | artplayer<br>修复连续切源的播放意图继承 | CORE-22, PKG-AUDIO-04 | 连续切源保留原播放意图,同时尊重显式暂停与最新来源,回归旧/新 audio | 真实暂停状态的受控测试、重入/取消/失败/销毁、三引擎和 main/legacy 产物验证通过;不恢复过期来源的公开副作用 | H | done |
| CORE-25 | artplayer<br>修复无 navigator 环境读取默认选项 | CORE-22 | 安全读取默认语言并保留浏览器已有默认值;实际 Node 与发布产物正反例 | 记录旧发布版 ReferenceError;候选在无 navigator 时可读默认选项,浏览器语言和公开类型不回归;不以补全局对象隐藏错误 | M | todo |
## 5 包迁移:artplayer-plugin-chapter
@@ -405,7 +406,7 @@
| ID | 范围 / 步骤 | 前置依赖 | 交付物 | 验收条件 | 风险 | 状态 |
| --- | --- | --- | --- | --- | --- | --- |
| REVIEW-01 | workspace<br>第一轮全项目架构与兼容性复盘 | REL-02, CORE-22, SITE-04, DOC-08, MOD-01, MOD-02, MOD-03 | 22 包结构、类型、旧 API/路径、测试盲区和维护文档的逐包报告 | 本轮阻断项由独立修复任务关闭并复测;环境缺口转交下一轮,不宣称已发布就绪 | H | todo |
| REVIEW-01 | workspace<br>第一轮全项目架构与兼容性复盘 | REL-02, CORE-22, SITE-04, DOC-08, MOD-01, MOD-02, MOD-03, CORE-25 | 22 包结构、类型、旧 API/路径、测试盲区和维护文档的逐包报告 | 本轮阻断项由独立修复任务关闭并复测;环境缺口转交下一轮,不宣称已发布就绪 | H | todo |
| REVIEW-02 | workspace<br>第二轮真实浏览器与生态集成复盘 | REVIEW-01, REL-03 | Chrome 交互、自动浏览器、新旧组合、真机/SDK/性能/资源的全范围报告 | 所需环境证据齐全,前轮修复再次核对;本轮阻断项关闭,无关 mock 不替代真实验收 | H | todo |
| REVIEW-03 | workspace<br>第三轮 npm 候选内容与发布准备复盘 | REVIEW-02, REL-04, CI-04 | 实际候选 integrity、干净安装构建/消费者、入口/许可/版本/tag/回退及前轮证据汇总 | 最终候选完整检查通过,发布阻断项为零;准备可审阅的批次报告,不自动执行 publish;逐包核对 major 目标,不能用大版本豁免旧 API 兼容 | H | todo |
@@ -454,7 +455,7 @@
- ENG-09: [记录](changes/2026-09-12-ENG-09-integration.md) [记录](baselines/engineering-integration.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)
- CI-01: [记录](ci-setup.md) [记录](changes/2026-09-13-CI-01-matrix-summary.md) [记录](baselines/ci-matrix-validation.json)
- CI-01: [记录](ci-setup.md) [记录](changes/2026-09-13-CI-01-matrix-summary.md) [记录](baselines/ci-matrix-validation.json) [记录](changes/2026-09-13-CI-01-node-consumers.md) [记录](baselines/node-consumer-validation.json)
- PILOT-01: [记录](changes/2026-09-10-PILOT-01-chapter.md) [记录](baselines/pilot-validation.json)
- CORE-01: [记录](changes/2026-09-10-CORE-01-typed-utils.md) [记录](baselines/core-utils-validation.json)
- CORE-02: [记录](changes/2026-09-11-CORE-02-typed-emitter.md) [记录](baselines/emitter-validation.json)
@@ -555,3 +556,4 @@
- PKG-TOOL-THUMB-03: [记录](changes/2026-09-13-PKG-TOOL-THUMB-03-input-checkpoint.md) [记录](baselines/thumbnail-input-checkpoint.json) [记录](changes/2026-09-13-PKG-TOOL-THUMB-03-lifecycle.md) [记录](baselines/thumbnail-lifecycle-validation.json)
- PKG-TOOL-THUMB-04: [记录](changes/2026-09-13-PKG-TOOL-THUMB-04-runtime-types.md) [记录](baselines/thumbnail-runtime-types-validation.json) [记录](changes/2026-09-13-PKG-TOOL-THUMB-04-public-types.md) [记录](baselines/thumbnail-public-types-validation.json) [记录](changes/2026-09-13-PKG-TOOL-THUMB-04-emitter.md) [记录](baselines/thumbnail-emitter-validation.json)
- PKG-FACTORY-01: [记录](baselines/factory-assignment-gaps.json) [记录](baselines/factory-compatibility-proposals.json) [记录](factory-compatibility-decision.md) [记录](changes/2026-09-12-PKG-FACTORY-01-decision.md)
- CORE-25: [记录](changes/2026-09-13-CI-01-node-consumers.md)
+15
View File
@@ -1,5 +1,20 @@
# 进度与证据
## CI-01 Node 消费者矩阵检查点(doing)
标准 Node 24.21.0 构建同一 core/chapter tarball,20.19.0/22.12.0/24.21.0 实际
offline/frozen 安装逐成员验证,每个运行 36 项候选与 31 项发布观察;五个旧类型和
八个精确类型模式通过。三个系统 workflow 已接入精确运行时切换,浏览器前恢复标准 Node。
首次 Node 20 发现无 navigator 读取静态 option 抛错,旧与候选均复现。新增 CORE-25
与 CORE-DEFAULTS-SSR-01,并加入 REVIEW-01 前置;严格 package:release 已验证因此失败。
兼容检查成功包括明确的未修复缺陷观察,不能作为发布成功;下一步优先独立修复 CORE-25。
完整本地 CI 2429 项通过(2031 单元、27 工程、371 基线),定向 44 项、元数据 9 项,
actionlint/定向 lint 通过。无生产源码/公开类型/依赖/版本改动,无新浏览器或远端运行。
见[变更](changes/2026-09-13-CI-01-node-consumers.md)和[证据](baselines/node-consumer-validation.json)。
224 项:121 done、16 doing、87 todo;201 风险。CI-01 的更早 Node/最低工具环境、
全生态安装/影响调度及 CI-04 远端仍待验收。独立本地检查点,不推送、不发布。
## CI-01 系统矩阵、缓存与结果汇总检查点(doing)
checks 扩为 Linux/Windows,浏览器扩为 Linux/Windows/macOS;coverage 保留双系统。
+1
View File
@@ -204,3 +204,4 @@
| MULTI-SUB-ENTITY-01 | open / 已复现 | Vendored cue entity decoding retains semicolons after lt, gt and amp entities | PKG-MULTI-SUB-05 |
| JASSUB-TYPE-01 | open / 已复现 | JASSUB declared required URLs, Promise methods and resize parameter order differ from actual historical behavior | PKG-JASSUB-04 |
| JASSUB-EXPORT-01 | open / 已复现 | JASSUB historical CommonJS object.default changes to a direct factory in 1.1.0 | PKG-JASSUB-04, PKG-JASSUB-06 |
| CORE-DEFAULTS-SSR-01 | open / 已复现 | Reading static defaults throws when navigator is absent | CORE-25 |
+18
View File
@@ -4496,6 +4496,24 @@
],
"compatibleResolution": "Preserve actual historical JS default/direct forms and validate their declared module views independently.",
"closureCriteria": "Installed CJS/ESM/global/legacy consumers and public types preserve the supported historical forms."
},
{
"id": "CORE-DEFAULTS-SSR-01",
"title": "Reading static defaults throws when navigator is absent",
"confirmation": "reproduced",
"status": "open",
"owners": [
"CORE-25"
],
"evidence": [
"test/package/runtime.cjs",
"packages/artplayer/src/option/defaults.ts",
"refactor/changes/2026-09-13-CI-01-node-consumers.md",
"refactor/baselines/node-consumer-validation.json"
],
"compatibleResolution": "Read navigator through a guarded global lookup while preserving browser language defaults and public declaration shape.",
"closureCriteria": "Published failure remains explicit; source and actual installed candidate defaults work without navigator on selected Node versions and browser language behavior stays unchanged.",
"workspaceState": "Node 20.19.0 actual installed probe fails with ReferenceError: navigator is not defined. Published 5.4.0 and candidate 5.4.1 both reproduce under explicitly absent navigator on every selected runtime. CI-01 records this known failure, then compares default values under the same controlled en-US navigator; this is not a production fix."
}
]
}
+16
View File
@@ -68,6 +68,22 @@ export function validateCIWorkflow(source) {
}
const browser = workflow.jobs['browser-smoke']
assert(browser.steps.some(step => step.run?.startsWith('yarn test:browser:install --with-deps') && !Object.hasOwn(step, 'if')), 'Browser dependencies must install even on cache hits')
let consumerIndex = browser.steps.findIndex(step => step.run?.startsWith('yarn test:package'))
for (const [id, version, command] of [
['consumer-node-20', '20.19.0', 'node scripts/package-runtime.mjs --expected-node 20.19.0 2>&1 | tee refactor/.cache/ci/consumer-node-20.log'],
['consumer-node-22', '22.12.0', 'node scripts/package-runtime.mjs --expected-node 22.12.0 2>&1 | tee refactor/.cache/ci/consumer-node-22.log'],
['restore-canonical-node', null, 'node scripts/package-runtime.mjs --canonical 2>&1 | tee refactor/.cache/ci/consumer-node-canonical.log'],
]) {
const index = browser.steps.findIndex(step => step.id === id)
assert(index > consumerIndex, 'Build once before each ordered consumer runtime switch')
const step = browser.steps[index]
assert(step.uses?.startsWith('actions/setup-node@') && !Object.hasOwn(step, 'if'), 'Consumer Node setup cannot be skipped')
assert.deepEqual(step.with, version ? { 'node-version': version, 'package-manager-cache': false } : { 'node-version-file': '.node-version', 'package-manager-cache': false }, 'Use exact consumer versions and restore the canonical browser runtime')
const probe = browser.steps[index + 1]
assert(probe?.run === command && !Object.hasOwn(probe, 'if'), 'Run the installed consumer on the selected Node')
consumerIndex = index + 1
}
assert(browser.steps.findIndex(step => step.run?.startsWith('yarn test:browser ')) > consumerIndex, 'Browser tools must run after canonical Node restoration')
const pages = workflow.jobs.checks.steps.find(step => step.uses?.startsWith('actions/upload-pages-artifact@'))
assert.equal(pages?.if, 'inputs.pages-artifact && github.ref == \'refs/heads/master\' && matrix.os == \'ubuntu-latest\'', 'Only one trusted matrix leg can prepare Pages')
return { jobs: requiredJobs, systems, summary: summary.name }
+3
View File
@@ -31,6 +31,9 @@ for (const [name, mutate] of [
['conditional frozen install', w => w.jobs.checks.steps.find(s => s.run?.includes('yarn install --frozen-lockfile')).if = 'false'],
['commented frozen install', w => w.jobs.checks.steps.find(s => s.run?.includes('yarn install --frozen-lockfile')).run = '# yarn install --frozen-lockfile --non-interactive'],
['conditional Node setup', w => w.jobs.checks.steps.find(s => s.uses?.startsWith('actions/setup-node@')).if = 'false'],
['wrong consumer Node version', w => w.jobs['browser-smoke'].steps.find(s => s.id === 'consumer-node-20').with['node-version'] = '24'],
['skipped installed consumer', w => w.jobs['browser-smoke'].steps.find(s => s.run?.includes('--expected-node 22.12.0')).if = 'false'],
['browser runtime not restored', w => w.jobs['browser-smoke'].steps.find(s => s.id === 'restore-canonical-node').with = { 'node-version': '22.12.0', 'package-manager-cache': false }],
['Pages uploaded by both matrix legs', w => w.jobs.checks.steps.find(s => s.uses?.startsWith('actions/upload-pages-artifact@')).if = 'inputs.pages-artifact && github.ref == \'refs/heads/master\''],
]) {
test(`CI rejects ${name}`, () => {
+23 -2
View File
@@ -756,7 +756,9 @@
"evidence": [
"ci-setup.md",
"changes/2026-09-13-CI-01-matrix-summary.md",
"baselines/ci-matrix-validation.json"
"baselines/ci-matrix-validation.json",
"changes/2026-09-13-CI-01-node-consumers.md",
"baselines/node-consumer-validation.json"
]
},
{
@@ -4362,7 +4364,8 @@
"DOC-08",
"MOD-01",
"MOD-02",
"MOD-03"
"MOD-03",
"CORE-25"
],
"status": "todo",
"risk": "H",
@@ -4478,6 +4481,24 @@
"factory-compatibility-decision.md",
"changes/2026-09-12-PKG-FACTORY-01-decision.md"
]
},
{
"id": "CORE-25",
"phase": "3-4 核心迁移",
"title": "修复无 navigator 环境读取默认选项",
"scope": [
"artplayer"
],
"dependsOn": [
"CORE-22"
],
"status": "todo",
"risk": "M",
"deliverable": "安全读取默认语言并保留浏览器已有默认值;实际 Node 与发布产物正反例",
"acceptance": "记录旧发布版 ReferenceError;候选在无 navigator 时可读默认选项,浏览器语言和公开类型不回归;不以补全局对象隐藏错误",
"evidence": [
"changes/2026-09-13-CI-01-node-consumers.md"
]
}
]
}
+5 -1
View File
@@ -9,7 +9,11 @@
3. 执行 `yarn check:toolchain --strict`,核对实际 Node/Yarn、20 个固定开发工具和 22 个 workspace 的声明及传递依赖锁条目。普通检查允许满足最低工具要求的 Node,同时打印标准版本。
4. 执行 `yarn test:playback`、`yarn test:dash-control`、`yarn build all` 和 `yarn workspace artplayer-vitepress build`。ENG-02 已拆分只读 lint 与 lint:fix;PR/主线入口和独立 Pages 流程见 ci-setup.md。
私有根包最低 Node 为 ^20.19.0 || >=22.12.0,与原本使用的 Vite 7 一致。本轮验证 Node 24.21.0,其他版本矩阵由 CI-01 接续,不能宣称所有最低环境已经通过。
私有根包最低 Node 为 ^20.19.0 || >=22.12.0,与原本使用的 Vite 7 一致。标准工具链验证
使用 Node 24.21.0;CI-01 已在 Windows 的 20.19.0/22.12.0/24.21.0 上重装消费相同
core/chapter tarball,见 [Node 记录](changes/2026-09-13-CI-01-node-consumers.md)。
这不是在最低 Node 上完成仓库工具链的干净安装/构建测试,也不是发布包的最低 Node 声明。
CORE-25 默认选项缺陷仍在严格发布检查中阻断;完整工具与消费范围由 CI-01 继续验证。
## 锁文件和依赖维护
+7 -2
View File
@@ -88,6 +88,7 @@ export async function checkPackages({ release = false } = {}) {
const installed = consumerDirectory()
try {
const oldRuntime = runtimeConsumer(baseline.dir, { baseline: true })
fs.cpSync(path.join(baseline.dir, 'node_modules'), path.join(output, 'published-artifacts'), { recursive: true })
const packages = []
for (const name of names) {
const archive = path.join(output, `${name}.tgz`)
@@ -119,13 +120,17 @@ export async function checkPackages({ release = false } = {}) {
assert.deepEqual(runtime.observations.api, oldRuntime.observations.api, 'Published API shape/defaults changed')
const types = typeConsumers(installed)
const preciseTypes = typeConsumers(installed, { precise: true })
const report = { task: 'ENG-07', capturedAt: new Date().toISOString(), node: process.versions.node, packages, runtime, publishedRuntime: oldRuntime, types, preciseTypes, knownTypeBlockers: [...types, ...preciseTypes].reduce((sum, result) => sum + result.diagnostics.length, 0) }
const source = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: workspace, encoding: 'utf8' }).trim()
const publishedPackages = baseline.releases.map(({ name, version, files }) => ({ name, version, files }))
const report = { task: 'ENG-07', capturedAt: new Date().toISOString(), source, toolchain: { yarn: '1.22.22', yarnPath: yarn }, node: process.versions.node, packages, runtime, publishedPackages, publishedRuntime: oldRuntime, types, preciseTypes, knownRuntimeBlockers: runtime.observations.defaultsWithoutNavigator.resolved ? 0 : 1, knownTypeBlockers: [...types, ...preciseTypes].reduce((sum, result) => sum + result.diagnostics.length, 0) }
writeJson(path.join(output, 'report.json'), report)
writeJson(path.join(output, 'browser-artifacts.json'), artifacts)
writeJson(path.join(parent, 'latest.json'), { output: path.relative(workspace, output).replaceAll('\\', '/') })
console.log(`Installed tarball contracts passed: ${runtime.checks.length} runtime checks; ${types.filter(t => !t.diagnostics.length).length}/${types.length} legacy type modes; ${preciseTypes.filter(t => !t.diagnostics.length).length}/${preciseTypes.length} precise type modes. Report: ${output}`)
if (release)
if (release) {
assert.equal(report.knownTypeBlockers, 0, 'Known type blockers remain; this candidate is not release-ready')
assert.equal(report.knownRuntimeBlockers, 0, 'Known runtime blockers remain; this candidate is not release-ready')
}
return report
}
finally {
+2 -7
View File
@@ -9,6 +9,8 @@ import ts from 'typescript'
import compat from 'typescript-compat'
import runtimeCompat from 'typescript-runtime-compat'
export { runtimeConsumer } from './package-runtime.mjs'
export const workspace = fileURLToPath(new URL('../', import.meta.url))
export const names = ['artplayer', 'artplayer-plugin-chapter']
export const readJson = file => JSON.parse(fs.readFileSync(file, 'utf8'))
@@ -27,13 +29,6 @@ export function removeConsumer(dir) {
fs.rmSync(dir, { recursive: true, force: true })
}
export function runtimeConsumer(dir, { baseline = false } = {}) {
fs.copyFileSync(path.join(workspace, 'test/package/runtime.cjs'), path.join(dir, 'runtime.cjs'))
fs.copyFileSync(path.join(workspace, 'test/contracts/emitter.js'), path.join(dir, 'emitter.mjs'))
writeJson(path.join(dir, 'expected.json'), { version: readJson(path.join(dir, 'node_modules/artplayer/package.json')).version, baseline })
return JSON.parse(run(['runtime.cjs'], dir))
}
export function typeConsumers(dir, { precise = false } = {}) {
const results = []
const matrix = precise
+98
View File
@@ -0,0 +1,98 @@
import assert from 'node:assert/strict'
import { execFileSync } from 'node:child_process'
import { createHash } from 'node:crypto'
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import process from 'node:process'
import { fileURLToPath } from 'node:url'
const workspace = fileURLToPath(new URL('../', import.meta.url))
const read = file => JSON.parse(fs.readFileSync(file, 'utf8'))
const write = (file, value) => fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`)
const hash = file => createHash('sha256').update(fs.readFileSync(file)).digest('hex')
const run = (args, cwd) => execFileSync(process.execPath, args, { cwd, encoding: 'utf8', timeout: 120000, maxBuffer: 8 * 1024 * 1024, windowsHide: true, env: { ...process.env, NODE_PATH: '' } })
export function runtimeConsumer(dir, { baseline = false } = {}) {
fs.copyFileSync(path.join(workspace, 'test/package/runtime.cjs'), path.join(dir, 'runtime.cjs'))
fs.copyFileSync(path.join(workspace, 'test/contracts/emitter.js'), path.join(dir, 'emitter.mjs'))
write(path.join(dir, 'expected.json'), { version: read(path.join(dir, 'node_modules/artplayer/package.json')).version, baseline })
const result = JSON.parse(run(['runtime.cjs'], dir))
assert.equal(result.node, process.versions.node, 'Consumer must run on the selected Node')
return result
}
export function verifyInstalledFiles(directory, packages) {
assert.deepEqual(packages.map(pkg => pkg.name).sort(), ['artplayer', 'artplayer-plugin-chapter'], 'Runtime probe scope must match the actual package fixture')
for (const pkg of packages) {
const root = path.join(directory, 'node_modules', pkg.name)
assert.equal(fs.realpathSync(root), root, 'Consumer cannot resolve workspace links')
for (const [member, digest] of Object.entries(pkg.files)) {
assert(member.startsWith('package/') && !member.includes('\\') && !member.split('/').includes('..'), 'Unsafe package member')
assert.equal(hash(path.join(root, member.slice(8))), digest, `Installed package bytes differ: ${pkg.name}/${member}`)
}
assert.equal(read(path.join(root, 'package.json')).version, pkg.version)
}
}
export function checkPackageRuntime({ expectedNode, output } = {}) {
assert.equal(process.versions.node, expectedNode, 'Select the requested exact Node before running consumers')
const parent = fs.realpathSync(path.join(workspace, 'refactor/.cache/packages'))
const selected = output || read(path.join(parent, 'latest.json')).output
const directory = fs.realpathSync(path.resolve(workspace, selected))
assert.equal(path.dirname(directory), parent, 'Use a direct package-check run directory')
assert(path.basename(directory).startsWith('run-'))
const built = read(path.join(directory, 'report.json'))
assert.equal(built.source, execFileSync('git', ['rev-parse', 'HEAD'], { cwd: workspace, encoding: 'utf8' }).trim(), 'Package build belongs to another commit')
assert.equal(built.toolchain?.yarn, '1.22.22', 'Use the recorded pinned package manager')
const yarn = built.toolchain.yarnPath
assert.equal(run([yarn, '--version'], workspace).trim(), '1.22.22')
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), 'artplayer-consumer-'))
const report = { task: 'CI-01', node: process.versions.node, source: built.source, buildNode: built.node, passed: false, packages: built.packages.map(pkg => ({ name: pkg.name, version: pkg.version, sha256: pkg.sha256 })) }
const prefix = path.join(directory, `runtime-node-${expectedNode}`)
try {
const baseline = path.join(temporary, 'published')
fs.mkdirSync(baseline)
fs.cpSync(path.join(directory, 'published-artifacts'), path.join(baseline, 'node_modules'), { recursive: true })
verifyInstalledFiles(baseline, built.publishedPackages)
report.publishedRuntime = runtimeConsumer(baseline, { baseline: true })
const dependencies = {}
for (const pkg of built.packages) {
assert.equal(path.basename(pkg.archive), pkg.archive, 'Archive must stay in the build directory')
const archive = path.join(directory, pkg.archive)
assert.equal(hash(archive), pkg.sha256, 'Candidate archive changed after package checks')
dependencies[pkg.name] = `file:${archive.replaceAll('\\', '/')}`
}
write(path.join(temporary, 'package.json'), { name: 'artplayer-isolated-consumer', private: true, dependencies })
const lock = fs.readFileSync(path.join(directory, 'consumer-yarn.lock'))
fs.writeFileSync(path.join(temporary, 'yarn.lock'), lock)
fs.writeFileSync(`${prefix}-install.log`, run([yarn, 'install', '--offline', '--frozen-lockfile', '--ignore-scripts', '--non-interactive'], temporary))
assert.deepEqual(fs.readFileSync(path.join(temporary, 'yarn.lock')), lock, 'Frozen consumer lock changed')
verifyInstalledFiles(temporary, built.packages)
report.runtime = runtimeConsumer(temporary)
assert.deepEqual(report.runtime.observations.api, report.publishedRuntime.observations.api, 'Published API shape/defaults changed on this Node')
assert.deepEqual(report.runtime.checks, built.runtime.checks, 'Runtime check coverage changed across Node versions')
report.knownRuntimeBlockers = report.runtime.observations.defaultsWithoutNavigator.resolved ? 0 : 1
report.passed = true
return report
}
catch (error) {
report.error = { name: error.name, message: error.message }
fs.writeFileSync(`${prefix}-failure.log`, `${error.stack}\n${error.stdout || ''}\n${error.stderr || ''}`)
throw error
}
finally {
write(`${prefix}.json`, report)
assert.equal(path.dirname(fs.realpathSync(temporary)), fs.realpathSync(os.tmpdir()))
assert(path.basename(temporary).startsWith('artplayer-consumer-'))
fs.rmSync(temporary, { recursive: true, force: true })
}
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
const args = process.argv.slice(2)
assert((args.length === 1 && args[0] === '--canonical') || (args.length === 2 && args[0] === '--expected-node' && /^\d+\.\d+\.\d+$/.test(args[1])), 'Use --canonical or --expected-node <exact version>')
const expectedNode = args[0] === '--canonical' ? fs.readFileSync(path.join(workspace, '.node-version'), 'utf8').trim() : args[1]
const result = checkPackageRuntime({ expectedNode })
console.log(`Installed runtime passed on Node ${result.node}: ${result.runtime.checks.length} candidate and ${result.publishedRuntime.checks.length} published checks`)
}
+1 -1
View File
@@ -22,7 +22,7 @@ test('Package checks reject missing files, missing wildcard exports and internal
test('Actual isolated runtime rejects removed default exports and required files', async () => {
const { dir } = await publishedConsumer()
try {
assert.equal(runtimeConsumer(dir, { baseline: true }).checks.length, 20 + Object.keys(emitterContracts).length)
assert.equal(runtimeConsumer(dir, { baseline: true }).checks.length, 22 + Object.keys(emitterContracts).length)
assert.throws(() => runtimeConsumer(dir), /Command failed/)
const esm = path.join(dir, 'node_modules/artplayer-plugin-chapter/dist/artplayer-plugin-chapter.mjs')
const original = fs.readFileSync(esm)
+62
View File
@@ -0,0 +1,62 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
// eslint-disable-next-line test/no-import-node-test -- Verify isolated package runtime evidence and rejected inputs.
import test from 'node:test'
import { publishedConsumer } from '../scripts/package-check.mjs'
import { removeConsumer } from '../scripts/package-consumer.mjs'
import { checkPackageRuntime, runtimeConsumer, verifyInstalledFiles } from '../scripts/package-runtime.mjs'
test('Runtime evidence records the actual child Node version with published contracts', async () => {
const { dir, releases } = await publishedConsumer()
try {
verifyInstalledFiles(dir, releases)
const result = runtimeConsumer(dir, { baseline: true })
assert.equal(result.node, process.versions.node)
assert(result.checks.includes('DIST.esm'))
assert(result.checks.includes('SSR.constructor-browser-only'))
}
finally { removeConsumer(dir) }
})
test('Installed runtime verification rejects tampered bytes, traversal and workspace links', async () => {
const { dir, releases } = await publishedConsumer()
try {
const manifest = path.join(dir, 'node_modules/artplayer/package.json')
const original = fs.readFileSync(manifest)
fs.appendFileSync(manifest, '\n')
assert.throws(() => verifyInstalledFiles(dir, releases), /Installed package bytes differ/)
fs.writeFileSync(manifest, original)
const unsafe = structuredClone(releases)
unsafe[0].files['package/../escape'] = '0'.repeat(64)
assert.throws(() => verifyInstalledFiles(dir, unsafe), /Unsafe package member/)
const root = path.join(dir, 'node_modules/artplayer')
const target = path.join(dir, 'linked-artplayer')
assert(path.dirname(target) === dir && path.dirname(path.dirname(root)) === dir)
fs.renameSync(root, target)
fs.symlinkSync(target, root, process.platform === 'win32' ? 'junction' : 'dir')
assert.throws(() => verifyInstalledFiles(dir, releases), /workspace links/)
}
finally { removeConsumer(dir) }
})
test('Runtime selection cannot silently fall back to a different Node version', () => {
assert.throws(() => checkPackageRuntime({ expectedNode: '0.0.0' }), /requested exact Node/)
})
test('Runtime package selection rejects other directories and stale source commits before installing', () => {
const parent = path.resolve('refactor/.cache/packages')
fs.mkdirSync(parent, { recursive: true })
const directory = fs.mkdtempSync(path.join(parent, 'run-runtime-negative-'))
try {
fs.writeFileSync(path.join(directory, 'report.json'), JSON.stringify({ source: '0'.repeat(40) }))
assert.throws(() => checkPackageRuntime({ expectedNode: process.versions.node, output: directory }), /another commit/)
assert.throws(() => checkPackageRuntime({ expectedNode: process.versions.node, output: parent }), /direct package-check run/)
}
finally {
assert.equal(path.dirname(fs.realpathSync(directory)), fs.realpathSync(parent))
assert(path.basename(directory).startsWith('run-runtime-negative-'))
fs.rmSync(directory, { recursive: true, force: true })
}
})
+16 -1
View File
@@ -35,9 +35,24 @@ type failures; all five groups now require zero diagnostics. Exact known
diagnostics are referenced by `known-types.json` and `test/types/known-diagnostics.json`.
Unexpected errors and unexpectedly removed errors both require investigation.
Remove a known case when its owning task fixes it; keep frozen release evidence intact.
`yarn test:package:release` additionally rejects any remaining known type errors.
`yarn test:package:release` additionally rejects remaining known type and runtime blockers.
Passing this command alone is not authorization or sufficient evidence to publish.
`scripts/package-runtime.mjs` uses only Node built-ins and can reinstall the exact checked
tarballs under a different Node. First run `yarn test:package` on the canonical toolchain,
then use the selected executable with `scripts/package-runtime.mjs --expected-node 20.19.0`
or `--expected-node 22.12.0`; `yarn test:package:runtime` verifies canonical Node.
Each run checks HEAD, archive digests, the frozen offline install, installed bytes and
the actual child version. Reports and failure logs are `runtime-node-<version>.*` in
the same package output directory. Old reports without source/toolchain fields must
be rebuilt. CI restores canonical Node before running browser tools.
CORE-25 remains an explicit defect: both published and candidate static defaults throw
without navigator. The fixture tests that exact failure on every runtime, then uses an
identical controlled language for default-value comparison and restores the global.
`knownRuntimeBlockers=1` prevents strict release success; ordinary observation tests
passing do not mean the defect is fixed. See [Node evidence](../../refactor/changes/2026-09-13-CI-01-node-consumers.md).
The optional core `artplayer/runtime` entry has eight additional strict consumer
groups: TS 5.1.6 and 5.9.3 each check Node10 CommonJS, NodeNext CJS/ESM and Bundler.
These fixtures cover accurate returns and getter/setter types, construction-stage
+18 -2
View File
@@ -186,8 +186,24 @@ const vm = require('node:vm');
checks.push(id)
}
const shape = value => Object.fromEntries(Object.entries(Object.getOwnPropertyDescriptors(value)).map(([key, d]) => [key, { enumerable: d.enumerable, configurable: d.configurable, writable: d.writable, get: typeof d.get, set: typeof d.set, value: typeof d.value }]))
observations.api = { static: shape(core), prototype: shape(core.prototype), emitter: shape(core.Emitter.prototype), factory: shape(chapter), defaults: JSON.parse(JSON.stringify(core.option, (_, value) => typeof value === 'function' ? '$function' : value)) }
process.stdout.write(JSON.stringify({ checks, observations }))
const defaultNavigator = Object.getOwnPropertyDescriptor(globalThis, 'navigator')
try {
assert(delete globalThis.navigator)
// CORE-25 tracks this reproduced published and candidate failure separately.
assert.throws(() => core.option, { name: 'ReferenceError', message: 'navigator is not defined' })
observations.defaultsWithoutNavigator = { name: 'ReferenceError', message: 'navigator is not defined', historical: !!expected.baseline, resolved: false }
check('SSR.defaults-missing-navigator-known-failure', true)
Object.defineProperty(globalThis, 'navigator', { configurable: true, value: { language: 'en-US' } })
observations.api = { static: shape(core), prototype: shape(core.prototype), emitter: shape(core.Emitter.prototype), factory: shape(chapter), defaults: JSON.parse(JSON.stringify(core.option, (_, value) => typeof value === 'function' ? '$function' : value)) }
check('API.defaults-controlled-language', observations.api.defaults.lang === 'en-us')
}
finally {
if (defaultNavigator)
Object.defineProperty(globalThis, 'navigator', defaultNavigator)
else
delete globalThis.navigator
}
process.stdout.write(JSON.stringify({ node: process.versions.node, checks, observations }))
})().catch((error) => {
console.error(error)
process.exitCode = 1