diff --git a/refactor/ambilight-validation.md b/refactor/ambilight-validation.md new file mode 100644 index 000000000..248993bd3 --- /dev/null +++ b/refactor/ambilight-validation.md @@ -0,0 +1,13 @@ +# Ambilight 测试维护 + +先读 [发布契约](baselines/ambilight-contract.md)。ambilight-release.json冻结两个真实 +发布及工作区Git来源,verifyAmbilightContract逐成员核对,不随候选更新历史哈希。 + +`node --test refactor/scripts/ambilight-contract.test.mjs`执行7组:归档、CJS/script/legacy、 +原生ESM、声明形状及三实现的配置/样式/坐标/同步返回。test/helpers/ambilight.js提供 +受控DOM/Canvas/RAF;模拟RGB只检查取样及写入顺序,不证明浏览器解码或跨域取色。 + +02复现频率/暂停/零尺寸/重复start-stop/销毁/异常;03拆分视图、取色和调度,修复已 +复现问题;04兼容类型及旧CJS.default;05/06做真实媒体、代理、浏览器和安装包验证。 +stop保留最后画面,核心destroy终止资源。zIndex仍忽略,参数省略及两版声明差异分别 +处理;Git关联不构成连续核心支持范围。 diff --git a/refactor/baselines/ambilight-contract-validation.json b/refactor/baselines/ambilight-contract-validation.json new file mode 100644 index 000000000..045542767 --- /dev/null +++ b/refactor/baselines/ambilight-contract-validation.json @@ -0,0 +1,76 @@ +{ + "schemaVersion": 1, + "task": "PKG-AMBILIGHT-01", + "status": "done", + "sourceCommit": "c57c7e4b164557d111bb92a9df17c2f28d2ee657", + "checks": [ + { + "command": "node --test refactor/scripts/ambilight-contract.test.mjs refactor/scripts/contracts.test.mjs", + "result": "pass", + "tests": 14, + "ambilightTests": 7, + "indexTests": 7, + "log": "refactor/.cache/ambilight-contract-final.log" + }, + { + "command": "eslint Ambilight helper/contract scripts and updated contracts test --no-fix", + "result": "pass", + "log": "refactor/.cache/ambilight-lint-final.log" + }, + { + "command": "yarn ci:check", + "result": "pass", + "tests": { + "unit": 832, + "engineering": 14, + "baseline": 79, + "mainTotal": 925, + "repeatedContractExecutions": 44 + }, + "log": "refactor/.cache/ambilight01-ci.log" + } + ], + "logs": [ + { + "file": "refactor/.cache/ambilight-contract-final.log", + "sha256": "a28f0c8a6b8b087174aeb39b1cb1ea3c792ed25fef86e8f636ffdb2349652177" + }, + { + "file": "refactor/.cache/ambilight-lint-final.log", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "file": "refactor/.cache/ambilight01-ci.log", + "sha256": "71fd0363f2e34df800730bdf5fc6e9d1c40819ae119f83ce9c5add3310b91ec2" + } + ], + "inputs": [ + { + "file": "refactor/scripts/ambilight-contract.mjs", + "sha256LF": "50cd644d2715812ae57fad7e8babab85269b8693fddd6a7600fd4e3446927867" + }, + { + "file": "refactor/scripts/ambilight-contract.test.mjs", + "sha256LF": "38b626131836a6087efb9bccdff8f46e907ed54d064fbcb4927d931ac39bb9d3" + }, + { + "file": "test/helpers/ambilight.js", + "sha256LF": "7775f147355d2aa08615a82079760924ae151beb7d9bdc94cccfd8195f5b19d8" + }, + { + "file": "refactor/contract-policy.json", + "sha256LF": "3da55d2e6e656831f2cc63678e8b78c9499640d8471463afd48369a02148aeaf" + }, + { + "file": "refactor/scripts/contracts.test.mjs", + "sha256LF": "1db5b256464bf17e969070375558c6ff854f116a57e5283b04e9c1327dd9c1e3" + } + ], + "limitations": [ + "Node-controlled Canvas/RAF is not real browser/media validation", + "Declarations inspected, not full installed compiler acceptance", + "Git core association is not a supported version interval", + "Lifecycle issues remain source observations until PKG-AMBILIGHT-02 reproduces them", + "No production source/type/artifact changes or remote publish" + ] +} diff --git a/refactor/baselines/ambilight-contract.md b/refactor/baselines/ambilight-contract.md new file mode 100644 index 000000000..719998075 --- /dev/null +++ b/refactor/baselines/ambilight-contract.md @@ -0,0 +1,41 @@ +# Ambilight 实际发布与工作区契约 + +PKG-AMBILIGHT-01;来源c57c7e4b。实际npm有1.0.0、1.1.0,后者为采集时latest。 +两份tarball各6成员,完整哈希见ambilight-release.json;1.1.0不含src,直接核对dist。 +工作区源码、声明、README、manifest和demo另以Git冻结。目标2.0.0由REL-09修改。 + +## 运行时 + +注册时才读取配置,默认blur=50px、opacity=0.5、frequency=10、duration=0.3。同步返回 +{name,start,stop},两方法返回undefined。省略option可用,null在注册时抛错;创建工厂 +后、注册前的配置修改可观察。没有额外公开事件或destroy方法。 + +video之前插入artplayer-plugin-ambilight,绝对定位3×3网格,zIndex固定9;声明里的 +zIndex从未读取。9子节点保留filter、opacity和background-color transition。ready自动 +start;stop停止更新并保留最后颜色,start可恢复。playing且达到频率间隔时,将9个 +视频区域分别缩至1×1读取RGB。暂停仍调度RAF、不取色。特殊频率语义由02补齐。 + +## 公开差异 + +| 维度 | npm1.0.0 | npm1.1.0/工作区 | 迁移处理 | +| --- | --- | --- | --- | +| CJS | namespace.default | 可调用工厂 | 保留两种调用形式 | +| ESM | 无独立mjs | mjs默认导出 | 保留真实ESM入口 | +| 类型导出 | export=、全局声明 | export default | 04验证旧require/import=及新默认导入 | +| Option字段 | 五字段必需 | 五字段可选 | 这是已发布历史变化,不是候选修正 | +| 工厂参数 | 声明要求option | 同左 | 运行时允许省略;兼容补齐声明 | +| zIndex | 声明有、运行时忽略 | 同左 | 继续接受旧输入并保留固定层级 | + +README只有demo链接;当前demo明确传blur/opacity/frequency/duration。不能凭声明里的 +zIndex悄悄启用新布局行为。01核对真实声明形状,不把正则检查冒充完整消费者编译。 + +## 后续验证 + +destroy只stop,未清DOM/监听器;逃逸start可能重启;RAF的truthy检查会混淆id=0; +空context、零尺寸和取色异常可能中断循环。目前登记源码观察,02必须复现、03修复。 +stop保留视图与核心destroy终止资源应分开,不能新增用户必须调用的方法。 + +registry gitHead对应核心5.1.7/5.3.1,仅是Git关联,不是tarball可复现或支持区间证明。 +未声明peerDependencies,05须实际验证旧核心/候选、Canvas代理及媒体能力。Canvas +依赖可取色媒体、2Dcontext和origin-clean;Node记录器不证明浏览器像素或真机行为。 +01共7组检查,维护入口见 [Ambilight测试说明](../ambilight-validation.md)。 diff --git a/refactor/baselines/ambilight-release.json b/refactor/baselines/ambilight-release.json new file mode 100644 index 000000000..015103cf5 --- /dev/null +++ b/refactor/baselines/ambilight-release.json @@ -0,0 +1,137 @@ +{ + "schemaVersion": 1, + "task": "PKG-AMBILIGHT-01", + "capturedAt": "2026-09-12T02:42:35.458Z", + "sourceCommit": "c57c7e4b164557d111bb92a9df17c2f28d2ee657", + "registryObservation": { + "versions": [ + "1.0.0", + "1.1.0" + ], + "latest": "1.1.0", + "workspaceVersion": "1.1.0" + }, + "release": { + "name": "artplayer-plugin-ambilight", + "version": "1.1.0", + "metadataUrl": "https://registry.npmjs.org/artplayer-plugin-ambilight/1.1.0", + "tarball": "https://registry.npmjs.org/artplayer-plugin-ambilight/-/artplayer-plugin-ambilight-1.1.0.tgz", + "integrity": "sha512-FkMI7bn9HovJMfEdgkKbo7/vPt6vrzcpELTaC/vM7B99FPSMWzAf6GAE0xnNxU3KibYcBV5gXnBgvHgpG2xq2A==", + "sha256": "e95bd60fd7bce52142cf0041bad811452305029cbe1129c7d89246eb260c5c86", + "archiveBytes": 2638, + "publishedAt": "2026-03-12T14:22:20.888Z", + "registryGitHead": "daf133b22630b4a0eecfa3336bbddab0e9119d96", + "files": { + "package/README.md": "53946ecc0fca163d52465515dfa13de3619a01c3b89d7f966eafe0610584dede", + "package/dist/artplayer-plugin-ambilight.js": "8f32290c65a13b0a3026dee0045fef4125a9c9ac788ecb1675aef05f79172693", + "package/dist/artplayer-plugin-ambilight.legacy.js": "8f32290c65a13b0a3026dee0045fef4125a9c9ac788ecb1675aef05f79172693", + "package/dist/artplayer-plugin-ambilight.mjs": "77486db6d24f50420c952de5b37d226e404089730c7c1c6be226c836f4e85f9c", + "package/package.json": "39aa2790a79cbc2a62ba925ee5ff53bbb649440a5d7b9f526b3de819ece4f792", + "package/types/artplayer-plugin-ambilight.d.ts": "a36998b4d96d9a3200022029bf92918cc516d5ae4f38dc78a1a76b53a736d086" + }, + "manifest": { + "name": "artplayer-plugin-ambilight", + "version": "1.1.0", + "description": "ambilight plugin for ArtPlayer", + "author": "Harvey Zhao ", + "license": "MIT", + "homepage": "https://artplayer.org", + "repository": { + "type": "git", + "url": "git+https://github.com/zhw2590582/ArtPlayer.git" + }, + "bugs": { + "url": "https://github.com/zhw2590582/ArtPlayer/issues" + }, + "keywords": [ + "html5", + "video", + "player" + ], + "exports": { + ".": { + "types": "./types/artplayer-plugin-ambilight.d.ts", + "import": "./dist/artplayer-plugin-ambilight.mjs", + "require": "./dist/artplayer-plugin-ambilight.js" + }, + "./legacy": { + "types": "./types/artplayer-plugin-ambilight.d.ts", + "import": "./dist/artplayer-plugin-ambilight.legacy.js", + "require": "./dist/artplayer-plugin-ambilight.legacy.js" + } + }, + "main": "./dist/artplayer-plugin-ambilight.js", + "module": "./dist/artplayer-plugin-ambilight.mjs", + "types": "./types/artplayer-plugin-ambilight.d.ts", + "legacy": "./dist/artplayer-plugin-ambilight.legacy.js", + "browserslist": "last 1 Chrome version" + }, + "historicalCore": { + "commit": "daf133b22630b4a0eecfa3336bbddab0e9119d96", + "file": "packages/artplayer/package.json", + "version": "5.3.1", + "sha256LF": "862f3697876fe915dd0ada178da766ce823aae7563ed30b3e5b11b811202929c", + "limitation": "Git association only; not a proof of support range or rebuilt tarball identity" + } + }, + "previous": [ + { + "name": "artplayer-plugin-ambilight", + "version": "1.0.0", + "metadataUrl": "https://registry.npmjs.org/artplayer-plugin-ambilight/1.0.0", + "tarball": "https://registry.npmjs.org/artplayer-plugin-ambilight/-/artplayer-plugin-ambilight-1.0.0.tgz", + "integrity": "sha512-7UxcQLI1XZXJBfv4gZsTd1YKNL9Y7Uw2w13FyqgHqgtKWT06+EEl72UHl/C/Je1s1GHlTnw+Hdg6CphZbQ9k/g==", + "sha256": "a95aa8c232ab48493e6b4d46b36edc8091230cbde5d18d7ec41912a4fa2b8956", + "archiveBytes": 4751, + "publishedAt": "2024-10-19T06:09:24.603Z", + "registryGitHead": "9221d42964700e6a3190943d288a6e6822596790", + "files": { + "package/README.md": "f9efabb68702c9c9cafdaa6b82a919ef09c2fb511568764c79854a955503cb98", + "package/dist/artplayer-plugin-ambilight.js": "caef43df8a2d4bd7aa5b42db2a16508a1f5b92443b8b76974e5e24b2a2c83911", + "package/dist/artplayer-plugin-ambilight.legacy.js": "c6eb26f3e17432f62ac24a86f78789d3dcd2a7ca03c3701b65574cc916387c49", + "package/package.json": "84c90d6d521c9d14a8140bef65171dce443b318630742f9901eb7c5ef5d10a3b", + "package/src/index.js": "7e9eff950bbde38bfc63913f2ed6e47217d70cf0ba918eed3389df37d9b94a4f", + "package/types/artplayer-plugin-ambilight.d.ts": "f08f24f814395cfd5f02bbcb1a9c99a02ed4692e9137d7a393c3ed395bba1515" + }, + "manifest": { + "license": "MIT", + "version": "1.0.0", + "name": "artplayer-plugin-ambilight", + "types": "types/artplayer-plugin-ambilight.d.ts", + "description": "ambilight plugin for ArtPlayer", + "main": "dist/artplayer-plugin-ambilight.js", + "legacy": "dist/artplayer-plugin-ambilight.legacy.js", + "author": "Harvey Zack ", + "browserslist": "last 1 Chrome version", + "homepage": "https://artplayer.org", + "repository": { + "type": "git", + "url": "git+https://github.com/zhw2590582/ArtPlayer.git" + }, + "bugs": { + "url": "https://github.com/zhw2590582/ArtPlayer/issues" + }, + "keywords": [ + "html5", + "video", + "player" + ] + }, + "historicalCore": { + "commit": "9221d42964700e6a3190943d288a6e6822596790", + "file": "packages/artplayer/package.json", + "version": "5.1.7", + "sha256LF": "8bd532103099c367fb2c289e0d410f144fdeb00b1b62cf850762a24b61e63c8a", + "limitation": "Git association only; not a proof of support range or rebuilt tarball identity" + } + } + ], + "source": { + "packages/artplayer-plugin-ambilight/src/index.js": "c56e0a1c33a2ff54b20e6bc7eafd3e14b163e3f967cce86d48e594a2f6d1f6fe", + "packages/artplayer-plugin-ambilight/types/artplayer-plugin-ambilight.d.ts": "a36998b4d96d9a3200022029bf92918cc516d5ae4f38dc78a1a76b53a736d086", + "packages/artplayer-plugin-ambilight/package.json": "39aa2790a79cbc2a62ba925ee5ff53bbb649440a5d7b9f526b3de819ece4f792", + "packages/artplayer-plugin-ambilight/README.md": "53946ecc0fca163d52465515dfa13de3619a01c3b89d7f966eafe0610584dede", + "docs/assets/example/ambilight.js": "f97c1404886ba6474b4e14339c563fd2de50b7d50fdc4c9d59d85a3142a1ad52" + }, + "limitation": "Frozen actual archives and workspace source; runtime/type compatibility and supported core ranges require separate tests" +} diff --git a/refactor/changes/2026-09-12-PKG-AMBILIGHT-01-contract.md b/refactor/changes/2026-09-12-PKG-AMBILIGHT-01-contract.md new file mode 100644 index 000000000..0e0025925 --- /dev/null +++ b/refactor/changes/2026-09-12-PKG-AMBILIGHT-01-contract.md @@ -0,0 +1,12 @@ +# PKG-AMBILIGHT-01 实际发布契约 + +冻结npm1.0.0/1.1.0共12成员及c57c7e4b源码/声明/README/demo。旧CJS.default、新 +可调用CJS、原生ESM和全局实际执行;三实现配置读取、9格坐标、样式及同步返回有 +受控断言。声明只核对真实文件形状,不冒充完整消费者编译。 + +zIndex一直忽略、运行时可省略参数而声明要求参数;1.0.0至1.1.0已发生导出形式及 +字段可选性变化。见 [契约](../baselines/ambilight-contract.md)。Git关联核心5.1.7/5.3.1 +不作支持区间。生命周期源码观察交02复现、03修复。索引新增本包两个实际发布点。 + +本步无生产源码/声明/产物修改。7组专项和完整CI记录在验证文件;独立提交后继续02。 +回退仅移除本包新增契约/测试/辅助记录及索引项,不回写旧发布文件。未推送或发布。 diff --git a/refactor/contract-coverage.md b/refactor/contract-coverage.md index f3481bc4f..d3f1ceadc 100644 --- a/refactor/contract-coverage.md +++ b/refactor/contract-coverage.md @@ -10,7 +10,7 @@ ENG-COVERAGE-01建立可校验的归属和证据索引,不把索引建成当 `contract-policy.json`维护逐包逐类责任、版本依据及稳定测试ID。版本用JSON pointer 指向冻结清单,核对包名、版本和发布archive integrity;工作区单独标识。当前22个 -工作区、7个发布对照点,不由单个版本推断连续支持区间。 +工作区、9个发布对照点(Ambilight新增两个),不由单个版本推断连续支持区间。 `contract-runs.json`引用归档报告及LF规范化SHA-256。报告保存当时定义、实际事件、 输入指纹、命令、HEAD和Node/OS。其他tasks.json历史JSON证据列为recorded-not-normalized, diff --git a/refactor/contract-index.md b/refactor/contract-index.md index c10b93a24..aaa7bc3f8 100644 --- a/refactor/contract-index.md +++ b/refactor/contract-index.md @@ -316,3 +316,5 @@ - PUB-artplayer-plugin-dash-control-1.1.0: artplayer-plugin-dash-control@1.1.0 (published-baseline);[来源](../refactor/baselines/dash-control-release.json) JSON pointer `/release`。 - PUB-artplayer-plugin-ads-1.0.6: artplayer-plugin-ads@1.0.6 (published-baseline);[来源](../refactor/baselines/ads-release.json) JSON pointer `/release`。 - PUB-artplayer-plugin-vast-1.0.0: artplayer-plugin-vast@1.0.0 (published-baseline);[来源](../refactor/baselines/vast-release.json) JSON pointer `/release`。 +- PUB-artplayer-plugin-ambilight-1.1.0: artplayer-plugin-ambilight@1.1.0 (published-baseline);[来源](../refactor/baselines/ambilight-release.json) JSON pointer `/release`。 +- PUB-artplayer-plugin-ambilight-1.0.0: artplayer-plugin-ambilight@1.0.0 (published-baseline);[来源](../refactor/baselines/ambilight-release.json) JSON pointer `/previous/0`。 diff --git a/refactor/contract-policy.json b/refactor/contract-policy.json index a1b406679..28866067b 100644 --- a/refactor/contract-policy.json +++ b/refactor/contract-policy.json @@ -114,7 +114,9 @@ "API-12": "PKG-AMBILIGHT-02" }, "versionIds": [ - "WS-artplayer-plugin-ambilight" + "WS-artplayer-plugin-ambilight", + "PUB-artplayer-plugin-ambilight-1.1.0", + "PUB-artplayer-plugin-ambilight-1.0.0" ], "supportWindow": "not-inferred-from-baseline", "scopeReview": "Each public member, callback, option, event and capability requires package contract review; family ownership alone is not exhaustive coverage" @@ -782,6 +784,24 @@ "file": "refactor/baselines/vast-release.json", "pointer": "/release", "archiveSha256": "35681d60fa3fa09b92f0f30887e39c73cf55e7b14d51a1c38fccbc6c1f253bab" + }, + { + "id": "PUB-artplayer-plugin-ambilight-1.1.0", + "package": "artplayer-plugin-ambilight", + "kind": "published-baseline", + "version": "1.1.0", + "file": "refactor/baselines/ambilight-release.json", + "pointer": "/release", + "archiveSha256": "e95bd60fd7bce52142cf0041bad811452305029cbe1129c7d89246eb260c5c86" + }, + { + "id": "PUB-artplayer-plugin-ambilight-1.0.0", + "package": "artplayer-plugin-ambilight", + "kind": "published-baseline", + "version": "1.0.0", + "file": "refactor/baselines/ambilight-release.json", + "pointer": "/previous/0", + "archiveSha256": "a95aa8c232ab48493e6b4d46b36edc8091230cbde5d18d7ec41912a4fa2b8956" } ], "cases": [ diff --git a/refactor/plan.md b/refactor/plan.md index 5b0fe70c1..017ce354a 100644 --- a/refactor/plan.md +++ b/refactor/plan.md @@ -4,7 +4,7 @@ 基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 220 项,范围 22 个包及工作区/示例。 -状态:todo 130 / doing 6 / blocked 0 / done 84 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。 +状态:todo 129 / doing 6 / blocked 0 / done 85 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。 前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。 @@ -148,7 +148,7 @@ | ID | 范围 / 步骤 | 前置依赖 | 交付物 | 验收条件 | 风险 | 状态 | | --- | --- | --- | --- | --- | --- | --- | -| PKG-AMBILIGHT-01 | artplayer-plugin-ambilight
核对包契约与历史用法 | BASE-05 | blur/opacity/frequency/duration、start/stop 与挂载样式 | 源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结 | M | todo | +| PKG-AMBILIGHT-01 | artplayer-plugin-ambilight
核对包契约与历史用法 | BASE-05 | blur/opacity/frequency/duration、start/stop 与挂载样式 | 源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结 | M | done | | PKG-AMBILIGHT-02 | artplayer-plugin-ambilight
建立特有行为与错误测试 | PKG-AMBILIGHT-01, ENG-03, ENG-05 | 跨域 canvas 失败、零尺寸、暂停、重复 start/stop 和 destroy | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | M | todo | | PKG-AMBILIGHT-03 | artplayer-plugin-ambilight
整理内部职责与资源 | PKG-AMBILIGHT-02, CORE-03, CORE-12 | 取色计算与绘帧分离,统一 RAF 和 DOM 清理 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | M | todo | | PKG-AMBILIGHT-04 | artplayer-plugin-ambilight
迁移自有源码和公开类型 | PKG-AMBILIGHT-03, ENG-04, ENG-06, CORE-07 | canvas 上下文、参数及 start/stop 的明确类型 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | M | todo | @@ -476,6 +476,7 @@ - PKG-CHAPTER-03: [记录](changes/2026-09-10-PKG-CHAPTER-03-typescript-modules.md) [记录](baselines/chapter-migration-validation.json) - PKG-CHAPTER-04: [记录](changes/2026-09-10-PKG-CHAPTER-04-public-types.md) [记录](baselines/chapter-types-validation.json) - PKG-CHAPTER-05: [记录](changes/2026-09-12-PKG-CHAPTER-05-combinations.md) [记录](baselines/chapter-combinations-checkpoint.json) +- PKG-AMBILIGHT-01: [记录](changes/2026-09-12-PKG-AMBILIGHT-01-contract.md) [记录](baselines/ambilight-contract.md) [记录](baselines/ambilight-release.json) [记录](baselines/ambilight-contract-validation.json) - PKG-AUDIO-01: [记录](changes/2026-09-12-PKG-AUDIO-01-contract.md) [记录](baselines/audio-track-contract.md) [记录](baselines/audio-track-release.json) - PKG-AUDIO-02: [记录](changes/2026-09-12-PKG-AUDIO-02-tests.md) [记录](audio-validation.md) [记录](baselines/audio-validation.json) - PKG-AUDIO-03: [记录](changes/2026-09-12-PKG-AUDIO-03-lifecycle.md) [记录](baselines/audio-lifecycle-validation.json) diff --git a/refactor/progress.md b/refactor/progress.md index 05de2f8c8..c8cf35433 100644 --- a/refactor/progress.md +++ b/refactor/progress.md @@ -1,5 +1,14 @@ # 进度与证据 +## 最新完成:Ambilight-01 实际发布契约 + +冻结npm1.0.0/1.1.0共12成员及工作区源码/声明/demo;真实执行旧CJS.default、新CJS/ +ESM/全局和三实现正常取色契约。zIndex一直忽略,参数省略与声明、历史导出形状的 +差异已登记。7组专项及7组索引反例通过,完整CI主链925项通过(另44项重复观察)。 +生命周期仍是源码观察,02复现、03修复;尚无生产源码改动或实际浏览器验收。当前 +220项:85 done、6 doing、129 todo。见 [实施记录](changes/2026-09-12-PKG-AMBILIGHT-01-contract.md) +及 [验证证据](baselines/ambilight-contract-validation.json)。下一步Ambilight-02/03。 + ## 最新完成:ENG-09 工程保障整合 提交审计、核心到22包影响映射、264行契约归属和文档检查整合核验通过;三个子项已有 diff --git a/refactor/risk-table.md b/refactor/risk-table.md index 87ddbc7c6..bd262b399 100644 --- a/refactor/risk-table.md +++ b/refactor/risk-table.md @@ -151,3 +151,6 @@ | VAST-TYPE-01 | open / 已复现 | VAST declares an async factory as synchronous and imports an unavailable SDK type dependency | PKG-VAST-04, PKG-VAST-06 | | VAST-LIFE-01 | open / 已复现 | VAST SDK and container lack core destruction ownership and pending work cancellation | PKG-VAST-02, PKG-VAST-03, PKG-VAST-05 | | VAST-DIST-01 | open / 已复现 | Published VAST CommonJS default namespace differs from the current callable distribution | PKG-VAST-04, PKG-VAST-06 | +| AMBILIGHT-LIFE-01 | open / 源码/产物事实 | Ambilight sampling and RAF/DOM ownership lack terminal/error handling | PKG-AMBILIGHT-02, PKG-AMBILIGHT-03, PKG-AMBILIGHT-05 | +| AMBILIGHT-TYPE-01 | open / 源码/产物事实 | Ambilight declarations disagree with optional factory argument and differ across published generations | PKG-AMBILIGHT-04, PKG-AMBILIGHT-06 | +| AMBILIGHT-DIST-01 | open / 已复现 | Ambilight 1.0.0 CommonJS default namespace differs from callable 1.1.0 | PKG-AMBILIGHT-04, PKG-AMBILIGHT-06 | diff --git a/refactor/risks.json b/refactor/risks.json index f0d506811..062174d09 100644 --- a/refactor/risks.json +++ b/refactor/risks.json @@ -3251,6 +3251,58 @@ ], "compatibleResolution": "Preserve require(pkg).default alongside callable factory, script global and current ESM/legacy paths.", "closureCriteria": "Real packed main/legacy/ESM, historical deep imports, export= consumers and local demo/editor all pass with immutable published comparisons." + }, + { + "id": "AMBILIGHT-LIFE-01", + "title": "Ambilight sampling and RAF/DOM ownership lack terminal/error handling", + "confirmation": "source-observed", + "owners": [ + "PKG-AMBILIGHT-02", + "PKG-AMBILIGHT-03", + "PKG-AMBILIGHT-05" + ], + "compatibleResolution": "Reproduce errors, zero sizes, RAF id zero, escaped methods and destroy before adding plugin-local ownership without changing stop semantics.", + "closureCriteria": "Published failures reproduced, candidate cleanup and real canvas/media/source-switch checks pass.", + "status": "open", + "evidence": [ + "refactor/baselines/ambilight-contract.md", + "refactor/baselines/ambilight-release.json", + "refactor/changes/2026-09-12-PKG-AMBILIGHT-01-contract.md" + ] + }, + { + "id": "AMBILIGHT-TYPE-01", + "title": "Ambilight declarations disagree with optional factory argument and differ across published generations", + "confirmation": "source-observed", + "owners": [ + "PKG-AMBILIGHT-04", + "PKG-AMBILIGHT-06" + ], + "compatibleResolution": "Preserve legal old/new calls and ignored zIndex, validate export= and default consumers without narrowing inferred options.", + "closureCriteria": "Installed old/current compiler consumers and documented historical limitations verified.", + "status": "open", + "evidence": [ + "refactor/baselines/ambilight-contract.md", + "refactor/baselines/ambilight-release.json", + "refactor/changes/2026-09-12-PKG-AMBILIGHT-01-contract.md" + ] + }, + { + "id": "AMBILIGHT-DIST-01", + "title": "Ambilight 1.0.0 CommonJS default namespace differs from callable 1.1.0", + "confirmation": "reproduced", + "owners": [ + "PKG-AMBILIGHT-04", + "PKG-AMBILIGHT-06" + ], + "compatibleResolution": "Keep callable factory with compatible self default alias.", + "closureCriteria": "Packed root/legacy/global/ESM and old namespace consumers pass.", + "status": "open", + "evidence": [ + "refactor/baselines/ambilight-contract.md", + "refactor/baselines/ambilight-release.json", + "refactor/changes/2026-09-12-PKG-AMBILIGHT-01-contract.md" + ] } ] } diff --git a/refactor/scripts/ambilight-contract.mjs b/refactor/scripts/ambilight-contract.mjs new file mode 100644 index 000000000..a1da44283 --- /dev/null +++ b/refactor/scripts/ambilight-contract.mjs @@ -0,0 +1,43 @@ +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 { archiveFiles, ensureArchive, hash, readMember, refactorDir } from './releases.mjs' + +export async function verifyAmbilightContract() { + const baseline = JSON.parse(fs.readFileSync(path.join(refactorDir, 'baselines/ambilight-release.json'), 'utf8')) + const archives = new Map() + for (const release of [baseline.release, ...baseline.previous]) { + const archive = await ensureArchive(release) + assert.deepEqual(archiveFiles(archive), Object.keys(release.files).sort()) + for (const [file, expected] of Object.entries(release.files)) + assert.equal(hash(readMember(archive, file)), expected, `${release.version}/${file}`) + assert.deepEqual(JSON.parse(readMember(archive, 'package/package.json')), release.manifest) + for (const field of ['main', 'module', 'types', 'legacy']) { + const entry = release.manifest[field] + if (entry) + assert(Object.hasOwn(release.files, `package/${entry.replace(/^\.\//, '')}`)) + } + if (release.historicalCore) { + const core = release.historicalCore + const content = execFileSync('git', ['show', `${core.commit}:${core.file}`], { encoding: 'utf8' }).replaceAll('\r\n', '\n') + assert.equal(hash(content), core.sha256LF) + assert.equal(JSON.parse(content).version, core.version) + } + archives.set(release.version, archive) + } + const sources = new Map() + for (const [file, expected] of Object.entries(baseline.source)) { + const content = execFileSync('git', ['show', `${baseline.sourceCommit}:${file}`], { encoding: 'utf8' }).replaceAll('\r\n', '\n') + assert.equal(hash(content), expected, `Ambilight frozen source: ${file}`) + sources.set(file, content) + } + return { baseline, archives, sources } +} + +if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + const { baseline } = await verifyAmbilightContract() + console.log(`Ambilight verified: ${[baseline.release, ...baseline.previous].map(release => release.version).join(', ')}; 12 members, frozen workspace and Git core associations`) +} diff --git a/refactor/scripts/ambilight-contract.test.mjs b/refactor/scripts/ambilight-contract.test.mjs new file mode 100644 index 000000000..f5a28a7dc --- /dev/null +++ b/refactor/scripts/ambilight-contract.test.mjs @@ -0,0 +1,85 @@ +import assert from 'node:assert/strict' +// eslint-disable-next-line test/no-import-node-test -- Freeze actual published plugin contracts before migration. +import test from 'node:test' +import vm from 'node:vm' +import { ambilightEnvironment, ambilightHistorical } from '../../test/helpers/ambilight.js' +import { verifyAmbilightContract } from './ambilight-contract.mjs' +import { readMember } from './releases.mjs' + +const contract = await verifyAmbilightContract() + +test('Ambilight archives, manifests and source snapshots retain their exact identities', () => { + assert.equal(contract.baseline.release.version, '1.1.0') + assert.equal(contract.baseline.previous[0].version, '1.0.0') + assert.equal([...contract.archives].length, 2) + assert.equal(contract.baseline.release.historicalCore.version, '5.3.1') + assert.equal(contract.baseline.previous[0].historicalCore.version, '5.1.7') +}) + +test('Ambilight published CommonJS and script globals preserve their distinct export shapes', () => { + for (const release of [contract.baseline.release, ...contract.baseline.previous]) { + for (const field of ['main', 'legacy']) { + const code = readMember(contract.archives.get(release.version), `package/${release.manifest[field].replace(/^\.\//, '')}`).toString() + const module = { exports: {} } + const window = {} + vm.runInNewContext(code, { module, exports: module.exports, window }, { timeout: 5000 }) + assert.equal(typeof module.exports, release.version === '1.0.0' ? 'object' : 'function') + assert.equal(typeof (module.exports.default || module.exports)(), 'function') + const global = { window: {} } + vm.runInNewContext(code, global, { timeout: 5000 }) + assert.equal(typeof (global.artplayerPluginAmbilight || global.window.artplayerPluginAmbilight), 'function') + } + } +}) + +test('Ambilight published declarations retain the required options argument and ignored zIndex field', () => { + const old = readMember(contract.archives.get('1.0.0'), 'package/types/artplayer-plugin-ambilight.d.ts').toString() + const current = readMember(contract.archives.get('1.1.0'), 'package/types/artplayer-plugin-ambilight.d.ts').toString() + assert.match(old, /export = artplayerPluginAmbilight/) + assert.match(current, /export default artplayerPluginAmbilight/) + for (const source of [old, current]) { + assert.match(source, /\(option: Option\)/) + assert.match(source, /zIndex\??: number/) + } + assert.match(current, /blur\?: string/) + assert.match(old, /blur: string/) +}) + +test('Ambilight 1.1.0 native ESM import exposes the default factory', async () => { + const source = readMember(contract.archives.get('1.1.0'), 'package/dist/artplayer-plugin-ambilight.mjs').toString() + const namespace = await import(`data:text/javascript,${encodeURIComponent(source)}`) + assert.deepEqual(Object.keys(namespace), ['default']) + assert.equal(typeof namespace.default, 'function') + assert.equal(typeof namespace.default(), 'function') +}) + +for (const implementation of await ambilightHistorical()) { + test(`Ambilight ${implementation.name}: defaults, option-read timing, styling, grid sampling and public returns`, async () => { + const env = await ambilightEnvironment(implementation) + const option = { opacity: 0.1, zIndex: -99 } + const attach = env.factory(option) + option.opacity = 0.7 + const result = attach(env.art) + const element = env.parent.children[0] + assert.equal(element.className, 'artplayer-plugin-ambilight') + assert.equal(element.children.length, 9) + assert.equal(element.style.zIndex, 9, 'Historical zIndex option is ignored') + assert.equal(element.children[0].style.opacity, 0.7) + assert.equal(element.children[0].style.filter, 'blur(50px)') + assert.equal(element.children[0].style.transition, 'background-color 0.3s ease') + assert.deepEqual(Object.keys(result), ['name', 'start', 'stop']) + assert.equal(result.name, 'artplayerPluginAmbilight') + assert.equal(result.start(), undefined) + assert.equal(env.frames.size, 1) + assert.equal(env.draws.length, 0) + env.art.playing = true + env.frame(100) + assert.equal(env.draws.length, 9) + assert.deepEqual([...env.draws[0].slice(1)], [0, 0, 100, 50, 0, 0, 1, 1]) + assert.deepEqual([...env.draws[8].slice(1)], [200, 100, 100, 50, 0, 0, 1, 1]) + assert.equal(element.children[8].style.backgroundColor, 'rgb(9, 20, 30)') + assert.equal(result.stop(), undefined) + assert.equal(env.frames.size, 0) + assert.equal(typeof env.factory(), 'function', 'Runtime accepts omitted options despite the old declaration') + }) +} diff --git a/refactor/scripts/contracts.test.mjs b/refactor/scripts/contracts.test.mjs index 10a38d924..fbb61e29a 100644 --- a/refactor/scripts/contracts.test.mjs +++ b/refactor/scripts/contracts.test.mjs @@ -44,7 +44,7 @@ function fixture(t) { test('Actual contract model assigns all 264 family rows and separates exact published versions from workspace snapshots', () => { assert.equal(actual.policy.packages.length, 22) assert.equal(actual.families.length, 12) - assert.equal(actual.policy.versions.filter(version => version.kind === 'published-baseline').length, 7) + assert.equal(actual.policy.versions.filter(version => version.kind === 'published-baseline').length, 9) assert.equal(actual.policy.versions.filter(version => version.kind === 'workspace-baseline').length, 22) assert.equal(actual.policy.cases.length, 10) const markdown = renderContracts(actual) diff --git a/refactor/tasks.json b/refactor/tasks.json index a449d3da0..7c31c358b 100644 --- a/refactor/tasks.json +++ b/refactor/tasks.json @@ -1458,11 +1458,16 @@ "dependsOn": [ "BASE-05" ], - "status": "todo", + "status": "done", "risk": "M", "deliverable": "blur/opacity/frequency/duration、start/stop 与挂载样式", "acceptance": "源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结", - "evidence": [] + "evidence": [ + "changes/2026-09-12-PKG-AMBILIGHT-01-contract.md", + "baselines/ambilight-contract.md", + "baselines/ambilight-release.json", + "baselines/ambilight-contract-validation.json" + ] }, { "id": "PKG-AMBILIGHT-02", diff --git a/test/helpers/ambilight.js b/test/helpers/ambilight.js new file mode 100644 index 000000000..875b7960a --- /dev/null +++ b/test/helpers/ambilight.js @@ -0,0 +1,132 @@ +import assert from 'node:assert/strict' +import vm from 'node:vm' +import { transform } from 'esbuild' +import { verifyAmbilightContract } from '../../refactor/scripts/ambilight-contract.mjs' +import { readMember } from '../../refactor/scripts/releases.mjs' + +export async function ambilightHistorical() { + const contract = await verifyAmbilightContract() + return [ + ...[contract.baseline.release, ...contract.baseline.previous].map(release => ({ name: `published-${release.version}`, source: readMember(contract.archives.get(release.version), `package/${release.manifest.main.replace(/^\.\//, '')}`).toString(), format: 'artifact' })), + { name: 'frozen-workspace', source: contract.sources.get('packages/artplayer-plugin-ambilight/src/index.js'), format: 'source' }, + ] +} + +export async function ambilightEnvironment(implementation, settings = {}) { + let time = 0 + let nextFrame = settings.firstFrame ?? 1 + const frames = new Map() + const draws = [] + const cancelled = [] + const nodes = [] + const listeners = new Map() + const state = { drawError: null, readError: null, draws, frames, cancelled, nodes, time: value => time = value } + const context = settings.noContext + ? null + : { + drawImage(...args) { + draws.push(args) + settings.onDraw?.() + if (state.drawError) + throw state.drawError + }, + getImageData() { + if (state.readError) + throw state.readError + return { data: [draws.length, 20, 30, 255] } + }, + } + function node(tag) { + let markup = '' + const value = { + tag, + children: [], + parentNode: null, + style: {}, + className: '', + width: 300, + height: 150, + get innerHTML() { return markup }, + set innerHTML(value) { + markup = value + this.children = Array.from({ length: (value.match(/
/g) || []).length }, () => node('div')) + }, + insertBefore(child, before) { + assert.equal(before.parentNode, this) + const index = this.children.indexOf(before) + this.children.splice(index, 0, child) + child.parentNode = this + }, + removeChild(child) { + assert.equal(child.parentNode, this) + this.children.splice(this.children.indexOf(child), 1) + child.parentNode = null + return child + }, + remove() { this.parentNode?.removeChild(this) }, + getContext: () => context, + } + nodes.push(value) + return value + } + const parent = node('div') + const video = Object.assign(node('video'), { videoWidth: 300, videoHeight: 150 }) + parent.children.push(video) + video.parentNode = parent + const art = { + template: { $video: video }, + playing: false, + isDestroy: false, + constructor: { utils: { createElement: node, addClass: (element, name) => element.className = name, setStyles: (element, styles) => Object.assign(element.style, styles) } }, + on(name, callback) { + if (!listeners.has(name)) + listeners.set(name, new Set()) + listeners.get(name).add(callback) + return art + }, + off(name, callback) { + listeners.get(name)?.delete(callback) + return art + }, + emit(name, ...args) { for (const callback of [...(listeners.get(name) || [])]) callback(...args) }, + destroy() { + art.isDestroy = true + art.emit('destroy') + }, + } + const module = { exports: {} } + const source = implementation.format === 'source' ? (await transform(implementation.source, { loader: 'js', format: 'cjs', target: 'es2020' })).code : implementation.source + vm.runInNewContext(source, { + module, + exports: module.exports, + window: {}, + performance: { now: () => time }, + requestAnimationFrame(callback) { + const id = nextFrame++ + frames.set(id, callback) + return id + }, + cancelAnimationFrame(id) { + cancelled.push(id) + frames.delete(id) + }, + }, { timeout: 5000 }) + const factory = module.exports.default || module.exports + return { + ...state, + art, + parent, + video, + listeners, + context, + factory, + frame(now) { + time = now + const pending = [...frames] + frames.clear() + for (const [, callback] of pending) callback(now) + }, + setDrawError(error) { state.drawError = error }, + setReadError(error) { state.readError = error }, + } +}