diff --git a/docs/QINGLONG_3_0_ARCHITECTURE_RFC.md b/docs/QINGLONG_3_0_ARCHITECTURE_RFC.md index cb1c44e4..e8022c3f 100644 --- a/docs/QINGLONG_3_0_ARCHITECTURE_RFC.md +++ b/docs/QINGLONG_3_0_ARCHITECTURE_RFC.md @@ -11,6 +11,24 @@ 最新增量证据(2026-08-18): +- D-348/ADR-0440(已接受;首份真实线上闭合待实际 release tag):补齐 D-347 确定性收据与实际 tag promotion 之间的 freshness 窗口。此前私有 + job 在收据创建时验证 24 小时 freshness,但 release-set aggregate 只检查 `observedAt` 可解析与 receipt digest;镜像构建、扫描和 attestation + 延迟后,创建时有效的收据可能在闭合时已经过期。现在 `cluster|all` aggregate 与紧随其后的 source-record audit 必须各自取得 runner-owned 当前 + 时钟,对两份 receipt 公开的 `observedAt` 重新执行 24 小时最大年龄和五分钟未来偏差门;缺失/非安全整数/过期/未来漂移均在 release-set 写文件、 + tag promotion 和 catalog publication 前失败关闭。当前时钟没有 CLI 参数、环境变量或 durable 字段,只参与瞬时准入;因此同一 source evidence 在 + 两个有效闭合时钟下仍生成逐字节相同的 receipt v2、release-set v3 与 catalog identity。Local scope 保持零私有收据且该门为 + `not_applicable`;历史 standalone inspection 继续验证结构/identity/self-digest 而不以今天的时钟淘汰合法 catalog,并诚实声明未重放 source + records/private reports。本 Gate 不修改 durable schema/media type,不新增 package、生产依赖、数据库、migration、Pod、controller、RBAC、 + listener、timer、设备工具或稳态资源。定向 release receipt/set/catalog/consumption/deployment-lock/image audit 回归 137/137;完整 backend + 1,365 项为 1,363 pass/2 条件 skip/0 fail,18-package clean build/test 退出 0。12 项 package boundary、Cluster dependency、Edge + import、Cluster/Worker/CloudNativePG 部署、backup、Barman/cert-manager selection、image release、deployment-lock surface 与 + release-version 审计全部 compatible;14 档 Local artifact 全部 compatible 且保持既有字节基线:默认 Edge/Standalone 为 + 2,589,890/2,589,968 bytes,adopted 为 2,809,185/2,809,308 bytes,application 为 3,632,769/3,632,889 bytes, + application-api 为 3,800,322/3,800,466 bytes,AI 为 3,069,143/3,069,233 bytes,application+AI 为 + 4,493,043/4,493,175 bytes,MCP 为 7,315,930/7,316,038 bytes。真实 PostgreSQL 18.6 arm64 physical HA 再次通过 142/142、 + timeline `1→2`,报告 SHA-256 为 `7566a54f86f3e4e0fee2096a49ea2877d827595575c7075bc44b65314cb2ba19`,离线审计通过且无 + `ql3-ha-*` Docker 容器、卷或网络残留;真实线上 closure 仍只能由实际受保护 `v3` release tag 取得。 + - D-347/ADR-0439(已接受;首份真实线上重放待实际 release tag):修正 D-346 收据把私有 runner `validatedAt` wall-clock 写入 durable JSON 导致同一 source/report 在 workflow 重跑时产生第二个 release identity 的问题。私有收据升为 `qinglong/private-release-evidence-receipt@v2`:创建时仍以当前私有时钟执行 source-aware gate 和 24 小时 freshness 双重验证,但 durable diff --git a/docs/adr/ADR-0440-release-set-closure-private-evidence-freshness.md b/docs/adr/ADR-0440-release-set-closure-private-evidence-freshness.md new file mode 100644 index 00000000..e88cb24a --- /dev/null +++ b/docs/adr/ADR-0440-release-set-closure-private-evidence-freshness.md @@ -0,0 +1,75 @@ +# ADR-0440:Release-set 闭合时私有证据 Freshness 重验证 + +- 状态:Accepted +- 日期:2026-08-18 +- 关联 RFC:QL-RFC-0001 D-03、D-14、D-335、D-346、D-347、D-348 +- 关联 ADR:ADR-0427、ADR-0438、ADR-0439 +- Extends:ADR-0439 的确定性私有证据收据与 release-set replay + +## 上下文 + +ADR-0439 从私有证据收据中移除了 runner `validatedAt` wall-clock,使同一 release identity、私有报告和静态 lock 在合法 workflow 重跑时生成相同 +receipt、release-set 与 catalog digest。创建私有收据时仍会用当前私有 runner 时钟执行 24 小时 freshness gate。 + +但 release-set job 可能在镜像构建、扫描、签名和 attestation 后才消费短期收据。D347 的聚合路径只验证 `observedAt` 可解析、receipt identity 和 +self digest,没有在 tag promotion 前以闭合时钟重新计算年龄。GitHub artifact 的一天 retention 是存储配置,不是精确的证据 freshness authority; +收据在创建时有效,不代表在 release-set 闭合时仍有效。 + +## 决策 + +1. `cluster|all` release-set closure 必须取得一个 runner-owned、safe-integer validation clock。该时钟不是 release-set input field,也不能由 CLI + 参数、环境变量、image record 或私有 receipt 指定。 +2. `inspectPrivateReleaseEvidenceReceipt` 在 closure caller 显式提供 validation clock 时,除 exact shape/source/report/self-digest 外,还必须用公开的 + `observedAt` 重新执行 24 小时最大年龄和五分钟未来偏差门。 +3. release-set `aggregate` 与紧随其后的 source-record `audit` 各自从进程内当前时钟执行该门;缺失、非安全整数、过期或未来漂移均在写 release-set、 + tag promotion 和 catalog publication 前失败关闭。 +4. validation clock 只参与准入,不进入 receipt、release-set、catalog plan、OCI manifest 或 digest。两个不同但都在有效窗口内的闭合时钟必须生成 + 逐字节相同的 release-set。 +5. `local` scope 必须保持零私有收据,closure freshness 为 `not_applicable`,不要求该时钟即可创建同一 Local release-set。 +6. `inspectReleaseSet` 是历史 durable catalog 的 standalone verifier,不以当前日期淘汰曾经合法的发布。它继续验证结构、identity、image/receipt 顺序与 + self digest,并诚实声明 source records/private reports 未重放;当前年龄不能证明过去发布时的 freshness。 +7. 不升级 receipt v2、release-set v3 或 OCI v3 media type,因为 durable bytes 和消费者结构没有变化;本 Gate 只收紧创建/审计时可接受的瞬时输入。 + +## 失败与恢复 + +- 收据在创建时有效、闭合时超过 24 小时:aggregate 在任何 durable 输出和 tag mutation 前失败,必须重新取得 source-aware 私有报告及其确定性收据。 +- 收据时间超过闭合时钟五分钟:视为时钟/报告异常并失败,不能通过推迟发布等待其“自然有效”。 +- aggregate response-loss 后以同一 source evidence 重跑:新的闭合时钟若仍在窗口内,生成相同 bytes;若已过期则必须重新取得私有证据,而不是复用旧收据。 +- 历史 catalog 多年后验真:standalone inspection 仍可通过 structural/digest/provenance 链;不得声称重新执行了当年的私有 freshness gate。 +- Local 发布:没有私有 evidence receipt,不执行 Cluster freshness 语义,也不新增设备或发布 runner 工作。 + +## 部署与资源影响 + +- Edge/Standalone 设备、Local image、selection 和 Compose 路径零变化,不安装发布工具,不增加 CPU/RSS、I/O、timer、listener 或 updater。 +- Cluster 节点和 Kubernetes object 零变化;重验证只发生在短生命周期 release-set runner,成本为两次时间读取和两个时间戳的常数比较。 +- 不新增 workspace package、生产依赖、数据库、migration、SQL、Pod、controller、RBAC、Secret、网络连接或持久状态。 + +## 被拒绝的替代方案 + +### 重新把 `validatedAt` 写入 receipt + +拒绝。它会恢复 D347 已消除的 wall-clock digest 漂移,让合法 workflow retry 产生第二个 catalog identity。 + +### 依赖一天的 GitHub artifact retention + +拒绝。retention 是删除上限,不保证下载瞬间的精确年龄、未来偏差或 tag promotion 前重验证,也不是 release contract 的可测试语义。 + +### 让 CLI 传入 `--validation-clock` + +拒绝。发布者可选择旧时钟会把 freshness gate 变成自报事实。测试通过显式 dependency injection 驱动边界,生产 CLI 不开放该输入面。 + +### 在所有历史 catalog inspection 中按当前时间拒绝 + +拒绝。这样所有合法发布都会在 24 小时后不可审计,并把“当前仍新鲜”错误等同于“发布时曾通过 freshness gate”。 + +## 验证 + +- 同一 Cluster source evidence 在两个不同有效 closure clock 下生成对象和 canonical bytes 完全相同; +- 超过 24 小时、未来偏差和缺失 validation clock 均在 release-set 写入前失败; +- CLI production path 使用内部 clock,测试证明 stale retry 不产生输出; +- Local scope、历史 standalone inspection、catalog consumption 和 deployment lock 保持兼容; +- 定向发布链 137/137,完整 backend 1,365 项为 1,363 pass/2 条件 skip/0 fail,18-package clean build/test 退出 0; +- 12 项静态审计和 14 档 Local artifact 全部 compatible,Edge/Standalone 至 MCP 的既有制品字节与 RSS 上限未漂移; +- PostgreSQL 18.6 arm64 physical HA 通过 142/142、timeline `1→2`,证据 SHA-256 为 + `7566a54f86f3e4e0fee2096a49ea2877d827595575c7075bc44b65314cb2ba19`,离线审计 compatible 且 Docker 资源零残留; +- 完整证据记录于 QL-RFC-0001 D-348;首份真实线上 closure 仍须由受保护 `v3` release tag 产生。 diff --git a/docs/adr/README.md b/docs/adr/README.md index 190fae5c..5bbef3d2 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -443,6 +443,7 @@ | [ADR-0437](./ADR-0437-post-publication-catalog-bound-local-release-gate.md) | 发布后 Catalog-bound Local Release Gate | Accepted(首份真实公开 catalog evidence 待实际 release tag) | | [ADR-0438](./ADR-0438-content-free-private-release-evidence-receipt-chain.md) | 内容无关的私有发布证据收据链 | Accepted(首份真实公开收据待实际 release tag) | | [ADR-0439](./ADR-0439-deterministic-private-evidence-receipts-and-release-set-replay.md) | 确定性私有证据收据与 Release-set 重放 | Accepted(首份真实线上重放待实际 release tag) | +| [ADR-0440](./ADR-0440-release-set-closure-private-evidence-freshness.md) | Release-set 闭合时私有证据 Freshness 重验证 | Accepted(首份真实线上闭合待实际 release tag) | ## 规则 diff --git a/docs/operations/ql3-release-set-deployment.md b/docs/operations/ql3-release-set-deployment.md index d82c9a1b..a09bbdcc 100644 --- a/docs/operations/ql3-release-set-deployment.md +++ b/docs/operations/ql3-release-set-deployment.md @@ -26,6 +26,14 @@ cert-manager 三项静态审计摘要。v2 收据不持久化私有 runner 的 w 从私有 job 交给 release-set job,随后完整嵌入 release-set v3 并由 durable catalog 长期保护。公开收据同时声明 `freshnessValidatedAtCreation=true` 与 `durableValidationClockPublished=false`,避免把未发布的临时时钟伪装成可离线重放的现场证据。 +创建时通过不等于可以无限期等待再闭合发布。`cluster|all` 的 release-set aggregate 与紧随其后的 independent audit 会各自从 runner 内部取得当前 +时钟,并对两份收据公开的 `observedAt` 重新执行同一 24 小时最大年龄和五分钟未来偏差门;当前时钟没有 CLI 参数、环境变量或 durable JSON 字段。 +任一收据在闭合时已过期,release-set 文件会在写入、tag promotion 与 catalog publication 前失败关闭。`local` scope 没有私有收据,因此该门为 +`not_applicable`,不会把 Cluster 私有证据成本带到 Local 发布。 + +长期 catalog 的 standalone inspection 故意不以“今天的时钟”拒绝历史发布;它验证 receipt/release/source/self-digest 闭包,但诚实返回私有现场 +证据未重放。发布时 freshness 由受保护 workflow 的 aggregate/audit 与 provenance 约束,历史 consumer 不得把当前年龄检查冒充当时的现场验证。 + ## 发布流水线内置 Local 与 Cluster 下游门 `local|all` scope 在 durable catalog 发布后启动独立 `release-catalog-local-deployment-live`。它与 publisher 权限隔离,从公开 catalog diff --git a/scripts/ql3-cluster-image-release-audit.cjs b/scripts/ql3-cluster-image-release-audit.cjs index 4404ac55..b3d9be85 100644 --- a/scripts/ql3-cluster-image-release-audit.cjs +++ b/scripts/ql3-cluster-image-release-audit.cjs @@ -1304,6 +1304,7 @@ function auditReleaseWorkflow(source) { sameRunRecords: true, sameRunPrivateEvidenceReceipts: true, deterministicPrivateEvidenceReceipts: true, + privateEvidenceFreshnessRevalidatedAtClosure: true, exactScopeClosure: true, standaloneInspection: true, tagPromotionAuthority: 'complete_verified_release_set', diff --git a/scripts/ql3-private-release-evidence-receipt-contract.cjs b/scripts/ql3-private-release-evidence-receipt-contract.cjs index 80ca65f7..5e385bdc 100644 --- a/scripts/ql3-private-release-evidence-receipt-contract.cjs +++ b/scripts/ql3-private-release-evidence-receipt-contract.cjs @@ -382,9 +382,20 @@ function inspectPrivateReleaseEvidenceReceipt(actual, options) { ) { fail('receipt shape or release binding is invalid'); } - if (!Number.isFinite(Date.parse(actual.evidence.observedAt))) { + const observedAtMs = Date.parse(actual.evidence.observedAt); + if (!Number.isFinite(observedAtMs)) { fail('receipt freshness binding is invalid'); } + if (options.validationClockMs !== undefined) { + const validationClockMs = options.validationClockMs; + if ( + !Number.isSafeInteger(validationClockMs) || + observedAtMs > validationClockMs + MAX_FUTURE_SKEW_MS || + validationClockMs - observedAtMs > MAX_EVIDENCE_AGE_SECONDS * 1000 + ) { + fail('receipt is outside the release freshness window'); + } + } const { receiptDigest, ...unsigned } = actual; if ( !DIGEST_PATTERN.test(receiptDigest || '') || diff --git a/scripts/ql3-release-set-contract.cjs b/scripts/ql3-release-set-contract.cjs index dc8b81e8..1ebf7fff 100644 --- a/scripts/ql3-release-set-contract.cjs +++ b/scripts/ql3-release-set-contract.cjs @@ -245,11 +245,14 @@ function expectedEvidenceKinds(releaseScope) { return releaseScope === 'local' ? [] : [...EVIDENCE_KINDS]; } -function validateEvidenceReceipts(receipts, candidate) { +function validateEvidenceReceipts(receipts, candidate, validationClockMs) { if (!Array.isArray(receipts)) { fail('private release evidence receipts must be an array'); } const expectedKinds = expectedEvidenceKinds(candidate.release.scope); + if (expectedKinds.length > 0 && !Number.isSafeInteger(validationClockMs)) { + fail('private evidence closure validation clock is invalid'); + } if (receipts.length !== expectedKinds.length) { fail('private release evidence receipt count differs from release scope'); } @@ -268,6 +271,7 @@ function validateEvidenceReceipts(receipts, candidate) { sourceRef: candidate.release.sourceRef, releaseScope: candidate.release.scope, evidenceKind, + validationClockMs, }); receiptsByKind.set(evidenceKind, receipt); } @@ -282,6 +286,7 @@ function createReleaseSet(options) { const evidenceReceipts = validateEvidenceReceipts( options.evidenceReceipts, candidate, + options.validationClockMs, ); if (!Array.isArray(options.records)) fail('image records must be an array'); if (options.records.length !== candidate.images.length) { @@ -363,6 +368,8 @@ function auditReleaseSet(actual, options) { evidenceReceiptDigests: Object.freeze( actual.evidenceReceipts.map((entry) => entry.receiptDigest), ), + privateEvidenceFreshnessRevalidatedAtClosure: + actual.evidenceReceipts.length === 0 ? 'not_applicable' : true, tagPromotionAuthority: actual.promotion.authority, }); } @@ -646,7 +653,12 @@ function parseArguments(argv) { }); } -function runCli(argv, root = DEFAULT_ROOT, output = process.stdout) { +function runCli( + argv, + root = DEFAULT_ROOT, + output = process.stdout, + dependencies = { now: Date.now }, +) { const options = parseArguments(argv); if (options.mode === 'inspect') { const report = readCanonicalJson(options.report, 'release set'); @@ -666,12 +678,20 @@ function runCli(argv, root = DEFAULT_ROOT, output = process.stdout) { options.evidenceReceipts, candidate, ); + let validationClockMs; + if (candidate.release.scope !== 'local') { + if (typeof dependencies?.now !== 'function') { + fail('release closure clock dependency is invalid'); + } + validationClockMs = dependencies.now(); + } if (options.mode === 'aggregate') { const releaseSet = createReleaseSet({ ...options, candidate, records, evidenceReceipts, + validationClockMs, root, }); writeNoReplace(options.output, releaseSet); @@ -684,6 +704,7 @@ function runCli(argv, root = DEFAULT_ROOT, output = process.stdout) { candidate, records, evidenceReceipts, + validationClockMs, root, }); output.write(canonicalJson(audit)); diff --git a/test/back/ql3ClusterImageReleaseAudit.test.cjs b/test/back/ql3ClusterImageReleaseAudit.test.cjs index 726e3d86..6511dead 100644 --- a/test/back/ql3ClusterImageReleaseAudit.test.cjs +++ b/test/back/ql3ClusterImageReleaseAudit.test.cjs @@ -100,6 +100,7 @@ test('accepts the reviewed native CI and digest release contracts', () => { sameRunRecords: true, sameRunPrivateEvidenceReceipts: true, deterministicPrivateEvidenceReceipts: true, + privateEvidenceFreshnessRevalidatedAtClosure: true, exactScopeClosure: true, standaloneInspection: true, tagPromotionAuthority: 'complete_verified_release_set', diff --git a/test/back/ql3DeploymentLockContract.test.cjs b/test/back/ql3DeploymentLockContract.test.cjs index 742a19eb..adf1b267 100644 --- a/test/back/ql3DeploymentLockContract.test.cjs +++ b/test/back/ql3DeploymentLockContract.test.cjs @@ -102,6 +102,7 @@ function releaseSet(scope) { records, evidenceReceipts: privateReleaseEvidenceReceipts(candidate.release), ...identity, + validationClockMs: Date.parse('2026-08-18T00:05:00.000Z'), releaseScope: scope, }); } diff --git a/test/back/ql3ReleaseCatalogContract.test.cjs b/test/back/ql3ReleaseCatalogContract.test.cjs index 435343c0..a7a7ce5d 100644 --- a/test/back/ql3ReleaseCatalogContract.test.cjs +++ b/test/back/ql3ReleaseCatalogContract.test.cjs @@ -70,6 +70,7 @@ function releaseSet(scope) { records, evidenceReceipts: privateReleaseEvidenceReceipts(candidate.release), ...identity, + validationClockMs: Date.parse('2026-08-18T00:05:00.000Z'), releaseScope: scope, }); } diff --git a/test/back/ql3ReleaseSetContract.test.cjs b/test/back/ql3ReleaseSetContract.test.cjs index 7575403f..455e3d27 100644 --- a/test/back/ql3ReleaseSetContract.test.cjs +++ b/test/back/ql3ReleaseSetContract.test.cjs @@ -30,6 +30,7 @@ const identity = Object.freeze({ sourceRevision: 'b'.repeat(40), sourceRef: `refs/tags/v${version}`, repositoryOwner: 'qinglong-release', + validationClockMs: Date.parse('2026-08-18T00:05:00.000Z'), }); function candidate(scope) { @@ -74,12 +75,13 @@ function writeCanonical(filePath, value) { test('aggregates the independent Local image into one immutable release set', () => { const releaseCandidate = candidate('local'); const records = recordsFor(releaseCandidate); + const { validationClockMs: _unused, ...localIdentity } = identity; const releaseSet = createReleaseSet({ root, candidate: releaseCandidate, records, evidenceReceipts: evidenceFor(releaseCandidate), - ...identity, + ...localIdentity, releaseScope: 'local', }); assert.deepEqual( @@ -97,7 +99,7 @@ test('aggregates the independent Local image into one immutable release set', () candidate: releaseCandidate, records, evidenceReceipts: evidenceFor(releaseCandidate), - ...identity, + ...localIdentity, releaseScope: 'local', }).compatible, true, @@ -187,6 +189,66 @@ test('requires exact private evidence receipts only for Cluster-capable scopes', ); }); +test('revalidates private evidence freshness at closure without changing release-set bytes', () => { + const releaseCandidate = candidate('cluster'); + const records = recordsFor(releaseCandidate); + const evidenceReceipts = evidenceFor(releaseCandidate); + const createAt = (validationClockMs) => + createReleaseSet({ + root, + candidate: releaseCandidate, + records, + evidenceReceipts, + ...identity, + validationClockMs, + releaseScope: 'cluster', + }); + const first = createAt(Date.parse('2026-08-18T00:05:00.000Z')); + const second = createAt(Date.parse('2026-08-18T00:15:00.000Z')); + assert.deepEqual(second, first); + assert.throws( + () => createAt(Date.parse('2026-08-19T00:00:01.000Z')), + /release freshness window/, + ); + assert.throws( + () => createAt(Date.parse('2026-08-17T23:54:59.000Z')), + /release freshness window/, + ); + assert.throws( + () => + createReleaseSet({ + root, + candidate: releaseCandidate, + records, + evidenceReceipts, + version: identity.version, + sourceRevision: identity.sourceRevision, + sourceRef: identity.sourceRef, + repositoryOwner: identity.repositoryOwner, + releaseScope: 'cluster', + }), + /closure validation clock/, + ); + assert.equal( + auditReleaseSet(first, { + root, + candidate: releaseCandidate, + records, + evidenceReceipts, + ...identity, + releaseScope: 'cluster', + }).privateEvidenceFreshnessRevalidatedAtClosure, + true, + ); + assert.equal( + inspectReleaseSet(first, { + ...identity, + releaseScope: 'cluster', + }).privateEvidenceReplayed, + false, + ); +}); + test('rejects missing, duplicate and cross-candidate image evidence', () => { const releaseCandidate = candidate('cluster'); const records = recordsFor(releaseCandidate); @@ -440,6 +502,70 @@ test('CLI records, aggregates and audits exact no-replace files', (t) => { ); }); +test('CLI rejects stale private receipts at closure and keeps valid retries byte-identical', (t) => { + const directory = temporaryDirectory(t); + const recordsDirectory = path.join(directory, 'records'); + const evidenceDirectory = path.join(directory, 'evidence'); + fs.mkdirSync(recordsDirectory); + fs.mkdirSync(evidenceDirectory); + const releaseCandidate = candidate('cluster'); + const candidatePath = path.join(directory, 'candidate.json'); + writeCanonical(candidatePath, releaseCandidate); + for (const record of recordsFor(releaseCandidate)) { + writeCanonical( + path.join(recordsDirectory, `${record.image.name}.json`), + record, + ); + } + for (const receipt of evidenceFor(releaseCandidate)) { + writeCanonical( + path.join(evidenceDirectory, `${receipt.evidenceKind}.json`), + receipt, + ); + } + const common = [ + '--mode=aggregate', + `--version=${version}`, + `--source-revision=${identity.sourceRevision}`, + `--source-ref=${identity.sourceRef}`, + '--release-scope=cluster', + `--repository-owner=${identity.repositoryOwner}`, + `--candidate=${candidatePath}`, + `--records=${recordsDirectory}`, + `--evidence-receipts=${evidenceDirectory}`, + ]; + const firstPath = path.join(directory, 'first.json'); + const secondPath = path.join(directory, 'second.json'); + runCli( + [...common, `--output=${firstPath}`], + root, + { write() {} }, + { now: () => Date.parse('2026-08-18T00:05:00.000Z') }, + ); + runCli( + [...common, `--output=${secondPath}`], + root, + { write() {} }, + { now: () => Date.parse('2026-08-18T00:15:00.000Z') }, + ); + assert.equal( + fs.readFileSync(secondPath, 'utf8'), + fs.readFileSync(firstPath, 'utf8'), + ); + const stalePath = path.join(directory, 'stale.json'); + assert.throws( + () => + runCli( + [...common, `--output=${stalePath}`], + root, + { write() {} }, + { now: () => Date.parse('2026-08-19T00:00:01.000Z') }, + ), + /release freshness window/, + ); + assert.equal(fs.existsSync(stalePath), false); +}); + test('CLI rejects extra records, symlinks and open argument shapes', (t) => { const directory = temporaryDirectory(t); const recordsDirectory = path.join(directory, 'records');