diff --git a/assets/demo/storyboard/catalog.zh-CN.json b/assets/demo/storyboard/catalog.zh-CN.json index 378b683..5a38115 100644 --- a/assets/demo/storyboard/catalog.zh-CN.json +++ b/assets/demo/storyboard/catalog.zh-CN.json @@ -18,6 +18,7 @@ "status": "owner-review", "next_gate": "在 GitHub 实际渲染中审阅 74 秒首页 GIF 与中文故事;通过后重导完整母版。", "freshness": { + "verified_product_sha": "f263104403db2090bda2a1a2f354d40bf167e0ce", "product_impact_ids": ["cli-and-lifecycle", "harness-capture", "viewer-surface", "timeline-navigation", "request-context", "tools", "protocol-and-raw", "translation-theme", "privacy-data"] }, "story": { @@ -63,6 +64,7 @@ "status": "owner-review", "next_gate": "审阅中文故事和协议边界,再决定是否进入正式配音与成片。", "freshness": { + "verified_product_sha": "f263104403db2090bda2a1a2f354d40bf167e0ce", "product_impact_ids": ["cli-and-lifecycle", "harness-capture", "viewer-surface", "request-context", "tools", "protocol-and-raw", "translation-theme", "privacy-data"] }, "story": { @@ -108,6 +110,7 @@ "status": "owner-review", "next_gate": "审阅中文排错故事与发布会式 v0.2 收束页;通过后重导母版,再决定是否进入配音。", "freshness": { + "verified_product_sha": "f263104403db2090bda2a1a2f354d40bf167e0ce", "product_impact_ids": ["harness-capture", "viewer-surface", "request-context", "tools", "protocol-and-raw", "privacy-data"] }, "story": { @@ -153,6 +156,7 @@ "status": "owner-review", "next_gate": "审阅中文翻译故事和原文兜底边界,再决定是否进入正式配音与成片。", "freshness": { + "verified_product_sha": "f263104403db2090bda2a1a2f354d40bf167e0ce", "product_impact_ids": ["viewer-surface", "request-context", "protocol-and-raw", "translation-theme", "privacy-data"] }, "story": { @@ -198,6 +202,7 @@ "status": "owner-review", "next_gate": "审阅中文故事;通过后导出 v0.3 新母版,并决定正式发布前是否用当前 Claude Code 重录同一最小任务。", "freshness": { + "verified_product_sha": "f263104403db2090bda2a1a2f354d40bf167e0ce", "product_impact_ids": ["harness-capture", "viewer-surface", "timeline-navigation", "request-context", "tools", "protocol-and-raw", "translation-theme"] }, "story": { @@ -243,6 +248,7 @@ "status": "owner-review", "next_gate": "审阅中文故事和术语边界,再决定是否进入正式配音与成片。", "freshness": { + "verified_product_sha": "f263104403db2090bda2a1a2f354d40bf167e0ce", "product_impact_ids": ["harness-capture", "viewer-surface", "timeline-navigation", "request-context", "tools", "skills", "protocol-and-raw", "translation-theme"] }, "story": { @@ -288,6 +294,7 @@ "status": "owner-review", "next_gate": "审阅异步回流的叙述是否清楚,再决定是否进入正式配音与成片。", "freshness": { + "verified_product_sha": "f263104403db2090bda2a1a2f354d40bf167e0ce", "product_impact_ids": ["harness-capture", "viewer-surface", "timeline-navigation", "request-context", "tools", "subagents", "protocol-and-raw", "translation-theme"] }, "story": { @@ -333,6 +340,7 @@ "status": "owner-review", "next_gate": "审阅压缩前后证据是否足以支撑结论,再决定是否进入正式配音与成片。", "freshness": { + "verified_product_sha": "f263104403db2090bda2a1a2f354d40bf167e0ce", "product_impact_ids": ["harness-capture", "viewer-surface", "timeline-navigation", "request-context", "context-lifecycle", "protocol-and-raw", "translation-theme"] }, "story": { @@ -378,6 +386,7 @@ "status": "owner-review", "next_gate": "审阅 Codex 与 Claude Code 的差异和发布会式 v0.2 收束页;通过后重导母版,再决定是否进入配音。", "freshness": { + "verified_product_sha": "f263104403db2090bda2a1a2f354d40bf167e0ce", "product_impact_ids": ["harness-capture", "viewer-surface", "timeline-navigation", "request-context", "context-lifecycle", "protocol-and-raw", "translation-theme"] }, "story": { @@ -423,6 +432,7 @@ "status": "owner-review", "next_gate": "审阅七次主线 Request 的叙述节奏,再决定是否进入正式配音与成片。", "freshness": { + "verified_product_sha": "f263104403db2090bda2a1a2f354d40bf167e0ce", "product_impact_ids": ["harness-capture", "viewer-surface", "timeline-navigation", "request-context", "tools", "agent-planning", "protocol-and-raw", "translation-theme"] }, "story": { diff --git a/docs/demo-chapter-production.zh-CN.md b/docs/demo-chapter-production.zh-CN.md index 5769321..ae407fa 100644 --- a/docs/demo-chapter-production.zh-CN.md +++ b/docs/demo-chapter-production.zh-CN.md @@ -123,7 +123,7 @@ node scripts/capture-storyboard-review-frames.mjs claude-planning \ - `subtitles=0` 用于干净画面母版:只隐藏网页字幕层,不改变镜头、标注、点击波纹或转场;带字幕内部预览必须显式选择,不能覆盖干净母版; - 未来可增加局部放大,但默认保持完整三栏,只有证据在全屏尺寸仍无法阅读时才启用。 -统一章节清单保存在 `assets/demo/storyboard/catalog.zh-CN.json`。每项必须声明 `timeline`、`guide`、`guide_section` 与 `review`;`review` 固定包含问题、观众、Source 边界、发布状态、下一道确认门、可执行的 `story` 叙事合同、产品新鲜度边界,以及旁白、字幕、manifest、1920 联系表和 1024 联系表。文档映射只能指向纳入审计的中文公开文档和其中真实存在的标题,审阅资料必须真实存在且可进入 Git。新增、删除或重命名章节时必须同步清单;生产审计要求 catalog 与所有同时具有 manifest 和时间线的可发布章节完全一致。 +统一章节清单保存在 `assets/demo/storyboard/catalog.zh-CN.json`。每项必须声明 `timeline`、`guide`、`guide_section` 与 `review`;`review` 固定包含问题、观众、Source 边界、发布状态、下一道确认门、可执行的 `story` 叙事合同、产品新鲜度边界,以及旁白、字幕、manifest、1920 联系表和 1024 联系表。`review.freshness.verified_product_sha` 指向已经完成真实产品复核的共享 `main` 提交;manifest 仍保留原始采集提交,两者不能互相覆盖。文档映射只能指向纳入审计的中文公开文档和其中真实存在的标题,审阅资料必须真实存在且可进入 Git。新增、删除或重命名章节时必须同步清单;生产审计要求 catalog 与所有同时具有 manifest 和时间线的可发布章节完全一致。 `story` 不是自动评价文案“好不好听”,而是防止已经确认的叙事骨架在后续改版中静默丢失。每章必须声明:30 秒内结束的开场镜头及其旁白 / 字幕关键短语、60 秒内明确 PMA 价值的镜头、至少两个来自真实 Viewer 画面的证据镜头,以及最后一幕的可复述结论。`demo-production-audit.mjs` 会把这些引用与当前时间线逐项核对,同时检查 manifest 仍有观众、问题、结论和不讲范围;人工故事审阅仍是发布门,关键短语门禁不能替代人的理解。 @@ -278,6 +278,6 @@ node scripts/demo-freshness-audit.mjs --target HEAD node scripts/demo-freshness-audit.mjs --target HEAD --chapter quickstart --json ``` -报告分别回答三个问题:manifest 记录的产品证据 SHA 之后,本章关注的运行时代码是否变化;确定性 Source 生成脚本更新后,Source 图片与 manifest 是否重建;共享播放器、时间线或 Source 图片更新后,已提交双尺寸复核帧是否重生成。第一项要求重新检查真实 Harness / Viewer,第二项要求用非敏感场景重建 Source,第三项只要求重渲染;任何一项都不能代替另外两项。完成对应更新和逐帧审视后,再以 `--strict` 验证选中的章节。 +报告分别回答三个问题:catalog 的共享产品复核检查点之后,本章关注的运行时代码是否变化;确定性 Source 生成脚本更新后,Source 图片与 manifest 是否重建;共享播放器、时间线或 Source 图片更新后,已提交双尺寸复核帧是否重生成。原始 manifest 采集 SHA 会同时出现在报告中用于 provenance,但 squash merge 后的新鲜度比较不能依赖只存在于功能分支的提交。第一项要求重新检查真实 Harness / Viewer,第二项要求用非敏感场景重建 Source,第三项只要求重渲染;任何一项都不能代替另外两项。完成对应更新和逐帧审视后,把复核后的共享 `main` SHA 写入 catalog,再以 `--strict` 验证选中的章节。 产品所有者主工作区的 heartbeat 会在 `origin/main` 变化后生成携带精确 SHA、受影响章节、产品证据状态、Source 生成脚本状态和复核帧状态的文档任务;它不能跳过真实页面复核,也不能自动发布未经审视的截图。共享 GitHub 工作流继续只生成只读 Job Summary,不会因克隆仓库而自动创建本地调度。 diff --git a/docs/documentation-maintenance.md b/docs/documentation-maintenance.md index 636fb7e..719e721 100644 --- a/docs/documentation-maintenance.md +++ b/docs/documentation-maintenance.md @@ -20,7 +20,7 @@ - `scripts/demo-production-audit.mjs` 跨章节核对 manifest、旁白、时间线、SRT、Source 图片、双尺寸审阅帧的真实像素、Git 可追踪性、媒体体积预算、章节审阅合同与常见隐私哨兵;catalog 中的叙事合同还要求开场在 30 秒内完成、PMA 价值在 60 秒内讲明、至少两个 Viewer 证据镜头真实存在、结尾能回到可复述结论;带 `review_points` 的章节可用 `--strict` 要求两档帧与稳定时点逐一对应;`smoke:governance` 会调用这项生产审计; - `scripts/documentation-consistency-audit.mjs` 核对中英文 README、快速开始、用户手册首页与十个任务章节的本地链接和章节锚点;同时检查 Node.js 要求、九条核心 CLI 事实、英文首页的支持协议/主 GIF/中文深读入口,以及十个演示章节到真实中文标题和审阅合同的映射;它已经由 `smoke:governance` 调用; - 同一脚本的 `--base` / `--changed-file` 模式会把功能变更映射成受影响文档与演示素材;JSON 同时包含精确目标 SHA、解析后的 base SHA、工作区状态、去重后的必查文档、必查演示和具体章节 id、验证命令与隐私限制,可以直接作为文档 Agent 的任务载荷; -- `scripts/demo-freshness-audit.mjs` 把“演示可能过时”拆成三类可验证结论:manifest 中的精确产品证据 SHA 之后,当前章节关注的运行时代码是否变化;章节的 Source 生成脚本更新后,Source 图片与 manifest 是否重建;以及共享播放器、当前时间线或 Source 图片更新后,已提交双尺寸复核帧是否重生成。默认模式只报告而不阻断;只有完成真实 Viewer 复核并准备交接时才使用 `--strict`; +- `scripts/demo-freshness-audit.mjs` 把“演示可能过时”拆成三类可验证结论:catalog 中已经在共享 `main` 完成产品复核的检查点之后,当前章节关注的运行时代码是否变化;章节的 Source 生成脚本更新后,Source 图片与 manifest 是否重建;以及共享播放器、当前时间线或 Source 图片更新后,已提交双尺寸复核帧是否重生成。报告同时保留 manifest 的原始采集 SHA 作为 provenance,但不要求 squash merge 后的分支提交永久可达。默认模式只报告而不阻断;只有完成真实 Viewer 复核并准备交接时才使用 `--strict`; - `.github/workflows/release-check.yml` 已在每个 PR 增加只读 `Documentation impact` job:它检出精确 head SHA,以 PR base SHA 到 head SHA 的 merge-base 范围生成 JSON,再把受影响文档、演示 Source、验证命令和隐私限制写入 GitHub Job Summary;job 只有 `contents: read`,不会评论 PR、创建 issue、读取 secrets 或发布素材; - `scripts/documentation-impact-summary.mjs` 负责校验 JSON 中的 head/base SHA 并生成防 Markdown 注入的摘要;路径很多时完整变更列表折叠显示,必查文档和演示保持在首屏; - 产品所有者的主文档工作区已启用名为 `peekMyAgent documentation and demo drift monitor` 的 Codex heartbeat:每天本地时间 10:00 轻量轮询一次 `origin/main`,用 Git 忽略的 `tmp/documentation-main-monitor.json` 保存最后扫描 SHA,并复用同一 JSON 影响映射唤醒当前长期文档任务;没有新 SHA 或没有映射边界时不通知、不截图、不运行大检查; @@ -82,7 +82,7 @@ --json ``` - `product_evidence.status: review-required` 表示 manifest 的产品证据之后,章节关注的 CLI、Capture、Viewer、工具、协议等运行时边界发生了变化,需要重新操作真实产品并判断 Source 是否失效;`source_recipe.status: regeneration-required` 表示该章的确定性生成脚本比 Source 图片与 manifest 更新;`tracked_review_frames.status: regeneration-required` 表示网页播放器、该章时间线或 Source 图比已提交复核帧更新,需要重新生成两档复核帧。三者互不替代:只改标注样式可能只要求重渲染,生成脚本变化要求重建 Source,产品功能变化则要求先复核真实产品; + `product_evidence.status: review-required` 表示 catalog 的 `verified_product_sha` 之后,章节关注的 CLI、Capture、Viewer、工具、协议等运行时边界发生了变化,需要重新操作真实产品并判断 Source 是否失效;`capture_sha` 仍指向 manifest 的原始采集证据,不作为 squash 后的可达性前提。`source_recipe.status: regeneration-required` 表示该章的确定性生成脚本比 Source 图片与 manifest 更新;`tracked_review_frames.status: regeneration-required` 表示网页播放器、该章时间线或 Source 图比已提交复核帧更新,需要重新生成两档复核帧。三者互不替代:只改标注样式可能只要求重渲染,生成脚本变化要求重建 Source,产品功能变化则要求先复核真实产品; 4. 运行确定性 `--verify` 和现有文档检查; 5. PR job 把匹配结果写入只读 Job Summary;功能贡献者更新对应文档/manifest,或记录公开行为未变化的具体证据; 6. 主工作区 heartbeat 在发现新 SHA 后生成同结构 JSON,按 `target SHA + impact_ids` 去重;存在映射边界时对 `required_demo_chapters` 运行新鲜度检查并唤醒当前长期文档任务,没有映射时只更新检查点; diff --git a/scripts/demo-freshness-audit.mjs b/scripts/demo-freshness-audit.mjs index 2cc65ca..821372d 100644 --- a/scripts/demo-freshness-audit.mjs +++ b/scripts/demo-freshness-audit.mjs @@ -62,7 +62,8 @@ function auditChapter({ chapter, targetSha }) { const timelinePath = repoPathFromAssetHref(chapter.timeline, `${chapter.id} timeline`); const manifest = JSON.parse(readRepoFile(manifestPath)); const timeline = JSON.parse(readRepoFile(timelinePath)); - const evidence = resolveEvidenceCommit(manifest, chapter.id); + const captureEvidence = resolveEvidenceCommit(manifest, chapter.id); + const evidence = resolveFreshnessCheckpoint(chapter, captureEvidence); resolveRevision(evidence.sha); const watchedImpactIds = chapter.review?.freshness?.product_impact_ids; @@ -118,6 +119,8 @@ function auditChapter({ chapter, targetSha }) { status: productStatus, evidence_sha: evidence.sha, evidence_field: evidence.field, + capture_sha: captureEvidence.sha, + capture_field: captureEvidence.field, watched_impact_ids: watchedImpactIds, matched_impact_ids: matchedImpacts.map((impact) => impact.id), changed_files: productChangedFiles, @@ -139,6 +142,19 @@ function auditChapter({ chapter, targetSha }) { }; } +function resolveFreshnessCheckpoint(chapter, captureEvidence) { + const verifiedProductSha = chapter.review?.freshness?.verified_product_sha; + if (verifiedProductSha === undefined) return captureEvidence; + assert.equal(typeof verifiedProductSha, "string", + `${chapter.id} freshness.verified_product_sha must be a string`); + assert.match(verifiedProductSha, shaPattern, + `${chapter.id} freshness.verified_product_sha must be a full lowercase SHA`); + return { + field: "catalog.review.freshness.verified_product_sha", + sha: verifiedProductSha, + }; +} + function resolveEvidenceCommit(manifest, chapterId) { const candidates = [ ["source.product_baseline_sha", manifest.source?.product_baseline_sha], diff --git a/scripts/documentation-consistency-audit.mjs b/scripts/documentation-consistency-audit.mjs index cdb6cc3..6136ccd 100644 --- a/scripts/documentation-consistency-audit.mjs +++ b/scripts/documentation-consistency-audit.mjs @@ -322,6 +322,9 @@ export function runDocumentationConsistencyAudit({ log = true } = {}) { assert.equal(typeof chapter.review?.next_gate, "string", `storyboard chapter ${chapter.id} needs a next review gate`); const productImpactIds = chapter.review?.freshness?.product_impact_ids; + const verifiedProductSha = chapter.review?.freshness?.verified_product_sha; + assert.match(verifiedProductSha || "", /^[0-9a-f]{40}$/, + `storyboard chapter ${chapter.id} needs freshness.verified_product_sha`); assert(Array.isArray(productImpactIds) && productImpactIds.length > 0, `storyboard chapter ${chapter.id} needs freshness.product_impact_ids`); assert.equal(new Set(productImpactIds).size, productImpactIds.length, diff --git a/scripts/governance-smoke.mjs b/scripts/governance-smoke.mjs index 7896b61..ddb1aec 100644 --- a/scripts/governance-smoke.mjs +++ b/scripts/governance-smoke.mjs @@ -300,11 +300,15 @@ assert.equal( ); assert(freshnessSummary.chapters.every((chapter) => ( /^[0-9a-f]{40}$/.test(chapter.product_evidence.evidence_sha) + && /^[0-9a-f]{40}$/.test(chapter.product_evidence.capture_sha) && /^[0-9a-f]{40}$/.test(chapter.source_recipe.generator_commit) && /^[0-9a-f]{40}$/.test(chapter.source_recipe.source_commit) && /^[0-9a-f]{40}$/.test(chapter.tracked_review_frames.dependency_commit) && /^[0-9a-f]{40}$/.test(chapter.tracked_review_frames.review_commit) )), "every chapter must name exact evidence and render commits"); +assert(freshnessSummary.chapters.every((chapter) => ( + chapter.product_evidence.evidence_field === "catalog.review.freshness.verified_product_sha" +)), "every chapter must use a shared main freshness checkpoint"); const storyboardCatalog = JSON.parse(fs.readFileSync("assets/demo/storyboard/catalog.zh-CN.json", "utf8")); assert.equal(storyboardCatalog.schema_version, 5);