opencode-skills-collection 4.0.35 → 4.0.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/bundled-skills/.antigravity-install-manifest.json +13 -1
  2. package/bundled-skills/agent-evaluation-reporting/SKILL.md +136 -0
  3. package/bundled-skills/agent-harness-fault-injection/SKILL.md +250 -0
  4. package/bundled-skills/anti-ui-slop/SKILL.md +115 -0
  5. package/bundled-skills/audit-agent-run-evidence/SKILL.md +165 -0
  6. package/bundled-skills/boost-asio-pro/SKILL.md +172 -0
  7. package/bundled-skills/boost-asio-pro/references/build.md +88 -0
  8. package/bundled-skills/boost-asio-pro/references/classic-boost.md +33 -0
  9. package/bundled-skills/boost-asio-pro/references/coroutines.md +415 -0
  10. package/bundled-skills/boost-asio-pro/references/pre-cpp20.md +164 -0
  11. package/bundled-skills/boost-asio-pro/references/ssl.md +38 -0
  12. package/bundled-skills/cross-platform-contract-propagation-audit/SKILL.md +159 -0
  13. package/bundled-skills/docs/integrations/jetski-cortex.md +3 -3
  14. package/bundled-skills/docs/integrations/jetski-gemini-loader/README.md +1 -1
  15. package/bundled-skills/docs/maintainers/repo-growth-seo.md +1 -1
  16. package/bundled-skills/docs/maintainers/skills-update-guide.md +1 -1
  17. package/bundled-skills/docs/users/aas-core.md +9 -1
  18. package/bundled-skills/docs/users/bundles.md +1 -1
  19. package/bundled-skills/docs/users/claude-code-skills.md +1 -1
  20. package/bundled-skills/docs/users/gemini-cli-skills.md +1 -1
  21. package/bundled-skills/docs/users/kiro-integration.md +1 -1
  22. package/bundled-skills/docs/users/usage.md +3 -3
  23. package/bundled-skills/docs/users/visual-guide.md +4 -4
  24. package/bundled-skills/dsh-deepread/SKILL.md +200 -0
  25. package/bundled-skills/liuguang-banlan-ui/SKILL.md +135 -0
  26. package/bundled-skills/liuguang-banlan-ui/assets/starter/favicon.svg +4 -0
  27. package/bundled-skills/liuguang-banlan-ui/assets/starter/index.html +32 -0
  28. package/bundled-skills/liuguang-banlan-ui/assets/starter/launcher.css +25 -0
  29. package/bundled-skills/liuguang-banlan-ui/assets/starter/obsidian/index.html +179 -0
  30. package/bundled-skills/liuguang-banlan-ui/assets/starter/obsidian/theme-config.js +105 -0
  31. package/bundled-skills/liuguang-banlan-ui/assets/starter/obsidian/theme.css +107 -0
  32. package/bundled-skills/liuguang-banlan-ui/assets/starter/opal/index.html +179 -0
  33. package/bundled-skills/liuguang-banlan-ui/assets/starter/opal/theme-config.js +105 -0
  34. package/bundled-skills/liuguang-banlan-ui/assets/starter/opal/theme.css +107 -0
  35. package/bundled-skills/liuguang-banlan-ui/assets/starter/shared/base.css +423 -0
  36. package/bundled-skills/liuguang-banlan-ui/assets/starter/shared/spectral-field.js +450 -0
  37. package/bundled-skills/liuguang-banlan-ui/assets/starter/shared/workbench.js +255 -0
  38. package/bundled-skills/liuguang-banlan-ui/references/style-contract.md +53 -0
  39. package/bundled-skills/liuguang-banlan-ui/references/verification.md +70 -0
  40. package/bundled-skills/liuguang-banlan-ui/scripts/measure_preview.py +144 -0
  41. package/bundled-skills/liuguang-banlan-ui/scripts/requirements.txt +2 -0
  42. package/bundled-skills/liuguang-banlan-ui/scripts/scaffold_template.py +43 -0
  43. package/bundled-skills/liuguang-banlan-ui/scripts/validate_manifest.py +173 -0
  44. package/bundled-skills/multi-source-search/SKILL.md +139 -0
  45. package/bundled-skills/multi-source-search/references/report-schema.md +47 -0
  46. package/bundled-skills/multi-source-search/scripts/validate_report.py +221 -0
  47. package/bundled-skills/review-multi-agent-orchestration/SKILL.md +201 -0
  48. package/bundled-skills/ui-slop-score/SKILL.md +80 -0
  49. package/bundled-skills/using-lwc/LICENSE +201 -0
  50. package/bundled-skills/using-lwc/README.md +926 -0
  51. package/bundled-skills/using-lwc/README.zh-CN.md +836 -0
  52. package/bundled-skills/using-lwc/SKILL.md +156 -0
  53. package/bundled-skills/using-lwc/agents/openai.yaml +4 -0
  54. package/bundled-skills/using-lwc/assets/global-purpose.md +17 -0
  55. package/bundled-skills/using-lwc/assets/global-schema.md +28 -0
  56. package/bundled-skills/using-lwc/references/active-memory.md +81 -0
  57. package/bundled-skills/using-lwc/references/agent-onboarding.md +84 -0
  58. package/bundled-skills/using-lwc/references/code-graph.md +60 -0
  59. package/bundled-skills/using-lwc/references/core-memory.md +51 -0
  60. package/bundled-skills/using-lwc/references/document-conversion.md +53 -0
  61. package/bundled-skills/using-lwc/references/document-graph.md +58 -0
  62. package/bundled-skills/using-lwc/references/llm-wiki.md +75 -0
  63. package/bundled-skills/using-lwc/references/memory-policy.md +551 -0
  64. package/bundled-skills/using-lwc/references/operations-manual.md +503 -0
  65. package/bundled-skills/using-lwc/references/recovery-maintenance.md +53 -0
  66. package/bundled-skills/using-lwc/references/strong-context.md +54 -0
  67. package/bundled-skills/using-lwc/references/trigger-playbook.md +53 -0
  68. package/bundled-skills/using-lwc/references/word-graph.md +45 -0
  69. package/bundled-skills/using-lwc/scripts/bootstrap.sh +359 -0
  70. package/bundled-skills/using-lwc/scripts/install-lwc.sh +172 -0
  71. package/bundled-skills/youtube-summarizer/SKILL.md +21 -7
  72. package/bundled-skills/youtube-summarizer/scripts/extract-transcript.py +45 -12
  73. package/package.json +3 -2
  74. package/skills_index.json +268 -0
