test(ambilight): [PKG-AMBILIGHT-01] freeze actual release and invocation contracts

This commit is contained in:
Harvey Zhao committed 2026-09-12 10:54:28 +08:00
1 parent c57c7e4b16
commit 06032a6906
17 files changed
+638 -7

No files matched your search

+13
View File
@@ -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关联不构成连续核心支持范围。
@@ -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"
]
}
+41
View File
@@ -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)。
+137
View File
@@ -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 <laozhaochaguan@gmail.com>",
"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 <laozhaochaguan@gmail.com>",
"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"
}
@@ -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。
回退仅移除本包新增契约/测试/辅助记录及索引项,不回写旧发布文件。未推送或发布。
+1 -1
View File
@@ -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,
+2
View File
@@ -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`。
+21 -1
View File
@@ -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": [
+3 -2
View File
@@ -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<br>核对包契约与历史用法 | BASE-05 | blur/opacity/frequency/duration、start/stop 与挂载样式 | 源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结 | M | todo |
| PKG-AMBILIGHT-01 | artplayer-plugin-ambilight<br>核对包契约与历史用法 | BASE-05 | blur/opacity/frequency/duration、start/stop 与挂载样式 | 源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结 | M | done |
| PKG-AMBILIGHT-02 | artplayer-plugin-ambilight<br>建立特有行为与错误测试 | PKG-AMBILIGHT-01, ENG-03, ENG-05 | 跨域 canvas 失败、零尺寸、暂停、重复 start/stop 和 destroy | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | M | todo |
| PKG-AMBILIGHT-03 | artplayer-plugin-ambilight<br>整理内部职责与资源 | PKG-AMBILIGHT-02, CORE-03, CORE-12 | 取色计算与绘帧分离,统一 RAF 和 DOM 清理 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | M | todo |
| PKG-AMBILIGHT-04 | artplayer-plugin-ambilight<br>迁移自有源码和公开类型 | 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)
+9
View File
@@ -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行契约归属和文档检查整合核验通过;三个子项已有
+3
View File
@@ -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 |
+52
View File
@@ -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"
]
}
]
}
+43
View File
@@ -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`)
}
@@ -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')
})
}
+1 -1
View File
@@ -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)
+7 -2
View File
@@ -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",
+132
View File
@@ -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(/<div>/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 },
}
}