From a4b7c0c5e17e08a8f351a66074889a9887f6f245 Mon Sep 17 00:00:00 2001 From: Renfei Wang Date: Sun, 26 Jul 2026 17:50:15 -0400 Subject: [PATCH] runtime: preserve semantic states and bound repair work --- README.md | 10 +- README.zh-CN.md | 10 +- apps/theme-manager/src-tauri/src/catalog.rs | 50 +++ .../src-tauri/src/skin_runtime.rs | 8 +- apps/theme-manager/src/renderer/app.js | 36 +- apps/theme-manager/src/renderer/index.html | 6 +- docs/CHANGELOG.md | 5 + docs/adapters.md | 32 +- docs/architecture.md | 16 +- docs/desktop-manager.md | 10 +- docs/native-testing.md | 17 +- package.json | 1 + packages/full-skin/runtime.css | 74 ++-- packages/full-skin/runtime.js | 131 ++++-- schemas/registry.schema.json | 51 ++- .../codex-beta-26.715.3651.0/manifest.json | 6 + scripts/benchmark-full-skin-runtime.mjs | 9 + scripts/capture-full-skin-screenshots.mjs | 8 +- scripts/generate-themes.mjs | 20 + scripts/lib/full-skin-runtime-benchmark.mjs | 77 ++++ scripts/prepare-desktop.mjs | 2 +- scripts/smoke-theme-manager.mjs | 2 +- scripts/validate.mjs | 58 ++- site/assets/app.js | 62 ++- site/index.html | 16 +- tests/fixtures/full-skin-semantic-states.html | 16 + tests/full-skin-runtime.test.mjs | 362 +++-------------- tests/lib/full-skin-runtime-fixture.mjs | 378 ++++++++++++++++++ tests/repository.test.mjs | 27 +- tests/source-contract.test.mjs | 69 ++++ 30 files changed, 1113 insertions(+), 456 deletions(-) create mode 100644 scripts/benchmark-full-skin-runtime.mjs create mode 100644 scripts/lib/full-skin-runtime-benchmark.mjs create mode 100644 tests/fixtures/full-skin-semantic-states.html create mode 100644 tests/lib/full-skin-runtime-fixture.mjs diff --git a/README.md b/README.md index 0950a0e..bcd94c1 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@

Awesome Codex Theme

- Full visual skins for Codex Desktop, backed by real app captures and code-free theme packs. + Full visual skins for Codex Desktop, backed by version-bound app-capture evidence and code-free theme packs.

@@ -23,9 +23,9 @@ Code-free packs