@@ -0,0 +1,255 @@
1
+ (function () {
2
+ "use strict";
3
+
4
+ const config = window.SPECTRAL_THEME;
5
+ const canvas = document.querySelector("#spectral-field");
6
+ const field = new window.SpectralField(canvas, config);
7
+ const panel = document.querySelector("#parameter-panel");
8
+ const panelToggle = document.querySelector("#parameter-toggle");
9
+ const panelClose = document.querySelector("#parameter-close");
10
+ const controls = document.querySelector("#parameter-controls");
11
+ const exportButton = document.querySelector("#export-parameters");
12
+ const copyButton = document.querySelector("#copy-parameters");
13
+ const motionButton = document.querySelector("#motion-toggle");
14
+ const menuButton = document.querySelector("#menu-toggle");
15
+ const sidebar = document.querySelector("#sidebar");
16
+ const liveStatus = document.querySelector("#live-status");
17
+ const defaultConfig = JSON.parse(JSON.stringify(config));
18
+
19
+ function formatNumber(value, digits = 2) {
20
+ return Number(value).toFixed(digits);
21
+ }
22
+
23
+ function applyThemeMetadata() {
24
+ document.documentElement.dataset.mode = config.mode;
25
+ document.querySelectorAll("[data-theme-name]").forEach((node) => {
26
+ node.textContent = config.label;
27
+ });
28
+ document.querySelector("#preset-value").textContent = config.preset;
29
+ document.querySelector("#seed-value").textContent = String(config.seed);
30
+ document.querySelector("#renderer-value").textContent = field.available ? "WebGL · live" : "CSS · fallback";
31
+ }
32
+
33
+ function renderControls() {
34
+ controls.replaceChildren();
35
+
36
+ const overall = document.createElement("div");
37
+ overall.className = "parameter-block parameter-master";
38
+ overall.innerHTML = `
39
+ <div class="parameter-heading">
40
+ <div>
41
+ <span class="parameter-kicker">全局色度预算</span>
42
+ <strong>总体彩色强度</strong>
43
+ </div>
44
+ <output for="overall-intensity">${formatNumber(config.overallColorIntensity)}</output>
45
+ </div>
46
+ <input id="overall-intensity" type="range" min="0" max="1" step="0.01" value="${config.overallColorIntensity}" aria-label="总体彩色强度">
47
+ <div class="range-labels"><span>中性基材</span><span>校准上限</span></div>
48
+ `;
49
+ controls.append(overall);
50
+
51
+ const overallInput = overall.querySelector("input");
52
+ overallInput.addEventListener("input", () => {
53
+ config.overallColorIntensity = Number(overallInput.value);
54
+ overall.querySelector("output").value = formatNumber(config.overallColorIntensity);
55
+ field.updateConfig(config);
56
+ announce(`总体彩色强度 ${formatNumber(config.overallColorIntensity)}`);
57
+ });
58
+
59
+ config.colors.forEach((color, index) => {
60
+ const row = document.createElement("div");
61
+ row.className = "parameter-block color-parameter";
62
+ row.innerHTML = `
63
+ <div class="parameter-heading">
64
+ <div class="color-name">
65
+ <span class="color-chip" style="--chip:${color.srgbFallback}"></span>
66
+ <div>
67
+ <strong>${color.label}</strong>
68
+ <span class="parameter-kicker">OKLCH ${formatNumber(color.oklch.l)} ${formatNumber(color.oklch.c, 3)} ${formatNumber(color.oklch.h, 0)}</span>
69
+ </div>
70
+ </div>
71
+ <output for="color-${index}">${formatNumber(color.intensity)}</output>
72
+ </div>
73
+ <input id="color-${index}" type="range" min="0" max="1" step="0.01" value="${color.intensity}" aria-label="${color.label}强度">
74
+ <dl class="parameter-facts">
75
+ <div><dt>峰值 α</dt><dd>${formatNumber(color.peakOpacity, 3)}</dd></div>
76
+ <div><dt>空间尺度</dt><dd>${formatNumber(color.fieldScale)}</dd></div>
77
+ <div><dt>相位</dt><dd>${color.phase.map((value) => formatNumber(value)).join(" / ")}</dd></div>
78
+ </dl>
79
+ `;
80
+ controls.append(row);
81
+ const input = row.querySelector("input");
82
+ input.addEventListener("input", () => {
83
+ color.intensity = Number(input.value);
84
+ row.querySelector("output").value = formatNumber(color.intensity);
85
+ field.updateConfig(config);
86
+ announce(`${color.label}强度 ${formatNumber(color.intensity)}`);
87
+ });
88
+ });
89
+ }
90
+
91
+ function serializableConfig() {
92
+ return {
93
+ ...config,
94
+ validationCapabilities: {
95
+ modelVision: "runtime-check-required",
96
+ screenshotCapture: "runtime-check-required",
97
+ deterministicPixelMetrics: "runtime-check-required",
98
+ visualVerificationMode: "unverified"
99
+ }
100
+ };
101
+ }
102
+
103
+ function downloadParameters() {
104
+ const blob = new Blob([JSON.stringify(serializableConfig(), null, 2)], { type: "application/json" });
105
+ const url = URL.createObjectURL(blob);
106
+ const anchor = document.createElement("a");
107
+ anchor.href = url;
108
+ anchor.download = `${config.mode}-parameters.json`;
109
+ document.body.append(anchor);
110
+ anchor.click();
111
+ anchor.remove();
112
+ URL.revokeObjectURL(url);
113
+ announce("参数 JSON 已导出");
114
+ }
115
+
116
+ async function copyParameters() {
117
+ try {
118
+ await navigator.clipboard.writeText(JSON.stringify(serializableConfig(), null, 2));
119
+ announce("参数 JSON 已复制");
120
+ } catch (_error) {
121
+ announce("当前环境无法访问剪贴板,请使用导出 JSON");
122
+ }
123
+ }
124
+
125
+ function announce(message) {
126
+ liveStatus.textContent = message;
127
+ }
128
+
129
+ function setPanel(open) {
130
+ panel.dataset.open = String(open);
131
+ panel.setAttribute("aria-hidden", String(!open));
132
+ panel.toggleAttribute("inert", !open);
133
+ panel.inert = !open;
134
+ panelToggle.setAttribute("aria-expanded", String(open));
135
+ if (open) panelClose.focus();
136
+ }
137
+
138
+ setPanel(panel.dataset.open === "true");
139
+ panelToggle.addEventListener("click", () => setPanel(panel.dataset.open !== "true"));
140
+ panelClose.addEventListener("click", () => {
141
+ setPanel(false);
142
+ panelToggle.focus();
143
+ });
144
+ exportButton.addEventListener("click", downloadParameters);
145
+ copyButton.addEventListener("click", copyParameters);
146
+
147
+ const isMobileViewport = () => window.matchMedia("(max-width: 760px)").matches;
148
+ let sidebarOpen = sidebar.dataset.open === "true";
149
+ function setSidebar(open) {
150
+ sidebarOpen = Boolean(open);
151
+ const mobile = isMobileViewport();
152
+ const visible = !mobile || sidebarOpen;
153
+ sidebar.dataset.open = String(visible);
154
+ sidebar.setAttribute("aria-hidden", String(!visible));
155
+ sidebar.toggleAttribute("inert", !visible);
156
+ sidebar.inert = !visible;
157
+ menuButton.setAttribute("aria-expanded", String(mobile && sidebarOpen));
158
+ menuButton.setAttribute("aria-label", visible ? "关闭导航菜单" : "打开导航菜单");
159
+ }
160
+ setSidebar(sidebarOpen);
161
+ window.addEventListener("resize", () => setSidebar(sidebarOpen), { passive: true });
162
+
163
+ const motionPreference = window.matchMedia("(prefers-reduced-motion: reduce)");
164
+ let motionPausedByUser = false;
165
+ let motionEnabled = false;
166
+ function applyMotion(announceChange = false) {
167
+ motionEnabled = !motionPreference.matches && !motionPausedByUser;
168
+ const enabled = motionEnabled;
169
+ field.setMotion(enabled);
170
+ motionButton.setAttribute("aria-pressed", String(!enabled));
171
+ motionButton.disabled = motionPreference.matches;
172
+ motionButton.querySelector("span").textContent = motionPreference.matches
173
+ ? "系统减少动态"
174
+ : enabled ? "暂停环境" : "恢复环境";
175
+ if (announceChange) {
176
+ announce(motionPreference.matches
177
+ ? "已遵循系统减少动态偏好"
178
+ : enabled ? "环境动画已恢复" : "环境动画已暂停");
179
+ }
180
+ }
181
+ motionButton.addEventListener("click", () => {
182
+ motionPausedByUser = motionEnabled;
183
+ applyMotion(true);
184
+ });
185
+ motionPreference.addEventListener("change", () => applyMotion(true));
186
+ applyMotion();
187
+
188
+ menuButton.addEventListener("click", () => {
189
+ setSidebar(!sidebarOpen);
190
+ });
191
+
192
+ document.querySelectorAll(".nav-item").forEach((button) => {
193
+ button.addEventListener("click", () => {
194
+ document.querySelectorAll(".nav-item").forEach((item) => item.removeAttribute("aria-current"));
195
+ button.setAttribute("aria-current", "page");
196
+ document.querySelector("#page-location").textContent = button.dataset.location;
197
+ if (isMobileViewport()) setSidebar(false);
198
+ });
199
+ });
200
+
201
+ document.querySelectorAll(".record-row").forEach((row) => {
202
+ row.addEventListener("click", () => {
203
+ document.querySelectorAll(".record-row").forEach((item) => item.removeAttribute("aria-selected"));
204
+ row.setAttribute("aria-selected", "true");
205
+ document.querySelector("#record-title").textContent = row.dataset.title;
206
+ document.querySelector("#record-time").textContent = row.dataset.time;
207
+ announce(`已选择 ${row.dataset.title}`);
208
+ });
209
+ });
210
+
211
+ const tabs = Array.from(document.querySelectorAll("[role='tab']"));
212
+ function activateTab(tab, focus = false) {
213
+ tabs.forEach((item) => {
214
+ const selected = item === tab;
215
+ item.setAttribute("aria-selected", String(selected));
216
+ item.tabIndex = selected ? 0 : -1;
217
+ });
218
+ document.querySelector("#detail-copy").textContent = tab.dataset.copy;
219
+ if (focus) tab.focus();
220
+ }
221
+ tabs.forEach((tab, index) => {
222
+ tab.addEventListener("click", () => {
223
+ activateTab(tab);
224
+ });
225
+ tab.addEventListener("keydown", (event) => {
226
+ let targetIndex = null;
227
+ if (event.key === "ArrowRight") targetIndex = (index + 1) % tabs.length;
228
+ if (event.key === "ArrowLeft") targetIndex = (index - 1 + tabs.length) % tabs.length;
229
+ if (event.key === "Home") targetIndex = 0;
230
+ if (event.key === "End") targetIndex = tabs.length - 1;
231
+ if (targetIndex === null) return;
232
+ event.preventDefault();
233
+ activateTab(tabs[targetIndex], true);
234
+ });
235
+ });
236
+ const selectedTab = tabs.find((tab) => tab.getAttribute("aria-selected") === "true") || tabs[0];
237
+ if (selectedTab) activateTab(selectedTab);
238
+
239
+ document.querySelector("#reset-parameters").addEventListener("click", () => {
240
+ Object.assign(config, JSON.parse(JSON.stringify(defaultConfig)));
241
+ renderControls();
242
+ field.updateConfig(config);
243
+ announce("参数已恢复为模板预设");
244
+ });
245
+
246
+ document.addEventListener("keydown", (event) => {
247
+ if (event.key === "Escape" && panel.dataset.open === "true") {
248
+ setPanel(false);
249
+ panelToggle.focus();
250
+ }
251
+ });
252
+
253
+ applyThemeMetadata();
254
+ renderControls();
255
+ })();
@@ -0,0 +1,53 @@
1
+ # 风格契约
2
+
3
+ ## 共通原则
4
+
5
+ - 把色场当作环境现象,不把它当成组件皮肤或装饰贴纸。
6
+ - 先建立稳定的信息层级:导航、列表/队列、详情、元数据、状态带。
7
+ - 让颜色跨越整个场景,但保持文字、边界、按钮和选择态可读。
8
+ - 用 OKLCH 编写颜色;为降级路径保留 sRGB 值。
9
+ - 使用连续的 fBm/domain-warp、流带或方向性流域。禁止明显的圆形光斑、中心聚光和硬彩虹条。
10
+ - 维持静、薄、净、密、活的工作台气质:表面轻、信息密、状态清楚、背景有生命但不抢语义。
11
+
12
+ ## 流光溢彩白 / `opal`
13
+
14
+ 定位:白色是第一视觉,色彩像被光照亮的珠光薄层,能看到变化但不破坏“白”。
15
+
16
+ | 参数 | 建议范围 | 作用 |
17
+ |---|---:|---|
18
+ | `overallColorIntensity` | 0.60–0.90 | 控制全局彩色预算,默认约 0.82 |
19
+ | `base.oklch.l` | 0.975–0.990 | 保留白色基材身份 |
20
+ | `base.oklch.c` | 0.002–0.008 | 只给基材轻微方向,不制造灰雾 |
21
+ | 颜色 `oklch.l` | 0.90–0.96 | 让色彩保持浅、薄、透 |
22
+ | 颜色 `oklch.c` | 0.030–0.065 | 足够识别,但避免糖果霓虹 |
23
+ | `peakOpacity` | 0.055–0.110 | 决定局部峰值,先调它再调总体 |
24
+ | `field.warpStrength` | 0.22–0.42 | 让颜色互相流动而非分块 |
25
+ | `field.luminanceCap` | 0.94–1.00 | 白场通常不需强行压暗 |
26
+
27
+ 建议使用玫瑰、海玻璃青、丁香、珠杏、薄荷和远空蓝等低饱和色相。至少让三种色相在同一截图中可辨,不要让整张图退化为普通暖白、灰雾或单一冷白。
28
+
29
+ ## 五彩斑斓黑 / `obsidian`
30
+
31
+ 定位:黑色是基材,彩色是沉在黑里的光谱现象;多色可见,但亮度必须受控。
32
+
33
+ | 参数 | 建议范围 | 作用 |
34
+ |---|---:|---|
35
+ | `overallColorIntensity` | 0.80–1.00 | 允许色相进入暗场,默认约 1.00 |
36
+ | `base.oklch.l` | 0.070–0.130 | 保留黑色基材身份 |
37
+ | `base.oklch.c` | 0.006–0.018 | 只给黑底轻微冷向 |
38
+ | 颜色 `oklch.l` | 0.18–0.34 | 控制暗彩的可见层级 |
39
+ | 颜色 `oklch.c` | 0.035–0.075 | 使色相可辨但不变霓虹 |
40
+ | `peakOpacity` | 0.14–0.32 | 暗场中色彩的局部峰值 |
41
+ | `field.warpStrength` | 0.30–0.50 | 形成连续暗流 |
42
+ | `field.luminanceCap` | 0.12–0.19 | 防止黑版被抬成灰版 |
43
+
44
+ 建议覆盖深潮青、石油蓝绿、海军蓝、暗靛、紫影和氧化暗金。确保至少五种色相在校准截图中有测得覆盖;不要只剩青黑、死黑或一圈彩色边。
45
+
46
+ ## 参数解释
47
+
48
+ - `intensity`:该色相参与场的权重,必须可被滑杆实时改变。
49
+ - `peakOpacity`:该色相的局部峰值预算;不要把它误写成整个页面透明度。
50
+ - `fieldScale`:该色相的空间频率;不同值能避免所有颜色同相叠加。
51
+ - `phase`:确定性相位,长度为 2 的数组;同一 seed 下保持稳定。
52
+ - `measuredCoverage`:截图中超过基材色度阈值、并归属该色相的像素占比。
53
+ - `effectiveShare`:可见色度能量中该色相的相对份额;这是近似归属,不是 shader 内部精确贡献。
@@ -0,0 +1,70 @@
1
+ # 验证契约
2
+
3
+ ## 能力门槛
4
+
5
+ 先确认执行模型是否具备原生视觉能力:让模型直接查看一张生成的页面截图,并确认它能描述布局、色场、文字可读性和明显缺陷。只有这一步通过,才可把 `visualVerificationMode` 记为 `native-vision-plus-deterministic-pixel-metrics`。
6
+
7
+ 若执行模型不能查看图片:
8
+
9
+ - 仍可运行语法、DOM、浏览器交互和像素统计;
10
+ - 把 `modelVision` 或 `visualVerificationMode` 标记为不可用/`visual-unverified`;
11
+ - 不得把截图尺寸、CSS 值或 DOM 状态当作视觉质量证明;
12
+ - 把需要人眼确认的项目明确列入未验证清单。
13
+
14
+ ## 浏览器检查
15
+
16
+ 用真实浏览器生成至少两种视口:桌面(建议 1440×960)和窄屏(建议 390×844)。至少检查:
17
+
18
+ 1. 页面初始渲染与 WebGL/CSS fallback 状态;
19
+ 2. 参数面板打开、关闭、Escape 返回焦点;
20
+ 3. 总体彩色强度和每个颜色滑杆实时改变画面与数值;
21
+ 4. 恢复预设、导出 JSON、复制失败时的可用提示;
22
+ 5. 导航、记录选择、标签切换、移动端导航收起;
23
+ 6. 控制台错误/警告和本地静态资源请求;
24
+ 7. reduced-motion 与页面隐藏时暂停/冻结行为。
25
+
26
+ 截图必须直接看过才能产生审美结论。重点检查:白版是否仍以白为主、黑版是否仍以黑为底、六色是否形成连续流域、文字是否可读,以及是否出现圆形大光斑、RGB 霓虹、灰雾、黑压死或全页玻璃。
27
+
28
+ ## 像素检查
29
+
30
+ 用 `scripts/measure_preview.py` 测量纯色场截图,而不是只测被面板遮挡的完整页面。脚本会:
31
+
32
+ - 读取 JS manifest 中的 base 与 palette;
33
+ - 将截图转换到 OKLab;
34
+ - 以“高于基材色度阈值”的像素作为可见色;
35
+ - 将可见像素分配给最近的配置色相;
36
+ - 输出可见色像素比例、OKLab chroma、亮度均值/P95/最大值、每色覆盖率和有效色度份额。
37
+
38
+ 低色度白场的色相归属只能作为近似统计,不能替代视觉检查。黑版必须同时检查最大亮度不超过 manifest 的 `luminanceCap` 意图;不要把截图中的 sRGB 值直接当成线性亮度。
39
+
40
+ ## 报告结构
41
+
42
+ 最终报告至少输出:
43
+
44
+ ```json
45
+ {
46
+ "validationCapabilities": {
47
+ "modelVision": true,
48
+ "screenshotCapture": true,
49
+ "deterministicPixelMetrics": true,
50
+ "visualVerificationMode": "native-vision-plus-deterministic-pixel-metrics"
51
+ },
52
+ "theme": {
53
+ "overallColorIntensity": 0.82,
54
+ "colors": [
55
+ {
56
+ "id": "cyan",
57
+ "intensity": 0.62,
58
+ "oklch": [0.928, 0.043, 201],
59
+ "peakOpacity": 0.078,
60
+ "fieldScale": 0.94,
61
+ "phase": [0.78, 0.21],
62
+ "measuredCoverage": 0.1499,
63
+ "effectiveShare": 0.227
64
+ }
65
+ ]
66
+ }
67
+ }
68
+ ```
69
+
70
+ 把“配置参数”和“截图实测”分开写。若某一项没有真实测量,使用 `null` 或 `unverified`,不要填一个看起来完整但无法复现的数字。
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env python3
2
+ """Measure a rendered spectral field against its parameter manifest."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import pathlib
9
+ import subprocess
10
+ import sys
11
+
12
+ try:
13
+ import numpy as np
14
+ from PIL import Image
15
+ except ModuleNotFoundError as exc:
16
+ package = {"PIL": "Pillow"}.get(exc.name, exc.name)
17
+ requirements = pathlib.Path(__file__).with_name("requirements.txt")
18
+ print(
19
+ json.dumps(
20
+ {
21
+ "status": "unavailable",
22
+ "reason": f"Optional measurement dependency is missing: {package}",
23
+ "install": f"python -m pip install -r {requirements}",
24
+ },
25
+ ensure_ascii=False,
26
+ ),
27
+ file=sys.stderr,
28
+ )
29
+ raise SystemExit(2)
30
+
31
+
32
+ def load_config(path: pathlib.Path) -> dict:
33
+ source = (
34
+ "global.window={};require(require('path').resolve(process.argv[1]));"
35
+ "process.stdout.write(JSON.stringify(window.SPECTRAL_THEME));"
36
+ )
37
+ result = subprocess.run(
38
+ ["node", "-e", source, str(path)],
39
+ check=True,
40
+ capture_output=True,
41
+ text=True,
42
+ encoding="utf-8",
43
+ )
44
+ return json.loads(result.stdout)
45
+
46
+
47
+ def srgb_to_oklab(rgb: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
48
+ linear = np.where(
49
+ rgb <= 0.04045,
50
+ rgb / 12.92,
51
+ ((rgb + 0.055) / 1.055) ** 2.4,
52
+ )
53
+ r, g, b = np.moveaxis(linear, -1, 0)
54
+ l = np.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b)
55
+ m = np.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b)
56
+ s = np.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b)
57
+ lab = np.stack(
58
+ (
59
+ 0.2104542553 * l + 0.7936177850 * m - 0.0040720468 * s,
60
+ 1.9779984951 * l - 2.4285922050 * m + 0.4505937099 * s,
61
+ 0.0259040371 * l + 0.7827717662 * m - 0.8086757660 * s,
62
+ ),
63
+ axis=-1,
64
+ )
65
+ luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b
66
+ return lab, luminance
67
+
68
+
69
+ def circular_distance(a: np.ndarray, b: float) -> np.ndarray:
70
+ delta = np.abs(a - b)
71
+ return np.minimum(delta, 360.0 - delta)
72
+
73
+
74
+ def measure(image_path: pathlib.Path, config: dict) -> dict:
75
+ image = Image.open(image_path).convert("RGB")
76
+ pixels = np.asarray(image, dtype=np.float32) / 255.0
77
+ lab, luminance = srgb_to_oklab(pixels)
78
+ chroma = np.hypot(lab[..., 1], lab[..., 2])
79
+ hue = (np.degrees(np.arctan2(lab[..., 2], lab[..., 1])) + 360.0) % 360.0
80
+
81
+ base_chroma = float(config["base"]["oklch"]["c"])
82
+ threshold = base_chroma + (0.003 if config["mode"] == "opal" else 0.004)
83
+ chroma_energy = np.maximum(chroma - threshold, 0.0)
84
+ visible = chroma_energy > 0
85
+
86
+ distances = np.stack(
87
+ [circular_distance(hue, float(color["oklch"]["h"])) for color in config["colors"]],
88
+ axis=-1,
89
+ )
90
+ assignments = np.argmin(distances, axis=-1)
91
+ total_pixels = pixels.shape[0] * pixels.shape[1]
92
+ total_energy = float(chroma_energy.sum())
93
+ colors = []
94
+ for index, color in enumerate(config["colors"]):
95
+ assigned = visible & (assignments == index)
96
+ energy = float(chroma_energy[assigned].sum())
97
+ colors.append(
98
+ {
99
+ "id": color["id"],
100
+ "label": color["label"],
101
+ "configuredIntensity": color["intensity"],
102
+ "oklch": color["oklch"],
103
+ "peakOpacity": color["peakOpacity"],
104
+ "measuredCoverage": round(float(assigned.sum()) / total_pixels, 4),
105
+ "effectiveShare": round(energy / total_energy, 4) if total_energy else 0.0,
106
+ }
107
+ )
108
+
109
+ return {
110
+ "image": image_path.name,
111
+ "dimensions": {"width": image.width, "height": image.height},
112
+ "method": "nearest configured OKLCH hue over pixels above the base-chroma visibility threshold",
113
+ "visibilityThresholdChroma": round(threshold, 4),
114
+ "chromaticPixelRatio": round(float(visible.mean()), 4),
115
+ "meanOklabChroma": round(float(chroma.mean()), 5),
116
+ "luminance": {
117
+ "mean": round(float(luminance.mean()), 5),
118
+ "p05": round(float(np.percentile(luminance, 5)), 5),
119
+ "p95": round(float(np.percentile(luminance, 95)), 5),
120
+ "max": round(float(luminance.max()), 5),
121
+ "configuredCap": config["field"]["luminanceCap"],
122
+ },
123
+ "colors": colors,
124
+ }
125
+
126
+
127
+ def main() -> None:
128
+ parser = argparse.ArgumentParser()
129
+ parser.add_argument("--image", required=True, type=pathlib.Path)
130
+ parser.add_argument("--config", required=True, type=pathlib.Path)
131
+ args = parser.parse_args()
132
+ config = load_config(args.config.resolve())
133
+ payload = {
134
+ "schemaVersion": "1.0",
135
+ "theme": config["label"],
136
+ "preset": config["preset"],
137
+ "overallColorIntensity": config["overallColorIntensity"],
138
+ "measurement": measure(args.image.resolve(), config),
139
+ }
140
+ print(json.dumps(payload, ensure_ascii=False, indent=2))
141
+
142
+
143
+ if __name__ == "__main__":
144
+ main()
@@ -0,0 +1,2 @@
1
+ numpy>=1.24,<3
2
+ Pillow>=10,<13
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env python3
2
+ """Copy the bundled neutral workbench starter into a new output directory."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import pathlib
8
+ import shutil
9
+
10
+
11
+ def copy_tree(source: pathlib.Path, target: pathlib.Path) -> None:
12
+ shutil.copytree(source, target, dirs_exist_ok=True)
13
+
14
+
15
+ def scaffold(skill_root: pathlib.Path, output: pathlib.Path, variant: str) -> None:
16
+ starter = skill_root / "assets" / "starter"
17
+ if output.exists():
18
+ raise SystemExit(f"Refusing to overwrite existing output: {output}")
19
+ output.mkdir(parents=True)
20
+
21
+ if variant == "both":
22
+ copy_tree(starter, output)
23
+ return
24
+
25
+ (output / "shared").mkdir()
26
+ copy_tree(starter / "shared", output / "shared")
27
+ shutil.copy2(starter / "favicon.svg", output / "favicon.svg")
28
+ copy_tree(starter / variant, output)
29
+ index = output / "index.html"
30
+ index.write_text(index.read_text(encoding="utf-8").replace("../", "./"), encoding="utf-8")
31
+
32
+
33
+ def main() -> None:
34
+ parser = argparse.ArgumentParser(description="Scaffold the two-mode spectral UI starter")
35
+ parser.add_argument("--output", required=True, type=pathlib.Path)
36
+ parser.add_argument("--variant", choices=("opal", "obsidian", "both"), default="both")
37
+ args = parser.parse_args()
38
+ scaffold(pathlib.Path(__file__).resolve().parents[1], args.output.resolve(), args.variant)
39
+ print(f"scaffolded {args.variant} -> {args.output.resolve()}")
40
+
41
+
42
+ if __name__ == "__main__":
43
+ main()