howone 0.1.50 → 0.1.52

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 (52) hide show
  1. package/package.json +1 -1
  2. package/templates/vite/.howone/skills/howone/03-ai-capabilities/01-ai-capability-architecture.md +16 -2
  3. package/templates/vite/.howone/skills/howone/03-ai-capabilities/02-workflow-contract-rules.md +107 -0
  4. package/templates/vite/.howone/skills/howone/03-ai-capabilities/03-service-capability-catalog.md +98 -22
  5. package/templates/vite/.howone/skills/howone/03-ai-capabilities/04-workflow-operations.md +8 -0
  6. package/templates/vite/.howone/skills/howone/03-ai-capabilities/05-ai-feature-playbooks.md +66 -1
  7. package/templates/vite/.howone/skills/web-clone/LICENSE +0 -21
  8. package/templates/vite/.howone/skills/web-clone/README.md +0 -179
  9. package/templates/vite/.howone/skills/web-clone/SKILL.md +0 -243
  10. package/templates/vite/.howone/skills/web-clone/references/assessment.md +0 -77
  11. package/templates/vite/.howone/skills/web-clone/references/complex-playbooks.md +0 -46
  12. package/templates/vite/.howone/skills/web-clone/references/deliverables.md +0 -144
  13. package/templates/vite/.howone/skills/web-clone/references/design-dna.md +0 -125
  14. package/templates/vite/.howone/skills/web-clone/references/effect-extraction.md +0 -73
  15. package/templates/vite/.howone/skills/web-clone/references/marbles-case.md +0 -31
  16. package/templates/vite/.howone/skills/web-clone/references/reverse-engineering.md +0 -34
  17. package/templates/vite/.howone/skills/web-clone/references/static-mirror.md +0 -72
  18. package/templates/vite/.howone/skills/web-clone/scripts/asset-harvest.mjs +0 -101
  19. package/templates/vite/.howone/skills/web-clone/scripts/audit-clone.mjs +0 -151
  20. package/templates/vite/.howone/skills/web-clone/scripts/compare-recon.mjs +0 -265
  21. package/templates/vite/.howone/skills/web-clone/scripts/dna-scaffold.mjs +0 -214
  22. package/templates/vite/.howone/skills/web-clone/scripts/init-clone.mjs +0 -136
  23. package/templates/vite/.howone/skills/web-clone/scripts/interaction-probe.mjs +0 -314
  24. package/templates/vite/.howone/skills/web-clone/scripts/lib/playwright-loader.mjs +0 -39
  25. package/templates/vite/.howone/skills/web-clone/scripts/mirror-site.mjs +0 -121
  26. package/templates/vite/.howone/skills/web-clone/scripts/network-capture.mjs +0 -127
  27. package/templates/vite/.howone/skills/web-clone/scripts/recon-site.mjs +0 -235
  28. package/templates/vite/.howone/skills/web-clone/scripts/route-crawl.mjs +0 -228
  29. package/templates/vite/.howone/skills/web-clone/scripts/sourcemap-hunt.mjs +0 -112
  30. package/templates/vite/.howone/skills/web-clone/scripts/visual-diff.mjs +0 -161
  31. package/templates/vite/.howone/skills/web-shader-extractor/SKILL.md +0 -123
  32. package/templates/vite/.howone/skills/web-shader-extractor/references/capture-backends.md +0 -201
  33. package/templates/vite/.howone/skills/web-shader-extractor/references/evidence-policy.md +0 -93
  34. package/templates/vite/.howone/skills/web-shader-extractor/references/operating-contract.md +0 -82
  35. package/templates/vite/.howone/skills/web-shader-extractor/references/qa-failure-policy.md +0 -100
  36. package/templates/vite/.howone/skills/web-shader-extractor/references/recon-kernel.md +0 -206
  37. package/templates/vite/.howone/skills/web-shader-extractor/references/replay-policy.md +0 -145
  38. package/templates/vite/.howone/skills/web-shader-extractor/references/shaders-com.md +0 -212
  39. package/templates/vite/.howone/skills/web-shader-extractor/references/source-analysis.md +0 -112
  40. package/templates/vite/.howone/skills/web-shader-extractor/references/surface-discovery.md +0 -113
  41. package/templates/vite/.howone/skills/web-shader-extractor/references/target-lock.md +0 -205
  42. package/templates/vite/.howone/skills/web-shader-extractor/references/three-shader-reconstruction.md +0 -155
  43. package/templates/vite/.howone/skills/web-shader-extractor/references/tool-capability-matrix.md +0 -57
  44. package/templates/vite/.howone/skills/web-shader-extractor/references/unicorn-studio.md +0 -387
  45. package/templates/vite/.howone/skills/web-shader-extractor/scripts/fetch-rendered-dom.mjs +0 -178
  46. package/templates/vite/.howone/skills/web-shader-extractor/scripts/scan-bundle.sh +0 -80
  47. package/templates/vite/.howone/skills/web-shader-extractor/templates/extraction-report.md +0 -41
  48. package/templates/vite/.howone/skills/web-shader-extractor/templates/known-gaps.md +0 -14
  49. package/templates/vite/.howone/skills/web-shader-extractor/templates/qa-report.md +0 -74
  50. package/templates/vite/.howone/skills/web-shader-extractor/templates/replay-manifest.json +0 -147
  51. package/templates/vite/.howone/skills/web-shader-extractor/templates/run-state.json +0 -75
  52. package/templates/vite/.howone/skills/web-shader-extractor/templates/scout-card.json +0 -106
