test(auto-thumbnail): [PKG-AUTO-THUMB-01] freeze published and workspace contracts

This commit is contained in:
Harvey Zhao committed 2026-09-13 05:47:40 +08:00
1 parent 17befee4b1
commit cdc252b158
15 files changed
+919 -8

No files matched your search

@@ -0,0 +1,93 @@
{
"schemaVersion": 1,
"task": "PKG-AUTO-THUMB-01",
"status": "done",
"parentCommit": "17befee4b1df7cf4ec29bbda4fd3271567f44223",
"checks": [
{
"command": "node --test refactor/scripts/auto-thumbnail-contract.test.mjs refactor/scripts/contracts.test.mjs refactor/scripts/risk-register.test.mjs",
"result": "pass",
"tests": 47,
"packageContract": 38,
"indexValidation": 7,
"riskValidation": 2,
"log": "refactor/.cache/auto-thumbnail01-focused.log"
},
{
"command": "focused eslint --no-fix",
"result": "pass"
},
{
"command": "yarn ci:check",
"result": "pass",
"tests": {
"unit": 1456,
"engineering": 14,
"baseline": 232,
"total": 1702
},
"strictProductionTypeScriptFiles": 339,
"log": "refactor/.cache/auto-thumbnail01-ci.log",
"logSha256": "5f47030026c9cfad99830281a0b825bc2247c52bab7368a831e4b86ba914c880",
"warnings": [
"Existing generated core editor declaration unused eslint suppression",
"Existing Yarn DEP0169 url.parse warning"
]
}
],
"archiveScope": {
"publishedVersions": [
"1.1.0",
"1.0.1",
"1.0.0"
],
"members": 16,
"workspaceFiles": 8,
"runnableRootVersions": [
"1.1.0",
"1.0.1"
],
"sourceOnlyFixture": "Actual 1.0.0 tarball source transformed by esbuild; not a runnable npm root entry",
"behaviorFixtures": 8,
"latestWorkspaceComparison": "All six matching 1.1.0 package members equal frozen workspace after LF normalization",
"supportedCoreRange": "Not inferred from Git associations 5.2.2/5.3.1"
},
"limitations": [
"Controlled DOM, seek and toBlob callbacks are not native decoding, pixel, CORS, device or browser evidence.",
"The eight fixtures include one explicitly source-only historical package; missing 1.0.0 main/legacy are never fabricated.",
"Lifecycle and encoder concerns remain source-observed pending task02 failure reproduction.",
"No production source, declarations, examples, versions or build artifacts change in task01.",
"Package-family exact case indexing remains incomplete and is not promoted by these frozen baseline results."
],
"completedAt": "2026-09-12T21:47:22.218Z",
"inputs": [
{
"file": "refactor/baselines/auto-thumbnail-release.json",
"sha256LF": "d87b151ed653dca83047132f1ed3ca474ef52124db7f6a20503204e74ba392ba"
},
{
"file": "refactor/baselines/auto-thumbnail-contract.md",
"sha256LF": "dc14c8e51afbd86cedd1da08100812c97e459680b13021269dcb0117ce12a6db"
},
{
"file": "refactor/scripts/auto-thumbnail-contract.mjs",
"sha256LF": "53871d5c387152b332f9e337ac23969de227533af8cd3196f8f5a444d2aa4f4d"
},
{
"file": "refactor/scripts/auto-thumbnail-contract.test.mjs",
"sha256LF": "df43a7f581aec0dc286383cfe4eb64f5737b38e9b75bac1a410c85fb15e4e461"
},
{
"file": "test/helpers/auto-thumbnail.js",
"sha256LF": "6c4fb8a27d5495578fb18df813458b2cb1ae467f0a182de14d75b72f2f760c3a"
},
{
"file": "refactor/contract-policy.json",
"sha256LF": "420064c3288adfab9813a6163a6ab294c2dc2459d008cef3c82011293a19c0bb"
},
{
"file": "refactor/scripts/contracts.test.mjs",
"sha256LF": "8492e6eafe5fa656cf84a726881a62766a13228f218d9d0894125b59759724e1"
}
]
}
@@ -0,0 +1,81 @@
# Auto-thumbnail 历史契约
`PKG-AUTO-THUMB-01` 的基线是实际 npm 归档和固定 Git 输入。
见 [归档清单](auto-thumbnail-release.json) 与 [验证结果](auto-thumbnail-contract-validation.json)。
本步没有修改生产源码、公开声明、包版本或 dist;目标版本仍按原计划为 `2.0.0`。
## 发布范围与来源
2026-09-13(北京时间)读取 npm registry,latest 为 `1.1.0`,全部版本是
`1.0.0`、`1.0.1`、`1.1.0`。三份原始 tarball 共 16 个文件逐一校验
registry SHA-512、归档 SHA-256、成员 SHA-256;离线缓存缺失时按固定 URL 重新下载并校验。
归档保存在忽略的 `.cache/releases`,可重跑脚本不依赖可变 latest。
| 版本 | 实际文件与入口 | 声明 | Git 关联核心 |
| --- | --- | --- | --- |
| 1.0.0 | 4 文件,只有源码/README/manifest/types;声明的 main、legacy 不存在 | export= / UMD namespace;错写 height、漏写 number | 5.2.2 |
| 1.0.1 | 6 文件,main/legacy 实际存在;CJS 为 `{ default: function }`;源码随包发布 | 同 1.0.0 | 5.2.2 |
| 1.1.0 | 6 文件,main/legacy 为直接函数;真实 mjs 仅 default;root/legacy exports | default;number 替换 height;仍误写同步 Result | 5.3.1 |
| 固定工作区 1.1.0 | 8 个输入,另含源码/demo;六个对应发布文件按 LF 完全一致 | 同 1.1.0 | 另测最终核心 |
registry gitHead 只证明关联仓库状态,不证明构建可重现或该核心组合通过。
三个 manifest 都没有 peerDependencies、运行依赖或声明支持范围。
后续05至少验证实际核心 5.2.2、5.3.1、已发布稳定核心 5.4.0 与最终候选核心;
这些是验证目标,不能称为已支持的整个版本区间。
八份行为夹具包含两份可运行发布包的 main/legacy、三个工作区 source/main/legacy,
以及 **1.0.0 随包源码经 esbuild 转换的 source-only 夹具**。
绝不将 source-only 写成可从 npm root 正常导入的 1.0.0。
1.0.0/1.0.1 随包源码格式化/minify 后相同;1.1.0 去掉无用途的 loadedCount
以及手写 window 注入,正常运行契约相同;新构建仍有脚本全局入口。
## 必须保留的可用行为
- 函数/global 名 `artplayerPluginAutoThumbnail`;工厂返回注册函数。
注册同步安装 `video:loadedmetadata` 监听器,实际返回 Promise,resolve 后仅有
`{ name: 'artplayerPluginAutoThumbnail' }`。不会在导入/注册时创建 video。
- options 是活引用,每次核心 metadata 事件重新读取;url 优先显式 option.url,
否则读取 `art.option.url`,不是任意假定的 `art.url`。width/number/scale 使用 `||`
默认 160/100/1,旧调用中的 0 和空字符串走默认。声明上 options 必填。
- 独立 video,`crossOrigin='anonymous'`;高度按视频比例向下取整。
永远十列;canvas 宽 width*10,高 height*ceil(number/10)。scale 只传给核心缩略图配置。
- 帧时间为 duration*index/number(从 0 开始),不是 Thumbnail 工具的中点策略。
输出 JPEG,每次更新赋值核心 thumbnails,包含 url/height/column/number/width/scale。
保留渐进可用的拼图更新,不偷偷改成整段生成完才更新。
- 旧声明中的 height 实际从未参与布局。迁移应兼容该参数的旧类型用法并明确它无效;
不能突然赋予它改变既有运行结果的新含义。
## 观察到的缺陷与迁移边界
以下是历史对照,**不是要求候选永久保留的缺陷**。
1. 类型把 async 注册写成同步 Result;1.0.x/1.1.0 的 Option 和导出声明不同。
04须用实际安装的旧消费者验证,不用 `any` 或仅类型断言伪造 Promise 行为。
若完整推导兼容确实冲突,要列出具体旧调用再按用户的兼容决策流程处理。
2. 1.0.1 CJS 使用者可能调用 `require(name).default`,1.1.0 直接调用 require 结果。
04/06须同时验证;不能因最新包不再有 .default 就忽略旧使用者。
3. 1.0.0 缺运行入口;无需复制坏发布,但新 tarball 必须验证真实 root/legacy/ESM/types
和历史源路径的处置。使用 `yarn pack`/仓库外安装,禁止以工作区软链接冒充安装证据。
4. 没有 destroy/restart 清理,重复 metadata 创建多个解码任务;最终 Blob URL 无人释放。
02需复现切源、迟到 metadata/seek/blob、destroy 和跨实例;03明确任务与资源所有权。
5. 每轮在 seek/draw **之前**排入 toBlob,包含第一个全空快照;第一轮先设 currentTime
再装 onseeked。编码回调不串行,可能乱序覆盖;没有处理 null Blob、媒体/画布/核心
setter 失败、非有限尺寸。02补可控故障回归和真实浏览器对照,再由03修复。
保留有效帧顺序/坐标/渐进更新;空白预览、失效更新、竞态不作为有效 API。
6. options 省略时注册 Promise 正常 resolve,直到 metadata 回调才抛 TypeError。
当前声明不允许省略;修复输入行为需单独说明,不将既有失效调用宣传为可用功能。
## 验证与后续职责
`node --test refactor/scripts/auto-thumbnail-contract.test.mjs` 的38项核对上述版本、
分发/声明/global/ESM、注册时序、活参数、默认值、帧时间、坐标、JPEG及旧URL替换顺序。
VM 的 DOM/video/toBlob 可控替身 **不证明真实解码、CORS、画布像素或浏览器兼容性**。
02补失败复现;03在自有入口/任务/抽帧/清理模块落实修复;04迁严格 TS 和消费声明;
05使用真实核心与原生 video/canvas/Blob 跑三浏览器、切源、销毁、并发实例和故障;
06再验实际安装、入口、README/demo/编辑器和资源。优先 Chrome,不可用使用内置浏览器;
Windows WebKit 不能代替受支持 Safari 实机。相关缺证据保持显式待办。
真实示例是 `docs/assets/example/auto.thumbnail.js`,README 链接参数 `example=auto.thumbnail`
正确;样例视频文件存在。本步只核对文件,8082 demo、Monaco 和像素验证尚未执行。
不要误改外部 thumbnail 插件示例,也不要混同独立 `artplayer-tool-thumbnail`。
@@ -0,0 +1,197 @@
{
"schemaVersion": 1,
"task": "PKG-AUTO-THUMB-01",
"capturedAt": "2026-09-12T21:35:30.078Z",
"sourceCommit": "17befee4b1df7cf4ec29bbda4fd3271567f44223",
"registryObservation": {
"url": "https://registry.npmjs.org/artplayer-plugin-auto-thumbnail",
"status": 200,
"sha256": "52f735ef9be271f06f806995635b7b9a6ba1aedea9c1ad2c307a63788cd28986",
"versions": [
"1.0.0",
"1.0.1",
"1.1.0"
],
"latest": "1.1.0",
"workspaceVersion": "1.1.0"
},
"release": {
"name": "artplayer-plugin-auto-thumbnail",
"version": "1.1.0",
"metadataUrl": "https://registry.npmjs.org/artplayer-plugin-auto-thumbnail/1.1.0",
"tarball": "https://registry.npmjs.org/artplayer-plugin-auto-thumbnail/-/artplayer-plugin-auto-thumbnail-1.1.0.tgz",
"integrity": "sha512-ALrcLv+qEDime+R8p4628RZTfMGlrpX6d7PLIQaYWsQEN3lPzVjcVcARCFpqnFI8gvRdqt21w72sJphf50xtVw==",
"sha256": "5ea83d4e45d42fdaf42d3e37b6c666857872adaa276ab329c74edcdaa3c1a978",
"archiveBytes": 2420,
"publishedAt": "2026-03-12T14:21:45.753Z",
"registryGitHead": "daf133b22630b4a0eecfa3336bbddab0e9119d96",
"files": {
"package/README.md": "fb5331337763bf50a3dfafab221dbbb4510cfb89a1816e81a42f6832b99a4546",
"package/dist/artplayer-plugin-auto-thumbnail.js": "ce1f1472bf02b836dd85b92eadf2f00786c02cde11a20ac2801136a0f675d12a",
"package/dist/artplayer-plugin-auto-thumbnail.legacy.js": "3561e51e0a5f294b56641bf0884a24a4a9e326582d2e99416a04f9526c7d0cc0",
"package/dist/artplayer-plugin-auto-thumbnail.mjs": "99da4748422bad54663c669d78963589a64c7cb02102e3544439fec7570a7f8b",
"package/package.json": "8d9e186c8767a88cd2f6b7d5fa07d35f9c5dad1a29bf321906d17dab7fc95504",
"package/types/artplayer-plugin-auto-thumbnail.d.ts": "33dc298c73da947f17bc3b8a73f3c4f96f4e176097de4f5a274f9d995aaf4e5b"
},
"manifest": {
"name": "artplayer-plugin-auto-thumbnail",
"version": "1.1.0",
"description": "auto-thumbnail 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-auto-thumbnail.d.ts",
"import": "./dist/artplayer-plugin-auto-thumbnail.mjs",
"require": "./dist/artplayer-plugin-auto-thumbnail.js"
},
"./legacy": {
"types": "./types/artplayer-plugin-auto-thumbnail.d.ts",
"import": "./dist/artplayer-plugin-auto-thumbnail.legacy.js",
"require": "./dist/artplayer-plugin-auto-thumbnail.legacy.js"
}
},
"main": "./dist/artplayer-plugin-auto-thumbnail.js",
"module": "./dist/artplayer-plugin-auto-thumbnail.mjs",
"types": "./types/artplayer-plugin-auto-thumbnail.d.ts",
"legacy": "./dist/artplayer-plugin-auto-thumbnail.legacy.js",
"browserslist": "last 1 Chrome version"
},
"missingEntrypoints": {},
"historicalCore": {
"version": "5.3.1",
"commit": "daf133b22630b4a0eecfa3336bbddab0e9119d96",
"file": "packages/artplayer/package.json",
"sha256LF": "862f3697876fe915dd0ada178da766ce823aae7563ed30b3e5b11b811202929c",
"scope": "Git association only; no npm core integration result is implied."
}
},
"previous": [
{
"name": "artplayer-plugin-auto-thumbnail",
"version": "1.0.1",
"metadataUrl": "https://registry.npmjs.org/artplayer-plugin-auto-thumbnail/1.0.1",
"tarball": "https://registry.npmjs.org/artplayer-plugin-auto-thumbnail/-/artplayer-plugin-auto-thumbnail-1.0.1.tgz",
"integrity": "sha512-jewDuGQNqa9e0dDJ3DzC4UghnbJ1+sHgFbTVcVCPzlvh3teHSjjU3/173V5Lq8jaPCRosY3dJWUUi2ZRULf8JQ==",
"sha256": "e7e0e09865cf4b46350097137ec292a64e6feedece2c90f724f0498993bfa812",
"archiveBytes": 7619,
"publishedAt": "2025-03-01T03:23:42.583Z",
"registryGitHead": "38ef03d31f4d8c4fcfe1f94009625cfb55eac6c1",
"files": {
"package/README.md": "e3eddeee9ea9baf00b4df05d9623585f512622a0892aabf1d6636262f9741590",
"package/dist/artplayer-plugin-auto-thumbnail.js": "3d9756b7437f24372d34138002b46937bb09e7392b82fb1caf5f09460f151c46",
"package/dist/artplayer-plugin-auto-thumbnail.legacy.js": "e8253301c9d6d36bf6623caae52ef4019a8bf30522a52ee20850c2ed3a11769b",
"package/package.json": "eb03c9825d5229adc486dee0b560e9dc731fb4b9d4f3cbca45249fe235295dfb",
"package/src/index.js": "4bfd833404aa6b3f25594dc4544d62f516df90193e80d82f32bf234d081acfc0",
"package/types/artplayer-plugin-auto-thumbnail.d.ts": "2deb15e0e21225809b304b9f84adc1bed6ecce3a016f5b2fd5456bd597d3e7df"
},
"manifest": {
"license": "MIT",
"version": "1.0.1",
"name": "artplayer-plugin-auto-thumbnail",
"types": "types/artplayer-plugin-auto-thumbnail.d.ts",
"description": "auto-thumbnail plugin for ArtPlayer",
"main": "dist/artplayer-plugin-auto-thumbnail.js",
"legacy": "dist/artplayer-plugin-auto-thumbnail.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"
]
},
"missingEntrypoints": {},
"historicalCore": {
"version": "5.2.2",
"commit": "38ef03d31f4d8c4fcfe1f94009625cfb55eac6c1",
"file": "packages/artplayer/package.json",
"sha256LF": "59b29c249778ee2ab4fc603a35e5de1509bad5104565315fcfdcb7a5e30944a9",
"scope": "Git association only; no npm core integration result is implied."
}
},
{
"name": "artplayer-plugin-auto-thumbnail",
"version": "1.0.0",
"metadataUrl": "https://registry.npmjs.org/artplayer-plugin-auto-thumbnail/1.0.0",
"tarball": "https://registry.npmjs.org/artplayer-plugin-auto-thumbnail/-/artplayer-plugin-auto-thumbnail-1.0.0.tgz",
"integrity": "sha512-IcGdX3ezuQSu4TtIxaYAu0je2MZYLKRzJiEfszKrnpRuPRqkYSbSioJo9e1dLmw5MpMLezqkuldrsb96FFUwww==",
"sha256": "0a907bc8dbf2ef773764645730309b2c95592f84b31f5c4f9cec636d29c0263e",
"archiveBytes": 1414,
"publishedAt": "2024-12-05T14:09:42.640Z",
"registryGitHead": "eb8cc22bf2524482c26187a6f9346fb0137bb9f6",
"files": {
"package/README.md": "e3eddeee9ea9baf00b4df05d9623585f512622a0892aabf1d6636262f9741590",
"package/package.json": "4f43e319293cd970215941476473185c87c67aafab2b027335fd30461310a707",
"package/src/index.js": "4bfd833404aa6b3f25594dc4544d62f516df90193e80d82f32bf234d081acfc0",
"package/types/artplayer-plugin-auto-thumbnail.d.ts": "2deb15e0e21225809b304b9f84adc1bed6ecce3a016f5b2fd5456bd597d3e7df"
},
"manifest": {
"license": "MIT",
"version": "1.0.0",
"name": "artplayer-plugin-auto-thumbnail",
"types": "types/artplayer-plugin-auto-thumbnail.d.ts",
"description": "auto-thumbnail plugin for ArtPlayer",
"main": "dist/artplayer-plugin-auto-thumbnail.js",
"legacy": "dist/artplayer-plugin-auto-thumbnail.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"
]
},
"missingEntrypoints": {
"main": "dist/artplayer-plugin-auto-thumbnail.js",
"legacy": "dist/artplayer-plugin-auto-thumbnail.legacy.js"
},
"historicalCore": {
"version": "5.2.2",
"commit": "eb8cc22bf2524482c26187a6f9346fb0137bb9f6",
"file": "packages/artplayer/package.json",
"sha256LF": "59b29c249778ee2ab4fc603a35e5de1509bad5104565315fcfdcb7a5e30944a9",
"scope": "Git association only; no npm core integration result is implied."
}
}
],
"source": {
"packages/artplayer-plugin-auto-thumbnail/package.json": "8d9e186c8767a88cd2f6b7d5fa07d35f9c5dad1a29bf321906d17dab7fc95504",
"packages/artplayer-plugin-auto-thumbnail/README.md": "fb5331337763bf50a3dfafab221dbbb4510cfb89a1816e81a42f6832b99a4546",
"packages/artplayer-plugin-auto-thumbnail/src/index.js": "bc9102c014fbb7b87fe94dc71b779e24a88afb92f5e70a97cf26fcaa2d5a864f",
"packages/artplayer-plugin-auto-thumbnail/types/artplayer-plugin-auto-thumbnail.d.ts": "33dc298c73da947f17bc3b8a73f3c4f96f4e176097de4f5a274f9d995aaf4e5b",
"packages/artplayer-plugin-auto-thumbnail/dist/artplayer-plugin-auto-thumbnail.js": "ce1f1472bf02b836dd85b92eadf2f00786c02cde11a20ac2801136a0f675d12a",
"packages/artplayer-plugin-auto-thumbnail/dist/artplayer-plugin-auto-thumbnail.legacy.js": "3561e51e0a5f294b56641bf0884a24a4a9e326582d2e99416a04f9526c7d0cc0",
"packages/artplayer-plugin-auto-thumbnail/dist/artplayer-plugin-auto-thumbnail.mjs": "99da4748422bad54663c669d78963589a64c7cb02102e3544439fec7570a7f8b",
"docs/assets/example/auto.thumbnail.js": "e0f706bbbab8e2d0f3201ea6b91f1d5530c6bb65f5c1745dc8ade9acc6d66295"
}
}
@@ -0,0 +1,22 @@
# PKG-AUTO-THUMB-01 发布和行为契约
固定 npm 三份真实归档/16成员、8份Git工作区输入,补可重跑归档验证与38项契约测试。
捕获1.0.0缺main/legacy,1.0.1 CJS default对象与1.1.0直接函数的差异。
首次类型断言发现1.0.x实际声明height而非number;测试改为分别锁定真实历史版本,
没有将旧类型假设覆盖到实际文件。原始下载和最终测试日志保存在.cache。
冻结正常异步注册、name、活参数/defaults、十列布局、帧时间、渐进JPEG更新和URL替换。
声明的同步Result、无效height、旧导出形状和资源/异步缺陷登记为后续任务风险。
1.0.0随包源码只作为source-only行为夹具,不冒充缺失的npm运行入口。
npm1.1.0六个成员与冻结工作区对应文件按LF相同;不声称Git关联核心等于已验支持范围。
契约索引新增三个发布点,22工作区+20实际发布;264家族和10精确case数量不变,
没有自动把新测试提升为最终核心/设备/分发验收。新增测试由既有test:baseline自动执行,
无需新增依赖。生产源码/声明/示例/包版本/构建文件不变,无需为纯基线任务重建。
见[完整契约](../baselines/auto-thumbnail-contract.md)和
[验证证据](../baselines/auto-thumbnail-contract-validation.json)。
回退此任务提交即可去掉本次基线及索引/风险记录,不影响生产行为;不推送、不发布。
最终47项定向检查、完整CI1702项(1456单元+14工程+232基线)和339个生产TS严格检查通过。
五项风险保持open,后续独立任务承接;本任务独立本地提交。
+3
View File
@@ -326,3 +326,6 @@
- PUB-artplayer-plugin-document-pip-1.0.0: artplayer-plugin-document-pip@1.0.0 (published-baseline);[来源](../refactor/baselines/dpip-release.json) JSON pointer `/previous/2`。
- PUB-artplayer-proxy-mediabunny-1.2.0: artplayer-proxy-mediabunny@1.2.0 (published-baseline);[来源](../refactor/baselines/mb-release.json) JSON pointer `/release`。
- PUB-artplayer-proxy-mediabunny-1.0.0: artplayer-proxy-mediabunny@1.0.0 (published-baseline);[来源](../refactor/baselines/mb-release.json) JSON pointer `/previous/0`。
- PUB-artplayer-plugin-auto-thumbnail-1.1.0: artplayer-plugin-auto-thumbnail@1.1.0 (published-baseline);[来源](../refactor/baselines/auto-thumbnail-release.json) JSON pointer `/release`。
- PUB-artplayer-plugin-auto-thumbnail-1.0.1: artplayer-plugin-auto-thumbnail@1.0.1 (published-baseline);[来源](../refactor/baselines/auto-thumbnail-release.json) JSON pointer `/previous/0`。
- PUB-artplayer-plugin-auto-thumbnail-1.0.0: artplayer-plugin-auto-thumbnail@1.0.0 (published-baseline);[来源](../refactor/baselines/auto-thumbnail-release.json) JSON pointer `/previous/1`。
+32 -2
View File
@@ -183,10 +183,13 @@
"API-12": "PKG-AUTO-THUMB-02"
},
"versionIds": [
"WS-artplayer-plugin-auto-thumbnail"
"WS-artplayer-plugin-auto-thumbnail",
"PUB-artplayer-plugin-auto-thumbnail-1.1.0",
"PUB-artplayer-plugin-auto-thumbnail-1.0.1",
"PUB-artplayer-plugin-auto-thumbnail-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"
"scopeReview": "Three verified npm archives and a separate workspace snapshot. 1.0.0 lacks main/legacy and is not runnable through its declared entries. See auto-thumbnail-contract.md; source-only tests and Git core associations do not establish a supported range."
},
{
"name": "artplayer-plugin-chapter",
@@ -882,6 +885,33 @@
"file": "refactor/baselines/mb-release.json",
"pointer": "/previous/0",
"archiveSha256": "d62f3ad75d07abbc8ca597846e213f34abd15b3742f7e87f294800fdef2e8962"
},
{
"id": "PUB-artplayer-plugin-auto-thumbnail-1.1.0",
"package": "artplayer-plugin-auto-thumbnail",
"kind": "published-baseline",
"version": "1.1.0",
"file": "refactor/baselines/auto-thumbnail-release.json",
"pointer": "/release",
"archiveSha256": "5ea83d4e45d42fdaf42d3e37b6c666857872adaa276ab329c74edcdaa3c1a978"
},
{
"id": "PUB-artplayer-plugin-auto-thumbnail-1.0.1",
"package": "artplayer-plugin-auto-thumbnail",
"kind": "published-baseline",
"version": "1.0.1",
"file": "refactor/baselines/auto-thumbnail-release.json",
"pointer": "/previous/0",
"archiveSha256": "e7e0e09865cf4b46350097137ec292a64e6feedece2c90f724f0498993bfa812"
},
{
"id": "PUB-artplayer-plugin-auto-thumbnail-1.0.0",
"package": "artplayer-plugin-auto-thumbnail",
"kind": "published-baseline",
"version": "1.0.0",
"file": "refactor/baselines/auto-thumbnail-release.json",
"pointer": "/previous/1",
"archiveSha256": "0a907bc8dbf2ef773764645730309b2c95592f84b31f5c4f9cec636d29c0263e"
}
],
"cases": [
+3 -2
View File
@@ -4,7 +4,7 @@
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 222 项,范围 22 个包及工作区/示例。
状态:todo 99 / doing 11 / blocked 0 / done 112 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
状态:todo 98 / doing 11 / blocked 0 / done 113 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
@@ -171,7 +171,7 @@
| ID | 范围 / 步骤 | 前置依赖 | 交付物 | 验收条件 | 风险 | 状态 |
| --- | --- | --- | --- | --- | --- | --- |
| PKG-AUTO-THUMB-01 | artplayer-plugin-auto-thumbnail<br>核对包契约与历史用法 | BASE-05 | url/width/number/scale、异步工厂和渐进缩略图更新 | 源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结 | H | todo |
| PKG-AUTO-THUMB-01 | artplayer-plugin-auto-thumbnail<br>核对包契约与历史用法 | BASE-05 | url/width/number/scale、异步工厂和渐进缩略图更新 | 源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结 | H | done |
| PKG-AUTO-THUMB-02 | artplayer-plugin-auto-thumbnail<br>建立特有行为与错误测试 | PKG-AUTO-THUMB-01, ENG-03, ENG-05 | 抽帧边界、短视频、失败、连续来源和销毁中的抽帧 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | H | todo |
| PKG-AUTO-THUMB-03 | artplayer-plugin-auto-thumbnail<br>整理内部职责与资源 | PKG-AUTO-THUMB-02, CORE-09, CORE-19 | 隐藏 video、seek 队列、canvas 编码与 Blob URL 清理 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | todo |
| PKG-AUTO-THUMB-04 | artplayer-plugin-auto-thumbnail<br>迁移自有源码和公开类型 | PKG-AUTO-THUMB-03, ENG-04, ENG-06, CORE-07 | 抽帧结果和真实异步插件返回类型兼容 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | H | todo |
@@ -494,6 +494,7 @@
- PKG-AUDIO-03: [记录](changes/2026-09-12-PKG-AUDIO-03-lifecycle.md) [记录](baselines/audio-lifecycle-validation.json)
- PKG-AUDIO-04: [记录](changes/2026-09-12-PKG-AUDIO-04-types.md) [记录](baselines/audio-types-validation.json)
- PKG-AUDIO-05: [记录](changes/2026-09-12-PKG-AUDIO-05-combinations.md) [记录](baselines/audio-combinations-first.json) [记录](baselines/audio-combinations-checkpoint.json) [记录](audio-validation.md)
- PKG-AUTO-THUMB-01: [记录](baselines/auto-thumbnail-release.json) [记录](baselines/auto-thumbnail-contract.md) [记录](changes/2026-09-13-PKG-AUTO-THUMB-01-contract.md) [记录](baselines/auto-thumbnail-contract-validation.json)
- PKG-HLS-01: [记录](baselines/hls-control-contract.md) [记录](baselines/hls-control-release.json) [记录](changes/2026-09-11-PKG-HLS-01-contract.md)
- PKG-HLS-02: [记录](hls-validation.md) [记录](baselines/hls-sdk.json) [记录](baselines/hls-validation.json) [记录](changes/2026-09-11-PKG-HLS-02-tests.md)
- PKG-HLS-03: [记录](changes/2026-09-12-PKG-HLS-03-modules.md) [记录](baselines/hls-modules-validation.json) [记录](hls-validation.md)
+12
View File
@@ -1,5 +1,17 @@
# 进度与证据
## PKG-AUTO-THUMB-01 完成:自动缩略图发布与行为基线
核实全部三个npm版本及16成员,冻结8份工作区输入。明确1.0.0缺main/legacy,
其随包源码仅作为source-only夹具;1.0.1 CJS为default对象,1.1.0为直接函数。
发现旧声明height无效且漏number、注册Promise错写同步Result。38项契约固定正常
注册/活参数/默认值/十列JPEG/渐进更新;47项定向检查及完整CI1702项通过,339生产TS。
登记5项公开类型/导出/分发/资源/编码风险。生产源码/类型/版本/构建未改;02开始故障
复现后03实施源码修复。真实解码、核心组合与设备证据仍属于后续05/06。
见[契约](baselines/auto-thumbnail-contract.md)及[验证](baselines/auto-thumbnail-contract-validation.json)。
222项:113 done、11 doing、98 todo。契约索引22工作区+20发布,未补齐的精确索引不冒充通过。
## PKG-TOOL-THUMB-04 事件注册表修复检查点(doing)
复现并修复特殊事件名碰撞对象原型、继承getter/setter干扰,以及嵌套emit导致once重复执行。
+5
View File
@@ -183,3 +183,8 @@
| THUMB-MEDIA-01 | open / 已复现 | Windows WebKit native Blob URLs cannot load tested MP4 media despite successful HTTP controls | PKG-TOOL-THUMB-05, PKG-TOOL-THUMB-06 |
| VENDOR-09 | resolved / 源码/产物事实 | Thumbnail tiny-emitter 改写来源与最终分发许可 | PKG-TOOL-THUMB-04, PKG-TOOL-THUMB-06 |
| THUMB-EVENT-01 | resolved / 已复现 | Thumbnail 原型事件名与嵌套 once 派发缺陷 | PKG-TOOL-THUMB-04 |
| AUTO-THUMB-TYPE-01 | open / 已复现 | Published declarations misdescribe async registration and switch height/number option fields across generations | PKG-AUTO-THUMB-04 |
| AUTO-THUMB-EXPORT-01 | open / 已复现 | Published 1.0.1 CommonJS default namespace differs from the direct function exported by 1.1.0 | PKG-AUTO-THUMB-04, PKG-AUTO-THUMB-06 |
| AUTO-THUMB-DIST-01 | open / 已复现 | Published 1.0.0 archive lacks declared main and legacy runtimes | PKG-AUTO-THUMB-06 |
| AUTO-THUMB-LIFE-01 | open / 源码/产物事实 | Extraction tasks, decoder handlers and final object URLs lack source-switch and destroy ownership | PKG-AUTO-THUMB-02, PKG-AUTO-THUMB-03, PKG-AUTO-THUMB-05 |
| AUTO-THUMB-ENCODE-01 | open / 源码/产物事实 | Unserialized encodes include blank output and lack media/canvas/input failure handling | PKG-AUTO-THUMB-02, PKG-AUTO-THUMB-03, PKG-AUTO-THUMB-05 |
+86 -1
View File
@@ -2,7 +2,7 @@
"schemaVersion": 1,
"task": "BASE-07",
"sourceCommit": "e01f39ff",
"updatedAt": "2026-09-12",
"updatedAt": "2026-09-13",
"note": "Open means closure evidence is outstanding; source-observed and unverified are not reproduced runtime bugs. A source issue already mitigated in the workspace still requires candidate regression evidence. No accepted-with-scope item is implied by baseline task completion.",
"items": [
{
@@ -3998,6 +3998,91 @@
"test/browser/thumbnail-emitter.spec.js"
],
"resolutionRationale": "Frozen workspace reproduces five failures. Own-key/data-descriptor registry and once-per-registration consumption pass source/main/legacy, actual browser and installed candidate checks while ordinary snapshot/ctx/off/duplicate behavior remains tested."
},
{
"id": "AUTO-THUMB-TYPE-01",
"title": "Published declarations misdescribe async registration and switch height/number option fields across generations",
"confirmation": "reproduced",
"status": "open",
"owners": [
"PKG-AUTO-THUMB-04"
],
"evidence": [
"refactor/baselines/auto-thumbnail-release.json",
"refactor/baselines/auto-thumbnail-contract.md",
"refactor/scripts/auto-thumbnail-contract.test.mjs"
],
"compatibleResolution": "Preserve actual Promise runtime and old valid consumers; explicitly retain the historically ignored height field and verify paired CJS/ESM declarations.",
"closureCriteria": "Old and current isolated consumers, negative type cases and matching runtime registration pass; any irreconcilable inference change has an explicit user decision."
},
{
"id": "AUTO-THUMB-EXPORT-01",
"title": "Published 1.0.1 CommonJS default namespace differs from the direct function exported by 1.1.0",
"confirmation": "reproduced",
"status": "open",
"owners": [
"PKG-AUTO-THUMB-04",
"PKG-AUTO-THUMB-06"
],
"evidence": [
"refactor/baselines/auto-thumbnail-release.json",
"refactor/baselines/auto-thumbnail-contract.md",
"refactor/scripts/auto-thumbnail-contract.test.mjs"
],
"compatibleResolution": "Exercise both old .default calls and direct require calls plus globals, actual ESM and legacy.",
"closureCriteria": "Installed tarballs and matching type consumers retain both usable calling forms with documented entry behavior."
},
{
"id": "AUTO-THUMB-DIST-01",
"title": "Published 1.0.0 archive lacks declared main and legacy runtimes",
"confirmation": "reproduced",
"status": "open",
"owners": [
"PKG-AUTO-THUMB-06"
],
"evidence": [
"refactor/baselines/auto-thumbnail-release.json",
"refactor/baselines/auto-thumbnail-contract.md",
"refactor/scripts/auto-thumbnail-contract.test.mjs"
],
"compatibleResolution": "Keep the historical missing-entry baseline explicit; never substitute Git source as a released artifact.",
"closureCriteria": "Candidate pack has all declared runtime/types/assets and reviewed historical deep paths; missing old entries remain explicitly recorded."
},
{
"id": "AUTO-THUMB-LIFE-01",
"title": "Extraction tasks, decoder handlers and final object URLs lack source-switch and destroy ownership",
"confirmation": "source-observed",
"status": "open",
"owners": [
"PKG-AUTO-THUMB-02",
"PKG-AUTO-THUMB-03",
"PKG-AUTO-THUMB-05"
],
"evidence": [
"refactor/baselines/auto-thumbnail-release.json",
"refactor/baselines/auto-thumbnail-contract.md",
"refactor/scripts/auto-thumbnail-contract.test.mjs"
],
"compatibleResolution": "Cancel obsolete work and release owned resources while retaining live options and progressive usable sheet updates.",
"closureCriteria": "Delayed callbacks, repeated metadata, switching, destroy, multiple instances and failure cleanup are reproduced then pass source/artifacts and native browser checks."
},
{
"id": "AUTO-THUMB-ENCODE-01",
"title": "Unserialized encodes include blank output and lack media/canvas/input failure handling",
"confirmation": "source-observed",
"status": "open",
"owners": [
"PKG-AUTO-THUMB-02",
"PKG-AUTO-THUMB-03",
"PKG-AUTO-THUMB-05"
],
"evidence": [
"refactor/baselines/auto-thumbnail-release.json",
"refactor/baselines/auto-thumbnail-contract.md",
"refactor/scripts/auto-thumbnail-contract.test.mjs"
],
"compatibleResolution": "Preserve JPEG grid/time/scale while bounding jobs, sequencing valid updates and handling null Blob, invalid media and thrown callbacks.",
"closureCriteria": "Fault controls and native valid/corrupt media prove no stale sheet replacement, leaked URLs or unhandled async callbacks; capability limitations remain explicit."
}
]
}
@@ -0,0 +1,38 @@
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 verifyAutoThumbnailContract() {
const baseline = JSON.parse(fs.readFileSync(path.join(refactorDir, 'baselines/auto-thumbnail-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)
const missing = Object.fromEntries(['main', 'module', 'types', 'legacy'].filter(field => release.manifest[field] && !Object.hasOwn(release.files, `package/${release.manifest[field].replace(/^\.\//, '')}`)).map(field => [field, release.manifest[field]]))
assert.deepEqual(missing, release.missingEntrypoints, 'Missing entries are historical defects, not runnable artifacts')
const core = release.historicalCore
const source = execFileSync('git', ['show', `${core.commit}:${core.file}`], { encoding: 'utf8' }).replaceAll('\r\n', '\n')
assert.equal(hash(source), core.sha256LF)
assert.equal(JSON.parse(source).version, core.version)
archives.set(release.version, archive)
}
const sources = new Map()
for (const [file, expected] of Object.entries(baseline.source)) {
const source = execFileSync('git', ['show', `${baseline.sourceCommit}:${file}`], { encoding: 'utf8' }).replaceAll('\r\n', '\n')
assert.equal(hash(source), expected, `Auto-thumbnail frozen source: ${file}`)
sources.set(file, source)
}
return { baseline, archives, sources }
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
await verifyAutoThumbnailContract()
console.log('Auto-thumbnail verified: 3 actual npm archives, 16 members, 8 frozen workspace inputs; 1.0.0 missing main/legacy explicitly retained')
}
@@ -0,0 +1,176 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
// eslint-disable-next-line test/no-import-node-test -- Node frozen contract runner.
import test from 'node:test'
import { transform } from 'esbuild'
import { autoThumbnailEnvironment, autoThumbnailHistorical } from '../../test/helpers/auto-thumbnail.js'
import { verifyAutoThumbnailContract } from './auto-thumbnail-contract.mjs'
import { readMember } from './releases.mjs'
const contract = await verifyAutoThumbnailContract()
const implementations = await autoThumbnailHistorical()
const name = 'artplayer-plugin-auto-thumbnail'
test('Auto-thumbnail freezes all three actual archives, sixteen members and eight workspace inputs', () => {
const { baseline } = contract
const releases = [baseline.release, ...baseline.previous]
assert.deepEqual(releases.map(item => item.version), ['1.1.0', '1.0.1', '1.0.0'])
assert.equal(releases.reduce((sum, item) => sum + Object.keys(item.files).length, 0), 16)
assert.equal(Object.keys(baseline.source).length, 8)
assert.deepEqual(releases.map(item => item.historicalCore.version), ['5.3.1', '5.2.2', '5.2.2'])
for (const release of releases) {
assert.equal(release.manifest.peerDependencies, undefined)
assert.equal(release.manifest.dependencies, undefined)
}
})
test('Auto-thumbnail 1.0.0 lacks both declared runtimes; its shipped source is explicitly a source-only fixture', async () => {
const { baseline, archives } = contract
const release = baseline.previous.find(item => item.version === '1.0.0')
assert.deepEqual(release.missingEntrypoints, { main: `dist/${name}.js`, legacy: `dist/${name}.legacy.js` })
assert(!implementations.some(item => item.name === 'published-1.0.0-main' || item.name === 'published-1.0.0-legacy'))
assert(implementations.some(item => item.name === 'published-1.0.0-source-only'))
const sources = ['1.0.0', '1.0.1'].map(version => readMember(archives.get(version), 'package/src/index.js').toString())
const compiled = await Promise.all(sources.map(source => transform(source, { format: 'esm', target: 'es2020', minify: true })))
assert.equal(compiled[0].code, compiled[1].code, 'The two shipped source implementations differ only in formatting')
})
test('Auto-thumbnail historical declarations require options and falsely describe async registration as synchronous', () => {
const { baseline, archives, sources } = contract
const declarations = [baseline.release, ...baseline.previous].map(release => ({ version: release.version, source: readMember(archives.get(release.version), `package/${release.manifest.types.replace(/^\.\//, '')}`).toString() }))
declarations.push({ version: 'workspace', source: sources.get(`packages/${name}/types/${name}.d.ts`) })
for (const { version, source } of declarations) {
assert.match(source, /\(option: Option\) => \(art: Artplayer\) => Result/)
assert.match(source, /url\?: string/)
for (const field of ['width', 'scale']) assert.match(source, new RegExp(`${field}\\?: number`))
if (version === '1.0.0' || version === '1.0.1') {
assert.match(source, /height\?: number/)
assert.doesNotMatch(source, /number\?: number/)
assert.match(source, /export = artplayerPluginAutoThumbnail/)
assert.match(source, /export as namespace artplayerPluginAutoThumbnail/)
}
else {
assert.match(source, /number\?: number/)
assert.doesNotMatch(source, /height\?: number/)
assert.match(source, /export default artplayerPluginAutoThumbnail/)
}
assert.match(source, /name: ['"]artplayerPluginAutoThumbnail['"]/)
assert.doesNotMatch(source, /Promise</)
}
})
test('Auto-thumbnail actual CommonJS generations and script globals remain distinct', () => {
for (const implementation of implementations.filter(item => !item.name.includes('source'))) {
const env = autoThumbnailEnvironment(implementation)
assert.equal(typeof env.factory, 'function')
assert.equal(typeof env.exported, implementation.name.startsWith('published-1.0.1-') ? 'object' : 'function')
if (typeof env.exported === 'object')
assert.deepEqual(Object.keys(env.exported), ['default'])
else assert.equal(env.exported.default, undefined)
const global = autoThumbnailEnvironment(implementation, { script: true })
assert.equal(typeof global.factory, 'function')
assert.equal(global.videos.length, 0, 'Import/global setup itself creates no decoder')
}
})
test('Auto-thumbnail native ESM has only a default function and preserves asynchronous registration', async () => {
const { baseline, archives, sources } = contract
const codes = [readMember(archives.get('1.1.0'), `package/${baseline.release.manifest.module.replace(/^\.\//, '')}`).toString(), sources.get(`packages/${name}/dist/${name}.mjs`)]
for (const code of codes) {
const exports = await import(`data:text/javascript,${encodeURIComponent(code)}`)
assert.deepEqual(Object.keys(exports), ['default'])
let registration
const result = exports.default({})({ on: (...args) => {
registration = args
} })
assert.equal(registration[0], 'video:loadedmetadata')
assert.equal(typeof result.then, 'function')
assert.deepEqual(await result, { name: 'artplayerPluginAutoThumbnail' })
}
})
test('Auto-thumbnail README selects its own runnable demo; workspace demo and declarations are frozen separately from external thumbnail plugin', () => {
const { baseline, archives, sources } = contract
for (const release of [baseline.release, ...baseline.previous]) {
const readme = readMember(archives.get(release.version), 'package/README.md').toString()
assert.match(readme, /example=auto\.thumbnail/)
}
assert.match(sources.get(`packages/${name}/README.md`), /example=auto\.thumbnail/)
const example = sources.get('docs/assets/example/auto.thumbnail.js')
assert.match(example, /artplayerPluginAutoThumbnail\(\{/)
assert.match(example, /\/assets\/sample\/video\.mp4/)
assert(fs.existsSync('docs/assets/sample/video.mp4'))
})
for (const implementation of implementations) {
test(`Auto-thumbnail ${implementation.name}: registration installs one listener synchronously and returns Promise of name only`, async () => {
const env = autoThumbnailEnvironment(implementation)
const register = env.factory({})
assert.equal(typeof register, 'function')
assert.equal(env.listeners.size, 0)
const result = register(env.art)
assert.equal(env.listeners.get('video:loadedmetadata').size, 1)
assert.equal(typeof result.then, 'function')
assert.equal(result.name, undefined)
assert.deepEqual({ ...await result }, { name: 'artplayerPluginAutoThumbnail' })
assert.equal(env.videos.length, 0)
assert.deepEqual([...env.listeners.keys()], ['video:loadedmetadata'])
})
test(`Auto-thumbnail ${implementation.name}: defaults use truthiness and options remain live until each metadata event`, async () => {
const env = autoThumbnailEnvironment(implementation)
const options = { url: '', width: 0, number: 0, scale: 0 }
await env.factory(options)(env.art)
env.art.emit('video:loadedmetadata')
assert.equal(env.videos[0].src, 'original.mp4')
assert.equal(env.videos[0].crossOrigin, 'anonymous')
env.metadata()
assert.equal(env.canvases[0].width, 1600)
assert.equal(env.canvases[0].height, 900)
env.finish()
assert.deepEqual({ ...env.updates[0] }, { url: 'blob:auto-thumbnail-1', height: 90, column: 10, number: 100, width: 160, scale: 1 })
Object.assign(options, { url: 'override.mp4', width: 80, number: 12, scale: 0.5 })
env.art.option.url = 'switched.mp4'
env.art.emit('video:loadedmetadata')
assert.equal(env.videos[1].src, 'override.mp4')
env.metadata()
assert.equal(env.canvases[1].width, 800)
assert.equal(env.canvases[1].height, 90)
env.finish(1)
assert.deepEqual({ ...env.updates[1] }, { url: 'blob:auto-thumbnail-2', height: 45, column: 10, number: 12, width: 80, scale: 0.5 })
})
test(`Auto-thumbnail ${implementation.name}: ten-column JPEG grid uses frame starts and progressive encodes including initial blank`, async () => {
const env = autoThumbnailEnvironment(implementation)
await env.factory({ width: 80, number: 12, scale: 2, height: 999 })(env.art)
env.art.emit('video:loadedmetadata')
env.metadata()
assert.equal(env.blobs.length, 1)
assert.equal(env.blobs[0].draws.length, 0, 'Old implementation encodes an empty sheet before drawing')
for (let i = 0; i < 12; i++) env.seeked()
assert.deepEqual(env.operations.filter(item => item.name === 'seek').map(item => item.value), Array.from({ length: 12 }, (_, i) => i * 10))
assert.equal(env.operations.find(item => item.name === 'seek').handlerInstalled, false, 'First seek occurs before onseeked installation')
assert.equal(env.blobs.length, 13)
assert.equal(env.blobs.at(-1).draws.length, 12)
assert.deepEqual(env.canvases[0].draws.map(args => args.slice(1)), Array.from({ length: 12 }, (_, i) => [(i % 10) * 80, Math.floor(i / 10) * 45, 80, 45]))
for (let i = 0; i < env.blobs.length; i++) {
assert.equal(env.blobs[i].type, 'image/jpeg')
env.finish(i)
}
assert.equal(env.updates.length, 13)
assert.deepEqual(env.operations.filter(item => item.name === 'revokeURL').map(item => item.url), [null, ...Array.from({ length: 12 }, (_, i) => `blob:auto-thumbnail-${i + 1}`)])
assert.deepEqual([...env.urls.keys()], ['blob:auto-thumbnail-13'])
assert.equal(env.updates.at(-1).number, 12)
assert.equal(env.updates.at(-1).scale, 2)
})
test(`Auto-thumbnail ${implementation.name}: omitted options fail only when metadata fires and no destroyed cleanup is registered`, async () => {
const env = autoThumbnailEnvironment(implementation)
const result = await env.factory()(env.art)
assert.equal(result.name, 'artplayerPluginAutoThumbnail')
assert.throws(() => env.art.emit('video:loadedmetadata'), /Cannot read properties of undefined/)
assert.equal(env.videos.length, 0)
assert.equal(env.listeners.has('destroy'), false)
assert.equal(env.listeners.has('restart'), false)
})
}
+2 -1
View File
@@ -44,7 +44,8 @@ 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, 17)
assert.equal(actual.policy.versions.filter(version => version.kind === 'published-baseline').length, 20)
assert.deepEqual(actual.policy.versions.filter(version => version.kind === 'published-baseline' && version.package === 'artplayer-plugin-auto-thumbnail').map(version => version.version).sort(), ['1.0.0', '1.0.1', '1.1.0'])
assert.deepEqual(actual.policy.versions.filter(version => version.kind === 'published-baseline' && version.package === 'artplayer-proxy-mediabunny').map(version => version.version).sort(), ['1.0.0', '1.2.0'])
assert.equal(actual.policy.versions.filter(version => version.kind === 'workspace-baseline').length, 22)
assert.equal(actual.policy.cases.length, 10)
+7 -2
View File
@@ -1732,11 +1732,16 @@
"dependsOn": [
"BASE-05"
],
"status": "todo",
"status": "done",
"risk": "H",
"deliverable": "url/width/number/scale、异步工厂和渐进缩略图更新",
"acceptance": "源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结",
"evidence": []
"evidence": [
"baselines/auto-thumbnail-release.json",
"baselines/auto-thumbnail-contract.md",
"changes/2026-09-13-PKG-AUTO-THUMB-01-contract.md",
"baselines/auto-thumbnail-contract-validation.json"
]
},
{
"id": "PKG-AUTO-THUMB-02",
+162
View File
@@ -0,0 +1,162 @@
import fs from 'node:fs'
import process from 'node:process'
import vm from 'node:vm'
import { transform } from 'esbuild'
import { verifyAutoThumbnailContract } from '../../refactor/scripts/auto-thumbnail-contract.mjs'
import { readMember } from '../../refactor/scripts/releases.mjs'
import { compilePackage } from './load.js'
export async function autoThumbnailHistorical() {
const { baseline, archives, sources } = await verifyAutoThumbnailContract()
const name = 'artplayer-plugin-auto-thumbnail'
const implementations = []
for (const release of [baseline.release, ...baseline.previous]) {
if (release.missingEntrypoints.main)
continue
for (const field of ['main', 'legacy']) {
implementations.push({ name: `published-${release.version}-${field}`, code: readMember(archives.get(release.version), `package/${release.manifest[field].replace(/^\.\//, '')}`).toString() })
}
}
implementations.push({ name: 'published-1.0.0-source-only', code: (await transform(readMember(archives.get('1.0.0'), 'package/src/index.js').toString(), { format: 'cjs', target: 'es2020' })).code })
implementations.push({ name: 'frozen-workspace-source', code: (await transform(sources.get(`packages/${name}/src/index.js`), { format: 'cjs', target: 'es2020' })).code })
for (const suffix of ['js', 'legacy.js']) {
implementations.push({ name: `frozen-workspace-${suffix}`, code: sources.get(`packages/${name}/dist/${name}.${suffix}`) })
}
return implementations
}
export async function autoThumbnailCandidate() {
if (process.env.ARTPLAYER_AUTO_THUMBNAIL_BASELINE === '1')
return (await autoThumbnailHistorical()).find(item => item.name === 'frozen-workspace-js')
return { name: 'candidate', code: process.env.ARTPLAYER_AUTO_THUMBNAIL_ARTIFACT ? fs.readFileSync(process.env.ARTPLAYER_AUTO_THUMBNAIL_ARTIFACT, 'utf8') : await compilePackage('artplayer-plugin-auto-thumbnail', 'umd') }
}
// Controlled media events and deferred encodes are not evidence of native decoding.
export function autoThumbnailEnvironment(implementation, { script = false } = {}) {
const operations = []
const videos = []
const canvases = []
const blobs = []
const urls = new Map()
const listeners = new Map()
const updates = []
let nextUrl = 0
const controls = { nullContext: false, drawError: null, encodeError: null, updateError: null }
const document = {
createElement(tag) {
if (tag === 'video') {
const video = {
duration: 120,
videoWidth: 1920,
videoHeight: 1080,
onloadedmetadata: null,
onseeked: null,
onerror: null,
pause() { operations.push({ name: 'pause', video }) },
load() { operations.push({ name: 'load', video }) },
removeAttribute(name) {
delete this[name]
operations.push({ name: 'removeAttribute', attribute: name, video })
},
}
let time = 0
Object.defineProperty(video, 'currentTime', {
get: () => time,
set(value) {
time = value
operations.push({ name: 'seek', value, handlerInstalled: typeof video.onseeked === 'function', video })
},
})
videos.push(video)
return video
}
if (tag === 'canvas') {
const draws = []
const ctx = { drawImage(...args) {
if (controls.drawError)
throw controls.drawError
draws.push(args)
operations.push({ name: 'draw', args })
} }
const canvas = {
width: 0,
height: 0,
draws,
getContext() { return controls.nullContext ? null : ctx },
toBlob(callback, type) {
if (controls.encodeError)
throw controls.encodeError
const encode = { callback, type, draws: draws.map(args => [...args]), canvas }
blobs.push(encode)
operations.push({ name: 'encode', encode })
},
}
canvases.push(canvas)
return canvas
}
throw new Error(`Unexpected element: ${tag}`)
},
}
const module = { exports: {} }
const box = { document, URL: {
createObjectURL(blob) {
if (blob == null)
throw new TypeError('Blob required')
const url = `blob:auto-thumbnail-${++nextUrl}`
urls.set(url, blob)
operations.push({ name: 'createURL', url, blob })
return url
},
revokeObjectURL(url) {
urls.delete(url)
operations.push({ name: 'revokeURL', url })
},
}, window: {} }
if (!script)
Object.assign(box, { module, exports: module.exports })
vm.runInNewContext(implementation.code, box)
const exported = script ? box.artplayerPluginAutoThumbnail || box.window.artplayerPluginAutoThumbnail : module.exports
const factory = typeof exported === 'function' ? exported : exported.default
const art = {
option: { url: 'original.mp4' },
on(name, fn) {
if (!listeners.has(name))
listeners.set(name, new Set())
listeners.get(name).add(fn)
return this
},
off(name, fn) {
listeners.get(name)?.delete(fn)
return this
},
emit(name, ...args) {
for (const fn of [...(listeners.get(name) || [])]) fn(...args)
return this
},
}
Object.defineProperty(art, 'thumbnails', {
get: () => updates.at(-1),
set(value) {
if (controls.updateError)
throw controls.updateError
updates.push(value)
},
})
return {
art,
factory,
exported,
box,
operations,
videos,
canvases,
blobs,
urls,
listeners,
updates,
controls,
metadata(video = videos.at(-1)) { video.onloadedmetadata?.() },
seeked(video = videos.at(-1)) { video.onseeked?.() },
finish(index = 0, blob = { type: 'image/jpeg' }) { blobs[index].callback(blob) },
}
}