-![Saint Tibo theme running in the real ChatGPT Beta test bench](screenshots/codex-beta-26.715.3651.0/tibo-reset-saint-dark.png) +![Saint Tibo theme in the ChatGPT Beta v2 reference capture](screenshots/codex-beta-26.715.3651.0/tibo-reset-saint-dark.png) -The screenshot above comes from ChatGPT Beta `26.715.3651.0` after the skin was applied and read back from the live renderer. It is not a wallpaper pasted into an app mockup. +The screenshot above is a real `act-full-skin-runtime-v2` capture from ChatGPT Beta `26.715.3651.0`, after the skin was applied and read back from the live renderer. It is not a wallpaper pasted into an app mockup. The current v3 runtime is source-tested and marked recapture-required, so this image is reference evidence rather than a claim of current v3 app verification. > Desktop builds are published only on the official [GitHub Releases page](https://github.com/rwang23/awesome-codex-theme/releases). If that page has no asset compatible with your operating system and CPU, there is no supported public installer for that target. @@ -131,7 +131,7 @@ The collection currently includes 68 themes and 136 light/dark modes. The Englis - 2 disclosed, unofficial Codex community tributes; - the broader original and fan-art catalog, available through language-aware discovery in the Gallery. -Every mode has a 1440×810 capture from the pinned Beta test bench. Registry records bind each capture to the exact app version, background hash, runtime hash, byte count, and renderer readback. +Every mode has a 1440×810 v2 reference capture from the pinned Beta test bench. Registry records bind each capture to the exact app version, background hash, runtime hash, byte count, and renderer readback; the current v3 runtime remains recapture-required until that exact test bench is rerun. “Saint Tibo” is an affectionate, unofficial community parody. It is not endorsed by OpenAI or by the person depicted, and no official product artwork is bundled. @@ -189,7 +189,7 @@ schemas/ Theme Pack and Registry schemas themes/catalog.json Hand-maintained theme catalog themes/source-art/ Image jobs, source art, and provenance themes/registry.json Generated public Registry -screenshots/ Version-pinned real app captures +screenshots/ Version-pinned app-capture evidence scripts/ Build, validation, capture, and site tooling site/ Dependency-free Gallery ``` diff --git a/README.zh-CN.md b/README.zh-CN.md index 83957f2..f0fc027 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -1,7 +1,7 @@

Awesome Codex Theme

- 面向 Codex 桌面版的完整视觉皮肤。主题包不带代码,展示图来自真实应用。 + 面向 Codex 桌面版的完整视觉皮肤。主题包不带代码,展示图是绑定版本的应用截图证据。

@@ -23,9 +23,9 @@ 主题包不带代码

-![提博大神主题在真实 ChatGPT Beta 测试台中的效果](screenshots/codex-beta-26.715.3651.0/tibo-reset-immortal-light.png) +![提博大神主题在 ChatGPT Beta v2 参考截图中的效果](screenshots/codex-beta-26.715.3651.0/tibo-reset-immortal-light.png) -上图来自 ChatGPT Beta `26.715.3651.0`:皮肤真正应用到页面并完成运行时读回后才截图,不是把壁纸贴进应用效果图。 +上图是 ChatGPT Beta `26.715.3651.0` 中 `act-full-skin-runtime-v2` 的真实截图:皮肤真正应用到页面并完成运行时读回后才截图,不是把壁纸贴进应用效果图。当前 v3 运行时已完成源码测试但标为需要重新采集,因此该图是参考证据,不是对当前 v3 实机验证的声明。 > 桌面安装包只会发布在官方 [GitHub Releases 页面](https://github.com/rwang23/awesome-codex-theme/releases)。如果页面上没有与你的操作系统和 CPU 匹配的文件,就表示当前没有支持该目标的公开安装包。 @@ -132,7 +132,7 @@ Windows 版“应用并保持完整皮肤”已经通过 ChatGPT Beta `26.715.36 - 5 套面向美国工作场景的原创主题,包括海岸工作室和深夜餐馆; - 1 套 2007 桌面怀旧 Q 版主题,以及 2 套 Codex 社区致意主题。 -每个模式都有固定 Beta 测试台生成的 1440×810 实机截图。Registry 会把截图与准确应用版本、背景哈希、运行时哈希、字节数和页面读回结果绑定。 +每个模式都有固定 Beta 测试台生成的 1440×810 v2 参考截图。Registry 会把截图与准确应用版本、背景哈希、运行时哈希、字节数和页面读回结果绑定;当前 v3 运行时要在该准确测试台重新采集后,才可成为当前实机验证。 “提博大神”是社区表达喜爱的非官方戏仿与致意,不代表 OpenAI 或被描绘者背书,也不包含官方产品素材。 @@ -189,7 +189,7 @@ schemas/ Theme Pack 与 Registry Schema themes/catalog.json 人工维护的主题目录 themes/source-art/ image job、源图与 provenance themes/registry.json 自动生成的公共 Registry -screenshots/ 绑定版本的真实应用截图 +screenshots/ 绑定版本的应用截图证据 scripts/ 构建、校验、截图与站点工具 site/ 无依赖 Gallery ``` diff --git a/apps/theme-manager/src-tauri/src/catalog.rs b/apps/theme-manager/src-tauri/src/catalog.rs index 6beb589..28fc3cf 100644 --- a/apps/theme-manager/src-tauri/src/catalog.rs +++ b/apps/theme-manager/src-tauri/src/catalog.rs @@ -155,6 +155,46 @@ fn valid_sha256(value: &str) -> bool { value.len() == 64 && value.chars().all(|character| character.is_ascii_hexdigit()) } +fn validate_runtime_evidence(value: &Value) -> Result<(), String> { + // Registries produced before evidence state existed remain safe to display as + // reference-only in the renderer. New registries must make that state explicit. + if value.is_null() { + return Ok(()); + } + let evidence = object(value, "runtimeEvidence")?; + let status = evidence["status"].as_str().unwrap_or_default(); + if !matches!(status, "verified" | "reference") { + return fail("Runtime 证据状态无效"); + } + let captured = object(&evidence["capturedRuntime"], "capturedRuntime")?; + let current = object(&evidence["currentRuntime"], "currentRuntime")?; + let captured_version = captured["implementationVersion"] + .as_str() + .unwrap_or_default(); + let captured_sha = captured["sha256"].as_str().unwrap_or_default(); + let current_version = current["implementationVersion"] + .as_str() + .unwrap_or_default(); + let current_sha = current["sha256"].as_str().unwrap_or_default(); + let verification = current["verification"].as_str().unwrap_or_default(); + if captured_version.is_empty() + || current_version.is_empty() + || !valid_sha256(captured_sha) + || !valid_sha256(current_sha) + { + return fail("Runtime 证据记录无效"); + } + if (status == "verified" + && (verification != "verified" + || captured_version != current_version + || captured_sha != current_sha)) + || (status == "reference" && verification != "source-tested-recapture-required") + { + return fail("Runtime 证据状态与版本不一致"); + } + Ok(()) +} + fn valid_hex_color(value: &str) -> bool { value.len() == 7 && value.starts_with('#') @@ -331,6 +371,7 @@ pub fn validate_registry(bytes: &[u8]) -> Result { { return fail("Registry 标准不受支持"); } + validate_runtime_evidence(®istry["runtimeEvidence"])?; let collections = array(®istry["collections"], "collections")?; let themes = array(®istry["themes"], "themes")?; @@ -670,6 +711,15 @@ mod tests { assert_eq!(registry["themes"].as_array().map(Vec::len), Some(68)); } + #[test] + fn registry_rejects_malformed_runtime_evidence() { + let mut registry: Value = + serde_json::from_slice(REGISTRY).expect("registry JSON should parse"); + registry["runtimeEvidence"]["status"] = json!("unknown"); + let encoded = serde_json::to_vec(®istry).expect("registry JSON should encode"); + assert!(validate_registry(&encoded).is_err()); + } + #[test] fn native_lookup_rejects_unknown_ids_and_modes() { let registry = validate_registry(REGISTRY).expect("registry should validate"); diff --git a/apps/theme-manager/src-tauri/src/skin_runtime.rs b/apps/theme-manager/src-tauri/src/skin_runtime.rs index e3c318b..f01cfa9 100644 --- a/apps/theme-manager/src-tauri/src/skin_runtime.rs +++ b/apps/theme-manager/src-tauri/src/skin_runtime.rs @@ -26,7 +26,7 @@ use crate::{ const RUNTIME_CSS: &str = include_str!("../../../../packages/full-skin/runtime.css"); const RUNTIME_JS: &str = include_str!("../../../../packages/full-skin/runtime.js"); const RUNTIME_FORMAT: &str = "act-full-skin-v1"; -const RUNTIME_IMPLEMENTATION: &str = "act-full-skin-runtime-v2"; +const RUNTIME_IMPLEMENTATION: &str = "act-full-skin-runtime-v3"; const MAX_ART_BYTES: usize = 16 * 1024 * 1024; const APPLY_TIMEOUT: Duration = Duration::from_secs(45); const CDP_CALL_TIMEOUT: Duration = Duration::from_secs(20); @@ -49,7 +49,7 @@ const TARGET_RUNTIME_STATE_TEMPLATE: &str = r#"(() => { healthy: Boolean( root.classList.contains("act-full-skin") && state?.version === "act-full-skin-v1" - && state?.implementationVersion === "act-full-skin-runtime-v2" + && state?.implementationVersion === "act-full-skin-runtime-v3" && state?.themeId === expected.themeId && state?.mode === expected.mode && state?.artwork?.loaded === true @@ -1145,7 +1145,7 @@ mod tests { }; let script = runtime_script(&skin, b"\x89PNG\r\n\x1a\n").expect("runtime should build"); assert!(script.contains("act-full-skin-v1")); - assert!(script.contains("act-full-skin-runtime-v2")); + assert!(script.contains("act-full-skin-runtime-v3")); assert!(script.contains("test-theme")); assert!(script.contains("DOMContentLoaded")); assert!(!script.contains("__ACT_THEME_JSON__")); @@ -1189,7 +1189,7 @@ mod tests { assert!(expression.contains("test-theme")); assert!(expression.contains("state?.version === \"act-full-skin-v1\"")); assert!( - expression.contains("state?.implementationVersion === \"act-full-skin-runtime-v2\"") + expression.contains("state?.implementationVersion === \"act-full-skin-runtime-v3\"") ); assert!(expression.contains("state.artUrl.startsWith(\"blob:\")")); assert!(expression.contains("artValue.includes(state.artUrl)")); diff --git a/apps/theme-manager/src/renderer/app.js b/apps/theme-manager/src/renderer/app.js index dfa2f69..a492e3a 100644 --- a/apps/theme-manager/src/renderer/app.js +++ b/apps/theme-manager/src/renderer/app.js @@ -27,7 +27,7 @@ const translations = { archiveEyebrow: "THEME LIBRARY", archiveTitleOne: "Find your next", archiveTitleTwo: "workspace.", - archiveDescription: "Filter by style and collection, then check the real Beta capture before applying.", + archiveDescription: "Filter by style and collection, then check the recorded Beta capture before applying.", searchPlaceholder: "Search themes, cities, or moods", allCollections: "All", rightsFacet: "Source", @@ -46,6 +46,11 @@ const translations = { verifiedCapture: "Real Beta capture", captureAlt: "real Beta capture", captureProof: "REAL APP CAPTURE", + referenceCapture: "Recorded Beta capture", + referenceCaptureAlt: "recorded Beta capture", + referenceCaptureProof: "RECORDED APP CAPTURE · RECAPTURE REQUIRED", + captureVersionVerified: "Beta {version} · current", + captureVersionReference: "Beta {version} · reference", originalRights: "Original · CC0", fanArtRights: "Unofficial fan art · non-commercial", applyStep: "APPLY TO CODEX", @@ -141,7 +146,7 @@ const translations = { archiveEyebrow: "主题浏览", archiveTitleOne: "找到适合你的", archiveTitleTwo: "工作区。", - archiveDescription: "按风格和系列筛选,应用前先看真实 Beta 截图。", + archiveDescription: "按风格和系列筛选,应用前先看已记录的 Beta 截图。", searchPlaceholder: "搜索主题、城市或氛围", allCollections: "全部", rightsFacet: "来源", @@ -160,6 +165,11 @@ const translations = { verifiedCapture: "Beta 实机截图", captureAlt: "Beta 实机截图", captureProof: "真实应用截图", + referenceCapture: "已记录 Beta 截图", + referenceCaptureAlt: "已记录 Beta 截图", + referenceCaptureProof: "已记录应用截图 · 需要重新采集", + captureVersionVerified: "Beta {version} · 当前验证", + captureVersionReference: "Beta {version} · 参考证据", originalRights: "原创 · CC0", fanArtRights: "非官方 Fan Art · 非商业", applyStep: "应用到 Codex", @@ -282,8 +292,10 @@ const elements = { themeList: document.querySelector("#themeList"), emptyState: document.querySelector("#emptyState"), modeSwitch: document.querySelector("#modeSwitch"), + captureBadgeLabel: document.querySelector("#captureBadgeLabel"), captureVersion: document.querySelector("#captureVersion"), themeCapture: document.querySelector("#themeCapture"), + captureProofLabel: document.querySelector("#captureProofLabel"), captureHash: document.querySelector("#captureHash"), collectionName: document.querySelector("#collectionName"), themeName: document.querySelector("#themeName"), @@ -376,6 +388,11 @@ function currentCollection() { return state.catalog?.collections.find((collection) => collection.id === theme?.collection); } +function captureEvidenceIsVerified() { + const evidence = state.catalog?.runtimeEvidence; + return evidence?.status === "verified" && evidence.currentRuntime?.verification === "verified"; +} + function audiencePriority(record) { const audience = record?.audience || "global"; if (audience === state.locale) return 0; @@ -604,6 +621,10 @@ function renderThemeList() { } else { elements.themeCapture.removeAttribute("src"); elements.themeCapture.alt = ""; + elements.captureBadgeLabel.textContent = ""; + elements.captureVersion.textContent = ""; + elements.captureProofLabel.textContent = ""; + elements.captureHash.textContent = ""; elements.collectionName.textContent = ""; elements.themeName.textContent = t("emptyState"); elements.themeTagline.textContent = ""; @@ -619,9 +640,16 @@ function renderSelectedTheme() { if (!theme) return; elements.applySkin.disabled = state.targets.length === 0; const preview = theme.previews[state.mode]; + const captureVerified = captureEvidenceIsVerified(); elements.themeCapture.src = preview.imageUrl; - elements.themeCapture.alt = localized(theme.name) + " · " + t(state.mode) + " · " + t("captureAlt"); - elements.captureVersion.textContent = preview.capture.appVersion; + elements.themeCapture.alt = localized(theme.name) + " · " + t(state.mode) + " · " + + t(captureVerified ? "captureAlt" : "referenceCaptureAlt"); + elements.captureBadgeLabel.textContent = t(captureVerified ? "verifiedCapture" : "referenceCapture"); + elements.captureVersion.textContent = t( + captureVerified ? "captureVersionVerified" : "captureVersionReference", + { version: preview.capture.appVersion }, + ); + elements.captureProofLabel.textContent = t(captureVerified ? "captureProof" : "referenceCaptureProof"); elements.captureHash.textContent = shortHash(preview.capture.sha256); elements.collectionName.textContent = localized(currentCollection()?.name) + " / " + t(theme.variant).toUpperCase(); elements.themeName.textContent = localized(theme.name); diff --git a/apps/theme-manager/src/renderer/index.html b/apps/theme-manager/src/renderer/index.html index 7001fdb..20111c0 100644 --- a/apps/theme-manager/src/renderer/index.html +++ b/apps/theme-manager/src/renderer/index.html @@ -53,7 +53,7 @@

主题浏览

找到适合你的工作区。

-

按风格和系列筛选,应用前先看真实 Beta 截图。

+

按风格和系列筛选,应用前先看已记录的 Beta 截图。

@@ -111,7 +111,7 @@

找到适合你的
- Beta 实机截图 + 已记录 Beta 截图

@@ -120,7 +120,7 @@

找到适合你的
- 真实应用截图 + 已记录应用截图 · 需要重新采集
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 035473d..e7b644c 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -2,6 +2,11 @@ ## Unreleased +- Preserved native Full Skin semantic colors and states, scoped runtime repair to + relevant document mutations, and added a synthetic long-session performance + budget. Existing Beta v2 screenshots are now explicit reference evidence; + the current v3 runtime is source-tested and recapture-required rather than + being presented as current app verification. - Changed the Gallery community section to clearly state that public community submissions are not open and removed its hosted-community link. Added a direct Theme Manager download action to the Gallery hero. diff --git a/docs/adapters.md b/docs/adapters.md index 3d5beb5..7d86180 100644 --- a/docs/adapters.md +++ b/docs/adapters.md @@ -16,7 +16,7 @@ The project does not export Dream Skin, HeiGe Skin Studio, CodeDrobe, or other t - background, surface, text, accent, muted, and border tokens; - the exact Codex version used for verification. -The manager downloads only the declared PNG and checks it against the Registry. The fixed repository-owned runtime assigns that verified image to a manager-created, fixed `` layer behind the app root, then applies the materials and tokens above it. It deliberately does not depend on an `app://` blob URL. +The manager downloads only the declared PNG and checks it against the Registry. The fixed repository-owned runtime decodes that image into a manager-created Blob URL and assigns it to the document-level `body` background, then applies the materials and tokens above it. It does not fetch artwork from the page or change an `app://` URL. Theme packages cannot override the runtime. They contain no CSS, JavaScript, shell commands, plugins, or remote URLs. @@ -35,10 +35,32 @@ ranges and other local software. The manager accepts the listener only when the owning executable path and package full name match the selected target. It connects only to loopback WebSockets whose target URL starts with `app://`. -The runtime is evaluated in the current page and registered through `Page.addScriptToEvaluateOnNewDocument`, so it survives an internal page rebuild. Its small, debounced observer restores the managed artwork layer when the page shell changes. Restore removes the registered script and calls the runtime cleanup function in every recorded target. +The runtime is evaluated in the current page and registered through `Page.addScriptToEvaluateOnNewDocument`, so it survives an internal page rebuild. Its observer watches `document.body` with only `childList` and `subtree`; it filters for route and runtime-marker changes before scheduling a repair. Unrelated output mutations do not query route state or call `ensure()`. A five-second fallback performs a health check, which also reconnects the observer if the body node is replaced. Restore removes the registered script and calls the runtime cleanup function in every recorded target. Restoring the native look does not close the debugging listener. The port closes when the user exits and reopens ChatGPT normally. +### Runtime compatibility and evidence boundary + +The current runtime implementation is `act-full-skin-runtime-v3`. It keeps native +error, warning, success, diff, link, disabled, selection, and status-icon colors +outside the structural surfaces that Full Skin owns. A semantic-state HTML fixture +and source contract prevent broad descendant or class-substring color overrides +from returning. It also supplies an explicit focus ring, reduced-motion behavior, +and a forced-colors fallback. + +`npm run runtime:benchmark` is a synthetic long-session regression check. It +emits high-volume unrelated output mutations and records callback count, repair +calls, callback wall time, CPU time, heap growth, and an event-loop-delay proxy +against an explicit budget. It is not GPU, browser-frame, or physical-device +proof. + +The CSS still uses `background-attachment: fixed`. That setting is intentionally +unchanged until a privacy-safe physical comparison exists on both Windows and +macOS. The committed 136 Beta screenshots were captured with runtime v2 and are +therefore marked as **reference** evidence. Runtime v3 is source-tested and +requires a fresh, exact-package capture before any Gallery, manager, or release +surface may describe it as current app verification. + ### Supported presentation Version 1 covers: @@ -48,7 +70,7 @@ Version 1 covers: - translucent header, sidebar, home cards, and composer materials; - readable native text and controls; - theme title and tagline on the home screen; -- reduced motion. +- reduced motion and forced-colors fallback. Version 1 does not: @@ -78,11 +100,11 @@ Unknown fields are rejected. Native v1 does not accept a background image, so th | Target | Version | Evidence | | --- | --- | --- | -| Full Skin | ChatGPT Beta `26.715.3651.0` | 136 real 1440×810 captures with `5.6 Sol Max`, runtime markers, selector readback, model restoration, cleanup | +| Full Skin | ChatGPT Beta `26.715.3651.0` | 136 real 1440×810 **v2 reference** captures with `5.6 Sol Max`, runtime markers, selector readback, model restoration, and cleanup; current v3 requires recapture | | Native fallback | ChatGPT Stable `26.715.2305.0` | strict payload generation and parser validation | Public compatibility evidence is version-bound. A detected Stable or Beta build may run one user-requested, loopback-only local probe; the manager automatically replays it only after that exact version has passed. A new Codex release must still pass a fresh probe, all repository checks, and the full screenshot run before its version is written into the public Registry. ## English summary -ACT Full Skin v1 applies a verified PNG, materials, colors, and theme copy through a manager-owned loopback CDP runtime. Theme packs remain declarative and code-free. Codex Native v1 is retained as a palette-only fallback. The project does not export third-party skin-engine formats or modify ChatGPT application files and private data. +ACT Full Skin v1 applies a verified PNG, materials, colors, and theme copy through a manager-owned loopback CDP runtime. The current v3 runtime has source and synthetic-performance coverage; its v2 screenshots remain reference evidence until the named Beta capture is rerun. Theme packs remain declarative and code-free. Codex Native v1 is retained as a palette-only fallback. The project does not export third-party skin-engine formats or modify ChatGPT application files and private data. diff --git a/docs/architecture.md b/docs/architecture.md index 746f3c5..a6ddf93 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -32,7 +32,7 @@ generator -> manifest / .act-theme / Registry / Native fallback - one code-free `.act-theme` archive; - `themes/registry.json`. -The Registry is the public index. Every path is repository-relative and paired with SHA-256 and byte count. A mode also contains Full Skin composition data and tokens. +The Registry is the public index. Every path is repository-relative and paired with SHA-256 and byte count. A mode also contains Full Skin composition data and tokens. Root-level `runtimeEvidence` separates the recorded capture runtime from the current source runtime, so UI surfaces can distinguish verified app evidence from a recapture-required reference. Canonical theme archives contain only: @@ -52,11 +52,11 @@ No theme package contains CSS or JavaScript. The runtime: -- creates a fixed, verified in-memory image element behind the Codex app root; +- decodes the verified image into a Blob URL held by the document-level `body` background; - sets mode, focus, safe-area, and token variables; -- styles native Codex surfaces without moving the DOM; +- styles only native structural surfaces without moving the DOM or flattening semantic-state colors; - adds a small home-screen theme caption; -- watches only for home/task context changes; +- observes `document.body` child-list mutations, filters unrelated output, and uses a bounded health check for route or marker repair; - exposes one cleanup function. Theme data is serialized with `serde_json` before it is inserted into the template. There is no executable field in `act-theme-pack-v1`. @@ -90,15 +90,15 @@ The site build copies: - theme packages and manifests; - Full Skin background assets; - Native fallback files; -- real Beta screenshots; +- recorded Beta screenshot evidence; - manager screenshot; - portable Windows Native helper. -Gallery cards use the Registry capture first and fall back to the reviewed preview only when capture evidence is absent. +Gallery cards use the Registry capture first and fall back to the reviewed preview only when capture evidence is absent. Their labels derive from `runtimeEvidence`, so recorded captures cannot be shown as current verification after a runtime change. ### Desktop application -The manager ships a bundled Registry and verified screenshots for browsing. Every catalog carries a monotonically increasing `catalogRevision`; a stale cache or remote descriptor can never replace a newer bundled catalog. The manager also checks the fixed catalog URL, revision, byte count, theme count, and SHA-256 before accepting a remote Registry. Full Skin PNGs are downloaded on demand and retained in the application cache only after hash verification. +The manager ships a bundled Registry and recorded screenshots for browsing. Every catalog carries a monotonically increasing `catalogRevision`; a stale cache or remote descriptor can never replace a newer bundled catalog. The manager also checks the fixed catalog URL, revision, byte count, theme count, and SHA-256 before accepting a remote Registry. Full Skin PNGs are downloaded on demand and retained in the application cache only after hash verification. ### Native fallback @@ -112,7 +112,7 @@ The portable helper and Gallery can copy a strict `codex-theme-v1:` string. Fina 4. Runtime code is manager-owned and versioned. 5. Remote images are accepted only after exact path, size, PNG signature, and SHA-256 checks. 6. CDP accepts only OS-selected loopback ports, exact package ownership, and `app://` targets. -7. Screenshots are evidence only for the named Beta package. +7. Screenshots are evidence only for the named Beta package and the recorded runtime hash; changed runtime source requires recapture. 8. Signing, notarization, and updater keys are release gates, not documentation claims. ## Product boundary diff --git a/docs/desktop-manager.md b/docs/desktop-manager.md index 6ffa491..1a37af6 100644 --- a/docs/desktop-manager.md +++ b/docs/desktop-manager.md @@ -1,6 +1,6 @@ # 桌面主题管理器 -Awesome Codex Theme Manager 是 Windows 与 macOS 共用的 Tauri 2 应用。它浏览 Registry 中的真实截图,并把所选 `ACT Full Skin v1` 应用到准确的 ChatGPT Stable 或 Beta 会话。 +Awesome Codex Theme Manager 是 Windows 与 macOS 共用的 Tauri 2 应用。它浏览 Registry 中带状态的截图证据,并把所选 `ACT Full Skin v1` 应用到准确的 ChatGPT Stable 或 Beta 会话。 ## 技术选择 @@ -30,7 +30,7 @@ Windows 开发不需要 Xcode。macOS 构建由 GitHub Actions 的 macOS runner - 根据系统语言自动选择中文或英文,并允许手动切换和记住选择; - 馆藏、素材来源与授权、视觉表达三类组合筛选; - 双语搜索和主题列表; -- 136 张真实 Beta 明暗截图; +- 136 张已记录的 Beta 明暗截图(当前为 v2 参考证据); - Stable/Beta 目标选择; - 应用、重新应用和恢复; - Windows 与 macOS 共用的“始终应用”开关、当前状态和一键注销; @@ -41,7 +41,7 @@ Windows 开发不需要 Xcode。macOS 构建由 GitHub Actions 的 macOS runner ![Theme Manager Windows 实机截图](assets/theme-manager-windows.png) -截图来自 release EXE 的真实 WebView2 页面。右侧显示 Windows “始终应用”已开启,内嵌预览来自同一 Beta 测试批次。 +截图来自 release EXE 的真实 WebView2 页面。右侧显示 Windows “始终应用”已开启,内嵌预览来自同一 Beta 测试批次;该 v2 批次在当前 v3 重新采集前显示为参考证据。 ## Rust 命令边界 @@ -65,7 +65,7 @@ install_app_update ## Registry 与素材 -管理器包含一个构建时 Registry,以及从 136 张已验证实机截图确定性缩放出的 +管理器包含一个构建时 Registry,以及从 136 张已记录的 v2 实机截图确定性缩放出的 720×405 浏览缩略图。完整 1440×810 证据仍保留在仓库和 Pages;桌面包不重复 携带全部原始 PNG。启动后,管理器会读取 GitHub Pages 的 `downloads/catalog.json`,先核对 Registry URL、单调递增的 @@ -135,7 +135,7 @@ Gatekeeper 和 Full Skin 读回仍需单独验收。 - 主按钮在 Beta 已经运行时完成应用内确认、受控重开和 `active` 回执,读回 2560×1440 Blob 图片,并在 Pull requests 与首页之间保持文档背景; - 关闭后 Theme Manager、Beta 进程、用户启动项和本次动态 CDP 端口全部无残留; -- 68 套主题、136 个模式完成实机截图; +- 68 套主题、136 个模式完成历史 v2 实机截图,当前 v3 仍待重新采集; - Pages 构建包含 136 张 Full Skin 原图。 ## 发布与签名 diff --git a/docs/native-testing.md b/docs/native-testing.md index 41fb5cf..783361a 100644 --- a/docs/native-testing.md +++ b/docs/native-testing.md @@ -18,7 +18,7 @@ 主题包和公开 Registry 继续使用 `act-full-skin-v1`。这个版本号描述声明式主题包的格式,没有发生不兼容变化。 -注入实现使用单独的 `act-full-skin-runtime-v2` 标记。管理器会同时检查两层版本,因此旧运行时不会被误判为健康,新实现也不会破坏现有主题包、Schema 或 Validator。 +注入实现当前使用单独的 `act-full-skin-runtime-v3` 标记。公开 manifest 中的 136 张截图仍记录 `act-full-skin-runtime-v2`,并以 `runtime.evidenceStatus: "reference"` 保存为历史实机参考;`runtime.currentRuntime` 记录 v3 的源码哈希和 `source-tested-recapture-required` 状态。管理器会同时检查主题包格式与运行时证据,因此旧截图不会被误判为当前运行时健康证明,新实现也不会破坏现有主题包、Schema 或 Validator。 ## 测试前提 @@ -58,9 +58,18 @@ screenshots/codex-beta-26.715.3651.0/manifest.json ``` -结果是 136/136 捕获成功,`fixture.project` 为 `none`, -`runtime.implementationVersion` 为 `act-full-skin-runtime-v2`, +历史批次结果是 136/136 捕获成功,`fixture.project` 为 `none`, +记录的 `runtime.implementationVersion` 为 `act-full-skin-runtime-v2`, `runtime.earlyInjection` 为 `true`,`runtime.removedAfterCapture` 为 `true`。 +这些是指定 Beta 包的真实 v2 截图,但不能作为 v3 的当前实机验证;重新采集前,Gallery 与 Theme Manager 会将它们显示为参考证据。 + +## Runtime v3 兼容性与长会话检查 + +v3 的 CSS 只为结构性表面设置材质,不再通过通配后代、`text-*` 类子串或全局 SVG 颜色覆盖原生错误、警告、成功、diff、链接与状态图标。`tests/fixtures/full-skin-semantic-states.html` 覆盖亮/暗模式下的语义标记、禁用、焦点和选中态;源码合同还覆盖 reduced motion 与 forced colors。 + +观察器只监听 `document.body` 的 `childList` 与 `subtree`,并先过滤与主区域、首页和运行时标记无关的节点。无关高输出 mutation 不会读取路由或调用 `ensure()`;标记移除、主区域替换、路由切换和重复导航仍会安排一次修复。`npm run runtime:benchmark` 用 10,000 次合成无关输出测量回调、ensure、CPU、堆增长和事件循环延迟代理,并使用显式预算门槛。这不是浏览器帧率、GPU 或真实设备功耗证明。 + +`background-attachment: fixed` 暂时保持不变。对该设置的任何规范性修改前,必须完成可复核的 Windows 与 macOS 实机对比;当前没有把源码测试或旧截图替代为这项物理证据。 ## Theme Manager 端到端测试 @@ -111,4 +120,4 @@ Validator 仍解析 136 份 `codex-theme-v1:` 字符串,核对字段白名单 ## English summary -The pinned Beta `26.715.3651.0` test bench produced 136 real Full Skin captures. The public package contract remains `act-full-skin-v1`; each capture also records the `act-full-skin-runtime-v2` implementation. The fixture clears the selected project before capture, verifies the Blob-backed document background, and binds every screenshot to the theme asset, runtime hash, exact package, and selector readback. A separate persistent-runtime smoke test opens a built-in route and a real conversation, returns to the home view, and confirms that the image remains visible through transparent content layers. The controller stays enabled after that test. These results apply only to the pinned Beta version and must be rerun after a Codex update. +The pinned Beta `26.715.3651.0` test bench produced 136 real Full Skin captures for `act-full-skin-runtime-v2`. The public package contract remains `act-full-skin-v1`; those captures are retained as v2 reference evidence, while the current `act-full-skin-runtime-v3` is source-tested and explicitly marked recapture-required. The fixture clears the selected project before capture, verifies the Blob-backed document background, and binds every screenshot to the theme asset, runtime hash, exact package, and selector readback. A fresh exact-package capture, plus a physical Windows/macOS fixed-background comparison before changing that behavior, remains required for current release evidence. diff --git a/package.json b/package.json index 9459561..9b30faa 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "art:generate": "node scripts/run-image-jobs.mjs", "generate": "node scripts/generate-themes.mjs", "generate:check": "node scripts/generate-themes.mjs --check", + "runtime:benchmark": "node scripts/benchmark-full-skin-runtime.mjs", "validate": "node scripts/ensure-generated.mjs && node scripts/validate.mjs", "test": "npm run test:source", "test:source": "node --test tests/source-contract.test.mjs tests/full-skin-runtime.test.mjs", diff --git a/packages/full-skin/runtime.css b/packages/full-skin/runtime.css index bd1116a..77dd571 100644 --- a/packages/full-skin/runtime.css +++ b/packages/full-skin/runtime.css @@ -18,6 +18,7 @@ html.act-full-skin body { background-position: var(--act-art-position) !important; background-repeat: no-repeat !important; background-size: cover !important; + /* Keep the existing fixed attachment until it has a physical Windows/macOS comparison. */ background-attachment: fixed !important; color: var(--act-text); } @@ -69,10 +70,10 @@ html.act-full-skin main.main-surface .app-shell-main-content-top-fade { } html.act-full-skin .composer-surface-chrome, -html.act-full-skin .composer-surface-chrome [class*="bg-token-side-bar-background"], -html.act-full-skin .composer-surface-chrome [class*="bg-token-bg-fog"], -html.act-full-skin.act-full-skin-home [role="main"] [class*="bg-token-side-bar-background"], -html.act-full-skin.act-full-skin-home [role="main"] [class*="bg-token-bg-fog"] { +html.act-full-skin .composer-surface-chrome .bg-token-side-bar-background, +html.act-full-skin .composer-surface-chrome .bg-token-bg-fog, +html.act-full-skin.act-full-skin-home [role="main"] .bg-token-side-bar-background, +html.act-full-skin.act-full-skin-home [role="main"] .bg-token-bg-fog { border-color: color-mix(in srgb, var(--act-border) 66%, transparent) !important; background: linear-gradient( @@ -87,7 +88,7 @@ html.act-full-skin.act-full-skin-home [role="main"] [class*="bg-token-bg-fog"] { } html.act-full-skin .group\/home-suggestions button, -html.act-full-skin [class*="home-suggestions"] button { +html.act-full-skin .group\/home-suggestions [role="button"] { border-color: color-mix(in srgb, var(--act-border) 65%, transparent) !important; background: linear-gradient( @@ -102,49 +103,38 @@ html.act-full-skin [class*="home-suggestions"] button { html.act-full-skin aside.app-shell-left-panel button:hover, html.act-full-skin aside.app-shell-left-panel a:hover, html.act-full-skin .group\/home-suggestions button:hover, -html.act-full-skin [class*="home-suggestions"] button:hover { +html.act-full-skin .group\/home-suggestions [role="button"]:hover { border-color: color-mix(in srgb, var(--act-accent) 55%, transparent) !important; background-color: color-mix(in srgb, var(--act-accent) 12%, transparent) !important; } -html.act-full-skin aside.app-shell-left-panel, -html.act-full-skin aside.app-shell-left-panel button, -html.act-full-skin aside.app-shell-left-panel a, -html.act-full-skin .group\/home-suggestions button, -html.act-full-skin [class*="home-suggestions"] button { - color: var(--act-text) !important; -} - -html.act-full-skin [class*="text-token-foreground"], -html.act-full-skin .group\/home-suggestions [class*="text-"], -html.act-full-skin [class*="home-suggestions"] [class*="text-"], -html.act-full-skin .composer-surface-chrome *, -html.act-full-skin .composer-surface-chrome [class*="text-"], -html.act-full-skin.act-full-skin-home [role="main"] [class*="bg-token-side-bar-background"] [class*="text-"], -html.act-full-skin.act-full-skin-home [role="main"] [class*="bg-token-bg-fog"] [class*="text-"] { - color: var(--act-text) !important; -} - -html.act-full-skin [class*="text-token-text-secondary"], -html.act-full-skin [class*="text-token-text-tertiary"], -html.act-full-skin [class*="text-token-foreground-secondary"], -html.act-full-skin [class*="text-token-foreground-tertiary"], +/* + * Semantic-state compatibility: only structural surfaces receive an ACT + * background. Native error, warning, success, diff, link, and status-icon + * colors retain their own foreground declarations instead of being flattened + * by wildcard descendants or class-substring color overrides. + */ html.act-full-skin .composer-surface-chrome textarea::placeholder, html.act-full-skin .composer-surface-chrome input::placeholder, -html.act-full-skin .composer-surface-chrome [data-placeholder]::before, -html.act-full-skin .composer-surface-chrome .is-editor-empty::before, html.act-full-skin [role="main"] textarea::placeholder, html.act-full-skin [role="main"] input::placeholder { color: var(--act-muted) !important; } -html.act-full-skin aside.app-shell-left-panel svg, -html.act-full-skin .group\/home-suggestions svg, -html.act-full-skin [class*="home-suggestions"] svg, -html.act-full-skin .composer-surface-chrome svg, -html.act-full-skin.act-full-skin-home [role="main"] [class*="bg-token-side-bar-background"] svg, -html.act-full-skin.act-full-skin-home [role="main"] [class*="bg-token-bg-fog"] svg { - color: var(--act-accent) !important; +html.act-full-skin :is(button, input, textarea, select):disabled, +html.act-full-skin [aria-disabled="true"] { + cursor: not-allowed; + opacity: 0.58; +} + +html.act-full-skin :focus-visible { + outline: 2px solid color-mix(in srgb, var(--act-accent) 78%, white) !important; + outline-offset: 2px; +} + +html.act-full-skin ::selection { + color: var(--act-accent-contrast); + background: var(--act-accent); } #act-full-skin-caption { @@ -224,3 +214,13 @@ html.act-full-skin[data-act-safe-area="center"] #act-full-skin-caption { transition-duration: 0.001ms !important; } } + +@media (forced-colors: active) { + html.act-full-skin { + forced-color-adjust: auto; + } + + html.act-full-skin :focus-visible { + outline-color: Highlight !important; + } +} diff --git a/packages/full-skin/runtime.js b/packages/full-skin/runtime.js index 853a4c5..64c09b4 100644 --- a/packages/full-skin/runtime.js +++ b/packages/full-skin/runtime.js @@ -3,7 +3,7 @@ const cssText = __ACT_CSS_JSON__; const imageData = __ACT_IMAGE_JSON__; const version = "act-full-skin-v1"; - const implementationVersion = "act-full-skin-runtime-v2"; + const implementationVersion = "act-full-skin-runtime-v3"; const stateKey = "__ACT_FULL_SKIN_STATE__"; const pendingKey = "__ACT_FULL_SKIN_PENDING__"; const installToken = {}; @@ -88,16 +88,27 @@ } let observer; + let observerRoot; let ensureTimer; let fallbackTimer; let retired = false; const metrics = { ensureCalls: 0, mutationCallbacks: 0, - scheduledEnsures: 0, - fallbackEnsures: 0, + relevantMutationCallbacks: 0, + ignoredMutationCallbacks: 0, + scheduledReconciles: 0, + fallbackHealthChecks: 0, + healthChecks: 0, + observerReconnects: 0, + ensureDurationMs: 0, + lastEnsureDurationMs: 0, }; + const now = () => typeof globalThis.performance?.now === "function" + ? globalThis.performance.now() + : Date.now(); + const applyRoot = () => { root.classList.add("act-full-skin"); root.dataset.actTheme = theme.id; @@ -159,27 +170,62 @@ )), ); - const homeMarkerPresent = () => Boolean( - document.querySelector("[data-testid='home-icon']") - || document.querySelector(".group\\/home-suggestions, [class*='home-suggestions']"), + const runtimeMissing = () => { + const style = document.getElementById("act-full-skin-style"); + const caption = document.getElementById("act-full-skin-caption"); + return !root.classList.contains("act-full-skin") + || style?.textContent !== cssText + || caption?.parentElement !== document.body + || !root.style.getPropertyValue("--act-art-image").includes(artUrl); + }; + + const routeState = () => { + const hasMainSurface = Boolean(document.querySelector(mainSelector)); + const home = hasMainSurface && detectHome(); + return { hasMainSurface, home }; + }; + + const routeChanged = ({ hasMainSurface, home }) => ( + root.classList.contains("act-full-skin-home") !== home + || root.classList.contains("act-full-skin-task") !== (hasMainSurface && !home) + ); + + const nodeMatches = (node, selector) => Boolean( + node + && (node.id === "act-full-skin-style" || node.id === "act-full-skin-caption" + || node.tagName === "BODY" + || node.matches?.(selector) + || node.querySelector?.(selector)), ); + const mutationMayAffectRuntime = (records) => records.some((record) => { + if (record.type !== "childList") return false; + const nodes = [...record.addedNodes, ...record.removedNodes]; + return nodes.some((node) => nodeMatches( + node, + "main.main-surface, main, [role='main'], [data-testid='home-icon'], .group\\/home-suggestions, [class*='home-suggestions'], h1, h2, [class*='title'], #act-full-skin-style, #act-full-skin-caption", + )); + }); + const ensure = () => { if (retired || window[stateKey]?.installToken !== installToken || !document.body) return false; + const startedAt = now(); metrics.ensureCalls += 1; applyRoot(); ensureStyle(); const caption = ensureCaption(); - const hasMainSurface = Boolean(document.querySelector(mainSelector)); - const home = hasMainSurface && detectHome(); + const { hasMainSurface, home } = routeState(); root.classList.toggle("act-full-skin-home", home); root.classList.toggle("act-full-skin-task", hasMainSurface && !home); caption.hidden = !home; + metrics.lastEnsureDurationMs = now() - startedAt; + metrics.ensureDurationMs += metrics.lastEnsureDurationMs; return true; }; const stopWatchers = () => { observer?.disconnect(); + observerRoot = undefined; clearTimeout(ensureTimer); clearInterval(fallbackTimer); }; @@ -210,6 +256,46 @@ return true; }; + const reconcile = () => { + if (retired || window[stateKey]?.installToken !== installToken || !document.body) return false; + metrics.healthChecks += 1; + const nextRoute = routeState(); + const needsRepair = runtimeMissing() || routeChanged(nextRoute); + if (needsRepair) ensure(); + if (observerRoot !== document.body) watchDocument(); + return needsRepair; + }; + + const scheduleReconcile = () => { + clearTimeout(ensureTimer); + ensureTimer = setTimeout(() => { + ensureTimer = undefined; + metrics.scheduledReconciles += 1; + reconcile(); + }, 180); + }; + + const onMutations = (records) => { + metrics.mutationCallbacks += 1; + if (!mutationMayAffectRuntime(records)) { + metrics.ignoredMutationCallbacks += 1; + return; + } + metrics.relevantMutationCallbacks += 1; + scheduleReconcile(); + }; + + const watchDocument = () => { + const nextRoot = document.body || root; + if (observer && observerRoot === nextRoot) return; + observer?.disconnect(); + observer = new MutationObserver(onMutations); + observer.observe(nextRoot, { childList: true, subtree: true }); + observerRoot = nextRoot; + metrics.observerReconnects += 1; + state.observer = observer; + }; + const state = { version, implementationVersion, @@ -220,39 +306,18 @@ artwork, metrics, ensure, + reconcile, retire, cleanup, }; window[stateKey] = state; - applyRoot(); - ensureStyle(); - ensureCaption(); previous?.retire?.(); ensure(); - - const scheduleEnsure = () => { - clearTimeout(ensureTimer); - ensureTimer = setTimeout(() => { - ensureTimer = undefined; - metrics.scheduledEnsures += 1; - ensure(); - }, 180); - }; - observer = new MutationObserver(() => { - metrics.mutationCallbacks += 1; - const routeChanged = homeMarkerPresent() !== root.classList.contains("act-full-skin-home"); - const runtimeMissing = !root.classList.contains("act-full-skin") - || !document.getElementById("act-full-skin-style") - || !document.getElementById("act-full-skin-caption") - || !root.style.getPropertyValue("--act-art-image").includes(artUrl); - if (routeChanged || runtimeMissing) scheduleEnsure(); - }); - observer.observe(document.documentElement, { childList: true, subtree: true }); + watchDocument(); fallbackTimer = setInterval(() => { - metrics.fallbackEnsures += 1; - ensure(); + metrics.fallbackHealthChecks += 1; + reconcile(); }, 5000); - state.observer = observer; state.fallbackTimer = fallbackTimer; delete window[pendingKey]; diff --git a/schemas/registry.schema.json b/schemas/registry.schema.json index 5cc7f9a..5c90ee7 100644 --- a/schemas/registry.schema.json +++ b/schemas/registry.schema.json @@ -4,7 +4,7 @@ "title": "Awesome Codex Theme Registry", "type": "object", "additionalProperties": false, - "required": ["schemaVersion", "standard", "generatedBy", "catalogRevision", "collections", "themes"], + "required": ["schemaVersion", "standard", "generatedBy", "catalogRevision", "runtimeEvidence", "collections", "themes"], "properties": { "schemaVersion": { "const": 1 @@ -19,6 +19,22 @@ "type": "integer", "minimum": 1 }, + "runtimeEvidence": { + "type": "object", + "additionalProperties": false, + "required": ["status", "capturedRuntime", "currentRuntime"], + "properties": { + "status": { + "enum": ["verified", "reference"] + }, + "capturedRuntime": { + "$ref": "#/$defs/runtimeEvidenceRecord" + }, + "currentRuntime": { + "$ref": "#/$defs/currentRuntimeEvidenceRecord" + } + } + }, "collections": { "type": "array", "minItems": 1, @@ -159,6 +175,39 @@ } }, "$defs": { + "runtimeEvidenceRecord": { + "type": "object", + "additionalProperties": false, + "required": ["implementationVersion", "sha256"], + "properties": { + "implementationVersion": { + "type": "string", + "pattern": "^act-full-skin-runtime-v[0-9]+$" + }, + "sha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + } + } + }, + "currentRuntimeEvidenceRecord": { + "type": "object", + "additionalProperties": false, + "required": ["implementationVersion", "sha256", "verification"], + "properties": { + "implementationVersion": { + "type": "string", + "pattern": "^act-full-skin-runtime-v[0-9]+$" + }, + "sha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "verification": { + "enum": ["verified", "source-tested-recapture-required"] + } + } + }, "localizedShort": { "type": "object", "additionalProperties": false, diff --git a/screenshots/codex-beta-26.715.3651.0/manifest.json b/screenshots/codex-beta-26.715.3651.0/manifest.json index 04a9be7..330da60 100644 --- a/screenshots/codex-beta-26.715.3651.0/manifest.json +++ b/screenshots/codex-beta-26.715.3651.0/manifest.json @@ -25,6 +25,12 @@ "format": "act-full-skin-v1", "implementationVersion": "act-full-skin-runtime-v2", "sha256": "6b598f36b5b621166e539f4dc23135562fdbb3bc8b92611b0033a41dcfa57079", + "evidenceStatus": "reference", + "currentRuntime": { + "implementationVersion": "act-full-skin-runtime-v3", + "sha256": "7e601b402ce1aae3b1a72e55cfaffc2b23cf03951401100d3e0918138ca2445d", + "verification": "source-tested-recapture-required" + }, "earlyInjection": true, "removedAfterCapture": true }, diff --git a/scripts/benchmark-full-skin-runtime.mjs b/scripts/benchmark-full-skin-runtime.mjs new file mode 100644 index 0000000..59d0268 --- /dev/null +++ b/scripts/benchmark-full-skin-runtime.mjs @@ -0,0 +1,9 @@ +import { + assertRuntimeBudget, + runFullSkinRuntimeBenchmark, +} from "./lib/full-skin-runtime-benchmark.mjs"; + +const iterations = Number(process.env.ACT_RUNTIME_BENCHMARK_ITERATIONS || 10000); +const result = await runFullSkinRuntimeBenchmark({ iterations }); +assertRuntimeBudget(result); +console.log(JSON.stringify(result, null, 2)); diff --git a/scripts/capture-full-skin-screenshots.mjs b/scripts/capture-full-skin-screenshots.mjs index bbb354c..f9f2777 100644 --- a/scripts/capture-full-skin-screenshots.mjs +++ b/scripts/capture-full-skin-screenshots.mjs @@ -17,7 +17,7 @@ const WIDTH = 1440; const HEIGHT = 810; const CAPTURE_VERSION = "26.715.3651.0"; const CAPTURE_MODEL_LABEL = "5.6 Sol Max"; -const RUNTIME_IMPLEMENTATION = "act-full-skin-runtime-v2"; +const RUNTIME_IMPLEMENTATION = "act-full-skin-runtime-v3"; const CAPTURE_ROOT = "screenshots/codex-beta-" + CAPTURE_VERSION; const CAPTURE_DIR = path.join(ROOT, ...CAPTURE_ROOT.split("/")); const MANIFEST_PATH = path.join(CAPTURE_DIR, "manifest.json"); @@ -863,6 +863,12 @@ async function main() { format: "act-full-skin-v1", implementationVersion: RUNTIME_IMPLEMENTATION, sha256: runtimeSha256, + evidenceStatus: "verified", + currentRuntime: { + implementationVersion: RUNTIME_IMPLEMENTATION, + sha256: runtimeSha256, + verification: "verified", + }, earlyInjection: true, removedAfterCapture: runtimeRemoved, }, diff --git a/scripts/generate-themes.mjs b/scripts/generate-themes.mjs index 6e8660f..95fa272 100644 --- a/scripts/generate-themes.mjs +++ b/scripts/generate-themes.mjs @@ -207,6 +207,24 @@ async function loadCaptureContext() { } } +function runtimeEvidenceFor(manifest) { + const capturedRuntime = manifest?.runtime; + if (!capturedRuntime) throw new Error("A capture manifest is required to build Registry runtime evidence"); + const currentRuntime = capturedRuntime.currentRuntime || { + implementationVersion: capturedRuntime.implementationVersion, + sha256: capturedRuntime.sha256, + verification: "verified", + }; + return { + status: capturedRuntime.evidenceStatus || "verified", + capturedRuntime: { + implementationVersion: capturedRuntime.implementationVersion, + sha256: capturedRuntime.sha256, + }, + currentRuntime, + }; +} + export async function buildGeneratedFiles() { const catalog = JSON.parse(await readFile(CATALOG_PATH, "utf8")); const existingRegistry = await readFile(path.join(ROOT, "themes", "registry.json"), "utf8") @@ -216,6 +234,7 @@ export async function buildGeneratedFiles() { (existingRegistry.themes || []).map((theme) => [theme.id, theme]), ); const captureContext = await loadCaptureContext(); + const runtimeEvidence = runtimeEvidenceFor(captureContext.manifest); const files = new Map(); const registryThemes = []; @@ -375,6 +394,7 @@ export async function buildGeneratedFiles() { standard: "act-theme-pack-v1", generatedBy: GENERATOR_ID, catalogRevision: catalog.catalogRevision, + runtimeEvidence, collections: catalog.collections.map((collection) => ({ ...collection, themeCount: catalog.themes.filter((theme) => theme.collection === collection.id).length, diff --git a/scripts/lib/full-skin-runtime-benchmark.mjs b/scripts/lib/full-skin-runtime-benchmark.mjs new file mode 100644 index 0000000..a7075c0 --- /dev/null +++ b/scripts/lib/full-skin-runtime-benchmark.mjs @@ -0,0 +1,77 @@ +import { readFile } from "node:fs/promises"; +import path from "node:path"; +import { performance } from "node:perf_hooks"; +import { fileURLToPath } from "node:url"; + +import { createFixture, executeRuntime } from "../../tests/lib/full-skin-runtime-fixture.mjs"; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", ".."); +const RUNTIME_PATH = path.join(ROOT, "packages", "full-skin", "runtime.js"); + +export const DEFAULT_RUNTIME_BUDGET = Object.freeze({ + maxEnsureCalls: 0, + maxEnsureDurationMs: 50, + maxCallbackWallMs: 2000, + maxCpuMs: 1500, + maxHeapGrowthBytes: 32 * 1024 * 1024, + maxEventLoopDelayMs: 100, +}); + +function wait(milliseconds) { + return new Promise((resolve) => setTimeout(resolve, milliseconds)); +} + +export async function runFullSkinRuntimeBenchmark({ iterations = 10000 } = {}) { + if (!Number.isSafeInteger(iterations) || iterations < 1 || iterations > 100000) { + throw new Error("Benchmark iterations must be an integer from 1 through 100000"); + } + + const runtime = await readFile(RUNTIME_PATH, "utf8"); + const fixture = createFixture(); + await executeRuntime(runtime, fixture, "long-session"); + const state = fixture.context.__ACT_FULL_SKIN_STATE__; + const baseline = { ...state.metrics }; + const cpuBefore = process.cpuUsage(); + const heapBefore = process.memoryUsage().heapUsed; + const callbackStartedAt = performance.now(); + + for (let index = 0; index < iterations; index += 1) fixture.emitOutputMutation(); + + const callbackWallMs = performance.now() - callbackStartedAt; + const cpu = process.cpuUsage(cpuBefore); + const responsivenessStartedAt = performance.now(); + await new Promise((resolve) => setImmediate(resolve)); + const eventLoopDelayMs = performance.now() - responsivenessStartedAt; + await wait(240); + + return { + fixture: "full-skin-long-session-v1", + iterations, + observerCallbacks: state.metrics.mutationCallbacks - baseline.mutationCallbacks, + ignoredMutationCallbacks: state.metrics.ignoredMutationCallbacks - baseline.ignoredMutationCallbacks, + relevantMutationCallbacks: state.metrics.relevantMutationCallbacks - baseline.relevantMutationCallbacks, + ensureCalls: state.metrics.ensureCalls - baseline.ensureCalls, + ensureDurationMs: state.metrics.ensureDurationMs - baseline.ensureDurationMs, + callbackWallMs, + cpuMs: (cpu.user + cpu.system) / 1000, + heapGrowthBytes: process.memoryUsage().heapUsed - heapBefore, + eventLoopDelayMs, + }; +} + +export function assertRuntimeBudget(result, budget = DEFAULT_RUNTIME_BUDGET) { + const violations = []; + if (result.ensureCalls > budget.maxEnsureCalls) violations.push(`ensureCalls ${result.ensureCalls}`); + if (result.ensureDurationMs > budget.maxEnsureDurationMs) violations.push(`ensureDurationMs ${result.ensureDurationMs}`); + if (result.callbackWallMs > budget.maxCallbackWallMs) violations.push(`callbackWallMs ${result.callbackWallMs}`); + if (result.cpuMs > budget.maxCpuMs) violations.push(`cpuMs ${result.cpuMs}`); + if (result.heapGrowthBytes > budget.maxHeapGrowthBytes) violations.push(`heapGrowthBytes ${result.heapGrowthBytes}`); + if (result.eventLoopDelayMs > budget.maxEventLoopDelayMs) violations.push(`eventLoopDelayMs ${result.eventLoopDelayMs}`); + if (result.observerCallbacks !== result.iterations) { + violations.push(`observerCallbacks ${result.observerCallbacks}`); + } + if (result.ignoredMutationCallbacks !== result.iterations || result.relevantMutationCallbacks !== 0) { + violations.push("unrelated output must not trigger runtime reconciliation"); + } + if (violations.length) throw new Error(`Full Skin runtime budget exceeded: ${violations.join(", ")}`); +} diff --git a/scripts/prepare-desktop.mjs b/scripts/prepare-desktop.mjs index 970a802..6062c9e 100644 --- a/scripts/prepare-desktop.mjs +++ b/scripts/prepare-desktop.mjs @@ -77,7 +77,7 @@ async function main() { const relative = theme.previews?.[mode]?.capture?.path; if (typeof relative !== "string" || !/^screenshots\/codex-beta-[A-Za-z0-9.-]+\/[a-z0-9-]+-(?:light|dark)\.png$/.test(relative)) { - throw new Error(theme.id + " " + mode + " has an unsafe desktop capture path"); + throw new Error(theme.id + " " + mode + " has an unsafe desktop capture evidence path"); } const source = path.join(ROOT, ...relative.split("/")); const destination = path.join(DESKTOP_CATALOG, ...relative.split("/")); diff --git a/scripts/smoke-theme-manager.mjs b/scripts/smoke-theme-manager.mjs index 371f2af..11ad18b 100644 --- a/scripts/smoke-theme-manager.mjs +++ b/scripts/smoke-theme-manager.mjs @@ -336,7 +336,7 @@ function expectedFullSkinStateExpression() { && bodyBackground.includes(runtime.artUrl) }; return state.version === "act-full-skin-v1" - && state.implementationVersion === "act-full-skin-runtime-v2" + && state.implementationVersion === "act-full-skin-runtime-v3" && state.themeId === ${JSON.stringify(THEME_ID)} && state.mode === ${JSON.stringify(MODE)} && state.root diff --git a/scripts/validate.mjs b/scripts/validate.mjs index d641655..f3fe18e 100644 --- a/scripts/validate.mjs +++ b/scripts/validate.mjs @@ -36,6 +36,10 @@ function sha256(buffer) { return createHash("sha256").update(buffer).digest("hex"); } +function runtimeImplementationVersionFor(runtimeJs) { + return /const implementationVersion = "([^"]+)";/u.exec(runtimeJs)?.[1] || ""; +} + function renderFingerprint(theme, mode, sourceProvenance, width, height) { return sha256(Buffer.from(JSON.stringify({ renderer: SOURCE_ART_RENDERER_ID, @@ -192,6 +196,22 @@ export async function validateRepository() { readFile(path.join(ROOT, "packages", "full-skin", "runtime.js"), "utf8"), ]); const runtimeSha256 = sha256(Buffer.from(runtimeCss + "\n" + runtimeJs, "utf8")); + const runtimeImplementationVersion = runtimeImplementationVersionFor(runtimeJs); + const capturedRuntime = captureManifest.runtime || {}; + const evidenceStatus = capturedRuntime.evidenceStatus || "verified"; + const currentRuntime = capturedRuntime.currentRuntime || { + implementationVersion: capturedRuntime.implementationVersion, + sha256: capturedRuntime.sha256, + verification: "verified", + }; + const expectedRuntimeEvidence = { + status: evidenceStatus, + capturedRuntime: { + implementationVersion: capturedRuntime.implementationVersion, + sha256: capturedRuntime.sha256, + }, + currentRuntime, + }; check(themeSchema.$schema?.includes("2020-12"), "Theme schema is not JSON Schema 2020-12", errors); check(registrySchema.$schema?.includes("2020-12"), "Registry schema is not JSON Schema 2020-12", errors); @@ -209,9 +229,37 @@ export async function validateRepository() { /^\d{8}$/u.test(catalogRevisionDate) && /^\d{8}$/u.test(captureDate) && catalogRevisionDate >= captureDate, - "Catalog revision predates the latest verified screenshot capture", + "Catalog revision predates the latest recorded screenshot capture", + errors, + ); + check(capturedRuntime.format === FULL_SKIN_FORMAT, "Capture runtime format mismatch", errors); + check(typeof capturedRuntime.implementationVersion === "string" && capturedRuntime.implementationVersion.length > 0, "Capture runtime implementation version is missing", errors); + check(SHA256.test(capturedRuntime.sha256 || ""), "Capture runtime hash is invalid", errors); + check(["verified", "reference"].includes(evidenceStatus), "Capture runtime evidence status is invalid", errors); + check(runtimeImplementationVersion.length > 0, "Current Full Skin runtime implementation version is missing", errors); + check(currentRuntime.implementationVersion === runtimeImplementationVersion, "Current runtime implementation version does not match source", errors); + check(currentRuntime.sha256 === runtimeSha256, "Current runtime hash does not match source", errors); + check( + currentRuntime.verification === (evidenceStatus === "verified" ? "verified" : "source-tested-recapture-required"), + "Current runtime verification state does not match capture evidence status", + errors, + ); + if (evidenceStatus === "verified") { + check(capturedRuntime.implementationVersion === runtimeImplementationVersion, "Verified capture implementation version does not match source", errors); + check(capturedRuntime.sha256 === runtimeSha256, "Verified capture runtime hash does not match source", errors); + } else { + check(capturedRuntime.sha256 !== runtimeSha256, "Reference capture runtime hash must differ from current source", errors); + } + check( + JSON.stringify(registry.runtimeEvidence) === JSON.stringify(expectedRuntimeEvidence), + "Registry runtime evidence does not match the capture manifest and current source", errors, ); + check(Array.isArray(captureManifest.captures), "Capture manifest captures are missing", errors); + for (const capture of captureManifest.captures || []) { + check(capture.runtimeSha256 === capturedRuntime.sha256, "Captured " + capture.themeId + " " + capture.mode + " runtime hash mismatch", errors); + check(capture.implementationVersion === capturedRuntime.implementationVersion, "Captured " + capture.themeId + " " + capture.mode + " runtime implementation version mismatch", errors); + } check(sourceJobs.workflow?.runner === "openai-image-job", "Source-art workflow mismatch", errors); check(sourceJobs.workflow?.size === SOURCE_WIDTH + "x" + SOURCE_HEIGHT, "Source-art dimensions mismatch", errors); check(Array.isArray(sourceJobs.jobs), "Source-art jobs are missing", errors); @@ -552,7 +600,7 @@ export async function validateRepository() { ); if (!duplicateOwner) nativeThemeOwners.set(nativeThemeSha256, theme.id + " " + mode); const capture = modeRecord.capture; - check(Boolean(capture), theme.id + " " + mode + " is missing a real Codex Beta capture", errors); + check(Boolean(capture), theme.id + " " + mode + " is missing a recorded Codex Beta capture", errors); if (capture) { captureCount += 1; check(isSafeRelativePath(capture.path), theme.id + " " + mode + " capture path is unsafe", errors); @@ -568,7 +616,7 @@ export async function validateRepository() { check(capture.bytes === captureBytes.length, theme.id + " " + mode + " capture byte count mismatch", errors); check(capture.sha256 === sha256(captureBytes), theme.id + " " + mode + " capture hash mismatch", errors); check(capture.assetSha256 === modeRecord.fullSkin.sha256, theme.id + " " + mode + " capture asset hash mismatch", errors); - check(capture.runtimeSha256 === runtimeSha256, theme.id + " " + mode + " capture runtime hash mismatch", errors); + check(capture.runtimeSha256 === capturedRuntime.sha256, theme.id + " " + mode + " capture runtime hash mismatch", errors); check(capture.markerVersion === FULL_SKIN_FORMAT, theme.id + " " + mode + " capture marker mismatch", errors); check(capture.selectors?.main === true, theme.id + " " + mode + " capture main selector was not verified", errors); check(capture.appVersion === CODEX_BETA_CAPTURE_VERSION, theme.id + " " + mode + " capture app version mismatch", errors); @@ -580,7 +628,7 @@ export async function validateRepository() { } } - check(captureCount === registry.themes.length * 2, "Registry must expose one real capture for every theme mode", errors); + check(captureCount === registry.themes.length * 2, "Registry must expose one recorded capture for every theme mode", errors); check(fullSkinCount === registry.themes.length * 2, "Registry must expose one full-skin record for every theme mode", errors); if (errors.length) { throw new Error("Validation failed:\n- " + errors.join("\n- ")); @@ -602,7 +650,7 @@ async function main() { "Validated " + result.sources + " source images, " + result.themes + " themes, " + result.modes + " modes, " + result.packages + " code-free packages, " + result.fullSkinExports + " full-skin records, " + result.nativeExports + " Codex Native fallbacks, and " - + result.captures + " real Beta captures.", + + result.captures + " recorded Beta captures.", ); } diff --git a/site/assets/app.js b/site/assets/app.js index 6507870..7d0e829 100644 --- a/site/assets/app.js +++ b/site/assets/app.js @@ -21,17 +21,17 @@ const translations = { statContrast: "contrast gate", managerEyebrow: "Windows + macOS desktop manager", managerTitle: "Find a theme that feels like your workspace.", - managerIntro: "Browse verified themes, compare light and dark modes, then apply the one you like to the desktop app you selected.", + managerIntro: "Browse themes, compare light and dark modes, then apply the one you like to the desktop app you selected.", managerFeatureOneTitle: "See the result before you choose", - managerFeatureOneBody: "Every mode includes a real app capture, so you can compare the look before applying it.", + managerFeatureOneBody: "Every mode includes a recorded app capture, so you can compare the look before applying it.", managerFeatureTwoTitle: "Apply a complete visual skin", - managerFeatureTwoBody: "Theme Manager applies the verified background, materials, colors, and theme copy together.", + managerFeatureTwoBody: "Theme Manager applies the background, materials, colors, and theme copy together.", managerFeatureThreeTitle: "Restore the original look anytime", managerFeatureThreeBody: "Choose Restore Native whenever you want to return to the app's original interface.", managerReleases: "Read the Theme Manager guide", legacyWindowsHelper: "Portable Windows helper", managerSigningNote: "Choose the build that matches your system and CPU. This beta may still show an operating-system security prompt.", - managerScreenshotCaption: "Theme Manager on Windows · real app theme capture inside", + managerScreenshotCaption: "Theme Manager on Windows · recorded app theme capture inside", collectionEyebrow: "Theme library", collectionTitle: "Original worlds, city light, and scenes fans remember.", collectionIntro: "The library now includes {original} original themes and {fanArt} clearly disclosed unofficial fan-art tributes. Every source image is generated through an image job and reviewed by hand.", @@ -58,10 +58,10 @@ const translations = { stepVerifyTitle: "Verify", stepVerifyBody: "SHA-256, file size, dimensions, package allowlist, rights fields, and WCAG contrast.", stepExportTitle: "Export", - stepExportBody: "Generate Full Skin records, real-app evidence, and a Native palette fallback.", + stepExportBody: "Generate Full Skin records, app-capture evidence, and a Native palette fallback.", compatibilityEyebrow: "ACT Full Skin + Native", compatibilityTitle: "Full Skin is the main delivery; Native is the lightweight fallback.", - compatibilityIntro: "The Theme Manager loads verified backgrounds and materials through one fixed, reviewable runtime without modifying app files or private data. Codex Native v1 remains available for palette import, but it cannot install a background.", + compatibilityIntro: "The Theme Manager loads reviewed backgrounds and materials through one fixed, reviewable runtime without modifying app files or private data. Codex Native v1 remains available for palette import, but it cannot install a background.", tableTarget: "Target", tableCoverage: "Coverage", tableDelivery: "Delivery", @@ -106,8 +106,12 @@ const translations = { fanArtNotice: "Unofficial AI fan art. No official stills, logos, or promotional assets were used. No license or endorsement from the underlying rights holders is claimed; personal, non-commercial use only.", coverArt: "GALLERY COVER", coverBadge: "COVER", - realCapture: "REAL BETA CAPTURE", - captureBadge: "CAPTURE", + realCapture: "VERIFIED BETA CAPTURE", + referenceCapture: "RECORDED BETA CAPTURE", + captureBadge: "VERIFIED", + referenceBadge: "REFERENCE", + verifiedVisualKind: "BETA CAPTURE", + referenceVisualKind: "CAPTURE REFERENCE", cinematic: "Cinematic", chibi: "Chibi", cityscape: "City study", @@ -117,7 +121,8 @@ const translations = { copied: "Codex Native theme copied.", copyFailed: "Copy failed. Select the theme string manually.", loadFailed: "The registry could not be loaded. Run the build and serve this directory over HTTP.", - capabilityNative: "ACT Full Skin v1 is verified with ChatGPT Beta 26.715.3651.0 and the available 5.6 Sol Max setting across all 106 theme modes. It applies the background, materials, colors, and theme copy; Native v1 remains a palette-only fallback.", + capabilityNative: "ACT Full Skin v1 has current app-capture evidence for the named ChatGPT Beta test bench. It applies the background, materials, colors, and theme copy; Native v1 remains a palette-only fallback.", + capabilityRuntimePending: "These Beta screenshots are recorded v2 reference evidence. The current Full Skin runtime is source-tested and must be recaptured on the named Beta test bench before it can be presented as current app verification; Native v1 remains a palette-only fallback.", resultCount: "themes shown", allCollections: "All collections", allCollectionsCaption: "Original, city, and unofficial fan-art themes", @@ -146,17 +151,17 @@ const translations = { statContrast: "对比度门槛", managerEyebrow: "Windows + macOS 桌面管理器", managerTitle: "选一套喜欢的主题,让工作区更像你自己。", - managerIntro: "浏览已验证的主题,比较明亮和暗色模式,然后应用到你选定的桌面应用。", + managerIntro: "浏览主题,比较明亮和暗色模式,然后应用到你选定的桌面应用。", managerFeatureOneTitle: "先看效果,再做选择", - managerFeatureOneBody: "每种模式都有真实应用截图,先看清楚再应用。", + managerFeatureOneBody: "每种模式都有已记录的应用截图,先看清楚再应用。", managerFeatureTwoTitle: "一键应用完整皮肤", - managerFeatureTwoBody: "Theme Manager 会把已验证的背景、材质、配色和主题文字一起应用。", + managerFeatureTwoBody: "Theme Manager 会把背景、材质、配色和主题文字一起应用。", managerFeatureThreeTitle: "随时恢复原样", managerFeatureThreeBody: "不喜欢时选择“恢复原生”,即可回到应用原本的界面。", managerReleases: "查看 Theme Manager 使用说明", legacyWindowsHelper: "Windows 便携助手", managerSigningNote: "请选择与你的系统和 CPU 匹配的版本。当前 Beta 仍可能显示系统安全提示。", - managerScreenshotCaption: "Windows 上的 Theme Manager · 内嵌真实主题截图", + managerScreenshotCaption: "Windows 上的 Theme Manager · 内嵌已记录主题截图", collectionEyebrow: "主题馆藏", collectionTitle: "原创世界、城市灯火,也有一眼认出的名场面。", collectionIntro: "馆藏现有 {original} 套原创主题,以及 {fanArt} 套明确标注的非官方 Fan Art。全部源图都通过 image job 生成并经过人工审查。", @@ -183,10 +188,10 @@ const translations = { stepVerifyTitle: "验证", stepVerifyBody: "自动检查 SHA-256、体积、尺寸、文件白名单、版权字段与 WCAG 对比度。", stepExportTitle: "导出", - stepExportBody: "生成 Full Skin 记录、实机证据和 Native 配色回退。", + stepExportBody: "生成 Full Skin 记录、应用截图证据和 Native 配色回退。", compatibilityEyebrow: "ACT Full Skin + Native", compatibilityTitle: "完整皮肤是主交付,原生配色是轻量回退。", - compatibilityIntro: "Theme Manager 用固定、可审查的运行时加载已校验背景与材质,不修改应用文件或私有数据。Codex Native v1 仍可单独导入配色,但它不能安装背景。", + compatibilityIntro: "Theme Manager 用固定、可审查的运行时加载经审查的背景与材质,不修改应用文件或私有数据。Codex Native v1 仍可单独导入配色,但它不能安装背景。", tableTarget: "目标", tableCoverage: "覆盖程度", tableDelivery: "交付方式", @@ -231,8 +236,12 @@ const translations = { fanArtNotice: "这是非官方 AI Fan Art,未使用官方剧照、Logo 或宣传素材,也不声称获得权利方授权或背书,仅限个人非商业使用。", coverArt: "馆藏封面", coverBadge: "封面", - realCapture: "Beta 实机截图", - captureBadge: "实机", + realCapture: "已验证 Beta 实机截图", + referenceCapture: "已记录 Beta 截图", + captureBadge: "已验证", + referenceBadge: "参考", + verifiedVisualKind: "BETA 截图", + referenceVisualKind: "截图参考", cinematic: "原画感", chibi: "Q 版", cityscape: "城市图景", @@ -242,7 +251,8 @@ const translations = { copied: "Codex Native 主题已复制。", copyFailed: "复制失败,请手动选择主题字符串。", loadFailed: "无法读取 Registry。请先构建项目,并通过 HTTP 服务访问。", - capabilityNative: "ACT Full Skin v1 已使用 ChatGPT Beta 26.715.3651.0 与当前测试台实际提供的 5.6 Sol Max 完成全部 106 个模式的实机验证,可应用背景、材质、配色和主题文字;Native v1 只保留为配色回退。", + capabilityNative: "ACT Full Skin v1 已具备指定 ChatGPT Beta 测试台的当前实机截图证据,可应用背景、材质、配色和主题文字;Native v1 只保留为配色回退。", + capabilityRuntimePending: "这里的 Beta 截图是 v2 的已记录参考证据。当前 Full Skin 运行时已完成源码测试,必须在指定 Beta 测试台重新采集后,才可称为当前实机验证;Native v1 仍只保留为配色回退。", resultCount: "套主题", allCollections: "全部系列", allCollectionsCaption: "原创、城市与非官方 Fan Art 主题", @@ -318,6 +328,11 @@ function t(key) { return translations[state.locale][key] || translations.en[key] || key; } +function captureEvidenceIsVerified() { + const evidence = state.registry?.runtimeEvidence; + return evidence?.status === "verified" && evidence.currentRuntime?.verification === "verified"; +} + function renderCollectionIntro() { if (!state.registry || !elements.collectionIntro) return; const original = state.registry.themes.filter(function (theme) { @@ -378,10 +393,11 @@ function reorderGalleryForLocale() { function visualFor(modeRecord) { if (modeRecord.capture) { + const verified = captureEvidenceIsVerified(); return { path: modeRecord.capture.path, - label: t("realCapture"), - badge: t("captureBadge") + " · BETA " + modeRecord.capture.appVersion + label: t(verified ? "realCapture" : "referenceCapture"), + badge: t(verified ? "captureBadge" : "referenceBadge") + " · BETA " + modeRecord.capture.appVersion }; } return { @@ -489,7 +505,9 @@ function updateHero(theme = state.heroTheme) { elements.heroPreview.alt = localized(theme.name) + " · " + visual.label + " · " + t(state.heroMode); elements.heroThemeName.textContent = localized(theme.name); elements.heroThemeMeta.textContent = visual.label + " · " + localized(collectionFor(theme)?.name) + " · " + t(state.heroMode); - elements.heroVisualKind.textContent = theme.previews[state.heroMode].capture ? "BETA CAPTURE" : "COVER ART"; + elements.heroVisualKind.textContent = theme.previews[state.heroMode].capture + ? t(captureEvidenceIsVerified() ? "verifiedVisualKind" : "referenceVisualKind") + : "COVER ART"; setActiveButton(elements.heroMode, "data-mode", state.heroMode); } @@ -694,7 +712,7 @@ function updateDialog() { elements.dialogPreview.src = assetUrl(visual.path); elements.dialogPreview.alt = localized(theme.name) + " · " + visual.label + " · " + t(state.dialogMode); elements.dialogVariant.textContent = visual.label + " · " + localized(collectionFor(theme)?.name) + " · " + t(theme.variant); - elements.capabilityNote.textContent = t("capabilityNative"); + elements.capabilityNote.textContent = t(captureEvidenceIsVerified() ? "capabilityNative" : "capabilityRuntimePending"); elements.dialogRights.hidden = theme.rightsProfile !== "fan-art"; elements.dialogRights.textContent = theme.rightsProfile === "fan-art" ? localized(theme.fanArt?.work) + " · " + t("fanArtNotice") diff --git a/site/index.html b/site/index.html index 9098926..b2d8bcb 100644 --- a/site/index.html +++ b/site/index.html @@ -3,7 +3,7 @@ - + Awesome Codex Theme | Codex 主题档案馆 @@ -104,11 +104,11 @@

Saint Tibo - Real Beta capture · Community Tributes · Dark + Recorded Beta capture · Community Tributes · Dark - + @@ -140,16 +140,16 @@

Windows + macOS 桌面管理器

选一套喜欢的主题,让工作区更像你自己。

- 浏览已验证的主题,比较明亮和暗色模式,然后应用到你选定的桌面应用。 + 浏览主题,比较明亮和暗色模式,然后应用到你选定的桌面应用。