@@ -1,151 +0,0 @@
1
- #!/usr/bin/env node
2
- import fs from "node:fs";
3
- import path from "node:path";
4
-
5
- function usage() {
6
- console.log(`Usage:
7
- node scripts/audit-clone.mjs --project <clone-dir> [--brand "KOKUYO,Original Brand"] [--out CLONE_AUDIT.md]
8
-
9
- Scans clone source files for tracking scripts, original-brand residue, Japanese residue, TODOs, and risky external dependencies.
10
- `);
11
- }
12
-
13
- function parseArgs(argv) {
14
- const out = { project: process.cwd(), brand: [], out: "CLONE_AUDIT.md" };
15
- for (let i = 0; i < argv.length; i += 1) {
16
- const arg = argv[i];
17
- if (arg === "--help" || arg === "-h") out.help = true;
18
- else if (arg === "--project") out.project = argv[++i] || process.cwd();
19
- else if (arg === "--brand") out.brand = (argv[++i] || "").split(",").map((s) => s.trim()).filter(Boolean);
20
- else if (arg === "--out") out.out = argv[++i] || "CLONE_AUDIT.md";
21
- else throw new Error(`Unexpected argument: ${arg}`);
22
- }
23
- return out;
24
- }
25
-
26
- const includeExt = new Set([".html", ".css", ".js", ".jsx", ".ts", ".tsx", ".json", ".md", ".txt", ".svg"]);
27
- const skipDirs = new Set([".git", "node_modules", "dist", "build", ".next", ".nuxt", "coverage", "RECON"]);
28
- const skipFiles = new Set(["NOTES.md", "TEARDOWN.md", "CLONE_REPORT.md", "CLONE_AUDIT.md", "REPLACE_GUIDE.md"]);
29
-
30
- function walk(dir, files = []) {
31
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
32
- if (skipDirs.has(entry.name)) continue;
33
- const full = path.join(dir, entry.name);
34
- if (entry.isDirectory()) {
35
- if (entry.name === "screenshots") continue;
36
- walk(full, files);
37
- } else if (!skipFiles.has(entry.name) && includeExt.has(path.extname(entry.name).toLowerCase())) {
38
- files.push(full);
39
- }
40
- }
41
- return files;
42
- }
43
-
44
- function lineNumber(text, index) {
45
- return text.slice(0, index).split("\n").length;
46
- }
47
-
48
- function collectMatches(file, text, checks) {
49
- const findings = [];
50
- for (const check of checks) {
51
- const regex = new RegExp(check.pattern, check.flags || "gi");
52
- for (const match of text.matchAll(regex)) {
53
- const matchedText = String(match[0]);
54
- if (check.type === "external" && /^https?:\/\/(www\.)?w3\.org\//i.test(matchedText)) continue;
55
- findings.push({
56
- type: check.type,
57
- label: check.label,
58
- file,
59
- line: lineNumber(text, match.index || 0),
60
- match: matchedText.slice(0, 160),
61
- });
62
- }
63
- }
64
- return findings;
65
- }
66
-
67
- function markdown(findings, project, scannedFiles) {
68
- const byType = new Map();
69
- for (const finding of findings) {
70
- if (!byType.has(finding.type)) byType.set(finding.type, []);
71
- byType.get(finding.type).push(finding);
72
- }
73
- const types = [
74
- ["tracking", "追踪脚本 / 统计像素"],
75
- ["brand", "原站品牌残留"],
76
- ["japanese", "日文残留"],
77
- ["todo", "TODO / 占位内容"],
78
- ["external", "外部依赖 / 外链风险"],
79
- ];
80
- const lines = [
81
- `# Clone Audit`,
82
- "",
83
- `- Project: ${project}`,
84
- `- Scanned files: ${scannedFiles}`,
85
- `- Findings: ${findings.length}`,
86
- "",
87
- ];
88
-
89
- for (const [type, title] of types) {
90
- const items = byType.get(type) || [];
91
- lines.push(`## ${title}`);
92
- if (!items.length) {
93
- lines.push("- 未发现");
94
- lines.push("");
95
- continue;
96
- }
97
- for (const item of items.slice(0, 200)) {
98
- lines.push(`- ${path.relative(project, item.file)}:${item.line} · ${item.label} · \`${item.match.replaceAll("`", "'")}\``);
99
- }
100
- if (items.length > 200) lines.push(`- 还有 ${items.length - 200} 条未展开`);
101
- lines.push("");
102
- }
103
-
104
- lines.push("## 结论");
105
- lines.push(findings.length ? "- 需要处理上面的残留项后再声明可部署。" : "- 未发现明显残留项;仍需人工核查素材授权和视觉截图。");
106
- return `${lines.join("\n")}\n`;
107
- }
108
-
109
- try {
110
- const args = parseArgs(process.argv.slice(2));
111
- if (args.help) {
112
- usage();
113
- process.exit(0);
114
- }
115
-
116
- const project = path.resolve(args.project);
117
- if (!fs.existsSync(project)) throw new Error(`Project not found: ${project}`);
118
-
119
- const brandPatterns = args.brand.map((brand) => ({
120
- type: "brand",
121
- label: `brand residue: ${brand}`,
122
- pattern: brand.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"),
123
- flags: "gi",
124
- }));
125
-
126
- const checks = [
127
- { type: "tracking", label: "Google Tag Manager", pattern: "googletagmanager|GTM-[A-Z0-9]+", flags: "gi" },
128
- { type: "tracking", label: "Google Analytics / gtag", pattern: "google-analytics|gtag\\s*\\(|ga\\s*\\(", flags: "gi" },
129
- { type: "tracking", label: "Meta Pixel / fbq", pattern: "connect\\.facebook\\.net|fbq\\s*\\(", flags: "gi" },
130
- { type: "tracking", label: "Hotjar / Clarity", pattern: "hotjar|clarity\\.ms|hj\\s*\\(", flags: "gi" },
131
- { type: "japanese", label: "Japanese kana residue", pattern: "[\\u3040-\\u30ff]{2,}", flags: "g" },
132
- { type: "todo", label: "TODO / placeholder content", pattern: "TODO|FIXME|lorem ipsum|待补|这里填写", flags: "gi" },
133
- { type: "external", label: "external URL", pattern: "https?://[^\\s\"')<>]+", flags: "gi" },
134
- ...brandPatterns,
135
- ];
136
-
137
- const files = walk(project);
138
- const findings = [];
139
- for (const file of files) {
140
- const text = fs.readFileSync(file, "utf8");
141
- findings.push(...collectMatches(file, text, checks));
142
- }
143
-
144
- const output = path.resolve(args.out);
145
- fs.mkdirSync(path.dirname(output), { recursive: true });
146
- fs.writeFileSync(output, markdown(findings, project, files.length));
147
- console.log(output);
148
- } catch (error) {
149
- console.error(`audit-clone failed: ${error.message}`);
150
- process.exit(1);
151
- }
@@ -1,265 +0,0 @@
1
- #!/usr/bin/env node
2
- import fs from "node:fs";
3
- import path from "node:path";
4
-
5
- function usage() {
6
- console.log(`Usage:
7
- node scripts/compare-recon.mjs --original <original-recon.json> --clone <clone-recon.json> [--visual-diff visual-diff.json] [--original-routes route-map.json] [--clone-routes route-map.json] [--original-interactions interactions.json] [--clone-interactions interactions.json] [--out CLONE_REPORT.md]
8
- `);
9
- }
10
-
11
- function parseArgs(argv) {
12
- const out = {
13
- original: "",
14
- clone: "",
15
- visualDiff: "",
16
- originalRoutes: "",
17
- cloneRoutes: "",
18
- originalInteractions: "",
19
- cloneInteractions: "",
20
- out: "CLONE_REPORT.md",
21
- };
22
- for (let i = 0; i < argv.length; i += 1) {
23
- const arg = argv[i];
24
- if (arg === "--help" || arg === "-h") out.help = true;
25
- else if (arg === "--original") out.original = argv[++i] || "";
26
- else if (arg === "--clone") out.clone = argv[++i] || "";
27
- else if (arg === "--visual-diff") out.visualDiff = argv[++i] || "";
28
- else if (arg === "--original-routes") out.originalRoutes = argv[++i] || "";
29
- else if (arg === "--clone-routes") out.cloneRoutes = argv[++i] || "";
30
- else if (arg === "--original-interactions") out.originalInteractions = argv[++i] || "";
31
- else if (arg === "--clone-interactions") out.cloneInteractions = argv[++i] || "";
32
- else if (arg === "--out") out.out = argv[++i] || "CLONE_REPORT.md";
33
- else throw new Error(`Unexpected argument: ${arg}`);
34
- }
35
- return out;
36
- }
37
-
38
- function readJson(file) {
39
- return JSON.parse(fs.readFileSync(file, "utf8"));
40
- }
41
-
42
- function firstSignals(recon) {
43
- return recon.captures?.[0]?.signals || {};
44
- }
45
-
46
- function boolList(flags = {}) {
47
- return Object.entries(flags).filter(([, value]) => value).map(([key]) => key);
48
- }
49
-
50
- function ratioScore(a, b) {
51
- if (a === 0 && b === 0) return 5;
52
- if (a === 0 || b === 0) return 1;
53
- const ratio = Math.min(a, b) / Math.max(a, b);
54
- if (ratio > 0.9) return 5;
55
- if (ratio > 0.75) return 4;
56
- if (ratio > 0.55) return 3;
57
- if (ratio > 0.3) return 2;
58
- return 1;
59
- }
60
-
61
- function sequenceSimilarity(a, b) {
62
- const left = a.map((item) => `${item.tag}:${item.text}`).filter(Boolean);
63
- const right = b.map((item) => `${item.tag}:${item.text}`).filter(Boolean);
64
- if (!left.length && !right.length) return 1;
65
- if (!left.length || !right.length) return 0;
66
- const rightSet = new Set(right);
67
- const hits = left.filter((item) => rightSet.has(item)).length;
68
- return hits / Math.max(left.length, right.length);
69
- }
70
-
71
- function inferComplexity(signals) {
72
- const frameworks = boolList(signals.frameworks);
73
- const counts = signals.counts || {};
74
- if ((counts.forms || 0) > 2 && (counts.inputs || 0) > 10) return "L6";
75
- if ((counts.canvas || 0) > 0 || signals.frameworks?.three) return "L5";
76
- if (signals.frameworks?.gsap || signals.frameworks?.lenis || (counts.video || 0) > 2) return "L4";
77
- if (frameworks.some((name) => ["react", "next", "vue", "nuxt", "svelte", "astro"].includes(name))) return "L3";
78
- if ((counts.links || 0) > 80 || (counts.images || 0) > 40) return "L2";
79
- return "L1";
80
- }
81
-
82
- function score(original, clone, visualDiff) {
83
- const o = firstSignals(original);
84
- const c = firstSignals(clone);
85
- const structureSimilarity = sequenceSimilarity(o.headings || [], c.headings || []);
86
- const structure = Math.max(1, Math.round(structureSimilarity * 5));
87
- const responsive = original.captures?.length === clone.captures?.length ? 4 : 2;
88
- const functionCounts = ["links", "forms", "buttons", "inputs"].map((key) => ratioScore(o.counts?.[key] || 0, c.counts?.[key] || 0));
89
- const functional = Math.round(functionCounts.reduce((sum, value) => sum + value, 0) / functionCounts.length);
90
- const motionCounts = ["canvas", "video"].map((key) => ratioScore(o.counts?.[key] || 0, c.counts?.[key] || 0));
91
- const interaction = Math.round(motionCounts.reduce((sum, value) => sum + value, 0) / motionCounts.length);
92
- return {
93
- sourceEvidence: 3,
94
- structure,
95
- visual: visualDiff ? `${visualDiff.visualScore}/5` : "需人工看截图或传 --visual-diff",
96
- interaction,
97
- responsive,
98
- functional,
99
- contentReplacement: "需人工看文案残留",
100
- legalRisk: "需人工核查 license / 素材",
101
- };
102
- }
103
-
104
- function line(value) {
105
- if (Array.isArray(value)) return value.join(", ") || "none";
106
- return value ?? "";
107
- }
108
-
109
- function routePath(url) {
110
- try {
111
- const parsed = new URL(url);
112
- return `${parsed.pathname}${parsed.search}` || "/";
113
- } catch {
114
- return url;
115
- }
116
- }
117
-
118
- function routesSection(files, evidence) {
119
- if (!evidence.originalRoutes || !evidence.cloneRoutes) {
120
- return `## 路由覆盖
121
- - 未提供 route-crawl 结果。多页面站需要传 --original-routes / --clone-routes。
122
- `;
123
- }
124
- const originalSet = new Set((evidence.originalRoutes.routes || []).map((route) => routePath(route.url)));
125
- const cloneSet = new Set((evidence.cloneRoutes.routes || []).map((route) => routePath(route.url)));
126
- const matched = Array.from(originalSet).filter((item) => cloneSet.has(item));
127
- const missing = Array.from(originalSet).filter((item) => !cloneSet.has(item));
128
- const extra = Array.from(cloneSet).filter((item) => !originalSet.has(item));
129
- const coverage = originalSet.size ? Math.round((matched.length / originalSet.size) * 100) : 100;
130
- return `## 路由覆盖
131
- - 原站路由: ${originalSet.size}
132
- - 克隆路由: ${cloneSet.size}
133
- - 覆盖率: ${coverage}%
134
- - 原站 route map: ${files.originalRoutes}
135
- - 克隆 route map: ${files.cloneRoutes}
136
- - 缺失路由: ${missing.join(", ") || "无"}
137
- - 额外路由: ${extra.join(", ") || "无"}
138
- `;
139
- }
140
-
141
- function changedActionCount(interactions) {
142
- return (interactions?.actions || []).filter((action) => action.changed).length;
143
- }
144
-
145
- function interactionSection(files, evidence) {
146
- if (!evidence.originalInteractions || !evidence.cloneInteractions) {
147
- return `## 交互覆盖
148
- - 未提供 interaction-probe 结果。交互站需要传 --original-interactions / --clone-interactions。
149
- `;
150
- }
151
- const originalActions = evidence.originalInteractions.actions || [];
152
- const cloneActions = evidence.cloneInteractions.actions || [];
153
- const originalChanged = changedActionCount(evidence.originalInteractions);
154
- const cloneChanged = changedActionCount(evidence.cloneInteractions);
155
- const originalCanvas = evidence.originalInteractions.discovered?.canvases?.length || 0;
156
- const cloneCanvas = evidence.cloneInteractions.discovered?.canvases?.length || 0;
157
- const originalInteractive = evidence.originalInteractions.discovered?.interactive?.length || 0;
158
- const cloneInteractive = evidence.cloneInteractions.discovered?.interactive?.length || 0;
159
- return `## 交互覆盖
160
- - 原站可见交互目标: ${originalInteractive}
161
- - 克隆可见交互目标: ${cloneInteractive}
162
- - 原站 canvas 目标: ${originalCanvas}
163
- - 克隆 canvas 目标: ${cloneCanvas}
164
- - 原站 changed actions: ${originalChanged}/${originalActions.length}
165
- - 克隆 changed actions: ${cloneChanged}/${cloneActions.length}
166
- - 原站 interaction probe: ${files.originalInteractions}
167
- - 克隆 interaction probe: ${files.cloneInteractions}
168
- - 判断: ${originalChanged === cloneChanged && originalCanvas === cloneCanvas ? "交互数量信号接近,仍需看截图确认状态质量。" : "交互数量信号不一致,需要检查缺失状态或过度实现。"}
169
- `;
170
- }
171
-
172
- function report(files, original, clone, evidence) {
173
- const o = firstSignals(original);
174
- const c = firstSignals(clone);
175
- const scores = score(original, clone, evidence.visualDiff);
176
- const complexity = inferComplexity(o);
177
- const originalFlags = boolList(o.frameworks);
178
- const cloneFlags = boolList(c.frameworks);
179
- const counts = ["sections", "links", "images", "video", "canvas", "forms", "buttons", "inputs", "interactive", "scripts"];
180
-
181
- return `# ${original.label || "original"} vs ${clone.label || "clone"} · 克隆评估报告
182
-
183
- ## 结论
184
- - 原站 URL: ${original.url}
185
- - 克隆 URL: ${clone.url}
186
- - 自动推断复杂度: ${complexity}
187
- - 复刻模式建议: ${complexity === "L5" ? "技术拆解 / 忠实复刻优先" : complexity === "L6" ? "展示层视觉复刻" : "视觉复刻 / 内容爆改"}
188
- - 自动报告边界: 结构、数量、框架、console 可自动比;传入 visual-diff 后可纳入像素差异分。内容残留和法务仍需审计。
189
-
190
- ## 技术信号
191
- | 项目 | 原站 | 克隆站 |
192
- |---|---|---|
193
- | title | ${o.title || ""} | ${c.title || ""} |
194
- | lang | ${o.lang || ""} | ${c.lang || ""} |
195
- | frameworks | ${line(originalFlags)} | ${line(cloneFlags)} |
196
- | scrollHeight | ${o.scrollHeight || 0} | ${c.scrollHeight || 0} |
197
- | h1 | ${line(o.h1)} | ${line(c.h1)} |
198
-
199
- ## 数量对比
200
- | 指标 | 原站 | 克隆站 | 自动评分 |
201
- |---|---:|---:|---:|
202
- ${counts.map((key) => `| ${key} | ${o.counts?.[key] || 0} | ${c.counts?.[key] || 0} | ${ratioScore(o.counts?.[key] || 0, c.counts?.[key] || 0)}/5 |`).join("\n")}
203
-
204
- ## 复刻评分
205
- - 源证据: ${scores.sourceEvidence}/5
206
- - 结构保真: ${scores.structure}/5
207
- - 视觉保真: ${scores.visual}
208
- - 动效/交互: ${scores.interaction}/5
209
- - 响应式: ${scores.responsive}/5
210
- - 功能完整: ${scores.functional}/5
211
- - 内容替换: ${scores.contentReplacement}
212
- - 法务/部署风险: ${scores.legalRisk}
213
-
214
- ## Console
215
- - 原站 console errors: ${original.console?.errors?.length || 0}
216
- - 克隆 console errors: ${clone.console?.errors?.length || 0}
217
- - 原站 page errors: ${original.console?.pageErrors?.length || 0}
218
- - 克隆 page errors: ${clone.console?.pageErrors?.length || 0}
219
-
220
- ${routesSection(files, evidence)}
221
-
222
- ${interactionSection(files, evidence)}
223
-
224
- ## 截图证据
225
- - 原站侦察: ${files.original}
226
- - 克隆侦察: ${files.clone}
227
- - 像素差异: ${files.visualDiff || "未提供"}
228
- - 像素差异率: ${evidence.visualDiff ? evidence.visualDiff.diffPixelRatio : "未提供"}
229
- - 原站截图: ${(original.captures || []).map((capture) => capture.screenshot).join(", ")}
230
- - 克隆截图: ${(clone.captures || []).map((capture) => capture.screenshot).join(", ")}
231
-
232
- ## 已知缺口
233
- - 未传入 visual-diff 时,视觉保真需要打开截图人工确认。
234
- - 法务、素材授权、品牌替换完整度需要人工核查。
235
- `;
236
- }
237
-
238
- try {
239
- const args = parseArgs(process.argv.slice(2));
240
- if (args.help || !args.original || !args.clone) {
241
- usage();
242
- process.exit(args.help ? 0 : 1);
243
- }
244
-
245
- const original = readJson(args.original);
246
- const clone = readJson(args.clone);
247
- const visualDiff = args.visualDiff ? readJson(args.visualDiff) : null;
248
- const originalRoutes = args.originalRoutes ? readJson(args.originalRoutes) : null;
249
- const cloneRoutes = args.cloneRoutes ? readJson(args.cloneRoutes) : null;
250
- const originalInteractions = args.originalInteractions ? readJson(args.originalInteractions) : null;
251
- const cloneInteractions = args.cloneInteractions ? readJson(args.cloneInteractions) : null;
252
- const output = path.resolve(args.out);
253
- fs.mkdirSync(path.dirname(output), { recursive: true });
254
- fs.writeFileSync(output, report(args, original, clone, {
255
- visualDiff,
256
- originalRoutes,
257
- cloneRoutes,
258
- originalInteractions,
259
- cloneInteractions,
260
- }));
261
- console.log(output);
262
- } catch (error) {
263
- console.error(`compare-recon failed: ${error.message}`);
264
- process.exit(1);
265
- }
@@ -1,214 +0,0 @@
1
- #!/usr/bin/env node
2
- // dna-scaffold.mjs — 生成 design-dna.json 骨架,best-effort 从 recon-site.mjs 的输出预填。
3
- // 用法:
4
- // node scripts/dna-scaffold.mjs --out <design-dna.json> [--recon <label-recon.json>] [--name <站名>]
5
- // 产物:
6
- // <out> 完整 DNA 骨架;有 --recon 时预填字体/色候选/框架特效信号,其余留 "" 待人工 Analyze。
7
- // 纪律: 只搬侦察里"真实抓到"的信号,绝不编造。拿不准角色(primary/accent)的色值统一丢进 _recon_signals 供人工指派。
8
-
9
- import fs from "node:fs";
10
- import path from "node:path";
11
-
12
- function parseArgs(argv) {
13
- const out = { recon: "", out: "", name: "", help: false };
14
- for (let i = 0; i < argv.length; i++) {
15
- const a = argv[i];
16
- if (a === "--help" || a === "-h") out.help = true;
17
- else if (a === "--recon") out.recon = argv[++i] || "";
18
- else if (a === "--out") out.out = argv[++i] || "";
19
- else if (a === "--name") out.name = argv[++i] || "";
20
- }
21
- return out;
22
- }
23
-
24
- function usage() {
25
- console.log(`dna-scaffold.mjs — 生成 design-dna.json 骨架并 best-effort 预填
26
-
27
- node scripts/dna-scaffold.mjs --out <design-dna.json> [--recon <label-recon.json>] [--name <站名>]
28
-
29
- 只用在「视觉复刻 / 内容爆改」模式。忠实复刻分支不需要 DNA(真源码即真相)。
30
- schema 与字段含义见 references/design-dna.md。`);
31
- }
32
-
33
- // 完整 DNA 骨架(与 references/design-dna.md 对齐)
34
- function skeleton(name) {
35
- const ts = () => ({ size: "", weight: "", line_height: "", tracking: "" });
36
- return {
37
- meta: { name: name || "", description: "", source_references: "", created_at: "" },
38
- design_system: {
39
- color: {
40
- palette_type: "",
41
- primary: { hex: "", role: "" },
42
- secondary: { hex: "", role: "" },
43
- accent: { hex: "", role: "" },
44
- neutral: { scale: "", usage: "" },
45
- semantic: { success: "", warning: "", error: "", info: "" },
46
- surface: { background: "", card: "", elevated: "" },
47
- contrast_strategy: "",
48
- },
49
- typography: {
50
- type_scale: {
51
- display: ts(), heading_1: ts(), heading_2: ts(), heading_3: ts(),
52
- body: ts(), body_small: ts(), caption: ts(), overline: ts(),
53
- },
54
- font_families: { heading: "", body: "", mono: "" },
55
- font_style_notes: "",
56
- },
57
- spacing: { base_unit: "", scale: "", content_density: "", section_rhythm: "" },
58
- layout: { grid_system: "", max_content_width: "", columns: "", gutter: "", breakpoints: "", alignment_tendency: "" },
59
- shape: { border_radius: { small: "", medium: "", large: "", pill: "" }, border_usage: "", divider_style: "" },
60
- elevation: { shadow_style: "", levels: { low: "", medium: "", high: "" }, depth_cues: "" },
61
- iconography: { style: "", stroke_weight: "", size_scale: "", preferred_set: "" },
62
- motion: { easing: "", duration_scale: { micro: "", normal: "", macro: "" }, entrance_pattern: "", exit_pattern: "", philosophy: "" },
63
- components: { button_style: "", input_style: "", card_style: "", navigation_pattern: "", modal_style: "", list_style: "", component_notes: "" },
64
- },
65
- design_style: {
66
- aesthetic: { mood: [], visual_metaphor: "", era_influence: "", genre: "", personality_traits: [], adjectives: [] },
67
- visual_language: { complexity: "", ornamentation: "", whitespace_usage: "", visual_weight_distribution: "", focal_strategy: "", contrast_level: "", texture_usage: "" },
68
- composition: { hierarchy_method: "", balance_type: "", flow_direction: "", grouping_strategy: "", negative_space_role: "" },
69
- imagery: { photo_treatment: "", illustration_style: "", graphic_elements: "", pattern_usage: "", image_shape: "" },
70
- interaction_feel: { feedback_style: "", hover_behavior: "", transition_personality: "", loading_style: "", microinteraction_density: "" },
71
- brand_voice_in_ui: { tone: "", formality: "", cta_style: "", empty_state_approach: "", error_tone: "" },
72
- },
73
- visual_effects: {
74
- overview: { effect_intensity: "", performance_tier: "", fallback_strategy: "", primary_technology: "" },
75
- background_effects: { type: "", description: "", technology: "", params: { color_palette: "", speed: "", density: "", opacity: "", blend_mode: "" } },
76
- particle_systems: { enabled: false, type: "", description: "", technology: "", params: { count: "", shape: "", size_range: "", movement_pattern: "", color_behavior: "", interaction: "", spawn_area: "" } },
77
- "3d_elements": { enabled: false, type: "", description: "", technology: "", params: { renderer: "", lighting: "", camera: "", materials: "", geometry: "", post_processing: [], interaction_model: "" } },
78
- shader_effects: { enabled: false, type: "", description: "", technology: "", params: { uniforms: "", vertex_manipulation: "", fragment_output: "", noise_type: "", distortion: "" } },
79
- scroll_effects: { parallax: { enabled: false, layers: "", depth_range: "", speed_curve: "" }, scroll_triggered_animations: { enabled: false, trigger_points: "", animation_type: "", scrub_behavior: "" }, scroll_morphing: { enabled: false, description: "" } },
80
- text_effects: { type: "", description: "", technology: "", params: { split_strategy: "", animation_per_unit: "", stagger: "", effect_style: "" } },
81
- cursor_effects: { enabled: false, type: "", description: "", params: { shape: "", size: "", blend_mode: "", trail: "", interaction_zone: "" } },
82
- image_effects: { type: "", description: "", technology: "", params: { filter_pipeline: "", hover_transform: "", reveal_animation: "", distortion_type: "" } },
83
- glassmorphism_neumorphism: { enabled: false, style: "", params: { blur_radius: "", transparency: "", border_treatment: "", shadow_type: "", light_source_angle: "" } },
84
- canvas_drawings: { enabled: false, type: "", description: "", technology: "", params: { draw_method: "", animation_loop: "", color_scheme: "", responsiveness: "", interaction: "" } },
85
- svg_animations: { enabled: false, type: "", description: "", params: { animation_method: "", path_morphing: "", stroke_animation: "", filter_effects: "" } },
86
- composite_notes: "",
87
- },
88
- };
89
- }
90
-
91
- const COLOR_RE = /(#[0-9a-fA-F]{3,8}\b|\brgba?\([^)]*\)|\bhsla?\([^)]*\))/;
92
-
93
- // recon-site.mjs 把信号按视口嵌在 captures[].signals 下;取最宽视口的 signals 摊平,
94
- // 兼容已是扁平结构的 recon 输入。摊平失败则原样返回(enrich 对缺字段已容错)。
95
- function flattenRecon(recon) {
96
- if (recon && Array.isArray(recon.captures) && recon.captures.length) {
97
- const widest = recon.captures
98
- .filter((c) => c && c.signals)
99
- .sort((a, b) => (b?.viewport?.width || 0) - (a?.viewport?.width || 0))[0];
100
- if (widest && widest.signals) {
101
- // 顶层 url 兜底进 href,便于 meta.source_references 预填
102
- return { href: recon.url, ...widest.signals };
103
- }
104
- }
105
- return recon;
106
- }
107
-
108
- function uniq(arr) {
109
- return Array.from(new Set(arr.filter(Boolean)));
110
- }
111
-
112
- // 从 recon JSON best-effort 抽信号,并预填 skeleton 的明确字段
113
- function enrich(dna, recon) {
114
- const signals = { fonts: [], color_candidates: [], frameworks: {}, canvas_count: 0, css_color_vars: [] };
115
-
116
- // 字体: fonts[] + sections[].style.fontFamily
117
- const fontList = uniq([
118
- ...(Array.isArray(recon.fonts) ? recon.fonts : []),
119
- ...((recon.sections || []).map((s) => s?.style?.fontFamily).filter(Boolean)),
120
- ]).map((f) => String(f).replace(/^["']|["']$/g, "").split(",")[0].trim()).filter(Boolean);
121
- signals.fonts = uniq(fontList);
122
- if (signals.fonts.length) {
123
- const mono = signals.fonts.find((f) => /mono|code|consol|courier/i.test(f)) || "";
124
- const nonMono = signals.fonts.filter((f) => f !== mono);
125
- dna.design_system.typography.font_families.heading = nonMono[0] || "";
126
- dna.design_system.typography.font_families.body = nonMono[1] || nonMono[0] || "";
127
- dna.design_system.typography.font_families.mono = mono;
128
- }
129
-
130
- // 颜色: CSS 变量里像颜色的 + sections 的 bg/color
131
- const cssVars = Array.isArray(recon.cssVariables) ? recon.cssVariables : [];
132
- for (const pair of cssVars) {
133
- const [name, val] = Array.isArray(pair) ? pair : [pair?.name, pair?.value];
134
- if (val && COLOR_RE.test(String(val))) signals.css_color_vars.push(`${name}: ${String(val).trim()}`);
135
- }
136
- const sectionColors = [];
137
- for (const s of recon.sections || []) {
138
- const bg = s?.style?.backgroundColor;
139
- const fg = s?.style?.color;
140
- if (bg && !/rgba?\(0, 0, 0, 0\)|transparent/i.test(bg)) sectionColors.push(bg);
141
- if (fg) sectionColors.push(fg);
142
- }
143
- signals.color_candidates = uniq([
144
- ...signals.css_color_vars.map((v) => v.split(":").slice(1).join(":").trim()),
145
- ...sectionColors,
146
- ]).slice(0, 24);
147
- // body/header 背景作为 surface.background 候选(第一个非透明的 section bg)
148
- const firstBg = (recon.sections || []).map((s) => s?.style?.backgroundColor)
149
- .find((c) => c && !/rgba?\(0, 0, 0, 0\)|transparent/i.test(c));
150
- if (firstBg) dna.design_system.color.surface.background = firstBg;
151
-
152
- // 框架/特效信号
153
- const fw = recon.frameworks || {};
154
- signals.frameworks = fw;
155
- signals.canvas_count = (recon.canvases && recon.canvases.length) || recon?.counts?.canvas || 0;
156
-
157
- if (fw.three) {
158
- dna.visual_effects.overview.primary_technology = "WebGL/Three.js";
159
- dna.visual_effects.overview.performance_tier = "heavy";
160
- dna.visual_effects["3d_elements"].enabled = true;
161
- dna.visual_effects["3d_elements"].technology = "Three.js";
162
- } else if (signals.canvas_count > 0) {
163
- dna.visual_effects.overview.primary_technology = "Canvas 2D";
164
- dna.visual_effects.canvas_drawings.enabled = true;
165
- } else if (fw.gsap) {
166
- dna.visual_effects.overview.primary_technology = "GSAP";
167
- }
168
- if (fw.gsap || fw.lenis) {
169
- dna.visual_effects.scroll_effects.scroll_triggered_animations.enabled = true;
170
- dna.visual_effects.scroll_effects.scroll_triggered_animations.scrub_behavior =
171
- fw.lenis ? "lenis smooth-scroll detected" : "gsap detected";
172
- }
173
-
174
- // meta 预填
175
- if (recon.href) dna.meta.source_references = recon.href;
176
- if (!dna.meta.name && recon.title) dna.meta.name = recon.title;
177
-
178
- // 把原始信号留在顶层供人工指派角色(不编造 primary/accent)
179
- dna._recon_signals = signals;
180
- dna._scaffold_note =
181
- "best-effort 预填来自 recon。font_families/surface.background/visual_effects 已据真实信号填写;" +
182
- "color 的 primary/secondary/accent 角色需人工从 _recon_signals.color_candidates 指派;" +
183
- "所有 \"\" 字段需人工 Analyze 补全(见 references/design-dna.md)。确认无误后可删除 _recon_signals 与本说明。";
184
- return dna;
185
- }
186
-
187
- try {
188
- const args = parseArgs(process.argv.slice(2));
189
- if (args.help || !args.out) {
190
- usage();
191
- process.exit(args.help ? 0 : 1);
192
- }
193
- let dna = skeleton(args.name);
194
- if (args.recon) {
195
- try {
196
- const recon = JSON.parse(fs.readFileSync(path.resolve(args.recon), "utf8"));
197
- dna = enrich(dna, flattenRecon(recon));
198
- } catch (e) {
199
- console.warn(`⚠️ 读 recon 失败(${e.message}),只输出空骨架。`);
200
- }
201
- }
202
- const outPath = path.resolve(args.out);
203
- fs.mkdirSync(path.dirname(outPath), { recursive: true });
204
- fs.writeFileSync(outPath, `${JSON.stringify(dna, null, 2)}\n`);
205
- console.log(`✅ design-dna 骨架已写入: ${outPath}`);
206
- if (dna._recon_signals) {
207
- const s = dna._recon_signals;
208
- console.log(` 预填: 字体 ${s.fonts.length} 个 / 色候选 ${s.color_candidates.length} 个 / canvas ${s.canvas_count} / three=${!!s.frameworks.three} gsap=${!!s.frameworks.gsap} lenis=${!!s.frameworks.lenis}`);
209
- }
210
- console.log(` 下一步: 人工 Analyze 补全 "",并从 _recon_signals 指派颜色角色。schema → references/design-dna.md`);
211
- } catch (e) {
212
- console.error(`dna-scaffold 失败: ${e.message}`);
213
- process.exit(1);
214
- }