fix(release): [REL-02] accept Yarn directory entries in candidate archives

This commit is contained in:
Harvey Zhao committed 2026-09-16 01:52:36 +08:00
1 parent e54fc998ea
commit 1b81888347
11 files changed
+425 -19

No files matched your search

@@ -0,0 +1,278 @@
{
"task": "REL-02",
"sourceCommit": "e54fc998ea711f686143be73b82945cf3c719442",
"capturedAt": "2026-09-15T17:51:35.519Z",
"node": "v24.21.0",
"yarn": "1.22.22",
"preparation": {
"command": "yarn test:package --browser",
"seconds": 106.36,
"report": {
"path": "refactor/.cache/packages/run-xQsBHN/report.json",
"sha256": "3b1f8ff09fb6f3d5c68783406c8c2922570f4592f8212d20ed1f7218a9c6d877"
},
"log": {
"path": "refactor/.cache/rel02-candidates-package.log",
"sha256": "7456f57c2ebdc7782a3030a775a64b2baa1f43e733869e79dfe7ec82908f8a9d"
},
"packages": 20,
"runtimeScope": [
"artplayer",
"artplayer-plugin-chapter"
],
"runtimeChecks": 36,
"legacyTypeModes": 5,
"accurateTypeModes": 8,
"additionalTypeScope": [
"artplayer-plugin-audio-track",
"artplayer-plugin-hls-control"
]
},
"thumbnail": {
"commands": [
"yarn build artplayer-tool-thumbnail",
"yarn test:thumbnail-types-package"
],
"buildSeconds": 1.66,
"consumerSeconds": 11.96,
"compilerCases": 12,
"report": {
"path": "refactor/.cache/thumbnail-package-types-rCbvkz/report.json",
"sha256": "4b793ffa328d8630ff9d039b80bbb0f68f7e4d865bbbb5ecb1128a77c44cac12"
},
"logs": [
{
"path": "refactor/.cache/rel02-candidates-thumbnail-build.log",
"sha256": "4cab99627eb04a08b9a6342d61aae2934b2c3df63388843f0857444026d51133"
},
{
"path": "refactor/.cache/rel02-candidates-thumbnail.log",
"sha256": "8e011afb610cb18e5118fc4e83f7167908d42e9c91c72f643219a2f4fb219717"
}
]
},
"browser": {
"command": "yarn test:browser:installed danmuku-timing-diagnostic.spec.js audio-buffering.spec.js --grep \"pending|sampling|sample|timing|held\"",
"artifactMap": {
"path": "refactor/.cache/packages/run-xQsBHN/browser-artifacts.json",
"sha256": "bf0ca0fdfd6f1aa336f7abe65e4bb2225b959ff1dfd15c8902438bbf2f3a967b"
},
"stats": {
"startTime": "2026-09-15T17:44:46.263Z",
"duration": 52819.146,
"expected": 36,
"skipped": 0,
"unexpected": 0,
"flaky": 0
},
"report": {
"path": "refactor/.cache/rel02-candidates-browser/report.json",
"sha256": "67aa867dec7eedaa006df3cc1b332d0685b1147a8160d1c167c8d1e076002641"
},
"invocation": {
"path": "refactor/.cache/rel02-candidates-browser/invocation.json",
"sha256": "f70fc5e116621af3c021c31ff5b6ac3b419b0f15c3a0ec007c55a293e30bdeb3"
},
"result": {
"path": "refactor/.cache/rel02-candidates-browser/result.json",
"sha256": "0132d84089bcff324bda8625ed72010ef60e2fbc3b6def23d5a75dcf512527d0"
},
"scope": "24 Danmuku native sampling/eligibility cases plus 12 Audio held-switch cases in three desktop engines; no native starvation closure"
},
"inputReports": [
{
"path": "refactor/.cache/release-ledger-G9z39H/report.json",
"sha256": "2e6b4993cd79e0c97d62b51376fb4e891c03ea549030e1741eb0626620a2d58b"
},
{
"path": "refactor/.cache/release-ledger-HnUiNG/report.json",
"sha256": "c9bc1cce17336957e5de7b94cea3ae80350b396a3197dcb4f80b08dee158b85f"
}
],
"failure": {
"actualArchive": "refactor/.cache/packages/run-xQsBHN/artplayer.tgz",
"error": "Invalid archive path: package",
"collectionLog": {
"path": "refactor/.cache/rel02-candidates-provenance.log",
"sha256": "1795db66a068399de6798c3b536a7c51ceca8b76768b6542a5bbc18430e40355"
},
"regressionLog": {
"path": "refactor/.cache/rel02-yarn-archive-before.log",
"sha256": "9ac61668623d0867e33cd8d82da544a3c51570e097916474cd91aaaee6bb07c1"
},
"regressionExitCode": 1
},
"correctedChecker": {
"actualArchives": [
{
"name": "artplayer",
"version": "6.0.0",
"path": "refactor/.cache/packages/run-xQsBHN/artplayer.tgz",
"sha256": "a9913669915970ec5070a4ed80a60ab885624cde7641265e350c07bee1f7ae9c",
"integrity": "sha512-tcg5Z64NAiVg2NyzZ/UvxIe1Q9gxG5NCHPVbnBaXukGDt7FUzDo1fUekUhzJtMZUKv0227WMtD968sRfvAy8Sw=="
},
{
"name": "artplayer-plugin-chapter",
"version": "2.0.0",
"path": "refactor/.cache/packages/run-xQsBHN/artplayer-plugin-chapter.tgz",
"sha256": "bd5b9d0d467bbd06fb2e60058d27f392bdf62a4ad2f5978cf0ce39fc1bba4ac1",
"integrity": "sha512-kw31aC1S+rT+DSC5PGsbwjli26pRJy/5PL75GLYbq8P+9ORm4p3xO3JHiM33qyg0PArckP3NSfNWuxNXVzLGJA=="
},
{
"name": "artplayer-plugin-ambilight",
"version": "2.0.0",
"path": "refactor/.cache/packages/run-xQsBHN/artplayer-plugin-ambilight.tgz",
"sha256": "c70002b5ef9260db5aa378360f25bcb3f547b65156182314ecefece7dad8ec49",
"integrity": "sha512-u8Sid+prhTO9muhdj4L1A5sacrAcD7yHQbhccIioaZVLZg+uNi6CLn/9yooPsYkgjIVU/2jeqkGCLbIf22602A=="
},
{
"name": "artplayer-proxy-canvas",
"version": "2.0.0",
"path": "refactor/.cache/packages/run-xQsBHN/artplayer-proxy-canvas.tgz",
"sha256": "5ae7532d29ccdac3114ccb40dab7ea18e024c38d0ea6fa8425571954aee180f6",
"integrity": "sha512-HH57q3Qhr2Fq1B8KlU6BKCHtSRxDAyHIv1n4SqzhPYxsU9JTN14UlqnA9tSuMTmf5W0qUslkXVdzzSI9z/Svtg=="
},
{
"name": "artplayer-plugin-document-pip",
"version": "2.0.0",
"path": "refactor/.cache/packages/run-xQsBHN/artplayer-plugin-document-pip.tgz",
"sha256": "e06b96ebd9f500b2ba14fb20dcc3ff85643e7c17b93c1a092130c520a23226db",
"integrity": "sha512-ONGOJAoFxM/uSXoZzrV7UaqNbnTekS9CU58uR6GuUKsP/miaav4ZLQKTdnET2QUdOZDmt6H9aZzV7/UEMqnBNA=="
},
{
"name": "artplayer-plugin-ads",
"version": "3.0.0",
"path": "refactor/.cache/packages/run-xQsBHN/artplayer-plugin-ads.tgz",
"sha256": "8227e59b9a33545964af42b1520763132ef0c6531d81564ea871d0fb6403191c",
"integrity": "sha512-duNwmozO5vwKaS0qnk2r9cmMTVabL1nsGZVV/eFq36tWShLVzq+ZGbCGPiGtXqlMORwf0A/PzKMg9DAaL95vKg=="
},
{
"name": "artplayer-plugin-audio-track",
"version": "2.0.0",
"path": "refactor/.cache/packages/run-xQsBHN/artplayer-plugin-audio-track.tgz",
"sha256": "0cd3c0b7e380880afcc8fc47af2e3274e7cf6d459e22d8e5d59a20b1f6b48c1d",
"integrity": "sha512-5DKXPYVMM8EbXGTCVShGOxyKTPhR4qv6YdIPprmQJ9ovnrCZOorMm6aATwH36GcZ5uxvftL0/19YwaarCEheag=="
},
{
"name": "artplayer-plugin-vtt-thumbnail",
"version": "2.0.0",
"path": "refactor/.cache/packages/run-xQsBHN/artplayer-plugin-vtt-thumbnail.tgz",
"sha256": "dab10ac4273ff19572e98218b43cb295b6b86ab6f3e58c5ed3a082b51951cd8a",
"integrity": "sha512-QBePktREdD7UEkv/7s8JsVpFelM/ZY6GP10WWL/2R5O97C/BiKGOqFreY0IAjzoXwgNy2WmTDcCwkPw5tMq+vQ=="
},
{
"name": "artplayer-plugin-multiple-subtitles",
"version": "2.0.0",
"path": "refactor/.cache/packages/run-xQsBHN/artplayer-plugin-multiple-subtitles.tgz",
"sha256": "4ef6783ebfa8b99ad770c2f922f3d7f4f5835148c3c560a43e90e2237cf6f0e7",
"integrity": "sha512-Jp8zkuKlZzgOZr859VEKy9rJgKF9agN4mc1YaOFOdXK/CTWPugvk1BwUkpTYdHvV4BPsXWT3LGF/JKDqsPyhAw=="
},
{
"name": "artplayer-plugin-hls-control",
"version": "2.0.0",
"path": "refactor/.cache/packages/run-xQsBHN/artplayer-plugin-hls-control.tgz",
"sha256": "5b87ad54604a6f084c2af7adf9edba004f0a4760efe2ca30f422fb02fd92f188",
"integrity": "sha512-VCJh4S9P+PnpWdees2a3j5Q29y8CMBiUwX92Zjcj1UrKjLfn6L9hFcvr+5Rc2bEMu+up+Qwcc7JbQqrBs+NXnA=="
},
{
"name": "artplayer-plugin-dash-control",
"version": "2.0.0",
"path": "refactor/.cache/packages/run-xQsBHN/artplayer-plugin-dash-control.tgz",
"sha256": "22885a9df3d00703ec4cc044ac55bb62738f24029b1ae4649139376ef2fbc6f1",
"integrity": "sha512-P9KwbaW4yOhHswJNkSgkxg9ZeXVGducq2dq9F/NjuxeqO/AipWDIUP924RBDbDjyc7XDQKQ6WALV8HUqyeWEGQ=="
},
{
"name": "artplayer-plugin-auto-thumbnail",
"version": "2.0.0",
"path": "refactor/.cache/packages/run-xQsBHN/artplayer-plugin-auto-thumbnail.tgz",
"sha256": "74ad1f31633da4614458c64ee47a8e43707ada7f79191356928f530fefd8a6d9",
"integrity": "sha512-OdpEqH39pAT94qBZ9f1wmPIxmmWvo5dLqIvztLU9hsOpX8luuuyxWa9ShCNq55PGggberAXMvGbfzk1p9dISag=="
},
{
"name": "artplayer-plugin-asr",
"version": "3.0.0",
"path": "refactor/.cache/packages/run-xQsBHN/artplayer-plugin-asr.tgz",
"sha256": "9b224b8a5e3ab6bf8a020102311b6e4781bdbb8c996a7d1a641404672f94ad6a",
"integrity": "sha512-2jLnZ8JJJ0k0fE8mAxW3vXcQPVGz/yRUpFcVARELvBF0JEgEmzVix+G2zpKaloQrZru8nliw0gBmuTQczNBrXA=="
},
{
"name": "artplayer-plugin-chromecast",
"version": "2.0.0",
"path": "refactor/.cache/packages/run-xQsBHN/artplayer-plugin-chromecast.tgz",
"sha256": "025de33f82160a136dba4abcf9024a26c0a663c2745bbdb8b9cbb77dfdfc9eda",
"integrity": "sha512-eP+lmJ3hulu5sBVPCSZI+Ll7nxAFQ/UTcIMoPVA9hIxhzn1APAMkOXUBjBjyrkiwdb8oU664Umbys4BRCPBbyg=="
},
{
"name": "artplayer-proxy-mediabunny",
"version": "2.0.0",
"path": "refactor/.cache/packages/run-xQsBHN/artplayer-proxy-mediabunny.tgz",
"sha256": "42d28808395d04cbb1839525e068680fbba935a1fa0cc21796d46503c2b6d620",
"integrity": "sha512-0Feras8kJkaXME9Pq17d5HGFwjIfkSVyULFnFwWakfPBn234Vm0EfPT17GbbE/aPFXcDMCcorbs/TcAt+rwcgQ=="
},
{
"name": "artplayer-plugin-jassub",
"version": "2.0.0",
"path": "refactor/.cache/packages/run-xQsBHN/artplayer-plugin-jassub.tgz",
"sha256": "27c9252c0ea648c3e835d7f08d10ca7df04ecda9eec66dbfdb4c45a59b806986",
"integrity": "sha512-6y/IJzxtwX6vjxd2kpEkoSFGOwyB9CkGfexZjb5gmJS4A5nuWnswUYYAKK1NhoD6U+KNgQOHvQ1xlBa11Bv2MQ=="
},
{
"name": "artplayer-plugin-danmuku",
"version": "6.0.0",
"path": "refactor/.cache/packages/run-xQsBHN/artplayer-plugin-danmuku.tgz",
"sha256": "cd679867754432d4d7129a72eb673cbaec40aac122dad033ff23239580990446",
"integrity": "sha512-zL/I+nCxg+BsuhNhcN4nbB/rQkeaxstsNRGJ5a6mr3HWfBwfs8tkMmAsYf+EvE3YECODnmBcVvlLyUDYhmejbg=="
},
{
"name": "artplayer-plugin-danmuku-mask",
"version": "2.0.0",
"path": "refactor/.cache/packages/run-xQsBHN/artplayer-plugin-danmuku-mask.tgz",
"sha256": "d82ccc30f91d44bc25c79bdb21975f48bd724c52bff01181755da630de020e4d",
"integrity": "sha512-LIcKv9Ny6Ts3g0/BEes4prGliqrr2XytTLh2u04hyZCdDAFPhUKPIGOtKPnojUrpd9DPX5i0bRv067DuXqyYVA=="
},
{
"name": "artplayer-tool-iframe",
"version": "2.0.0",
"path": "refactor/.cache/packages/run-xQsBHN/artplayer-tool-iframe.tgz",
"sha256": "66adbe3dd7d9a5a0f9c42e2ffbc98a97677e9d410353c671ce046bcad31ffb45",
"integrity": "sha512-8be0ck6LtxZQqjHMTeLVFOOo2eq1yL5rLASEVuZK7z9/CVaBl2hoatAuOnMRD8QloE58nL3a+J4mbrH10vX0mQ=="
},
{
"name": "artplayer-plugin-vast",
"version": "2.0.0",
"path": "refactor/.cache/packages/run-xQsBHN/artplayer-plugin-vast.tgz",
"sha256": "ab0b1d57e7c12a9f77679e5a57bf424a47b3498fa0cff20551036d387eb7043d",
"integrity": "sha512-rp/ZfsPIGCPr7dvR369tg72bQn9M9iVARcL2Axo+u+CuO976LhBhk+hteUeFs1NpQDwB745ZzyCtNS8Ap3ITTA=="
},
{
"name": "artplayer-tool-thumbnail",
"version": "5.0.0",
"path": "refactor/.cache/thumbnail-package-types-rCbvkz/candidate.tgz",
"sha256": "6eea43186a3020f12a463fc4cdef3dc9972ed1cbc04f2cad5c158054e51cf459",
"integrity": "sha512-FUGZyHcM3ZyIGAKU0fRX8HJot+56fU+zntCp0fuBR08I5yCpuf943uHH35roLp7NYJbzC8qlGaq2eIdXlaEb2A=="
}
],
"passed": 21,
"regressionTests": {
"passed": 80,
"failed": 0,
"skipped": 0,
"durationMs": 2774.1443,
"log": {
"path": "refactor/.cache/rel02-yarn-archive-after.log",
"sha256": "b4abe3606cb95409a64e7e1c33f87120e875c5e4bd75d3d1756746562ff7f663"
}
},
"checks": [
"targeted ESLint exit 0",
"tsc -p scripts/tsconfig.release.json --noEmit exit 0"
]
},
"limitations": [
"These build/browser reports preceded the archive-checker correction and are not rebound to its new fingerprint.",
"All 21 archives are accepted by the corrected content checker, but no candidate or release gate has been registered.",
"The only before/after input change was the browser-generated docs/uncompiled/artplayer-plugin-danmuku/index.js, affecting site inputs only; library fingerprints were unchanged until the checker fix.",
"Complete combinations, physical devices, rollback, remote CI and user-guided formal reviews remain open."
],
"publicationAuthorized": false
}
@@ -0,0 +1,52 @@
# REL-02 checkpoint: accept actual Yarn candidate archives
The 20-package browser preparation on source commit
`e54fc998ea711f686143be73b82945cf3c719442` passed in 106.36 seconds.
It built in an isolated snapshot, packed and installed the target majors,
verified frozen consumer locks and installed members, and ran the core/Chapter
36 runtime checks, 5 legacy and 8 accurate type modes, plus Audio/HLS fixtures.
Thumbnail 5.0.0 was built separately and passed its 12 historical/candidate
compiler cases. This is not all-plugin runtime or type acceptance.
Using the actual 20-package installed map, the focused desktop Chromium,
Firefox and Windows WebKit run passed 36 tests with zero retries, failures or
skips: 24 Danmuku sampling/eligibility cases and 12 Audio held-switch cases.
The native starvation failures, other combinations and physical device gaps
remain open. Windows WebKit does not establish physical Safari behavior.
Candidate registration then exposed a real checker defect: Yarn writes its
root and nested directory entries as `package` and `package/dist`, without
trailing slashes. `checkedCandidate` incorrectly used the historical archive
inventory helper, which rejected the root as `Invalid archive path: package`.
No candidate was registered and no release gate was bypassed.
The type-aware tar listing logic already used by isolated installation is now
in `scripts/package-archive.mjs`. Both installation and candidate registration
use it; `scripts/package-check.mjs` retains its existing `packedFiles` export.
The helper accepts directory entries with or without trailing slashes and
rejects links, special files, duplicate members, wrong roots and traversal.
Historical archive inventory remains unchanged. No dependency, package API,
runtime implementation or npm entry changed.
A self-contained tar fixture reproduces Yarn's directory layout. It failed
before the fix, and checks both accepted directory forms plus hard links,
symlinks, FIFO entries, a regular-file root, duplicates and invalid paths after
the fix. The combined ledger/bundle/verifier/package tests passed 80/80 in
2774.1443 ms; targeted lint and strict release TypeScript checks passed.
The corrected checker also accepted all 21 actual prepared archives, checking
their recorded SHA-256, SHA-512, manifest names and target versions.
Detailed reports, logs, immutable archive bindings and the preserved browser
report are indexed in [the validation record](../baselines/major-candidate-archive-validation.json).
Before/after library input fingerprints were unchanged during preparation.
The browser server generated a Danmuku development bundle, changing only the
site input inventory. The archive-checker correction subsequently changed
shared validation inputs, so the earlier build/browser results are retained
as observed and are not rebound to the new fingerprint.
REL-02 remains doing. Commit this checker correction before rebuilding and
registering candidates with its exact committed validation inputs. Candidate
registration, complete combinations and candidate-specific rollback remain
unfinished. Reverting this checkpoint restores the incorrect Yarn rejection;
existing historical reports remain available. No formal review, remote write,
deployment or publication was performed.
+1 -1
View File
@@ -698,7 +698,7 @@
- REL-08: [记录](changes/2026-09-14-REL-08-release-ledger.md) [记录](baselines/release-ledger-validation.json) [记录](release-ledger.md) [记录](release-ledger.json)
- REL-01: [记录](version-plan.md) [记录](version-plan.json) [记录](baselines/version-registry-2026-09-16.json) [记录](changes/2026-09-16-REL-01-version-plan.md)
- REL-09: [记录](changes/2026-09-16-REL-09-major-versions.md) [记录](baselines/major-version-preparation.json) [记录](baselines/version-registry-prepared-2026-09-16.json) [记录](version-plan.md)
- REL-02: [记录](changes/2026-09-16-REL-02-types-inputs.md) [记录](baselines/major-installed-types-validation.json)
- REL-02: [记录](changes/2026-09-16-REL-02-types-inputs.md) [记录](baselines/major-installed-types-validation.json) [记录](changes/2026-09-16-REL-02-yarn-candidate-archives.md) [记录](baselines/major-candidate-archive-validation.json)
- REL-04: [记录](changes/2026-09-15-REL-04-consumer-checkpoint.md) [记录](rollback-rehearsal.md) [记录](baselines/rollback-consumer-validation.json) [记录](changes/2026-09-15-REL-04-workflows-checkpoint.md) [记录](rollback-inventory.md) [记录](baselines/rollback-workflows-validation.json) [记录](changes/2026-09-15-REL-04-pages-recovery.md) [记录](baselines/pages-recovery-validation.json) [记录](changes/2026-09-15-REL-04-acceptance.md) [记录](baselines/rollback-acceptance-validation.json)
- PKG-FACTORY-01: [记录](baselines/factory-assignment-gaps.json) [记录](baselines/factory-compatibility-proposals.json) [记录](factory-compatibility-decision.md) [记录](changes/2026-09-12-PKG-FACTORY-01-decision.md) [记录](type-compatibility-policy.md) [记录](baselines/factory-compatibility-validation.json) [记录](changes/2026-09-13-PKG-FACTORY-01-compatible-types.md)
- CORE-25: [记录](changes/2026-09-13-CORE-25-defaults-ssr.md) [记录](baselines/defaults-ssr-validation.json)
+10
View File
@@ -1,5 +1,15 @@
# 进度与证据
## REL-02 Yarn候选归档检查修复
统一20包构建/隔离安装通过,Thumbnail单独构建及12组类型消费通过;实际安装
产物的弹幕采样24项和Audio等待顺序12项三浏览器回归全部通过,保留原断流缺口。
登记时发现发布校验器误拒绝Yarn无尾斜杠目录,现与安装流程共用条目类型校验,
80项相关测试、lint及严格TS通过,并实际接受21个归档。见[记录](changes/2026-09-16-REL-02-yarn-candidate-archives.md)
和[证据](baselines/major-candidate-archive-validation.json)。此前产物报告不套用修复后
的新指纹;下一步固定提交再准备登记候选。REL-02仍doing,数量224/21/40不变。
未启动用户保留的复盘,也没有推送、远端派发、部署或发布。
## REL-02 全包安装类型验证与输入指纹修复
21个库的18组隔离消费命令全部通过,包含核心、所有插件/proxy/工具的目标major
+4
View File
@@ -65,6 +65,10 @@ site-manifest文件包含package、version、outputRoot和files,每个file为p
outputRoot必须在仓库内;目录完整文件集须等于files,新增、缺失、替换或符号链接输出
均拒绝。清单SRI和每个实际部署文件字节分别验证;URL正确性仍必须有site-urls报告。
候选tar成员检查与隔离安装共用scripts/package-archive.mjs,按实际条目类型识别
目录,接受Yarn pack生成的无尾斜杠package和子目录;拒绝链接、特殊文件、重复
成员和越界路径。冻结历史archive的原校验器仍用于历史内容核对,不用于新候选。
`inputFingerprint`包含本包、其实际依赖闭包,以及共享构建/工具链/测试/CI输入。
依赖来自impact-model的显式关系和源码/声明/manifest扫描;未解释动态导入按全部包
保守处理。站点还包含docs及example。第三方清单、本校验器、兼容/环境/版本/复盘
+3 -2
View File
@@ -5,9 +5,10 @@ import path from 'node:path'
import process from 'node:process'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { parseArgs } from 'node:util'
import { packedFiles } from '../../scripts/package-archive.mjs'
import { readImpactModel, repositoryPath } from './impact-model.mjs'
import { dependencyClosure, evaluatePackage, findingPackages, validateLedger } from './release-ledger-model.mjs'
import { archiveFiles, hash, readMember } from './releases.mjs'
import { hash, readMember } from './releases.mjs'
export const root = fileURLToPath(new URL('../../', import.meta.url))
@@ -74,7 +75,7 @@ export function checkedCandidate(directory, row) {
}
else {
assert.equal(binding.kind, 'npm-tarball')
archiveFiles(artifact)
packedFiles(artifact)
const manifest = JSON.parse(readMember(artifact, 'package/package.json'))
assert.equal(manifest.name, row.name, 'Tarball package name mismatch')
assert.equal(manifest.version, binding.version, 'Tarball version mismatch')
+48
View File
@@ -1,10 +1,12 @@
import assert from 'node:assert/strict'
import { Buffer } from 'node:buffer'
import { execFileSync } from 'node:child_process'
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
// eslint-disable-next-line test/no-import-node-test -- Release evidence must fail closed under stale or mis-scoped input.
import test from 'node:test'
import { gzipSync } from 'node:zlib'
import { dependencyClosure, evaluatePackage, findingPackages, libraryGates, sharedTasks, siteGates, validateLedger } from './release-ledger-model.mjs'
import { checkedCandidate, checkedReport, fingerprintInputs, fingerprintOf, localFile, root } from './release-ledger.mjs'
import { hash } from './releases.mjs'
@@ -218,3 +220,49 @@ test('Release candidate checks actual tarball manifest and detects digest/versio
save('candidate.tgz', 'tampered')
assert.match(checkedCandidate(directory, row).errors[0], /Candidate integrity mismatch/)
})
function candidateArchive(entries) {
const blocks = []
for (const { name, type = '0', content = '', link = '' } of entries) {
const bytes = Buffer.from(content)
const header = Buffer.alloc(512)
header.write(name)
header.write('0000755\0', 100)
header.write('0000000\0', 108)
header.write('0000000\0', 116)
header.write(`${bytes.length.toString(8).padStart(11, '0')}\0`, 124)
header.write('00000000000\0', 136)
header.fill(32, 148, 156)
header.write(type, 156)
header.write(link, 157)
header.write('ustar\0', 257)
header.write('00', 263)
header.write(`${header.reduce((sum, byte) => sum + byte, 0).toString(8).padStart(6, '0')}\0 `, 148)
blocks.push(header, bytes, Buffer.alloc((512 - bytes.length % 512) % 512))
}
return gzipSync(Buffer.concat([...blocks, Buffer.alloc(1024)]))
}
test('Release candidate accepts Yarn directory entries without trailing slashes and rejects unsafe entry types', (t) => {
const { directory, save } = temp(t)
execFileSync('git', ['init', '-q', directory])
execFileSync('git', ['-c', 'user.name=Ledger test', '-c', 'user.email=ledger@example.invalid', 'commit', '--allow-empty', '-qm', 'fixture'], { cwd: directory })
const sourceCommit = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: directory, encoding: 'utf8' }).trim()
const manifest = { name: 'example', version: '2.0.0' }
const entries = [{ name: 'package', type: '5' }, { name: 'package/dist', type: '5' }, { name: 'package/package.json', content: JSON.stringify(manifest) }, { name: 'package/dist/index.js', content: 'export default 1' }]
const row = { name: manifest.name, distribution: 'npm', candidate: { kind: 'npm-tarball', path: 'candidate.tgz', version: manifest.version, sourceCommit } }
const verify = (members) => {
const bytes = candidateArchive(members)
save('candidate.tgz', bytes)
row.candidate.integrity = `sha512-${hash(bytes, 'sha512', 'base64')}`
return checkedCandidate(directory, row).errors
}
assert.deepEqual(verify(entries), [])
assert.deepEqual(verify(entries.map(entry => entry.type === '5' ? { ...entry, name: `${entry.name}/` } : entry)), [])
for (const type of ['1', '2', '6'])
assert.match(verify([...entries, { name: 'package/dist/link.js', type, link: 'package/dist/index.js' }])[0], /links and special files/)
assert.match(verify([{ name: 'package', content: 'not a directory' }, ...entries.slice(1)])[0], /Invalid package root/)
assert.match(verify([...entries, entries[2]])[0], /Duplicate archive members/)
assert.match(verify([...entries, { name: 'outside.json', content: '{}' }])[0], /Invalid package root/)
assert.match(verify([...entries, { name: 'package/../outside.json', content: '{}' }])[0], /Unsafe package member/)
})
+3 -1
View File
@@ -5015,7 +5015,9 @@
"acceptance": "隔离安装和可自动化组合通过;设备结论不伪造,最终发布绑定同一候选内容;在目标 major 版本确定后构建 pack,不在测试后改版本;逐包核验完整回退产物、实际候选版本/integrity、应用导入与冻结锁,重跑对应回退并保留报告;缺失历史分发不准入",
"evidence": [
"changes/2026-09-16-REL-02-types-inputs.md",
"baselines/major-installed-types-validation.json"
"baselines/major-installed-types-validation.json",
"changes/2026-09-16-REL-02-yarn-candidate-archives.md",
"baselines/major-candidate-archive-validation.json"
]
},
{
+17
View File
@@ -0,0 +1,17 @@
import assert from 'node:assert/strict'
import { execFileSync } from 'node:child_process'
export function packedFiles(archive) {
const args = { encoding: 'utf8', windowsHide: true }
const names = execFileSync('tar', ['-tzf', archive], args).trim().split(/\r?\n/)
const entries = execFileSync('tar', ['-tvzf', archive], args).trim().split(/\r?\n/)
assert.equal(names.length, entries.length, 'Archive listing mismatch')
assert.equal(new Set(names).size, names.length, 'Duplicate archive members')
return names.filter((name, index) => {
const type = entries[index][0]
assert(['d', '-'].includes(type), 'Package links and special files are unsupported')
assert((type === 'd' && name === 'package') || name.startsWith('package/'), 'Invalid package root')
assert(!name.includes('\\') && !name.split('/').includes('..'), 'Unsafe package member')
return type === '-'
}).sort()
}
+3 -15
View File
@@ -7,8 +7,11 @@ import { fileURLToPath } from 'node:url'
import { ensureArchive, hash, readMember } from '../refactor/scripts/releases.mjs'
import { installedPackages } from './browser-validation/scope.ts'
import { installedPluginTypes } from './consumers/packages.ts'
import { packedFiles } from './package-archive.mjs'
import { consumerDirectory, names as defaultNames, readJson, removeConsumer, run, runtimeConsumer, typeConsumers, workspace, writeJson } from './package-consumer.mjs'
export { packedFiles } from './package-archive.mjs'
export function packageOptions(args) {
assert(args.every(arg => ['--release', '--browser'].includes(arg) || arg.startsWith('--include=')), 'Unknown package-check option')
const includes = args.filter(arg => arg.startsWith('--include='))
@@ -60,21 +63,6 @@ export function historicalDistributionFiles(name, { baseline, sources }) {
return members
}
export function packedFiles(archive) {
const args = { encoding: 'utf8', windowsHide: true }
const names = execFileSync('tar', ['-tzf', archive], args).trim().split(/\r?\n/)
const entries = execFileSync('tar', ['-tvzf', archive], args).trim().split(/\r?\n/)
assert.equal(names.length, entries.length, 'Archive listing mismatch')
assert.equal(new Set(names).size, names.length, 'Duplicate archive members')
return names.filter((name, index) => {
const type = entries[index][0]
assert(['d', '-'].includes(type), 'Package links and special files are unsupported')
assert((type === 'd' && name === 'package') || name.startsWith('package/'), 'Invalid package root')
assert(!name.includes('\\') && !name.split('/').includes('..'), 'Unsafe package member')
return type === '-'
}).sort()
}
export async function publishedConsumer() {
const dir = consumerDirectory()
const releases = readJson(path.join(workspace, 'refactor/baselines/releases.json')).releases
+6
View File
@@ -28,6 +28,12 @@ reports are not evidence that an ArtPlayer package is ready to publish.
## Responsibilities
- `../package-archive.mjs` checks actual tar entry types and paths for both
package installation checks and candidate registration. It accepts Yarn's
directory entries without a trailing slash while rejecting links, special
files, duplicate members and paths outside `package/`. The legacy
`package-check.mjs` export forwards to this helper for existing scripts.
- `../prepare-release.mjs` is the Node entry and failure exit-code boundary.
- `prepare.ts` parses the explicit package batch/tag, requires the canonical
Node/Yarn, and checks clean Git state before both ledger reads. Ignored generated