@tangle-network/browser-agent-driver 0.33.2 → 0.34.0
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.
- package/README.md +42 -1
- package/discovery/llms.txt +27 -0
- package/discovery/tangle-agent.json +36 -0
- package/dist/brain/action-parse.d.ts +16 -0
- package/dist/brain/action-parse.d.ts.map +1 -0
- package/dist/brain/action-parse.js +191 -0
- package/dist/brain/action-parse.js.map +1 -0
- package/dist/brain/decide.d.ts +58 -0
- package/dist/brain/decide.d.ts.map +1 -0
- package/dist/brain/decide.js +418 -0
- package/dist/brain/decide.js.map +1 -0
- package/dist/brain/history-compact.d.ts +19 -0
- package/dist/brain/history-compact.d.ts.map +1 -0
- package/dist/brain/history-compact.js +153 -0
- package/dist/brain/history-compact.js.map +1 -0
- package/dist/brain/index.d.ts +78 -152
- package/dist/brain/index.d.ts.map +1 -1
- package/dist/brain/index.js +62 -2268
- package/dist/brain/index.js.map +1 -1
- package/dist/brain/model-client.d.ts +101 -0
- package/dist/brain/model-client.d.ts.map +1 -0
- package/dist/brain/model-client.js +391 -0
- package/dist/brain/model-client.js.map +1 -0
- package/dist/brain/plan.d.ts +57 -0
- package/dist/brain/plan.d.ts.map +1 -0
- package/dist/brain/plan.js +194 -0
- package/dist/brain/plan.js.map +1 -0
- package/dist/brain/prompts.d.ts +37 -0
- package/dist/brain/prompts.d.ts.map +1 -0
- package/dist/brain/prompts.js +455 -0
- package/dist/brain/prompts.js.map +1 -0
- package/dist/brain/provider-fetch.d.ts +26 -0
- package/dist/brain/provider-fetch.d.ts.map +1 -0
- package/dist/brain/provider-fetch.js +45 -0
- package/dist/brain/provider-fetch.js.map +1 -0
- package/dist/brain/snapshot-budget.d.ts +20 -0
- package/dist/brain/snapshot-budget.d.ts.map +1 -0
- package/dist/brain/snapshot-budget.js +202 -0
- package/dist/brain/snapshot-budget.js.map +1 -0
- package/dist/brain/system-prompt.d.ts +67 -0
- package/dist/brain/system-prompt.d.ts.map +1 -0
- package/dist/brain/system-prompt.js +128 -0
- package/dist/brain/system-prompt.js.map +1 -0
- package/dist/brain/tasks/design-audit.d.ts +33 -0
- package/dist/brain/tasks/design-audit.d.ts.map +1 -0
- package/dist/brain/tasks/design-audit.js +100 -0
- package/dist/brain/tasks/design-audit.js.map +1 -0
- package/dist/brain/tasks/evaluate.d.ts +25 -0
- package/dist/brain/tasks/evaluate.d.ts.map +1 -0
- package/dist/brain/tasks/evaluate.js +55 -0
- package/dist/brain/tasks/evaluate.js.map +1 -0
- package/dist/brain/tasks/goal-verification.d.ts +32 -0
- package/dist/brain/tasks/goal-verification.d.ts.map +1 -0
- package/dist/brain/tasks/goal-verification.js +68 -0
- package/dist/brain/tasks/goal-verification.js.map +1 -0
- package/dist/brain/tasks/knowledge.d.ts +26 -0
- package/dist/brain/tasks/knowledge.d.ts.map +1 -0
- package/dist/brain/tasks/knowledge.js +56 -0
- package/dist/brain/tasks/knowledge.js.map +1 -0
- package/dist/brain/tasks/link-scout.d.ts +37 -0
- package/dist/brain/tasks/link-scout.d.ts.map +1 -0
- package/dist/brain/tasks/link-scout.js +69 -0
- package/dist/brain/tasks/link-scout.js.map +1 -0
- package/dist/brain/types.d.ts +55 -0
- package/dist/brain/types.d.ts.map +1 -0
- package/dist/brain/types.js +7 -0
- package/dist/brain/types.js.map +1 -0
- package/dist/browser/stealth-init-script.d.ts +9 -0
- package/dist/browser/stealth-init-script.d.ts.map +1 -0
- package/dist/browser/stealth-init-script.js +128 -0
- package/dist/browser/stealth-init-script.js.map +1 -0
- package/dist/browser/storage-state.d.ts +29 -0
- package/dist/browser/storage-state.d.ts.map +1 -0
- package/dist/browser/storage-state.js +47 -0
- package/dist/browser/storage-state.js.map +1 -0
- package/dist/cli/args.d.ts +141 -0
- package/dist/cli/args.d.ts.map +1 -0
- package/dist/cli/args.js +167 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/benchmark-sync.d.ts +9 -0
- package/dist/cli/benchmark-sync.d.ts.map +1 -0
- package/dist/cli/benchmark-sync.js +43 -0
- package/dist/cli/benchmark-sync.js.map +1 -0
- package/dist/cli/build-config.d.ts +11 -0
- package/dist/cli/build-config.d.ts.map +1 -0
- package/dist/cli/build-config.js +355 -0
- package/dist/cli/build-config.js.map +1 -0
- package/dist/cli/commands/auth.d.ts +3 -0
- package/dist/cli/commands/auth.d.ts.map +1 -0
- package/dist/cli/commands/auth.js +36 -0
- package/dist/cli/commands/auth.js.map +1 -0
- package/dist/cli/commands/chrome-debug.d.ts +7 -0
- package/dist/cli/commands/chrome-debug.d.ts.map +1 -0
- package/dist/cli/commands/chrome-debug.js +11 -0
- package/dist/cli/commands/chrome-debug.js.map +1 -0
- package/dist/cli/commands/design-audit.d.ts +3 -0
- package/dist/cli/commands/design-audit.d.ts.map +1 -0
- package/dist/cli/commands/design-audit.js +68 -0
- package/dist/cli/commands/design-audit.js.map +1 -0
- package/dist/cli/commands/preview.d.ts +14 -0
- package/dist/cli/commands/preview.d.ts.map +1 -0
- package/dist/cli/commands/preview.js +31 -0
- package/dist/cli/commands/preview.js.map +1 -0
- package/dist/cli/commands/run.d.ts +3 -0
- package/dist/cli/commands/run.d.ts.map +1 -0
- package/dist/cli/commands/run.js +728 -0
- package/dist/cli/commands/run.js.map +1 -0
- package/dist/cli/commands/runs.d.ts +8 -0
- package/dist/cli/commands/runs.d.ts.map +1 -0
- package/dist/cli/commands/runs.js +35 -0
- package/dist/cli/commands/runs.js.map +1 -0
- package/dist/cli/commands/share.d.ts +10 -0
- package/dist/cli/commands/share.d.ts.map +1 -0
- package/dist/cli/commands/share.js +32 -0
- package/dist/cli/commands/share.js.map +1 -0
- package/dist/cli/commands/showcase.d.ts +17 -0
- package/dist/cli/commands/showcase.d.ts.map +1 -0
- package/dist/cli/commands/showcase.js +20 -0
- package/dist/cli/commands/showcase.js.map +1 -0
- package/dist/cli/commands/snapshot.d.ts +12 -0
- package/dist/cli/commands/snapshot.d.ts.map +1 -0
- package/dist/cli/commands/snapshot.js +24 -0
- package/dist/cli/commands/snapshot.js.map +1 -0
- package/dist/cli/commands/view.d.ts +7 -0
- package/dist/cli/commands/view.d.ts.map +1 -0
- package/dist/cli/commands/view.js +23 -0
- package/dist/cli/commands/view.js.map +1 -0
- package/dist/cli/constants.d.ts +6 -0
- package/dist/cli/constants.d.ts.map +1 -0
- package/dist/cli/constants.js +3 -0
- package/dist/cli/constants.js.map +1 -0
- package/dist/cli/version.d.ts +7 -0
- package/dist/cli/version.d.ts.map +1 -0
- package/dist/cli/version.js +16 -0
- package/dist/cli/version.js.map +1 -0
- package/dist/cli-design-audit.d.ts +44 -36
- package/dist/cli-design-audit.d.ts.map +1 -1
- package/dist/cli-design-audit.js +137 -1646
- package/dist/cli-design-audit.js.map +1 -1
- package/dist/cli-jobs.js +2 -1
- package/dist/cli-jobs.js.map +1 -1
- package/dist/cli.js +66 -1697
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +5 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +8 -0
- package/dist/config.js.map +1 -1
- package/dist/design/audit/evolve/agent.d.ts +23 -0
- package/dist/design/audit/evolve/agent.d.ts.map +1 -0
- package/dist/design/audit/evolve/agent.js +272 -0
- package/dist/design/audit/evolve/agent.js.map +1 -0
- package/dist/design/audit/evolve/css.d.ts +10 -0
- package/dist/design/audit/evolve/css.d.ts.map +1 -0
- package/dist/design/audit/evolve/css.js +206 -0
- package/dist/design/audit/evolve/css.js.map +1 -0
- package/dist/design/audit/evolve/index.d.ts +13 -0
- package/dist/design/audit/evolve/index.d.ts.map +1 -0
- package/dist/design/audit/evolve/index.js +12 -0
- package/dist/design/audit/evolve/index.js.map +1 -0
- package/dist/design/audit/evolve/report.d.ts +3 -0
- package/dist/design/audit/evolve/report.d.ts.map +1 -0
- package/dist/design/audit/evolve/report.js +45 -0
- package/dist/design/audit/evolve/report.js.map +1 -0
- package/dist/design/audit/evolve/types.d.ts +14 -0
- package/dist/design/audit/evolve/types.d.ts.map +1 -0
- package/dist/design/audit/evolve/types.js +2 -0
- package/dist/design/audit/evolve/types.js.map +1 -0
- package/dist/design/audit/pipeline.d.ts +12 -0
- package/dist/design/audit/pipeline.d.ts.map +1 -1
- package/dist/design/audit/pipeline.js +43 -27
- package/dist/design/audit/pipeline.js.map +1 -1
- package/dist/design/audit/reference/artifact/build.d.ts +33 -0
- package/dist/design/audit/reference/artifact/build.d.ts.map +1 -0
- package/dist/design/audit/reference/artifact/build.js +64 -0
- package/dist/design/audit/reference/artifact/build.js.map +1 -0
- package/dist/design/audit/reference/artifact/render.d.ts +39 -0
- package/dist/design/audit/reference/artifact/render.d.ts.map +1 -0
- package/dist/design/audit/reference/artifact/render.js +213 -0
- package/dist/design/audit/reference/artifact/render.js.map +1 -0
- package/dist/design/audit/reference/artifact/text.d.ts +11 -0
- package/dist/design/audit/reference/artifact/text.d.ts.map +1 -0
- package/dist/design/audit/reference/artifact/text.js +20 -0
- package/dist/design/audit/reference/artifact/text.js.map +1 -0
- package/dist/design/audit/reference/artifact/to-findings.d.ts +57 -0
- package/dist/design/audit/reference/artifact/to-findings.d.ts.map +1 -0
- package/dist/design/audit/reference/artifact/to-findings.js +314 -0
- package/dist/design/audit/reference/artifact/to-findings.js.map +1 -0
- package/dist/design/audit/reference/config.d.ts +50 -0
- package/dist/design/audit/reference/config.d.ts.map +1 -0
- package/dist/design/audit/reference/config.js +108 -0
- package/dist/design/audit/reference/config.js.map +1 -0
- package/dist/design/audit/reference/contracts.d.ts +1030 -0
- package/dist/design/audit/reference/contracts.d.ts.map +1 -0
- package/dist/design/audit/reference/contracts.js +28 -0
- package/dist/design/audit/reference/contracts.js.map +1 -0
- package/dist/design/audit/reference/corpus/build.d.ts +115 -0
- package/dist/design/audit/reference/corpus/build.d.ts.map +1 -0
- package/dist/design/audit/reference/corpus/build.js +152 -0
- package/dist/design/audit/reference/corpus/build.js.map +1 -0
- package/dist/design/audit/reference/corpus/schema.d.ts +33 -0
- package/dist/design/audit/reference/corpus/schema.d.ts.map +1 -0
- package/dist/design/audit/reference/corpus/schema.js +249 -0
- package/dist/design/audit/reference/corpus/schema.js.map +1 -0
- package/dist/design/audit/reference/corpus/store.d.ts +26 -0
- package/dist/design/audit/reference/corpus/store.d.ts.map +1 -0
- package/dist/design/audit/reference/corpus/store.js +104 -0
- package/dist/design/audit/reference/corpus/store.js.map +1 -0
- package/dist/design/audit/reference/dna/delta.d.ts +21 -0
- package/dist/design/audit/reference/dna/delta.d.ts.map +1 -0
- package/dist/design/audit/reference/dna/delta.js +126 -0
- package/dist/design/audit/reference/dna/delta.js.map +1 -0
- package/dist/design/audit/reference/dna/derive.d.ts +31 -0
- package/dist/design/audit/reference/dna/derive.d.ts.map +1 -0
- package/dist/design/audit/reference/dna/derive.js +381 -0
- package/dist/design/audit/reference/dna/derive.js.map +1 -0
- package/dist/design/audit/reference/dna/descriptor.d.ts +36 -0
- package/dist/design/audit/reference/dna/descriptor.d.ts.map +1 -0
- package/dist/design/audit/reference/dna/descriptor.js +209 -0
- package/dist/design/audit/reference/dna/descriptor.js.map +1 -0
- package/dist/design/audit/reference/dna/page-adapter.d.ts +17 -0
- package/dist/design/audit/reference/dna/page-adapter.d.ts.map +1 -0
- package/dist/design/audit/reference/dna/page-adapter.js +35 -0
- package/dist/design/audit/reference/dna/page-adapter.js.map +1 -0
- package/dist/design/audit/reference/dna/scroll-capture.d.ts +93 -0
- package/dist/design/audit/reference/dna/scroll-capture.d.ts.map +1 -0
- package/dist/design/audit/reference/dna/scroll-capture.js +374 -0
- package/dist/design/audit/reference/dna/scroll-capture.js.map +1 -0
- package/dist/design/audit/reference/engine/budget.d.ts +63 -0
- package/dist/design/audit/reference/engine/budget.d.ts.map +1 -0
- package/dist/design/audit/reference/engine/budget.js +110 -0
- package/dist/design/audit/reference/engine/budget.js.map +1 -0
- package/dist/design/audit/reference/engine/core.d.ts +32 -0
- package/dist/design/audit/reference/engine/core.d.ts.map +1 -0
- package/dist/design/audit/reference/engine/core.js +261 -0
- package/dist/design/audit/reference/engine/core.js.map +1 -0
- package/dist/design/audit/reference/engine/guard.d.ts +43 -0
- package/dist/design/audit/reference/engine/guard.d.ts.map +1 -0
- package/dist/design/audit/reference/engine/guard.js +43 -0
- package/dist/design/audit/reference/engine/guard.js.map +1 -0
- package/dist/design/audit/reference/engine/score-core.d.ts +48 -0
- package/dist/design/audit/reference/engine/score-core.d.ts.map +1 -0
- package/dist/design/audit/reference/engine/score-core.js +134 -0
- package/dist/design/audit/reference/engine/score-core.js.map +1 -0
- package/dist/design/audit/reference/engine/wiring.d.ts +35 -0
- package/dist/design/audit/reference/engine/wiring.d.ts.map +1 -0
- package/dist/design/audit/reference/engine/wiring.js +80 -0
- package/dist/design/audit/reference/engine/wiring.js.map +1 -0
- package/dist/design/audit/reference/eval/taste-core.d.ts +77 -0
- package/dist/design/audit/reference/eval/taste-core.d.ts.map +1 -0
- package/dist/design/audit/reference/eval/taste-core.js +95 -0
- package/dist/design/audit/reference/eval/taste-core.js.map +1 -0
- package/dist/design/audit/reference/generate/generator.d.ts +49 -0
- package/dist/design/audit/reference/generate/generator.d.ts.map +1 -0
- package/dist/design/audit/reference/generate/generator.js +94 -0
- package/dist/design/audit/reference/generate/generator.js.map +1 -0
- package/dist/design/audit/reference/generate/parse.d.ts +28 -0
- package/dist/design/audit/reference/generate/parse.d.ts.map +1 -0
- package/dist/design/audit/reference/generate/parse.js +180 -0
- package/dist/design/audit/reference/generate/parse.js.map +1 -0
- package/dist/design/audit/reference/generate/prompt.d.ts +36 -0
- package/dist/design/audit/reference/generate/prompt.d.ts.map +1 -0
- package/dist/design/audit/reference/generate/prompt.js +124 -0
- package/dist/design/audit/reference/generate/prompt.js.map +1 -0
- package/dist/design/audit/reference/index.d.ts +26 -0
- package/dist/design/audit/reference/index.d.ts.map +1 -0
- package/dist/design/audit/reference/index.js +31 -0
- package/dist/design/audit/reference/index.js.map +1 -0
- package/dist/design/audit/reference/judge/image-clamp.d.ts +34 -0
- package/dist/design/audit/reference/judge/image-clamp.d.ts.map +1 -0
- package/dist/design/audit/reference/judge/image-clamp.js +88 -0
- package/dist/design/audit/reference/judge/image-clamp.js.map +1 -0
- package/dist/design/audit/reference/judge/pairwise.d.ts +34 -0
- package/dist/design/audit/reference/judge/pairwise.d.ts.map +1 -0
- package/dist/design/audit/reference/judge/pairwise.js +105 -0
- package/dist/design/audit/reference/judge/pairwise.js.map +1 -0
- package/dist/design/audit/reference/judge/parse.d.ts +31 -0
- package/dist/design/audit/reference/judge/parse.d.ts.map +1 -0
- package/dist/design/audit/reference/judge/parse.js +113 -0
- package/dist/design/audit/reference/judge/parse.js.map +1 -0
- package/dist/design/audit/reference/judge/prompt.d.ts +38 -0
- package/dist/design/audit/reference/judge/prompt.d.ts.map +1 -0
- package/dist/design/audit/reference/judge/prompt.js +111 -0
- package/dist/design/audit/reference/judge/prompt.js.map +1 -0
- package/dist/design/audit/reference/judge/quality.d.ts +28 -0
- package/dist/design/audit/reference/judge/quality.d.ts.map +1 -0
- package/dist/design/audit/reference/judge/quality.js +62 -0
- package/dist/design/audit/reference/judge/quality.js.map +1 -0
- package/dist/design/audit/reference/judge/rank.d.ts +41 -0
- package/dist/design/audit/reference/judge/rank.d.ts.map +1 -0
- package/dist/design/audit/reference/judge/rank.js +196 -0
- package/dist/design/audit/reference/judge/rank.js.map +1 -0
- package/dist/design/audit/reference/judge/text-judge.d.ts +31 -0
- package/dist/design/audit/reference/judge/text-judge.d.ts.map +1 -0
- package/dist/design/audit/reference/judge/text-judge.js +43 -0
- package/dist/design/audit/reference/judge/text-judge.js.map +1 -0
- package/dist/design/audit/reference/judge/vision-judge.d.ts +59 -0
- package/dist/design/audit/reference/judge/vision-judge.d.ts.map +1 -0
- package/dist/design/audit/reference/judge/vision-judge.js +150 -0
- package/dist/design/audit/reference/judge/vision-judge.js.map +1 -0
- package/dist/design/audit/reference/judge/vision-model.d.ts +65 -0
- package/dist/design/audit/reference/judge/vision-model.d.ts.map +1 -0
- package/dist/design/audit/reference/judge/vision-model.js +110 -0
- package/dist/design/audit/reference/judge/vision-model.js.map +1 -0
- package/dist/design/audit/reference/pipeline/evaluate-reference.d.ts +68 -0
- package/dist/design/audit/reference/pipeline/evaluate-reference.d.ts.map +1 -0
- package/dist/design/audit/reference/pipeline/evaluate-reference.js +52 -0
- package/dist/design/audit/reference/pipeline/evaluate-reference.js.map +1 -0
- package/dist/design/audit/reference/reference-context.d.ts +31 -0
- package/dist/design/audit/reference/reference-context.d.ts.map +1 -0
- package/dist/design/audit/reference/reference-context.js +73 -0
- package/dist/design/audit/reference/reference-context.js.map +1 -0
- package/dist/design/audit/reference/retrieval/embedding-hash.d.ts +30 -0
- package/dist/design/audit/reference/retrieval/embedding-hash.d.ts.map +1 -0
- package/dist/design/audit/reference/retrieval/embedding-hash.js +87 -0
- package/dist/design/audit/reference/retrieval/embedding-hash.js.map +1 -0
- package/dist/design/audit/reference/retrieval/embedding-openai.d.ts +38 -0
- package/dist/design/audit/reference/retrieval/embedding-openai.d.ts.map +1 -0
- package/dist/design/audit/reference/retrieval/embedding-openai.js +73 -0
- package/dist/design/audit/reference/retrieval/embedding-openai.js.map +1 -0
- package/dist/design/audit/reference/retrieval/matcher.d.ts +34 -0
- package/dist/design/audit/reference/retrieval/matcher.d.ts.map +1 -0
- package/dist/design/audit/reference/retrieval/matcher.js +107 -0
- package/dist/design/audit/reference/retrieval/matcher.js.map +1 -0
- package/dist/design/audit/reference/run.d.ts +59 -0
- package/dist/design/audit/reference/run.d.ts.map +1 -0
- package/dist/design/audit/reference/run.js +99 -0
- package/dist/design/audit/reference/run.js.map +1 -0
- package/dist/design/audit/report.d.ts +35 -0
- package/dist/design/audit/report.d.ts.map +1 -0
- package/dist/design/audit/report.js +195 -0
- package/dist/design/audit/report.js.map +1 -0
- package/dist/design/audit/tokens/extract.d.ts +71 -0
- package/dist/design/audit/tokens/extract.d.ts.map +1 -0
- package/dist/design/audit/tokens/extract.js +1034 -0
- package/dist/design/audit/tokens/extract.js.map +1 -0
- package/dist/design/audit/types.d.ts +8 -0
- package/dist/design/audit/types.d.ts.map +1 -1
- package/dist/design/compare.d.ts.map +1 -1
- package/dist/design/compare.js +2 -6
- package/dist/design/compare.js.map +1 -1
- package/dist/design/cookie-consent.d.ts +3 -0
- package/dist/design/cookie-consent.d.ts.map +1 -0
- package/dist/design/cookie-consent.js +26 -0
- package/dist/design/cookie-consent.js.map +1 -0
- package/dist/design/viewports.d.ts +18 -0
- package/dist/design/viewports.d.ts.map +1 -0
- package/dist/design/viewports.js +10 -0
- package/dist/design/viewports.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/memory/store.d.ts +19 -1
- package/dist/memory/store.d.ts.map +1 -1
- package/dist/memory/store.js +47 -0
- package/dist/memory/store.js.map +1 -1
- package/dist/provider-defaults.d.ts +18 -0
- package/dist/provider-defaults.d.ts.map +1 -1
- package/dist/provider-defaults.js +33 -0
- package/dist/provider-defaults.js.map +1 -1
- package/dist/runner/allowed-domains.d.ts +11 -0
- package/dist/runner/allowed-domains.d.ts.map +1 -0
- package/dist/runner/allowed-domains.js +14 -0
- package/dist/runner/allowed-domains.js.map +1 -0
- package/dist/runner/batch-fill.d.ts +28 -0
- package/dist/runner/batch-fill.d.ts.map +1 -0
- package/dist/runner/batch-fill.js +80 -0
- package/dist/runner/batch-fill.js.map +1 -0
- package/dist/runner/completion-policy.d.ts +26 -0
- package/dist/runner/completion-policy.d.ts.map +1 -0
- package/dist/runner/completion-policy.js +76 -0
- package/dist/runner/completion-policy.js.map +1 -0
- package/dist/runner/constants.d.ts +14 -0
- package/dist/runner/constants.d.ts.map +1 -0
- package/dist/runner/constants.js +23 -0
- package/dist/runner/constants.js.map +1 -0
- package/dist/runner/decision-screenshot.d.ts +24 -0
- package/dist/runner/decision-screenshot.d.ts.map +1 -0
- package/dist/runner/decision-screenshot.js +23 -0
- package/dist/runner/decision-screenshot.js.map +1 -0
- package/dist/runner/domain-boundary.d.ts +36 -0
- package/dist/runner/domain-boundary.d.ts.map +1 -0
- package/dist/runner/domain-boundary.js +81 -0
- package/dist/runner/domain-boundary.js.map +1 -0
- package/dist/runner/effect-verify.d.ts +28 -0
- package/dist/runner/effect-verify.d.ts.map +1 -0
- package/dist/runner/effect-verify.js +42 -0
- package/dist/runner/effect-verify.js.map +1 -0
- package/dist/runner/execute-plan.d.ts +58 -0
- package/dist/runner/execute-plan.d.ts.map +1 -0
- package/dist/runner/execute-plan.js +426 -0
- package/dist/runner/execute-plan.js.map +1 -0
- package/dist/runner/max-turns-extension.d.ts +19 -0
- package/dist/runner/max-turns-extension.d.ts.map +1 -0
- package/dist/runner/max-turns-extension.js +25 -0
- package/dist/runner/max-turns-extension.js.map +1 -0
- package/dist/runner/micro-plan.d.ts +11 -0
- package/dist/runner/micro-plan.d.ts.map +1 -0
- package/dist/runner/micro-plan.js +29 -0
- package/dist/runner/micro-plan.js.map +1 -0
- package/dist/runner/prompt-snippets.d.ts +5 -0
- package/dist/runner/prompt-snippets.d.ts.map +1 -0
- package/dist/runner/prompt-snippets.js +13 -0
- package/dist/runner/prompt-snippets.js.map +1 -0
- package/dist/runner/replay/contracts.d.ts +300 -0
- package/dist/runner/replay/contracts.d.ts.map +1 -0
- package/dist/runner/replay/contracts.js +42 -0
- package/dist/runner/replay/contracts.js.map +1 -0
- package/dist/runner/replay/controller.d.ts +27 -0
- package/dist/runner/replay/controller.d.ts.map +1 -0
- package/dist/runner/replay/controller.js +197 -0
- package/dist/runner/replay/controller.js.map +1 -0
- package/dist/runner/replay/guard.d.ts +28 -0
- package/dist/runner/replay/guard.d.ts.map +1 -0
- package/dist/runner/replay/guard.js +100 -0
- package/dist/runner/replay/guard.js.map +1 -0
- package/dist/runner/runner.d.ts +44 -51
- package/dist/runner/runner.d.ts.map +1 -1
- package/dist/runner/runner.js +155 -851
- package/dist/runner/runner.js.map +1 -1
- package/dist/runner/scout-feedback.d.ts +51 -0
- package/dist/runner/scout-feedback.d.ts.map +1 -0
- package/dist/runner/scout-feedback.js +149 -0
- package/dist/runner/scout-feedback.js.map +1 -0
- package/dist/skills/macro-loader.d.ts +1 -1
- package/dist/test-runner.d.ts.map +1 -1
- package/dist/test-runner.js +25 -0
- package/dist/test-runner.js.map +1 -1
- package/dist/types/actions.d.ts +241 -0
- package/dist/types/actions.d.ts.map +1 -0
- package/dist/types/actions.js +5 -0
- package/dist/types/actions.js.map +1 -0
- package/dist/types/config.d.ts +211 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +2 -0
- package/dist/types/config.js.map +1 -0
- package/dist/types/design-audit.d.ts +92 -0
- package/dist/types/design-audit.d.ts.map +1 -0
- package/dist/types/design-audit.js +5 -0
- package/dist/types/design-audit.js.map +1 -0
- package/dist/types/design-tokens.d.ts +138 -0
- package/dist/types/design-tokens.d.ts.map +1 -0
- package/dist/types/design-tokens.js +5 -0
- package/dist/types/design-tokens.js.map +1 -0
- package/dist/types/page.d.ts +16 -0
- package/dist/types/page.d.ts.map +1 -0
- package/dist/types/page.js +2 -0
- package/dist/types/page.js.map +1 -0
- package/dist/types/plan.d.ts +50 -0
- package/dist/types/plan.d.ts.map +1 -0
- package/dist/types/plan.js +2 -0
- package/dist/types/plan.js.map +1 -0
- package/dist/types/preview.d.ts +9 -0
- package/dist/types/preview.d.ts.map +1 -0
- package/dist/types/preview.js +5 -0
- package/dist/types/preview.js.map +1 -0
- package/dist/types/result.d.ts +62 -0
- package/dist/types/result.d.ts.map +1 -0
- package/dist/types/result.js +2 -0
- package/dist/types/result.js.map +1 -0
- package/dist/types/scenario.d.ts +20 -0
- package/dist/types/scenario.d.ts.map +1 -0
- package/dist/types/scenario.js +5 -0
- package/dist/types/scenario.js.map +1 -0
- package/dist/types/test-runner.d.ts +145 -0
- package/dist/types/test-runner.d.ts.map +1 -0
- package/dist/types/test-runner.js +10 -0
- package/dist/types/test-runner.js.map +1 -0
- package/dist/types/trajectory.d.ts +38 -0
- package/dist/types/trajectory.d.ts.map +1 -0
- package/dist/types/trajectory.js +2 -0
- package/dist/types/trajectory.js.map +1 -0
- package/dist/types/turn.d.ts +50 -0
- package/dist/types/turn.d.ts.map +1 -0
- package/dist/types/turn.js +2 -0
- package/dist/types/turn.js.map +1 -0
- package/dist/types.d.ts +16 -1017
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +15 -8
- package/dist/types.js.map +1 -1
- package/dist/wallet/rpc-interception.d.ts +15 -0
- package/dist/wallet/rpc-interception.d.ts.map +1 -0
- package/dist/wallet/rpc-interception.js +95 -0
- package/dist/wallet/rpc-interception.js.map +1 -0
- package/package.json +7 -3
package/dist/cli-design-audit.js
CHANGED
|
@@ -6,71 +6,17 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import * as fs from 'node:fs';
|
|
8
8
|
import * as path from 'node:path';
|
|
9
|
-
import { execSync } from 'node:child_process';
|
|
10
9
|
import chalk from 'chalk';
|
|
11
10
|
import { chromium } from 'playwright';
|
|
12
11
|
import { Brain } from './brain/index.js';
|
|
13
12
|
import { PlaywrightDriver } from './drivers/playwright.js';
|
|
14
|
-
import { resolveProviderApiKey, resolveProviderModelName } from './provider-defaults.js';
|
|
13
|
+
import { resolveDefaultProvider, resolveProviderApiKey, resolveProviderModelName, isSupportedProvider, SUPPORTED_PROVIDERS } from './provider-defaults.js';
|
|
15
14
|
import { loadLocalEnvFiles } from './env-loader.js';
|
|
16
15
|
import { cliError } from './cli-ui.js';
|
|
17
16
|
import { auditOnePage } from './design/audit/pipeline.js';
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if (!input)
|
|
22
|
-
return undefined;
|
|
23
|
-
const tags = input.split(',').map(s => s.trim()).filter(Boolean);
|
|
24
|
-
return tags.length > 0 ? tags : undefined;
|
|
25
|
-
}
|
|
26
|
-
/** Pretty-print the ethics-violation report for a set of pages. Prints
|
|
27
|
-
* nothing when no page tripped a rule. Each rule is shown with severity,
|
|
28
|
-
* remediation, and citation so the operator can act without re-running. */
|
|
29
|
-
function printEthicsViolations(pages) {
|
|
30
|
-
const offenders = pages.filter(p => (p.ethicsViolations?.length ?? 0) > 0);
|
|
31
|
-
if (offenders.length === 0)
|
|
32
|
-
return;
|
|
33
|
-
console.log('');
|
|
34
|
-
console.log(` ${chalk.bgRed.white.bold(' ETHICS VIOLATIONS ')}`);
|
|
35
|
-
for (const page of offenders) {
|
|
36
|
-
console.log(` ${chalk.dim('Page:')} ${page.url}`);
|
|
37
|
-
for (const v of page.ethicsViolations ?? []) {
|
|
38
|
-
const sevColor = v.severity === 'critical-floor' ? chalk.red : chalk.yellow;
|
|
39
|
-
console.log(` ${sevColor('•')} ${chalk.bold(v.ruleId)} ${chalk.dim('—')} ${sevColor(v.severity)} ${chalk.dim(`(rollup capped at ${v.rollupCap})`)}`);
|
|
40
|
-
console.log(` ${chalk.dim('fix:')} ${v.remediation}`);
|
|
41
|
-
if (v.citation)
|
|
42
|
-
console.log(` ${chalk.dim('cite:')} ${v.citation}`);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
/** Lowest rollup cap across all violated pages, or undefined if none fired. */
|
|
47
|
-
function lowestRollupCap(pages) {
|
|
48
|
-
const caps = pages.flatMap(p => p.ethicsViolations ?? []).map(v => v.rollupCap);
|
|
49
|
-
return caps.length === 0 ? undefined : Math.min(...caps);
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* Layer 1 — print the per-dimension breakdown for one page when an
|
|
53
|
-
* `auditResult` is attached. Five dim lines + one rollup line; each shows
|
|
54
|
-
* score, range, and confidence so an agent can reason about uncertainty.
|
|
55
|
-
*/
|
|
56
|
-
function printScoreBreakdown(page) {
|
|
57
|
-
const result = page.auditResult;
|
|
58
|
-
if (!result || !result.scores || !result.rollup)
|
|
59
|
-
return;
|
|
60
|
-
const dimOrder = ['product_intent', 'visual_craft', 'trust_clarity', 'workflow', 'content_ia'];
|
|
61
|
-
for (const dim of dimOrder) {
|
|
62
|
-
const s = result.scores[dim];
|
|
63
|
-
if (!s)
|
|
64
|
-
continue;
|
|
65
|
-
const sevColor = s.score >= 8 ? chalk.green : s.score >= 5 ? chalk.yellow : chalk.red;
|
|
66
|
-
const confColor = s.confidence === 'high' ? chalk.green : s.confidence === 'medium' ? chalk.yellow : chalk.dim;
|
|
67
|
-
console.log(` ${chalk.dim(dim.padEnd(15))} ${sevColor(`${s.score}/10`)} ${chalk.dim(`[${s.range[0]}-${s.range[1]}]`)} ${confColor(s.confidence)}`);
|
|
68
|
-
}
|
|
69
|
-
const r = result.rollup;
|
|
70
|
-
const rColor = r.score >= 8 ? chalk.green : r.score >= 5 ? chalk.yellow : chalk.red;
|
|
71
|
-
const confColor = r.confidence === 'high' ? chalk.green : r.confidence === 'medium' ? chalk.yellow : chalk.dim;
|
|
72
|
-
console.log(` ${chalk.dim('rollup'.padEnd(15))} ${rColor(`${r.score.toFixed(1)}/10`)} ${chalk.dim(`[${r.range[0].toFixed(1)}-${r.range[1].toFixed(1)}]`)} ${confColor(r.confidence)} ${chalk.dim(r.rule)}`);
|
|
73
|
-
}
|
|
17
|
+
import { extractDesignTokens, createZipBundle } from './design/audit/tokens/extract.js';
|
|
18
|
+
import { runEvolveLoop, runAgentEvolveLoop, generateEvolveReport, buildApplyPrompt } from './design/audit/evolve/index.js';
|
|
19
|
+
import { parseTagList, printEthicsViolations, lowestRollupCap, printScoreBreakdown, generateDesignReport, } from './design/audit/report.js';
|
|
74
20
|
import { resolveAuditPasses } from './design/audit/evaluate.js';
|
|
75
21
|
import { detectSystemicFindings, topByRoi } from './design/audit/roi.js';
|
|
76
22
|
import { getTelemetry, setInvocation } from './telemetry/index.js';
|
|
@@ -122,122 +68,87 @@ async function discoverPages(page, startUrl, maxPages) {
|
|
|
122
68
|
}
|
|
123
69
|
return discovered;
|
|
124
70
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Parse a `--judge-models` list (`"prov:model,prov:model"` or bare `"model"`)
|
|
73
|
+
* into `ModelRef[]`. Splits on `,` then the FIRST `:` (model names never contain
|
|
74
|
+
* one); a colon-less entry leaves `provider` unset so the wiring fills the
|
|
75
|
+
* ambient default. Blank entries are skipped; an empty/absent string ⇒ `[]`.
|
|
76
|
+
*/
|
|
77
|
+
export function parseModelRefs(spec) {
|
|
78
|
+
if (!spec)
|
|
79
|
+
return [];
|
|
80
|
+
const refs = [];
|
|
81
|
+
for (const raw of spec.split(',')) {
|
|
82
|
+
const entry = raw.trim();
|
|
83
|
+
if (!entry)
|
|
84
|
+
continue;
|
|
85
|
+
const colon = entry.indexOf(':');
|
|
86
|
+
if (colon < 0) {
|
|
87
|
+
refs.push({ model: entry });
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const provider = entry.slice(0, colon).trim();
|
|
91
|
+
const model = entry.slice(colon + 1).trim();
|
|
92
|
+
if (!model)
|
|
93
|
+
continue;
|
|
94
|
+
refs.push(provider ? { provider: provider, model } : { model });
|
|
142
95
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
96
|
+
return refs;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Default location for the reference corpus — mirrors
|
|
100
|
+
* `reference/config.ts` `DEFAULT_REFERENCE_CONFIG.corpusDir`. Checked with a
|
|
101
|
+
* plain fs read (no engine import) so reference-grounded can be the default
|
|
102
|
+
* without forcing the heavy engine to load on the v1 path.
|
|
103
|
+
*/
|
|
104
|
+
const DEFAULT_REFERENCE_CORPUS_DIR = 'bench/design/reference-corpus';
|
|
105
|
+
/** True iff the default corpus dir holds at least one exemplar (`.json` other
|
|
106
|
+
* than the tracked `sources.example.json`). Drives the corpus-aware default. */
|
|
107
|
+
function hasPopulatedCorpus() {
|
|
108
|
+
try {
|
|
109
|
+
return fs
|
|
110
|
+
.readdirSync(path.resolve(DEFAULT_REFERENCE_CORPUS_DIR))
|
|
111
|
+
.some((f) => f.endsWith('.json') && f !== 'sources.example.json');
|
|
151
112
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
lines.push(`**Findings:** ${allFindings.length} (${critical} critical, ${major} major, ${minor} minor)`);
|
|
155
|
-
if (totalTokens > 0)
|
|
156
|
-
lines.push(`**Tokens used:** ${totalTokens.toLocaleString()}`);
|
|
157
|
-
lines.push('');
|
|
158
|
-
// Score bar
|
|
159
|
-
const scoreBar = '█'.repeat(Math.round(avgScore)) + '░'.repeat(10 - Math.round(avgScore));
|
|
160
|
-
lines.push(`\`${scoreBar}\` ${avgScore.toFixed(1)}/10`);
|
|
161
|
-
lines.push('');
|
|
162
|
-
// ── Top Fixes (by ROI) — the headline section users actually read first ──
|
|
163
|
-
if (topFixes.length > 0) {
|
|
164
|
-
lines.push('## Top Fixes (by ROI)');
|
|
165
|
-
lines.push('');
|
|
166
|
-
lines.push('Fix these first — sorted by impact × blast / effort:');
|
|
167
|
-
lines.push('');
|
|
168
|
-
for (let i = 0; i < topFixes.length; i++) {
|
|
169
|
-
const f = topFixes[i];
|
|
170
|
-
const tags = [];
|
|
171
|
-
if (f.pageCount && f.pageCount >= 2)
|
|
172
|
-
tags.push(`appears on ${f.pageCount} pages`);
|
|
173
|
-
if (f.blast === 'system')
|
|
174
|
-
tags.push('SYSTEMIC');
|
|
175
|
-
const tagStr = tags.length > 0 ? ` _(${tags.join(', ')})_` : '';
|
|
176
|
-
const roiStr = f.roi !== undefined ? f.roi.toFixed(1) : '—';
|
|
177
|
-
lines.push(`### ${i + 1}. [${f.severity}] ${f.description}${tagStr}`);
|
|
178
|
-
lines.push('');
|
|
179
|
-
lines.push(`- **ROI:** ${roiStr} · Impact ${f.impact ?? '—'} · Effort ${f.effort ?? '—'} · Blast ${f.blast ?? '—'}`);
|
|
180
|
-
lines.push(`- **Location:** ${f.location}`);
|
|
181
|
-
lines.push(`- **Fix:** ${f.suggestion}`);
|
|
182
|
-
if (f.cssSelector && f.cssFix) {
|
|
183
|
-
lines.push(`- **CSS:** \`${f.cssSelector} { ${f.cssFix} }\``);
|
|
184
|
-
}
|
|
185
|
-
lines.push('');
|
|
186
|
-
}
|
|
187
|
-
lines.push('---');
|
|
188
|
-
lines.push('');
|
|
113
|
+
catch {
|
|
114
|
+
return false;
|
|
189
115
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
lines.push('| Sev | Category | Description | Location | Fix | CSS |');
|
|
226
|
-
lines.push('|-----|----------|-------------|----------|-----|-----|');
|
|
227
|
-
for (const f of result.findings) {
|
|
228
|
-
const esc = (s) => s.replace(/\|/g, '\\|').replace(/\n/g, ' ').slice(0, 120);
|
|
229
|
-
const icon = f.severity === 'critical' ? '🔴' : f.severity === 'major' ? '🟡' : '⚪';
|
|
230
|
-
const cssFix = f.cssFix ? `\`${esc(f.cssFix)}\`` : '';
|
|
231
|
-
lines.push(`| ${icon} ${f.severity} | ${f.category} | ${esc(f.description)} | ${esc(f.location)} | ${esc(f.suggestion)} | ${cssFix} |`);
|
|
232
|
-
}
|
|
233
|
-
lines.push('');
|
|
234
|
-
}
|
|
235
|
-
else if (!result.error) {
|
|
236
|
-
lines.push('No issues found.');
|
|
237
|
-
lines.push('');
|
|
238
|
-
}
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Acquire the reference-grounded eval bundle once: lazily load the engine,
|
|
119
|
+
* resolve the operator reference + corpus, and pick the taste judge. Reached
|
|
120
|
+
* only when reference-grounded mode is active, so the v1 path never loads the
|
|
121
|
+
* engine. Fails closed with a clear diagnostic (a module-load failure or an
|
|
122
|
+
* unresolvable reference) instead of an unhandled rejection.
|
|
123
|
+
*/
|
|
124
|
+
async function setupReferenceGrounded(opts, modelName, provider) {
|
|
125
|
+
try {
|
|
126
|
+
const { resolveReferenceConfig, createFileCorpusStore, resolveReferenceContext } = await import('./design/audit/reference/index.js');
|
|
127
|
+
const { createPageDnaExtractor } = await import('./design/audit/reference/dna/page-adapter.js');
|
|
128
|
+
// Vision judge: `--judge vision` selects it; `--judge-models` customises the
|
|
129
|
+
// ensemble (and implies vision when `--judge` is omitted). With vision on but
|
|
130
|
+
// no explicit models, default to the audit's OWN provider/model so the screenshot
|
|
131
|
+
// judge "just works" with whatever key the run already uses. Subjects without a
|
|
132
|
+
// screenshot (unrendered directions) fall back to the text judge inside the engine.
|
|
133
|
+
const visionRefs = parseModelRefs(opts.judgeModels);
|
|
134
|
+
const wantsVision = opts.judge === 'vision' || (!opts.judge && visionRefs.length > 0);
|
|
135
|
+
const referenceConfig = resolveReferenceConfig({
|
|
136
|
+
model: modelName,
|
|
137
|
+
...(wantsVision ? { judge: 'vision' } : opts.judge === 'text' ? { judge: 'text' } : {}),
|
|
138
|
+
...(wantsVision
|
|
139
|
+
? { visionModels: visionRefs.length > 0 ? visionRefs : [{ provider, model: modelName }] }
|
|
140
|
+
: {}),
|
|
141
|
+
});
|
|
142
|
+
const reference = await resolveReferenceContext(opts.reference, { extractor: createPageDnaExtractor() }, { headless: opts.headless ?? true });
|
|
143
|
+
const corpus = await createFileCorpusStore(referenceConfig.corpusDir).load();
|
|
144
|
+
const refLabel = reference ? `, reference ${opts.reference ?? ''}` : '';
|
|
145
|
+
console.log(` ${chalk.cyan('reference-grounded')} ${chalk.dim('—')} corpus ${chalk.bold(String(corpus.length))} exemplar${corpus.length !== 1 ? 's' : ''}${refLabel}`);
|
|
146
|
+
return { evalMode: 'reference-grounded', reference, corpus, referenceConfig };
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
cliError(`reference-grounded setup failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
150
|
+
process.exit(1);
|
|
239
151
|
}
|
|
240
|
-
return lines.join('\n');
|
|
241
152
|
}
|
|
242
153
|
export async function runDesignAudit(opts) {
|
|
243
154
|
loadLocalEnvFiles(process.cwd());
|
|
@@ -250,7 +161,13 @@ export async function runDesignAudit(opts) {
|
|
|
250
161
|
// selects a single type fragment instead of letting the classifier decide.
|
|
251
162
|
const profile = opts.profile;
|
|
252
163
|
const maxPages = opts.pages ?? 5;
|
|
253
|
-
|
|
164
|
+
// Validate a user-supplied --provider before casting, so an unknown value
|
|
165
|
+
// fails fast with a clear message instead of surfacing deep in the pipeline.
|
|
166
|
+
if (opts.provider !== undefined && !isSupportedProvider(opts.provider)) {
|
|
167
|
+
cliError(`unknown --provider "${opts.provider}". Supported: ${SUPPORTED_PROVIDERS.join(', ')}`);
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
const provider = (opts.provider ?? resolveDefaultProvider());
|
|
254
171
|
const modelName = resolveProviderModelName(provider, opts.model);
|
|
255
172
|
const apiKey = opts.apiKey ?? resolveProviderApiKey(provider);
|
|
256
173
|
const auditPasses = resolveAuditPasses(opts.auditPasses);
|
|
@@ -266,6 +183,23 @@ export async function runDesignAudit(opts) {
|
|
|
266
183
|
audienceVulnerability: parseTagList(opts.audienceVulnerability),
|
|
267
184
|
modality: parseTagList(opts.modality),
|
|
268
185
|
};
|
|
186
|
+
// ── Reference-grounded eval — acquire-once. Default: reference-grounded WHEN a
|
|
187
|
+
// populated corpus is present (it needs exemplars to ground against), else v1.
|
|
188
|
+
// `--reference`/`--reference-grounded` force it on; `--v1` forces it off. The
|
|
189
|
+
// corpus check is a plain fs read (no engine import), so the v1 path stays
|
|
190
|
+
// lazy: evalMode 'v1' yields an empty bundle, `...referenceCommonOpts` adds only
|
|
191
|
+
// `evalMode:'v1'`, and the pipeline's stage-6 branch is never entered. When ON,
|
|
192
|
+
// resolve the operator reference and load the exemplar corpus a SINGLE time here
|
|
193
|
+
// — before the page/rep loops — so a multi-page/rep run never re-reads them
|
|
194
|
+
// (protects the ±0.5 reproducibility gate). The engine is imported lazily. ──
|
|
195
|
+
const evalMode = opts.v1
|
|
196
|
+
? 'v1'
|
|
197
|
+
: opts.referenceGrounded || opts.reference || hasPopulatedCorpus()
|
|
198
|
+
? 'reference-grounded'
|
|
199
|
+
: 'v1';
|
|
200
|
+
const referenceCommonOpts = evalMode === 'reference-grounded'
|
|
201
|
+
? await setupReferenceGrounded(opts, modelName, provider)
|
|
202
|
+
: { evalMode };
|
|
269
203
|
// Telemetry: every design-audit invocation gets a stable runId. Child
|
|
270
204
|
// envelopes link back via parentRunId so fleet rollups can reconstruct
|
|
271
205
|
// the run tree.
|
|
@@ -335,6 +269,7 @@ export async function runDesignAudit(opts) {
|
|
|
335
269
|
provider,
|
|
336
270
|
model: modelName,
|
|
337
271
|
...ethicsCommonOpts,
|
|
272
|
+
...referenceCommonOpts,
|
|
338
273
|
});
|
|
339
274
|
const result = gen2;
|
|
340
275
|
results.push(result);
|
|
@@ -356,8 +291,35 @@ export async function runDesignAudit(opts) {
|
|
|
356
291
|
const deduped = detectSystemicFindings(perPage);
|
|
357
292
|
topFixes = topByRoi(deduped, 5);
|
|
358
293
|
}
|
|
294
|
+
// ── Reference-grounded: surface the rich redesign brief. Reference mode only —
|
|
295
|
+
// v1 results carry no `referenceArtifact`, so this block is skipped and both the
|
|
296
|
+
// report and the on-disk output are byte-identical to the legacy path. The
|
|
297
|
+
// render module is imported lazily (and is already module-cached in this mode),
|
|
298
|
+
// so a default audit never loads the reference engine. ──
|
|
299
|
+
let redesignSection;
|
|
300
|
+
if (evalMode === 'reference-grounded') {
|
|
301
|
+
const withArtifact = results.filter((r) => r.referenceArtifact);
|
|
302
|
+
if (withArtifact.length > 0) {
|
|
303
|
+
const { renderArtifactMarkdown, renderRedesignDirectionsSummary, renderRedesignTarget, artifactSlug } = await import('./design/audit/reference/index.js');
|
|
304
|
+
const sections = ['## Redesign directions', ''];
|
|
305
|
+
for (const r of withArtifact) {
|
|
306
|
+
const artifact = r.referenceArtifact;
|
|
307
|
+
const briefFile = `${artifactSlug(r.url)}.redesign.md`;
|
|
308
|
+
fs.writeFileSync(path.join(outputDir, briefFile), renderArtifactMarkdown(artifact));
|
|
309
|
+
sections.push(renderRedesignDirectionsSummary(artifact, briefFile));
|
|
310
|
+
console.log(` ${chalk.dim('Redesign brief →')} ${path.join(outputDir, briefFile)}`);
|
|
311
|
+
// Default (non-spawning) apply path: emit the implementation prompt a
|
|
312
|
+
// coding agent reads and runs ITSELF to apply this grounded redesign in
|
|
313
|
+
// its project. Spawning a coding agent stays opt-in (`--evolve --agent`).
|
|
314
|
+
const applyFile = `${artifactSlug(r.url)}.apply-prompt.md`;
|
|
315
|
+
fs.writeFileSync(path.join(outputDir, applyFile), buildApplyPrompt([r], profile, renderRedesignTarget(artifact)));
|
|
316
|
+
console.log(` ${chalk.dim('Apply prompt →')} ${path.join(outputDir, applyFile)}`);
|
|
317
|
+
}
|
|
318
|
+
redesignSection = sections.join('\n').trimEnd();
|
|
319
|
+
}
|
|
320
|
+
}
|
|
359
321
|
// Generate report
|
|
360
|
-
const report =
|
|
322
|
+
const report = generateDesignReport(results, profile, topFixes, redesignSection);
|
|
361
323
|
const reportPath = path.join(outputDir, 'report.md');
|
|
362
324
|
fs.writeFileSync(reportPath, report);
|
|
363
325
|
const allFindings = results.flatMap(r => r.findings);
|
|
@@ -420,6 +382,7 @@ export async function runDesignAudit(opts) {
|
|
|
420
382
|
model: modelName,
|
|
421
383
|
parentRunId: runId,
|
|
422
384
|
...ethicsCommonOpts,
|
|
385
|
+
...referenceCommonOpts,
|
|
423
386
|
});
|
|
424
387
|
repResults.push(gen2);
|
|
425
388
|
}
|
|
@@ -453,11 +416,11 @@ export async function runDesignAudit(opts) {
|
|
|
453
416
|
if (evolveMode !== 'css') {
|
|
454
417
|
// Agent-dispatched evolve — a coding agent edits the actual source code
|
|
455
418
|
const projectDir = opts.projectDir ?? process.cwd();
|
|
456
|
-
evolveResult = await runAgentEvolveLoop(brain, driver, page, pages, evolveProfile, results, outputDir, opts.evolveRounds ?? 3, evolveMode, projectDir, opts.debug, auditPasses, runId, provider, modelName);
|
|
419
|
+
evolveResult = await runAgentEvolveLoop(brain, driver, page, pages, evolveProfile, results, outputDir, opts.evolveRounds ?? 3, evolveMode, projectDir, opts.debug, auditPasses, runId, provider, modelName, referenceCommonOpts);
|
|
457
420
|
}
|
|
458
421
|
else {
|
|
459
422
|
// CSS-injection evolve — ephemeral fixes injected into the browser page
|
|
460
|
-
evolveResult = await runEvolveLoop(brain, driver, page, pages, evolveProfile, results, outputDir, opts.evolveRounds ?? 3, auditPasses, runId, provider, modelName);
|
|
423
|
+
evolveResult = await runEvolveLoop(brain, driver, page, pages, evolveProfile, results, outputDir, opts.evolveRounds ?? 3, auditPasses, runId, provider, modelName, referenceCommonOpts);
|
|
461
424
|
}
|
|
462
425
|
// Write evolve report
|
|
463
426
|
const evolvePath = path.join(outputDir, 'evolve-report.md');
|
|
@@ -528,1478 +491,6 @@ export async function runDesignAudit(opts) {
|
|
|
528
491
|
});
|
|
529
492
|
await browser.close();
|
|
530
493
|
}
|
|
531
|
-
async function runEvolveLoop(brain, driver, page, pages, profile, initialResults, outputDir, maxRounds, auditPasses, parentRunId, provider, model) {
|
|
532
|
-
const telemetry = parentRunId ? getTelemetry() : undefined;
|
|
533
|
-
const initialAvg = initialResults.reduce((s, r) => s + r.score, 0) / initialResults.length;
|
|
534
|
-
const scoreHistory = [initialAvg];
|
|
535
|
-
const appliedFixes = [];
|
|
536
|
-
const skippedFixes = [];
|
|
537
|
-
let cumulativeCSS = '';
|
|
538
|
-
let currentResults = initialResults;
|
|
539
|
-
let currentAvg = initialAvg;
|
|
540
|
-
console.log('');
|
|
541
|
-
console.log(` ${chalk.bold('Design Evolve')} — ${maxRounds} rounds max`);
|
|
542
|
-
console.log(` ${chalk.dim('Initial score:')} ${currentAvg.toFixed(1)}/10`);
|
|
543
|
-
console.log('');
|
|
544
|
-
for (let round = 1; round <= maxRounds; round++) {
|
|
545
|
-
console.log(` ${chalk.dim(`Round ${round}/${maxRounds}`)}`);
|
|
546
|
-
// Collect all findings with CSS fixes across all pages
|
|
547
|
-
const fixableFixes = currentResults
|
|
548
|
-
.flatMap(r => r.findings)
|
|
549
|
-
.filter(f => f.cssSelector && f.cssFix);
|
|
550
|
-
if (fixableFixes.length === 0) {
|
|
551
|
-
console.log(` ${chalk.dim(' No CSS-fixable findings — generating fixes via LLM…')}`);
|
|
552
|
-
// Ask the LLM to generate CSS fixes for the top findings
|
|
553
|
-
const topFindings = currentResults
|
|
554
|
-
.flatMap(r => r.findings)
|
|
555
|
-
.filter(f => f.severity === 'critical' || f.severity === 'major')
|
|
556
|
-
.slice(0, 10);
|
|
557
|
-
if (topFindings.length === 0) {
|
|
558
|
-
console.log(` ${chalk.green(' No major/critical findings remaining')}`);
|
|
559
|
-
break;
|
|
560
|
-
}
|
|
561
|
-
const fixPrompt = buildFixGenerationPrompt(topFindings);
|
|
562
|
-
const fixResult = await brain.auditDesign(await driver.observe(), 'Generate CSS fixes for the design issues listed below', [], fixPrompt);
|
|
563
|
-
// Parse generated fixes
|
|
564
|
-
try {
|
|
565
|
-
let text = fixResult.raw.trim();
|
|
566
|
-
if (text.startsWith('```'))
|
|
567
|
-
text = text.replace(/^```(?:json)?\n?/, '').replace(/\n?```$/, '');
|
|
568
|
-
const start = text.indexOf('{');
|
|
569
|
-
const end = text.lastIndexOf('}');
|
|
570
|
-
if (start >= 0 && end > start)
|
|
571
|
-
text = text.slice(start, end + 1);
|
|
572
|
-
const parsed = JSON.parse(text);
|
|
573
|
-
if (Array.isArray(parsed.fixes)) {
|
|
574
|
-
for (const fix of parsed.fixes) {
|
|
575
|
-
if (fix.cssSelector && fix.cssFix) {
|
|
576
|
-
fixableFixes.push({
|
|
577
|
-
category: 'ux',
|
|
578
|
-
severity: 'major',
|
|
579
|
-
description: fix.description || '',
|
|
580
|
-
location: fix.location || '',
|
|
581
|
-
suggestion: fix.cssFix,
|
|
582
|
-
cssSelector: fix.cssSelector,
|
|
583
|
-
cssFix: fix.cssFix,
|
|
584
|
-
});
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
catch { /* failed to parse fixes */ }
|
|
590
|
-
}
|
|
591
|
-
if (fixableFixes.length === 0) {
|
|
592
|
-
console.log(` ${chalk.dim(' Could not generate fixable CSS — stopping')}`);
|
|
593
|
-
break;
|
|
594
|
-
}
|
|
595
|
-
// Build CSS override from all fixable findings
|
|
596
|
-
const roundCSS = fixableFixes
|
|
597
|
-
.map(f => `/* ${f.severity}: ${f.description?.slice(0, 80)} */\n${f.cssSelector} { ${f.cssFix} }`)
|
|
598
|
-
.join('\n\n');
|
|
599
|
-
cumulativeCSS += '\n' + roundCSS;
|
|
600
|
-
// Track applied fixes
|
|
601
|
-
for (const f of fixableFixes) {
|
|
602
|
-
appliedFixes.push({
|
|
603
|
-
cssSelector: f.cssSelector,
|
|
604
|
-
cssFix: f.cssFix,
|
|
605
|
-
finding: f.description,
|
|
606
|
-
});
|
|
607
|
-
}
|
|
608
|
-
console.log(` ${chalk.dim(` Applying ${fixableFixes.length} CSS fixes…`)}`);
|
|
609
|
-
// Re-audit each page with CSS injected
|
|
610
|
-
const roundResults = [];
|
|
611
|
-
for (const url of pages) {
|
|
612
|
-
try {
|
|
613
|
-
await page.goto(url, { waitUntil: 'networkidle', timeout: 20_000 }).catch(() => page.goto(url, { waitUntil: 'domcontentloaded', timeout: 15_000 }));
|
|
614
|
-
await page.waitForTimeout(1500);
|
|
615
|
-
// Inject cumulative CSS fixes
|
|
616
|
-
await page.addStyleTag({ content: cumulativeCSS });
|
|
617
|
-
await page.waitForTimeout(500);
|
|
618
|
-
// Take screenshot of fixed state
|
|
619
|
-
const screenshotDir = path.join(outputDir, `screenshots-round-${round}`);
|
|
620
|
-
fs.mkdirSync(screenshotDir, { recursive: true });
|
|
621
|
-
const result = (await auditOnePage({
|
|
622
|
-
brain,
|
|
623
|
-
driver,
|
|
624
|
-
page,
|
|
625
|
-
url,
|
|
626
|
-
profileOverride: profile,
|
|
627
|
-
screenshotDir,
|
|
628
|
-
auditPasses,
|
|
629
|
-
...(parentRunId ? { runId: parentRunId, parentRunId } : {}),
|
|
630
|
-
...(provider ? { provider } : {}),
|
|
631
|
-
...(model ? { model } : {}),
|
|
632
|
-
}));
|
|
633
|
-
roundResults.push(result);
|
|
634
|
-
}
|
|
635
|
-
catch {
|
|
636
|
-
roundResults.push({
|
|
637
|
-
url,
|
|
638
|
-
score: currentAvg,
|
|
639
|
-
summary: 'Re-audit failed',
|
|
640
|
-
strengths: [],
|
|
641
|
-
findings: [],
|
|
642
|
-
error: 'Re-audit with CSS injection failed',
|
|
643
|
-
});
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
const roundAvg = roundResults.reduce((s, r) => s + r.score, 0) / roundResults.length;
|
|
647
|
-
scoreHistory.push(roundAvg);
|
|
648
|
-
const delta = roundAvg - currentAvg;
|
|
649
|
-
const deltaStr = delta >= 0 ? chalk.green(`+${delta.toFixed(1)}`) : chalk.red(delta.toFixed(1));
|
|
650
|
-
console.log(` ${chalk.dim(' Score:')} ${roundAvg.toFixed(1)}/10 (${deltaStr})`);
|
|
651
|
-
if (telemetry && parentRunId) {
|
|
652
|
-
telemetry.emit({
|
|
653
|
-
kind: 'design-evolve-round',
|
|
654
|
-
runId: parentRunId,
|
|
655
|
-
parentRunId,
|
|
656
|
-
ok: true,
|
|
657
|
-
durationMs: 0, // round-level wall time would require a per-round timer; the parent run captures total duration
|
|
658
|
-
...(provider && model ? { model: { provider, name: model } } : {}),
|
|
659
|
-
data: {
|
|
660
|
-
mode: 'css',
|
|
661
|
-
round,
|
|
662
|
-
fixesApplied: fixableFixes.length,
|
|
663
|
-
},
|
|
664
|
-
metrics: {
|
|
665
|
-
round,
|
|
666
|
-
beforeScore: currentAvg,
|
|
667
|
-
afterScore: roundAvg,
|
|
668
|
-
delta,
|
|
669
|
-
fixesApplied: fixableFixes.length,
|
|
670
|
-
},
|
|
671
|
-
});
|
|
672
|
-
}
|
|
673
|
-
currentResults = roundResults;
|
|
674
|
-
currentAvg = roundAvg;
|
|
675
|
-
// Check convergence — if no improvement, stop
|
|
676
|
-
if (delta <= 0.1 && round > 1) {
|
|
677
|
-
console.log(` ${chalk.dim(' Converged — no further improvement')}`);
|
|
678
|
-
break;
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
const totalDelta = currentAvg - initialAvg;
|
|
682
|
-
const deltaColor = totalDelta >= 2 ? chalk.green : totalDelta > 0 ? chalk.yellow : chalk.red;
|
|
683
|
-
console.log('');
|
|
684
|
-
console.log(` ${chalk.bold('Evolve complete')}`);
|
|
685
|
-
console.log(` ${chalk.dim('Score:')} ${initialAvg.toFixed(1)} → ${currentAvg.toFixed(1)} (${deltaColor(`+${totalDelta.toFixed(1)}`)})`);
|
|
686
|
-
console.log(` ${chalk.dim('Rounds:')} ${scoreHistory.length - 1}`);
|
|
687
|
-
console.log(` ${chalk.dim('Fixes applied:')} ${appliedFixes.length}`);
|
|
688
|
-
console.log('');
|
|
689
|
-
return {
|
|
690
|
-
beforeScore: initialAvg,
|
|
691
|
-
afterScore: currentAvg,
|
|
692
|
-
delta: totalDelta,
|
|
693
|
-
rounds: scoreHistory.length - 1,
|
|
694
|
-
appliedFixes,
|
|
695
|
-
skippedFixes,
|
|
696
|
-
scoreHistory,
|
|
697
|
-
cssOverride: cumulativeCSS.trim(),
|
|
698
|
-
};
|
|
699
|
-
}
|
|
700
|
-
function buildFixGenerationPrompt(findings) {
|
|
701
|
-
const findingList = findings.map((f, i) => `${i + 1}. [${f.severity}/${f.category}] ${f.description}\n Location: ${f.location}\n Suggestion: ${f.suggestion}`).join('\n');
|
|
702
|
-
return `You are a CSS engineer fixing design issues. For each finding, generate a precise CSS fix.
|
|
703
|
-
|
|
704
|
-
FINDINGS TO FIX:
|
|
705
|
-
${findingList}
|
|
706
|
-
|
|
707
|
-
RULES:
|
|
708
|
-
- Use specific, targeted CSS selectors. Prefer class-based or semantic selectors.
|
|
709
|
-
- Each fix should be a single CSS rule (selector + property:value pairs).
|
|
710
|
-
- Fixes must not break other elements — be surgical.
|
|
711
|
-
- For spacing: use consistent values (multiples of 4 or 8px).
|
|
712
|
-
- For colors: ensure WCAG AA contrast (4.5:1 for text, 3:1 for large text).
|
|
713
|
-
- For typography: use a limited scale (14px, 16px, 20px, 24px, 32px, 48px).
|
|
714
|
-
|
|
715
|
-
RESPOND WITH ONLY a JSON object:
|
|
716
|
-
{
|
|
717
|
-
"fixes": [
|
|
718
|
-
{
|
|
719
|
-
"cssSelector": "main > section:first-child",
|
|
720
|
-
"cssFix": "padding-bottom: 48px; margin-bottom: 0",
|
|
721
|
-
"description": "Standardize hero section bottom spacing",
|
|
722
|
-
"location": "Hero → features transition"
|
|
723
|
-
}
|
|
724
|
-
]
|
|
725
|
-
}`;
|
|
726
|
-
}
|
|
727
|
-
function generateEvolveReport(result) {
|
|
728
|
-
const lines = [];
|
|
729
|
-
lines.push('# Design Evolve Report');
|
|
730
|
-
lines.push('');
|
|
731
|
-
lines.push(`**Score:** ${result.beforeScore.toFixed(1)} → ${result.afterScore.toFixed(1)} (+${result.delta.toFixed(1)})`);
|
|
732
|
-
lines.push(`**Rounds:** ${result.rounds}`);
|
|
733
|
-
lines.push(`**Score progression:** ${result.scoreHistory.map(s => s.toFixed(1)).join(' → ')}`);
|
|
734
|
-
lines.push('');
|
|
735
|
-
if (result.appliedFixes.length > 0) {
|
|
736
|
-
lines.push('## Applied Fixes');
|
|
737
|
-
lines.push('');
|
|
738
|
-
for (const fix of result.appliedFixes) {
|
|
739
|
-
lines.push(`- \`${fix.cssSelector}\`: \`${fix.cssFix}\``);
|
|
740
|
-
if (fix.finding)
|
|
741
|
-
lines.push(` - ${fix.finding}`);
|
|
742
|
-
}
|
|
743
|
-
lines.push('');
|
|
744
|
-
}
|
|
745
|
-
if (result.cssOverride) {
|
|
746
|
-
lines.push('## Generated CSS Override');
|
|
747
|
-
lines.push('');
|
|
748
|
-
lines.push('```css');
|
|
749
|
-
lines.push(result.cssOverride);
|
|
750
|
-
lines.push('```');
|
|
751
|
-
lines.push('');
|
|
752
|
-
lines.push('Apply this CSS to your app to fix the identified design issues:');
|
|
753
|
-
lines.push('```html');
|
|
754
|
-
lines.push('<link rel="stylesheet" href="design-fixes.css">');
|
|
755
|
-
lines.push('```');
|
|
756
|
-
}
|
|
757
|
-
return lines.join('\n');
|
|
758
|
-
}
|
|
759
|
-
// ---------------------------------------------------------------------------
|
|
760
|
-
// Agent-dispatched evolve — sends findings to a coding agent that edits source
|
|
761
|
-
// ---------------------------------------------------------------------------
|
|
762
|
-
const AGENT_COMMANDS = {
|
|
763
|
-
'claude-code': (prompt, dir) => ['claude', '-p', prompt, '--dangerously-skip-permissions', '--add-dir', dir],
|
|
764
|
-
'codex': (prompt, dir) => ['codex', 'exec', prompt, '-c', `cwd="${dir}"`],
|
|
765
|
-
'opencode': (prompt, dir) => ['opencode', 'run', prompt],
|
|
766
|
-
};
|
|
767
|
-
function resolveAgentCommand(agent, prompt, projectDir) {
|
|
768
|
-
const builder = AGENT_COMMANDS[agent];
|
|
769
|
-
if (builder) {
|
|
770
|
-
const [cmd, ...args] = builder(prompt, projectDir);
|
|
771
|
-
return { cmd, args, cwd: projectDir };
|
|
772
|
-
}
|
|
773
|
-
// Custom command — treat the agent string as a command template
|
|
774
|
-
// e.g. "aider --message" becomes: aider --message "<prompt>"
|
|
775
|
-
const parts = agent.split(/\s+/);
|
|
776
|
-
return { cmd: parts[0], args: [...parts.slice(1), prompt], cwd: projectDir };
|
|
777
|
-
}
|
|
778
|
-
function buildAgentFixPrompt(results, profile, round) {
|
|
779
|
-
const allFindings = results.flatMap(r => r.findings);
|
|
780
|
-
const critical = allFindings.filter(f => f.severity === 'critical');
|
|
781
|
-
const major = allFindings.filter(f => f.severity === 'major');
|
|
782
|
-
const minor = allFindings.filter(f => f.severity === 'minor');
|
|
783
|
-
const findingsList = [...critical, ...major, ...minor.slice(0, 5)]
|
|
784
|
-
.map((f, i) => {
|
|
785
|
-
let entry = `${i + 1}. [${f.severity}/${f.category}] ${f.description}`;
|
|
786
|
-
entry += `\n Location: ${f.location}`;
|
|
787
|
-
entry += `\n Suggestion: ${f.suggestion}`;
|
|
788
|
-
if (f.cssSelector)
|
|
789
|
-
entry += `\n CSS Selector: ${f.cssSelector}`;
|
|
790
|
-
if (f.cssFix)
|
|
791
|
-
entry += `\n CSS Fix: ${f.cssFix}`;
|
|
792
|
-
return entry;
|
|
793
|
-
})
|
|
794
|
-
.join('\n\n');
|
|
795
|
-
const scoreBreakdowns = results
|
|
796
|
-
.filter(r => r.designSystemScore)
|
|
797
|
-
.map(r => {
|
|
798
|
-
const ds = r.designSystemScore;
|
|
799
|
-
return ` ${r.url}: ${Object.entries(ds).map(([k, v]) => `${k}=${v}`).join(', ')}`;
|
|
800
|
-
})
|
|
801
|
-
.join('\n');
|
|
802
|
-
return `You are fixing design issues found by an automated design audit.
|
|
803
|
-
|
|
804
|
-
AUDIT PROFILE: ${profile}
|
|
805
|
-
ROUND: ${round} (${round === 1 ? 'initial fixes' : 'fixing remaining issues from previous round'})
|
|
806
|
-
CURRENT SCORES:
|
|
807
|
-
Overall: ${(results.reduce((s, r) => s + r.score, 0) / results.length).toFixed(1)}/10
|
|
808
|
-
${scoreBreakdowns}
|
|
809
|
-
|
|
810
|
-
FINDINGS TO FIX (${critical.length} critical, ${major.length} major, ${minor.length} minor):
|
|
811
|
-
|
|
812
|
-
${findingsList}
|
|
813
|
-
|
|
814
|
-
INSTRUCTIONS:
|
|
815
|
-
1. Read the project's source files to understand the styling approach (Tailwind, CSS modules, plain CSS, styled-components, etc.)
|
|
816
|
-
2. Fix the findings by editing the ACTUAL SOURCE FILES — not by creating new CSS override files
|
|
817
|
-
3. Match the project's existing styling conventions
|
|
818
|
-
4. Fix the design SYSTEM (shared components, tokens, globals) not individual instances
|
|
819
|
-
5. Prioritize critical and major findings
|
|
820
|
-
6. Only change visual/styling properties — never change business logic, state, or event handlers
|
|
821
|
-
7. After making changes, verify the dev server is still running (no build errors)
|
|
822
|
-
|
|
823
|
-
Do NOT:
|
|
824
|
-
- Create new standalone CSS override files — edit the existing styles
|
|
825
|
-
- Add comments explaining what you changed — just change it
|
|
826
|
-
- Refactor unrelated code
|
|
827
|
-
- Change component structure or HTML semantics unless a finding specifically requires it`;
|
|
828
|
-
}
|
|
829
|
-
async function runAgentEvolveLoop(brain, driver, page, pages, profile, initialResults, outputDir, maxRounds, agentName, projectDir, debug, auditPasses, parentRunId, provider, model) {
|
|
830
|
-
const telemetry = parentRunId ? getTelemetry() : undefined;
|
|
831
|
-
const initialAvg = initialResults.reduce((s, r) => s + r.score, 0) / initialResults.length;
|
|
832
|
-
const scoreHistory = [initialAvg];
|
|
833
|
-
const appliedFixes = [];
|
|
834
|
-
let currentResults = initialResults;
|
|
835
|
-
let currentAvg = initialAvg;
|
|
836
|
-
const resolvedProjectDir = path.resolve(projectDir);
|
|
837
|
-
if (!fs.existsSync(resolvedProjectDir)) {
|
|
838
|
-
cliError(`project directory not found: ${resolvedProjectDir}`);
|
|
839
|
-
process.exit(1);
|
|
840
|
-
}
|
|
841
|
-
console.log('');
|
|
842
|
-
console.log(` ${chalk.bold('Design Evolve')} ${chalk.dim('via')} ${chalk.cyan(agentName)}`);
|
|
843
|
-
console.log(` ${chalk.dim('Project:')} ${resolvedProjectDir}`);
|
|
844
|
-
console.log(` ${chalk.dim('Initial score:')} ${currentAvg.toFixed(1)}/10`);
|
|
845
|
-
console.log(` ${chalk.dim('Max rounds:')} ${maxRounds}`);
|
|
846
|
-
console.log('');
|
|
847
|
-
for (let round = 1; round <= maxRounds; round++) {
|
|
848
|
-
console.log(` ${chalk.dim(`Round ${round}/${maxRounds}`)}`);
|
|
849
|
-
// Build the prompt for the agent
|
|
850
|
-
const prompt = buildAgentFixPrompt(currentResults, profile, round);
|
|
851
|
-
// Write the prompt to a file for debugging
|
|
852
|
-
const promptPath = path.join(outputDir, `agent-prompt-round-${round}.txt`);
|
|
853
|
-
fs.writeFileSync(promptPath, prompt);
|
|
854
|
-
// Also write the full report JSON so the agent could read it
|
|
855
|
-
const findingsPath = path.join(outputDir, `findings-round-${round}.json`);
|
|
856
|
-
fs.writeFileSync(findingsPath, JSON.stringify({
|
|
857
|
-
round,
|
|
858
|
-
score: currentAvg,
|
|
859
|
-
results: currentResults.map(r => ({
|
|
860
|
-
url: r.url,
|
|
861
|
-
score: r.score,
|
|
862
|
-
designSystemScore: r.designSystemScore,
|
|
863
|
-
findings: r.findings,
|
|
864
|
-
})),
|
|
865
|
-
}, null, 2));
|
|
866
|
-
// Dispatch to the coding agent
|
|
867
|
-
const { cmd, args, cwd } = resolveAgentCommand(agentName, prompt, resolvedProjectDir);
|
|
868
|
-
console.log(` ${chalk.dim(` Dispatching to ${agentName}…`)}`);
|
|
869
|
-
if (debug) {
|
|
870
|
-
console.log(` ${chalk.dim(` cmd: ${cmd} ${args.map(a => a.length > 80 ? a.slice(0, 80) + '…' : a).join(' ')}`)}`);
|
|
871
|
-
}
|
|
872
|
-
try {
|
|
873
|
-
const result = execSync(`${cmd} ${args.map(a => JSON.stringify(a)).join(' ')}`, {
|
|
874
|
-
cwd,
|
|
875
|
-
stdio: debug ? 'inherit' : 'pipe',
|
|
876
|
-
timeout: 300_000, // 5min max per agent round
|
|
877
|
-
env: { ...process.env },
|
|
878
|
-
});
|
|
879
|
-
if (!debug && result) {
|
|
880
|
-
const agentOutputPath = path.join(outputDir, `agent-output-round-${round}.txt`);
|
|
881
|
-
fs.writeFileSync(agentOutputPath, result.toString());
|
|
882
|
-
}
|
|
883
|
-
console.log(` ${chalk.dim(' Agent completed')}`);
|
|
884
|
-
}
|
|
885
|
-
catch (err) {
|
|
886
|
-
const exitCode = err.status ?? 'unknown';
|
|
887
|
-
console.log(` ${chalk.yellow(` Agent exited with code ${exitCode} — continuing with re-audit`)}`);
|
|
888
|
-
// Write stderr if available
|
|
889
|
-
const stderr = err.stderr;
|
|
890
|
-
if (stderr) {
|
|
891
|
-
const errPath = path.join(outputDir, `agent-error-round-${round}.txt`);
|
|
892
|
-
fs.writeFileSync(errPath, stderr.toString());
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
// Wait for hot reload to settle
|
|
896
|
-
console.log(` ${chalk.dim(' Waiting for hot reload…')}`);
|
|
897
|
-
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
898
|
-
// Re-audit
|
|
899
|
-
console.log(` ${chalk.dim(' Re-auditing…')}`);
|
|
900
|
-
const roundResults = [];
|
|
901
|
-
const roundScreenshotDir = path.join(outputDir, `screenshots-round-${round}`);
|
|
902
|
-
fs.mkdirSync(roundScreenshotDir, { recursive: true });
|
|
903
|
-
for (const url of pages) {
|
|
904
|
-
const result = (await auditOnePage({
|
|
905
|
-
brain,
|
|
906
|
-
driver,
|
|
907
|
-
page,
|
|
908
|
-
url,
|
|
909
|
-
profileOverride: profile,
|
|
910
|
-
screenshotDir: roundScreenshotDir,
|
|
911
|
-
auditPasses,
|
|
912
|
-
...(parentRunId ? { runId: parentRunId, parentRunId } : {}),
|
|
913
|
-
...(provider ? { provider } : {}),
|
|
914
|
-
...(model ? { model } : {}),
|
|
915
|
-
}));
|
|
916
|
-
roundResults.push(result);
|
|
917
|
-
}
|
|
918
|
-
const roundAvg = roundResults.reduce((s, r) => s + r.score, 0) / roundResults.length;
|
|
919
|
-
scoreHistory.push(roundAvg);
|
|
920
|
-
const delta = roundAvg - currentAvg;
|
|
921
|
-
const deltaStr = delta >= 0 ? chalk.green(`+${delta.toFixed(1)}`) : chalk.red(delta.toFixed(1));
|
|
922
|
-
console.log(` ${chalk.dim(' Score:')} ${roundAvg.toFixed(1)}/10 (${deltaStr})`);
|
|
923
|
-
// Track what changed
|
|
924
|
-
const prevFindingCount = currentResults.flatMap(r => r.findings).length;
|
|
925
|
-
const newFindingCount = roundResults.flatMap(r => r.findings).length;
|
|
926
|
-
const resolvedCount = Math.max(0, prevFindingCount - newFindingCount);
|
|
927
|
-
if (resolvedCount > 0) {
|
|
928
|
-
appliedFixes.push({
|
|
929
|
-
cssSelector: `round-${round}`,
|
|
930
|
-
cssFix: `${agentName} resolved ${resolvedCount} findings`,
|
|
931
|
-
finding: `Score: ${currentAvg.toFixed(1)} → ${roundAvg.toFixed(1)}`,
|
|
932
|
-
});
|
|
933
|
-
}
|
|
934
|
-
if (telemetry && parentRunId) {
|
|
935
|
-
telemetry.emit({
|
|
936
|
-
kind: 'design-evolve-round',
|
|
937
|
-
runId: parentRunId,
|
|
938
|
-
parentRunId,
|
|
939
|
-
ok: true,
|
|
940
|
-
durationMs: 0,
|
|
941
|
-
...(provider && model ? { model: { provider, name: model } } : {}),
|
|
942
|
-
data: {
|
|
943
|
-
mode: `agent:${agentName}`,
|
|
944
|
-
round,
|
|
945
|
-
resolvedCount,
|
|
946
|
-
},
|
|
947
|
-
metrics: {
|
|
948
|
-
round,
|
|
949
|
-
beforeScore: currentAvg,
|
|
950
|
-
afterScore: roundAvg,
|
|
951
|
-
delta,
|
|
952
|
-
findingsResolved: resolvedCount,
|
|
953
|
-
},
|
|
954
|
-
});
|
|
955
|
-
}
|
|
956
|
-
currentResults = roundResults;
|
|
957
|
-
currentAvg = roundAvg;
|
|
958
|
-
// Check convergence
|
|
959
|
-
if (delta <= 0.1 && round > 1) {
|
|
960
|
-
console.log(` ${chalk.dim(' Converged — no further improvement')}`);
|
|
961
|
-
break;
|
|
962
|
-
}
|
|
963
|
-
}
|
|
964
|
-
const totalDelta = currentAvg - initialAvg;
|
|
965
|
-
const deltaColor = totalDelta >= 2 ? chalk.green : totalDelta > 0 ? chalk.yellow : chalk.red;
|
|
966
|
-
console.log('');
|
|
967
|
-
console.log(` ${chalk.bold('Evolve complete')} ${chalk.dim('via')} ${chalk.cyan(agentName)}`);
|
|
968
|
-
console.log(` ${chalk.dim('Score:')} ${initialAvg.toFixed(1)} → ${currentAvg.toFixed(1)} (${deltaColor(totalDelta >= 0 ? `+${totalDelta.toFixed(1)}` : totalDelta.toFixed(1))})`);
|
|
969
|
-
console.log(` ${chalk.dim('Rounds:')} ${scoreHistory.length - 1}`);
|
|
970
|
-
console.log('');
|
|
971
|
-
return {
|
|
972
|
-
beforeScore: initialAvg,
|
|
973
|
-
afterScore: currentAvg,
|
|
974
|
-
delta: totalDelta,
|
|
975
|
-
rounds: scoreHistory.length - 1,
|
|
976
|
-
appliedFixes,
|
|
977
|
-
skippedFixes: [],
|
|
978
|
-
scoreHistory,
|
|
979
|
-
cssOverride: '', // no CSS override in agent mode — agent edited source directly
|
|
980
|
-
};
|
|
981
|
-
}
|
|
982
|
-
// ---------------------------------------------------------------------------
|
|
983
|
-
// Design Token Extraction — pure DOM, no LLM calls
|
|
984
|
-
// ---------------------------------------------------------------------------
|
|
985
|
-
const VIEWPORTS = [
|
|
986
|
-
{ name: 'mobile', width: 375, height: 812 },
|
|
987
|
-
{ name: 'tablet', width: 768, height: 1024 },
|
|
988
|
-
{ name: 'desktop', width: 1440, height: 900 },
|
|
989
|
-
];
|
|
990
|
-
// The in-page extraction function — runs inside page.evaluate()
|
|
991
|
-
function extractTokensFromDOM() {
|
|
992
|
-
const MAX_ELEMENTS = 50_000;
|
|
993
|
-
// --- CSS Custom Properties & Font Files ---
|
|
994
|
-
const customProperties = {};
|
|
995
|
-
const fontFiles = [];
|
|
996
|
-
const externalStylesheetUrls = [];
|
|
997
|
-
for (const sheet of document.styleSheets) {
|
|
998
|
-
if (sheet.href && (sheet.href.startsWith('http://') || sheet.href.startsWith('https://'))) {
|
|
999
|
-
externalStylesheetUrls.push(sheet.href);
|
|
1000
|
-
}
|
|
1001
|
-
try {
|
|
1002
|
-
for (const rule of sheet.cssRules) {
|
|
1003
|
-
if (rule instanceof CSSStyleRule &&
|
|
1004
|
-
(rule.selectorText === ':root' || rule.selectorText?.includes('html') || rule.selectorText?.includes('body'))) {
|
|
1005
|
-
for (let i = 0; i < rule.style.length; i++) {
|
|
1006
|
-
const prop = rule.style[i];
|
|
1007
|
-
if (prop.startsWith('--')) {
|
|
1008
|
-
customProperties[prop] = rule.style.getPropertyValue(prop).trim();
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
|
-
}
|
|
1012
|
-
if (rule instanceof CSSFontFaceRule) {
|
|
1013
|
-
const family = rule.style.getPropertyValue('font-family').replace(/['"]/g, '').trim();
|
|
1014
|
-
const weight = rule.style.getPropertyValue('font-weight') || '400';
|
|
1015
|
-
const style = rule.style.getPropertyValue('font-style') || 'normal';
|
|
1016
|
-
const src = rule.style.getPropertyValue('src');
|
|
1017
|
-
const urlMatch = src.match(/url\(["']?([^"')]+)["']?\)/);
|
|
1018
|
-
const formatMatch = src.match(/format\(["']?([^"')]+)["']?\)/);
|
|
1019
|
-
if (urlMatch) {
|
|
1020
|
-
fontFiles.push({
|
|
1021
|
-
family,
|
|
1022
|
-
weight,
|
|
1023
|
-
style,
|
|
1024
|
-
src: urlMatch[1],
|
|
1025
|
-
format: formatMatch?.[1] || 'unknown',
|
|
1026
|
-
});
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
|
-
catch { /* cross-origin stylesheet */ }
|
|
1032
|
-
}
|
|
1033
|
-
// Supplement with document.fonts API (catches cross-origin @font-face)
|
|
1034
|
-
const seenFontSrcs = new Set(fontFiles.map(f => f.src));
|
|
1035
|
-
try {
|
|
1036
|
-
for (const face of document.fonts) {
|
|
1037
|
-
if (face.status === 'loaded') {
|
|
1038
|
-
// face.family is the CSS font-family, face.weight/style are strings
|
|
1039
|
-
// Check if we already captured this via @font-face rules
|
|
1040
|
-
const family = face.family.replace(/['"]/g, '').trim();
|
|
1041
|
-
const weight = face.weight || '400';
|
|
1042
|
-
const style = face.style || 'normal';
|
|
1043
|
-
// document.fonts doesn't expose the URL, but we can match against loaded resources
|
|
1044
|
-
if (!fontFiles.some(f => f.family === family && f.weight === weight && f.style === style)) {
|
|
1045
|
-
// Will be supplemented by Performance API below
|
|
1046
|
-
}
|
|
1047
|
-
}
|
|
1048
|
-
}
|
|
1049
|
-
}
|
|
1050
|
-
catch { /* older browser */ }
|
|
1051
|
-
// Use Performance API to find all loaded font resources (works cross-origin)
|
|
1052
|
-
try {
|
|
1053
|
-
const resources = performance.getEntriesByType('resource');
|
|
1054
|
-
for (const r of resources) {
|
|
1055
|
-
if (/\.(woff2?|ttf|otf|eot)(\?|$)/i.test(r.name) && !seenFontSrcs.has(r.name)) {
|
|
1056
|
-
seenFontSrcs.add(r.name);
|
|
1057
|
-
const formatMatch = r.name.match(/\.(woff2?|ttf|otf|eot)/i);
|
|
1058
|
-
fontFiles.push({
|
|
1059
|
-
family: '', // can't determine from URL alone
|
|
1060
|
-
weight: '',
|
|
1061
|
-
style: '',
|
|
1062
|
-
src: r.name,
|
|
1063
|
-
format: formatMatch?.[1] || 'unknown',
|
|
1064
|
-
});
|
|
1065
|
-
}
|
|
1066
|
-
}
|
|
1067
|
-
}
|
|
1068
|
-
catch { /* Performance API not available */ }
|
|
1069
|
-
// --- Element traversal ---
|
|
1070
|
-
const colorMap = new Map();
|
|
1071
|
-
const spacingMap = new Map();
|
|
1072
|
-
const borderRadiusMap = new Map();
|
|
1073
|
-
const shadowMap = new Map();
|
|
1074
|
-
const fontFamilyMap = new Map();
|
|
1075
|
-
const typeScaleMap = new Map();
|
|
1076
|
-
const animationMap = new Map();
|
|
1077
|
-
const backgroundImageUrls = new Set();
|
|
1078
|
-
function addColor(value, property) {
|
|
1079
|
-
if (!value || value === 'transparent' || value === 'rgba(0, 0, 0, 0)' || value === 'inherit' || value === 'initial')
|
|
1080
|
-
return;
|
|
1081
|
-
const entry = colorMap.get(value);
|
|
1082
|
-
if (entry) {
|
|
1083
|
-
entry.count++;
|
|
1084
|
-
entry.properties.add(property);
|
|
1085
|
-
}
|
|
1086
|
-
else {
|
|
1087
|
-
colorMap.set(value, { count: 1, properties: new Set([property]) });
|
|
1088
|
-
}
|
|
1089
|
-
}
|
|
1090
|
-
function addSpacing(value, property) {
|
|
1091
|
-
if (!value || value === '0px' || value === 'auto' || value === 'normal')
|
|
1092
|
-
return;
|
|
1093
|
-
// Normalize multi-value shorthands — take individual values
|
|
1094
|
-
const vals = value.split(/\s+/).filter(v => v !== '0px' && v !== 'auto');
|
|
1095
|
-
for (const v of vals) {
|
|
1096
|
-
if (!v.endsWith('px') && !v.endsWith('rem') && !v.endsWith('em'))
|
|
1097
|
-
continue;
|
|
1098
|
-
const entry = spacingMap.get(v);
|
|
1099
|
-
if (entry) {
|
|
1100
|
-
entry.count++;
|
|
1101
|
-
entry.properties.add(property);
|
|
1102
|
-
}
|
|
1103
|
-
else {
|
|
1104
|
-
spacingMap.set(v, { count: 1, properties: new Set([property]) });
|
|
1105
|
-
}
|
|
1106
|
-
}
|
|
1107
|
-
}
|
|
1108
|
-
function parseGradientColors(bg) {
|
|
1109
|
-
const colorRegex = /(?:rgba?\([^)]+\)|hsla?\([^)]+\)|#[0-9a-fA-F]{3,8})/g;
|
|
1110
|
-
const matches = bg.match(colorRegex) || [];
|
|
1111
|
-
for (const c of matches)
|
|
1112
|
-
addColor(c, 'gradient');
|
|
1113
|
-
}
|
|
1114
|
-
function parseShadowColors(shadow) {
|
|
1115
|
-
if (shadow === 'none')
|
|
1116
|
-
return;
|
|
1117
|
-
const colorRegex = /(?:rgba?\([^)]+\)|hsla?\([^)]+\)|#[0-9a-fA-F]{3,8})/g;
|
|
1118
|
-
const matches = shadow.match(colorRegex) || [];
|
|
1119
|
-
for (const c of matches)
|
|
1120
|
-
addColor(c, 'boxShadow');
|
|
1121
|
-
}
|
|
1122
|
-
const HEADING_TAGS = new Set(['H1', 'H2', 'H3', 'H4', 'H5', 'H6']);
|
|
1123
|
-
const MONO_FAMILIES = /mono|consolas|courier|fira\s*code|jetbrains|source\s*code/i;
|
|
1124
|
-
let elementCount = 0;
|
|
1125
|
-
// Per-element extraction — shared between light DOM walker and shadow DOM walker
|
|
1126
|
-
function processElement(el) {
|
|
1127
|
-
const htmlEl = el;
|
|
1128
|
-
if (htmlEl.offsetWidth === 0 && htmlEl.offsetHeight === 0)
|
|
1129
|
-
return;
|
|
1130
|
-
const cs = getComputedStyle(el);
|
|
1131
|
-
// Colors
|
|
1132
|
-
addColor(cs.color, 'color');
|
|
1133
|
-
addColor(cs.backgroundColor, 'backgroundColor');
|
|
1134
|
-
addColor(cs.borderTopColor, 'borderColor');
|
|
1135
|
-
addColor(cs.borderRightColor, 'borderColor');
|
|
1136
|
-
addColor(cs.borderBottomColor, 'borderColor');
|
|
1137
|
-
addColor(cs.borderLeftColor, 'borderColor');
|
|
1138
|
-
addColor(cs.outlineColor, 'outlineColor');
|
|
1139
|
-
if (cs.backgroundImage.includes('gradient'))
|
|
1140
|
-
parseGradientColors(cs.backgroundImage);
|
|
1141
|
-
if (cs.backgroundImage.includes('url(')) {
|
|
1142
|
-
const urlMatches = cs.backgroundImage.matchAll(/url\(["']?([^"')]+)["']?\)/g);
|
|
1143
|
-
for (const m of urlMatches) {
|
|
1144
|
-
const u = m[1];
|
|
1145
|
-
if (u && !u.startsWith('data:') && !u.startsWith('blob:'))
|
|
1146
|
-
backgroundImageUrls.add(u);
|
|
1147
|
-
}
|
|
1148
|
-
}
|
|
1149
|
-
if (cs.boxShadow !== 'none')
|
|
1150
|
-
parseShadowColors(cs.boxShadow);
|
|
1151
|
-
// Spacing
|
|
1152
|
-
addSpacing(cs.paddingTop, 'padding');
|
|
1153
|
-
addSpacing(cs.paddingRight, 'padding');
|
|
1154
|
-
addSpacing(cs.paddingBottom, 'padding');
|
|
1155
|
-
addSpacing(cs.paddingLeft, 'padding');
|
|
1156
|
-
addSpacing(cs.marginTop, 'margin');
|
|
1157
|
-
addSpacing(cs.marginRight, 'margin');
|
|
1158
|
-
addSpacing(cs.marginBottom, 'margin');
|
|
1159
|
-
addSpacing(cs.marginLeft, 'margin');
|
|
1160
|
-
addSpacing(cs.gap, 'gap');
|
|
1161
|
-
addSpacing(cs.rowGap, 'gap');
|
|
1162
|
-
addSpacing(cs.columnGap, 'gap');
|
|
1163
|
-
// Border radius
|
|
1164
|
-
const br = cs.borderRadius;
|
|
1165
|
-
if (br && br !== '0px') {
|
|
1166
|
-
borderRadiusMap.set(br, (borderRadiusMap.get(br) ?? 0) + 1);
|
|
1167
|
-
}
|
|
1168
|
-
// Shadows
|
|
1169
|
-
if (cs.boxShadow !== 'none') {
|
|
1170
|
-
shadowMap.set(cs.boxShadow, (shadowMap.get(cs.boxShadow) ?? 0) + 1);
|
|
1171
|
-
}
|
|
1172
|
-
// Typography — only for elements with text content
|
|
1173
|
-
if (el.childNodes.length > 0 && Array.from(el.childNodes).some(n => n.nodeType === Node.TEXT_NODE && n.textContent?.trim())) {
|
|
1174
|
-
const family = cs.fontFamily;
|
|
1175
|
-
const weight = parseInt(cs.fontWeight) || 400;
|
|
1176
|
-
const isHeading = HEADING_TAGS.has(el.tagName);
|
|
1177
|
-
const isMono = MONO_FAMILIES.test(family);
|
|
1178
|
-
const fEntry = fontFamilyMap.get(family);
|
|
1179
|
-
if (fEntry) {
|
|
1180
|
-
fEntry.weights.add(weight);
|
|
1181
|
-
if (isHeading)
|
|
1182
|
-
fEntry.headingUse = true;
|
|
1183
|
-
if (isMono)
|
|
1184
|
-
fEntry.monoUse = true;
|
|
1185
|
-
}
|
|
1186
|
-
else {
|
|
1187
|
-
fontFamilyMap.set(family, { weights: new Set([weight]), headingUse: isHeading, monoUse: isMono });
|
|
1188
|
-
}
|
|
1189
|
-
const scaleKey = `${cs.fontSize}|${cs.fontWeight}|${cs.lineHeight}|${cs.letterSpacing}|${family}`;
|
|
1190
|
-
const scaleEntry = typeScaleMap.get(scaleKey);
|
|
1191
|
-
if (scaleEntry) {
|
|
1192
|
-
scaleEntry.count++;
|
|
1193
|
-
}
|
|
1194
|
-
else {
|
|
1195
|
-
typeScaleMap.set(scaleKey, {
|
|
1196
|
-
fontSize: cs.fontSize,
|
|
1197
|
-
fontWeight: cs.fontWeight,
|
|
1198
|
-
lineHeight: cs.lineHeight,
|
|
1199
|
-
letterSpacing: cs.letterSpacing,
|
|
1200
|
-
fontFamily: family,
|
|
1201
|
-
tag: el.tagName.toLowerCase(),
|
|
1202
|
-
count: 1,
|
|
1203
|
-
});
|
|
1204
|
-
}
|
|
1205
|
-
}
|
|
1206
|
-
// Animations/transitions
|
|
1207
|
-
if (cs.transition && cs.transition !== 'all 0s ease 0s' && cs.transition !== 'none') {
|
|
1208
|
-
const key = cs.transition;
|
|
1209
|
-
const aEntry = animationMap.get(key);
|
|
1210
|
-
if (aEntry) {
|
|
1211
|
-
aEntry.count++;
|
|
1212
|
-
}
|
|
1213
|
-
else {
|
|
1214
|
-
animationMap.set(key, { property: 'transition', value: cs.transition, count: 1 });
|
|
1215
|
-
}
|
|
1216
|
-
}
|
|
1217
|
-
if (cs.animationName && cs.animationName !== 'none') {
|
|
1218
|
-
const key = `animation:${cs.animationName}`;
|
|
1219
|
-
const aEntry = animationMap.get(key);
|
|
1220
|
-
if (aEntry) {
|
|
1221
|
-
aEntry.count++;
|
|
1222
|
-
}
|
|
1223
|
-
else {
|
|
1224
|
-
animationMap.set(key, { property: 'animation', value: `${cs.animationName} ${cs.animationDuration} ${cs.animationTimingFunction}`, count: 1 });
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1227
|
-
}
|
|
1228
|
-
// --- Light DOM walk ---
|
|
1229
|
-
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT);
|
|
1230
|
-
let el = walker.currentNode;
|
|
1231
|
-
while (el && elementCount < MAX_ELEMENTS) {
|
|
1232
|
-
elementCount++;
|
|
1233
|
-
processElement(el);
|
|
1234
|
-
el = walker.nextNode();
|
|
1235
|
-
}
|
|
1236
|
-
// --- Shadow DOM walk ---
|
|
1237
|
-
function walkShadowRoots(root) {
|
|
1238
|
-
const shadowWalker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
|
1239
|
-
// currentNode starts at root (ShadowRoot, not Element) — advance to first element
|
|
1240
|
-
let shadowEl = shadowWalker.nextNode();
|
|
1241
|
-
while (shadowEl && elementCount < MAX_ELEMENTS) {
|
|
1242
|
-
elementCount++;
|
|
1243
|
-
processElement(shadowEl);
|
|
1244
|
-
if (shadowEl.shadowRoot) {
|
|
1245
|
-
walkShadowRoots(shadowEl.shadowRoot);
|
|
1246
|
-
}
|
|
1247
|
-
shadowEl = shadowWalker.nextNode();
|
|
1248
|
-
}
|
|
1249
|
-
}
|
|
1250
|
-
const allElements = document.body.querySelectorAll('*');
|
|
1251
|
-
for (const bodyEl of allElements) {
|
|
1252
|
-
if (bodyEl.shadowRoot && elementCount < MAX_ELEMENTS) {
|
|
1253
|
-
walkShadowRoots(bodyEl.shadowRoot);
|
|
1254
|
-
}
|
|
1255
|
-
}
|
|
1256
|
-
// --- Components: Buttons ---
|
|
1257
|
-
const buttonStyles = [];
|
|
1258
|
-
const btnFingerprints = new Map();
|
|
1259
|
-
const BUTTON_STYLE_KEYS = ['backgroundColor', 'color', 'borderRadius', 'padding', 'fontSize', 'fontWeight', 'border', 'boxShadow', 'textTransform', 'letterSpacing'];
|
|
1260
|
-
for (const btn of document.querySelectorAll('button, [role="button"], input[type="submit"], input[type="button"]')) {
|
|
1261
|
-
const htmlBtn = btn;
|
|
1262
|
-
if (htmlBtn.offsetWidth === 0 && htmlBtn.offsetHeight === 0)
|
|
1263
|
-
continue;
|
|
1264
|
-
const cs = getComputedStyle(btn);
|
|
1265
|
-
const styles = {};
|
|
1266
|
-
for (const k of BUTTON_STYLE_KEYS)
|
|
1267
|
-
styles[k] = cs[k];
|
|
1268
|
-
const fp = JSON.stringify(styles);
|
|
1269
|
-
const existing = btnFingerprints.get(fp);
|
|
1270
|
-
if (existing) {
|
|
1271
|
-
existing.count++;
|
|
1272
|
-
}
|
|
1273
|
-
else {
|
|
1274
|
-
const entry = { fingerprint: fp, count: 1, exampleText: htmlBtn.textContent?.trim().slice(0, 60) || '', styles };
|
|
1275
|
-
btnFingerprints.set(fp, entry);
|
|
1276
|
-
}
|
|
1277
|
-
}
|
|
1278
|
-
buttonStyles.push(...btnFingerprints.values());
|
|
1279
|
-
// --- Components: Inputs ---
|
|
1280
|
-
const inputStyles = [];
|
|
1281
|
-
const inputFingerprints = new Map();
|
|
1282
|
-
const INPUT_STYLE_KEYS = ['backgroundColor', 'color', 'borderRadius', 'padding', 'fontSize', 'border', 'boxShadow', 'outline'];
|
|
1283
|
-
for (const inp of document.querySelectorAll('input[type="text"], input[type="email"], input[type="password"], input[type="search"], input[type="number"], input[type="tel"], input[type="url"], textarea, select, [role="textbox"], [role="combobox"]')) {
|
|
1284
|
-
const htmlInp = inp;
|
|
1285
|
-
if (htmlInp.offsetWidth === 0 && htmlInp.offsetHeight === 0)
|
|
1286
|
-
continue;
|
|
1287
|
-
const cs = getComputedStyle(inp);
|
|
1288
|
-
const styles = {};
|
|
1289
|
-
for (const k of INPUT_STYLE_KEYS)
|
|
1290
|
-
styles[k] = cs[k];
|
|
1291
|
-
const fp = JSON.stringify(styles);
|
|
1292
|
-
const existing = inputFingerprints.get(fp);
|
|
1293
|
-
if (existing) {
|
|
1294
|
-
existing.count++;
|
|
1295
|
-
}
|
|
1296
|
-
else {
|
|
1297
|
-
const entry = { fingerprint: fp, count: 1, exampleText: inp.placeholder?.slice(0, 60) || '', styles };
|
|
1298
|
-
inputFingerprints.set(fp, entry);
|
|
1299
|
-
}
|
|
1300
|
-
}
|
|
1301
|
-
inputStyles.push(...inputFingerprints.values());
|
|
1302
|
-
// --- Components: Cards (elements with shadow or border-radius + multiple children) ---
|
|
1303
|
-
const cardStyles = [];
|
|
1304
|
-
const cardFingerprints = new Map();
|
|
1305
|
-
const CARD_STYLE_KEYS = ['borderRadius', 'boxShadow', 'padding', 'backgroundColor', 'border'];
|
|
1306
|
-
for (const el of document.querySelectorAll('div, section, article, li')) {
|
|
1307
|
-
const htmlEl = el;
|
|
1308
|
-
if (htmlEl.offsetWidth === 0 || htmlEl.children.length < 2)
|
|
1309
|
-
continue;
|
|
1310
|
-
const cs = getComputedStyle(el);
|
|
1311
|
-
if (cs.boxShadow === 'none' && cs.borderRadius === '0px' && cs.border === '0px none rgb(0, 0, 0)')
|
|
1312
|
-
continue;
|
|
1313
|
-
const styles = {};
|
|
1314
|
-
for (const k of CARD_STYLE_KEYS)
|
|
1315
|
-
styles[k] = cs[k];
|
|
1316
|
-
const fp = JSON.stringify(styles);
|
|
1317
|
-
const existing = cardFingerprints.get(fp);
|
|
1318
|
-
if (existing) {
|
|
1319
|
-
existing.count++;
|
|
1320
|
-
}
|
|
1321
|
-
else {
|
|
1322
|
-
const entry = { fingerprint: fp, count: 1, styles };
|
|
1323
|
-
cardFingerprints.set(fp, entry);
|
|
1324
|
-
}
|
|
1325
|
-
}
|
|
1326
|
-
// Only keep card patterns with 2+ instances (skip one-off containers)
|
|
1327
|
-
for (const c of cardFingerprints.values()) {
|
|
1328
|
-
if (c.count >= 2)
|
|
1329
|
-
cardStyles.push(c);
|
|
1330
|
-
}
|
|
1331
|
-
// --- Components: Navigation ---
|
|
1332
|
-
const navPatterns = [];
|
|
1333
|
-
for (const nav of document.querySelectorAll('nav, [role="navigation"], header')) {
|
|
1334
|
-
const cs = getComputedStyle(nav);
|
|
1335
|
-
const links = nav.querySelectorAll('a');
|
|
1336
|
-
if (links.length === 0)
|
|
1337
|
-
continue;
|
|
1338
|
-
const firstLinkCs = getComputedStyle(links[0]);
|
|
1339
|
-
navPatterns.push({
|
|
1340
|
-
selector: nav.tagName.toLowerCase() + (nav.className ? '.' + nav.className.split(/\s+/)[0] : ''),
|
|
1341
|
-
layout: {
|
|
1342
|
-
display: cs.display,
|
|
1343
|
-
flexDirection: cs.flexDirection,
|
|
1344
|
-
gap: cs.gap,
|
|
1345
|
-
alignItems: cs.alignItems,
|
|
1346
|
-
justifyContent: cs.justifyContent,
|
|
1347
|
-
backgroundColor: cs.backgroundColor,
|
|
1348
|
-
padding: cs.padding,
|
|
1349
|
-
},
|
|
1350
|
-
linkCount: links.length,
|
|
1351
|
-
linkStyles: {
|
|
1352
|
-
color: firstLinkCs.color,
|
|
1353
|
-
fontSize: firstLinkCs.fontSize,
|
|
1354
|
-
fontWeight: firstLinkCs.fontWeight,
|
|
1355
|
-
textDecoration: firstLinkCs.textDecoration,
|
|
1356
|
-
},
|
|
1357
|
-
});
|
|
1358
|
-
}
|
|
1359
|
-
// --- Logos & Icons ---
|
|
1360
|
-
const logos = [];
|
|
1361
|
-
const icons = [];
|
|
1362
|
-
// SVG logos (in header/nav or with logo-related attributes)
|
|
1363
|
-
for (const svg of document.querySelectorAll('svg')) {
|
|
1364
|
-
const parent = svg.closest('header, nav, [role="banner"]');
|
|
1365
|
-
const classes = (svg.getAttribute('class') || '') + ' ' + (svg.parentElement?.getAttribute('class') || '');
|
|
1366
|
-
const ariaLabel = svg.getAttribute('aria-label') || '';
|
|
1367
|
-
const isLogo = parent !== null || /logo|brand|mark/i.test(classes) || /logo|brand/i.test(ariaLabel);
|
|
1368
|
-
if (isLogo) {
|
|
1369
|
-
const content = svg.outerHTML.slice(0, 4096);
|
|
1370
|
-
logos.push({
|
|
1371
|
-
type: 'svg',
|
|
1372
|
-
width: svg.width?.baseVal?.value || undefined,
|
|
1373
|
-
height: svg.height?.baseVal?.value || undefined,
|
|
1374
|
-
svgContent: content,
|
|
1375
|
-
});
|
|
1376
|
-
}
|
|
1377
|
-
else if (svg.outerHTML.length < 2048) {
|
|
1378
|
-
// Collect as icon (small SVGs)
|
|
1379
|
-
icons.push({
|
|
1380
|
-
selector: svg.tagName + (svg.getAttribute('class') ? '.' + svg.getAttribute('class').split(/\s+/)[0] : ''),
|
|
1381
|
-
viewBox: svg.getAttribute('viewBox') || undefined,
|
|
1382
|
-
width: svg.width?.baseVal?.value || undefined,
|
|
1383
|
-
height: svg.height?.baseVal?.value || undefined,
|
|
1384
|
-
content: svg.outerHTML,
|
|
1385
|
-
});
|
|
1386
|
-
}
|
|
1387
|
-
}
|
|
1388
|
-
// IMG logos
|
|
1389
|
-
for (const img of document.querySelectorAll('img')) {
|
|
1390
|
-
const parent = img.closest('header, nav, [role="banner"]');
|
|
1391
|
-
const src = img.src || '';
|
|
1392
|
-
const alt = img.alt || '';
|
|
1393
|
-
const classes = img.className || '';
|
|
1394
|
-
if (parent !== null || /logo|brand|mark/i.test(src) || /logo|brand/i.test(alt) || /logo|brand/i.test(classes)) {
|
|
1395
|
-
logos.push({
|
|
1396
|
-
type: 'img',
|
|
1397
|
-
src: img.src,
|
|
1398
|
-
alt: img.alt || undefined,
|
|
1399
|
-
width: img.naturalWidth || undefined,
|
|
1400
|
-
height: img.naturalHeight || undefined,
|
|
1401
|
-
});
|
|
1402
|
-
}
|
|
1403
|
-
}
|
|
1404
|
-
// Limit icons to top 50 unique by viewBox
|
|
1405
|
-
const seenViewBoxes = new Set();
|
|
1406
|
-
const dedupedIcons = icons.filter(icon => {
|
|
1407
|
-
const key = icon.viewBox || icon.content.slice(0, 100);
|
|
1408
|
-
if (seenViewBoxes.has(key))
|
|
1409
|
-
return false;
|
|
1410
|
-
seenViewBoxes.add(key);
|
|
1411
|
-
return true;
|
|
1412
|
-
}).slice(0, 50);
|
|
1413
|
-
// --- Brand Meta ---
|
|
1414
|
-
const getMeta = (sel) => document.querySelector(sel)?.getAttribute('content') || undefined;
|
|
1415
|
-
const getLink = (sel) => {
|
|
1416
|
-
const el = document.querySelector(sel);
|
|
1417
|
-
return el ? el.href || el.getAttribute('href') || undefined : undefined;
|
|
1418
|
-
};
|
|
1419
|
-
const brand = {
|
|
1420
|
-
title: getMeta('meta[property="og:title"]') || document.title || undefined,
|
|
1421
|
-
description: getMeta('meta[property="og:description"]') || getMeta('meta[name="description"]'),
|
|
1422
|
-
themeColor: getMeta('meta[name="theme-color"]'),
|
|
1423
|
-
favicon: getLink('link[rel="icon"]') || getLink('link[rel="shortcut icon"]'),
|
|
1424
|
-
ogImage: getMeta('meta[property="og:image"]'),
|
|
1425
|
-
appleTouchIcon: getLink('link[rel="apple-touch-icon"]'),
|
|
1426
|
-
manifestUrl: getLink('link[rel="manifest"]'),
|
|
1427
|
-
};
|
|
1428
|
-
// --- All Image URLs ---
|
|
1429
|
-
const imageUrls = new Set();
|
|
1430
|
-
for (const img of document.querySelectorAll('img')) {
|
|
1431
|
-
const src = img.src;
|
|
1432
|
-
if (src && !src.startsWith('data:') && !src.startsWith('blob:'))
|
|
1433
|
-
imageUrls.add(src);
|
|
1434
|
-
const srcset = img.srcset;
|
|
1435
|
-
if (srcset) {
|
|
1436
|
-
for (const part of srcset.split(',')) {
|
|
1437
|
-
const url = part.trim().split(/\s+/)[0];
|
|
1438
|
-
if (url && !url.startsWith('data:') && !url.startsWith('blob:'))
|
|
1439
|
-
imageUrls.add(url);
|
|
1440
|
-
}
|
|
1441
|
-
}
|
|
1442
|
-
}
|
|
1443
|
-
// --- Video URLs ---
|
|
1444
|
-
const videoUrls = new Set();
|
|
1445
|
-
for (const video of document.querySelectorAll('video')) {
|
|
1446
|
-
const src = video.src;
|
|
1447
|
-
if (src && !src.startsWith('data:') && !src.startsWith('blob:'))
|
|
1448
|
-
videoUrls.add(src);
|
|
1449
|
-
const poster = video.poster;
|
|
1450
|
-
if (poster && !poster.startsWith('data:') && !poster.startsWith('blob:'))
|
|
1451
|
-
imageUrls.add(poster);
|
|
1452
|
-
for (const source of video.querySelectorAll('source')) {
|
|
1453
|
-
const ssrc = source.src;
|
|
1454
|
-
if (ssrc && !ssrc.startsWith('data:') && !ssrc.startsWith('blob:'))
|
|
1455
|
-
videoUrls.add(ssrc);
|
|
1456
|
-
}
|
|
1457
|
-
}
|
|
1458
|
-
// Orphan <source> elements (audio, picture)
|
|
1459
|
-
for (const source of document.querySelectorAll('source[src]')) {
|
|
1460
|
-
const ssrc = source.src;
|
|
1461
|
-
if (ssrc && /\.(mp4|webm|mov|ogg|avi|m3u8)/i.test(ssrc))
|
|
1462
|
-
videoUrls.add(ssrc);
|
|
1463
|
-
}
|
|
1464
|
-
// <picture> <source> with srcset
|
|
1465
|
-
for (const source of document.querySelectorAll('picture source[srcset]')) {
|
|
1466
|
-
const srcset = source.srcset;
|
|
1467
|
-
if (srcset) {
|
|
1468
|
-
for (const part of srcset.split(',')) {
|
|
1469
|
-
const url = part.trim().split(/\s+/)[0];
|
|
1470
|
-
if (url && !url.startsWith('data:') && !url.startsWith('blob:'))
|
|
1471
|
-
imageUrls.add(url);
|
|
1472
|
-
}
|
|
1473
|
-
}
|
|
1474
|
-
}
|
|
1475
|
-
// --- Lazy-load data attributes ---
|
|
1476
|
-
for (const el of document.querySelectorAll('[data-src], [data-bg], [data-background-image], [data-poster]')) {
|
|
1477
|
-
for (const attr of ['data-src', 'data-bg', 'data-background-image', 'data-poster']) {
|
|
1478
|
-
const val = el.getAttribute(attr);
|
|
1479
|
-
if (!val || val.startsWith('data:') || val.startsWith('blob:'))
|
|
1480
|
-
continue;
|
|
1481
|
-
try {
|
|
1482
|
-
const resolved = new URL(val, document.location.href).href;
|
|
1483
|
-
if (/\.(mp4|webm|mov|ogg|avi|m3u8)/i.test(val))
|
|
1484
|
-
videoUrls.add(resolved);
|
|
1485
|
-
else
|
|
1486
|
-
imageUrls.add(resolved);
|
|
1487
|
-
}
|
|
1488
|
-
catch { /* bad URL */ }
|
|
1489
|
-
}
|
|
1490
|
-
}
|
|
1491
|
-
// --- Link Assets (favicons, icons, manifests) ---
|
|
1492
|
-
const linkAssetUrls = [];
|
|
1493
|
-
for (const link of document.querySelectorAll('link[href]')) {
|
|
1494
|
-
const href = link.href;
|
|
1495
|
-
const rel = link.rel || '';
|
|
1496
|
-
if (href && !href.startsWith('data:') && !href.startsWith('blob:') &&
|
|
1497
|
-
/^(icon|shortcut icon|apple-touch-icon|manifest|preload)$/i.test(rel)) {
|
|
1498
|
-
linkAssetUrls.push({ url: href, rel });
|
|
1499
|
-
}
|
|
1500
|
-
}
|
|
1501
|
-
// --- Inline script library detection ---
|
|
1502
|
-
const detectedLibraries = [];
|
|
1503
|
-
const seenLibs = new Set();
|
|
1504
|
-
for (const script of document.querySelectorAll('script')) {
|
|
1505
|
-
const src = script.src || '';
|
|
1506
|
-
const text = script.textContent || '';
|
|
1507
|
-
const content = src + ' ' + text.slice(0, 10_000);
|
|
1508
|
-
const checks = [
|
|
1509
|
-
[/gsap|ScrollTrigger|ScrollSmoother/i, 'gsap'],
|
|
1510
|
-
[/new\s+p5\b|p5\.setup|p5\.draw|p5\.js/i, 'p5.js'],
|
|
1511
|
-
[/THREE\.|new\s+THREE\.|from\s+['"]three/i, 'three.js'],
|
|
1512
|
-
[/lottie|bodymovin/i, 'lottie'],
|
|
1513
|
-
[/anime\s*\(|animejs/i, 'anime.js'],
|
|
1514
|
-
[/framer-motion|motion\.div/i, 'framer-motion'],
|
|
1515
|
-
[/webflow/i, 'webflow'],
|
|
1516
|
-
[/swiper/i, 'swiper'],
|
|
1517
|
-
[/locomotive.?scroll/i, 'locomotive-scroll'],
|
|
1518
|
-
[/rive\.?app|@rive-app/i, 'rive'],
|
|
1519
|
-
[/spline/i, 'spline'],
|
|
1520
|
-
];
|
|
1521
|
-
for (const [regex, name] of checks) {
|
|
1522
|
-
if (!seenLibs.has(name) && regex.test(content)) {
|
|
1523
|
-
seenLibs.add(name);
|
|
1524
|
-
detectedLibraries.push(name);
|
|
1525
|
-
}
|
|
1526
|
-
}
|
|
1527
|
-
}
|
|
1528
|
-
// --- Assemble result ---
|
|
1529
|
-
return {
|
|
1530
|
-
customProperties,
|
|
1531
|
-
colors: Array.from(colorMap.entries())
|
|
1532
|
-
.map(([value, data]) => ({ value, count: data.count, properties: Array.from(data.properties) }))
|
|
1533
|
-
.sort((a, b) => b.count - a.count),
|
|
1534
|
-
typography: {
|
|
1535
|
-
families: Array.from(fontFamilyMap.entries()).map(([family, data]) => ({
|
|
1536
|
-
family,
|
|
1537
|
-
weights: Array.from(data.weights).sort((a, b) => a - b),
|
|
1538
|
-
headingUse: data.headingUse,
|
|
1539
|
-
monoUse: data.monoUse,
|
|
1540
|
-
})),
|
|
1541
|
-
scale: Array.from(typeScaleMap.values()).sort((a, b) => b.count - a.count),
|
|
1542
|
-
},
|
|
1543
|
-
brand,
|
|
1544
|
-
logos,
|
|
1545
|
-
icons: dedupedIcons,
|
|
1546
|
-
spacing: Array.from(spacingMap.entries())
|
|
1547
|
-
.map(([value, data]) => ({ value, count: data.count, properties: Array.from(data.properties) }))
|
|
1548
|
-
.sort((a, b) => b.count - a.count),
|
|
1549
|
-
borders: Array.from(borderRadiusMap.entries())
|
|
1550
|
-
.map(([borderRadius, count]) => ({ borderRadius, count }))
|
|
1551
|
-
.sort((a, b) => b.count - a.count),
|
|
1552
|
-
shadows: Array.from(shadowMap.entries())
|
|
1553
|
-
.map(([value, count]) => ({ value, count }))
|
|
1554
|
-
.sort((a, b) => b.count - a.count),
|
|
1555
|
-
components: {
|
|
1556
|
-
buttons: buttonStyles.sort((a, b) => b.count - a.count),
|
|
1557
|
-
inputs: inputStyles.sort((a, b) => b.count - a.count),
|
|
1558
|
-
cards: cardStyles.sort((a, b) => b.count - a.count),
|
|
1559
|
-
nav: navPatterns,
|
|
1560
|
-
},
|
|
1561
|
-
animations: Array.from(animationMap.values()).sort((a, b) => b.count - a.count),
|
|
1562
|
-
fontFiles,
|
|
1563
|
-
imageUrls: Array.from(imageUrls),
|
|
1564
|
-
backgroundImageUrls: Array.from(backgroundImageUrls),
|
|
1565
|
-
videoUrls: Array.from(videoUrls),
|
|
1566
|
-
externalStylesheetUrls,
|
|
1567
|
-
linkAssetUrls,
|
|
1568
|
-
detectedLibraries,
|
|
1569
|
-
};
|
|
1570
|
-
}
|
|
1571
|
-
// ---------------------------------------------------------------------------
|
|
1572
|
-
// Color clustering — assign semantic roles to extracted colors
|
|
1573
|
-
// ---------------------------------------------------------------------------
|
|
1574
|
-
function rgbaToHsl(rgba) {
|
|
1575
|
-
const match = rgba.match(/rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)(?:\s*,\s*([\d.]+))?\s*\)/);
|
|
1576
|
-
if (!match)
|
|
1577
|
-
return null;
|
|
1578
|
-
const r = parseFloat(match[1]) / 255;
|
|
1579
|
-
const g = parseFloat(match[2]) / 255;
|
|
1580
|
-
const b = parseFloat(match[3]) / 255;
|
|
1581
|
-
const a = match[4] !== undefined ? parseFloat(match[4]) : 1;
|
|
1582
|
-
const max = Math.max(r, g, b), min = Math.min(r, g, b);
|
|
1583
|
-
const l = (max + min) / 2;
|
|
1584
|
-
if (max === min)
|
|
1585
|
-
return { h: 0, s: 0, l, a };
|
|
1586
|
-
const d = max - min;
|
|
1587
|
-
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
1588
|
-
let h = 0;
|
|
1589
|
-
if (max === r)
|
|
1590
|
-
h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
|
|
1591
|
-
else if (max === g)
|
|
1592
|
-
h = ((b - r) / d + 2) / 6;
|
|
1593
|
-
else
|
|
1594
|
-
h = ((r - g) / d + 4) / 6;
|
|
1595
|
-
return { h: h * 360, s: s * 100, l: l * 100, a };
|
|
1596
|
-
}
|
|
1597
|
-
function rgbaToHex(rgba) {
|
|
1598
|
-
const match = rgba.match(/rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)(?:\s*,\s*([\d.]+))?\s*\)/);
|
|
1599
|
-
if (!match)
|
|
1600
|
-
return rgba;
|
|
1601
|
-
const r = Math.round(parseFloat(match[1]));
|
|
1602
|
-
const g = Math.round(parseFloat(match[2]));
|
|
1603
|
-
const b = Math.round(parseFloat(match[3]));
|
|
1604
|
-
const a = match[4] !== undefined ? parseFloat(match[4]) : 1;
|
|
1605
|
-
const hex = `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
|
|
1606
|
-
if (a < 1)
|
|
1607
|
-
return `${hex}${Math.round(a * 255).toString(16).padStart(2, '0')}`;
|
|
1608
|
-
return hex;
|
|
1609
|
-
}
|
|
1610
|
-
function clusterColors(colors) {
|
|
1611
|
-
const result = [];
|
|
1612
|
-
let primaryHue = -1;
|
|
1613
|
-
let secondaryHue = -1;
|
|
1614
|
-
for (const c of colors) {
|
|
1615
|
-
const hsl = rgbaToHsl(c.value);
|
|
1616
|
-
const hex = rgbaToHex(c.value);
|
|
1617
|
-
let cluster;
|
|
1618
|
-
if (!hsl || hsl.s < 10) {
|
|
1619
|
-
// Low saturation = neutral (grays, black, white)
|
|
1620
|
-
if (hsl && hsl.l > 90)
|
|
1621
|
-
cluster = 'background';
|
|
1622
|
-
else
|
|
1623
|
-
cluster = 'neutral';
|
|
1624
|
-
}
|
|
1625
|
-
else if (c.properties.includes('borderColor') && !c.properties.includes('color') && !c.properties.includes('backgroundColor')) {
|
|
1626
|
-
cluster = 'border';
|
|
1627
|
-
}
|
|
1628
|
-
else if (primaryHue < 0) {
|
|
1629
|
-
primaryHue = hsl.h;
|
|
1630
|
-
cluster = 'primary';
|
|
1631
|
-
}
|
|
1632
|
-
else if (Math.abs(hsl.h - primaryHue) < 30 || Math.abs(hsl.h - primaryHue) > 330) {
|
|
1633
|
-
cluster = 'primary';
|
|
1634
|
-
}
|
|
1635
|
-
else if (secondaryHue < 0) {
|
|
1636
|
-
secondaryHue = hsl.h;
|
|
1637
|
-
cluster = 'secondary';
|
|
1638
|
-
}
|
|
1639
|
-
else if (Math.abs(hsl.h - secondaryHue) < 30 || Math.abs(hsl.h - secondaryHue) > 330) {
|
|
1640
|
-
cluster = 'secondary';
|
|
1641
|
-
}
|
|
1642
|
-
else {
|
|
1643
|
-
cluster = 'accent';
|
|
1644
|
-
}
|
|
1645
|
-
result.push({ value: c.value, hex, count: c.count, properties: c.properties, cluster });
|
|
1646
|
-
}
|
|
1647
|
-
return result;
|
|
1648
|
-
}
|
|
1649
|
-
// ---------------------------------------------------------------------------
|
|
1650
|
-
// Grid base unit detection
|
|
1651
|
-
// ---------------------------------------------------------------------------
|
|
1652
|
-
function detectGridUnit(spacingValues) {
|
|
1653
|
-
// Build frequency-weighted pairs
|
|
1654
|
-
const pairs = spacingValues
|
|
1655
|
-
.map(s => ({ px: parseFloat(s.value), count: s.count }))
|
|
1656
|
-
.filter(p => p.px > 0 && p.px <= 200 && Number.isInteger(p.px));
|
|
1657
|
-
if (pairs.length < 3)
|
|
1658
|
-
return undefined;
|
|
1659
|
-
// Score common grid units by how much of the total usage they explain
|
|
1660
|
-
const CANDIDATES = [4, 5, 6, 8, 10];
|
|
1661
|
-
let bestUnit = 0;
|
|
1662
|
-
let bestScore = 0;
|
|
1663
|
-
for (const unit of CANDIDATES) {
|
|
1664
|
-
let aligned = 0;
|
|
1665
|
-
let total = 0;
|
|
1666
|
-
for (const p of pairs) {
|
|
1667
|
-
total += p.count;
|
|
1668
|
-
if (p.px % unit === 0)
|
|
1669
|
-
aligned += p.count;
|
|
1670
|
-
}
|
|
1671
|
-
const score = aligned / total;
|
|
1672
|
-
if (score > bestScore) {
|
|
1673
|
-
bestScore = score;
|
|
1674
|
-
bestUnit = unit;
|
|
1675
|
-
}
|
|
1676
|
-
}
|
|
1677
|
-
// Require at least 60% of usage aligns with the grid
|
|
1678
|
-
return bestScore >= 0.6 ? bestUnit : undefined;
|
|
1679
|
-
}
|
|
1680
|
-
// ---------------------------------------------------------------------------
|
|
1681
|
-
// Token extraction orchestrator
|
|
1682
|
-
// ---------------------------------------------------------------------------
|
|
1683
|
-
async function dismissCookieBanners(page) {
|
|
1684
|
-
for (const sel of ['button:has-text("Accept all")', 'button:has-text("Accept")', 'button:has-text("Reject all")', 'button:has-text("Got it")', 'button:has-text("Close")']) {
|
|
1685
|
-
const btn = page.locator(sel).first();
|
|
1686
|
-
if (await btn.isVisible({ timeout: 500 }).catch(() => false)) {
|
|
1687
|
-
await btn.click({ timeout: 2000 }).catch(() => null);
|
|
1688
|
-
await page.waitForTimeout(500);
|
|
1689
|
-
break;
|
|
1690
|
-
}
|
|
1691
|
-
}
|
|
1692
|
-
}
|
|
1693
|
-
/**
|
|
1694
|
-
* Extract design tokens from a URL at multiple viewports.
|
|
1695
|
-
* Pure DOM extraction — no LLM calls.
|
|
1696
|
-
*
|
|
1697
|
-
* ```typescript
|
|
1698
|
-
* import { extractDesignTokens } from '@tangle-network/browser-agent-driver'
|
|
1699
|
-
*
|
|
1700
|
-
* const { tokens } = await extractDesignTokens({ url: 'https://stripe.com' })
|
|
1701
|
-
* console.log(tokens.colors.filter(c => c.cluster === 'primary'))
|
|
1702
|
-
* console.log(tokens.typography.families)
|
|
1703
|
-
* console.log(tokens.brand)
|
|
1704
|
-
* ```
|
|
1705
|
-
*/
|
|
1706
|
-
export async function extractDesignTokens(opts) {
|
|
1707
|
-
const viewports = opts.viewports ?? [...VIEWPORTS];
|
|
1708
|
-
const outputDir = opts.outputDir ?? `./audit-results/${new URL(opts.url).hostname}-tokens-${Date.now()}`;
|
|
1709
|
-
fs.mkdirSync(outputDir, { recursive: true });
|
|
1710
|
-
const screenshotDir = path.join(outputDir, 'screenshots');
|
|
1711
|
-
fs.mkdirSync(screenshotDir, { recursive: true });
|
|
1712
|
-
const browser = await chromium.launch({ headless: opts.headless ?? true });
|
|
1713
|
-
const allCustomProps = {};
|
|
1714
|
-
const allColors = new Map();
|
|
1715
|
-
const allFamilies = new Map();
|
|
1716
|
-
const allScaleEntries = [];
|
|
1717
|
-
let brand = {};
|
|
1718
|
-
const allLogos = [];
|
|
1719
|
-
const allIcons = [];
|
|
1720
|
-
const allFontFiles = new Map();
|
|
1721
|
-
const allImageUrls = new Set();
|
|
1722
|
-
const allBackgroundImageUrls = new Set();
|
|
1723
|
-
const allVideoUrls = new Set();
|
|
1724
|
-
const allExternalStylesheetUrls = new Set();
|
|
1725
|
-
const allLinkAssetUrls = new Map(); // url -> rel
|
|
1726
|
-
const allDetectedLibraries = new Set();
|
|
1727
|
-
const responsiveTokens = {};
|
|
1728
|
-
const screenshotPaths = {};
|
|
1729
|
-
for (const vp of viewports) {
|
|
1730
|
-
const context = await browser.newContext({ viewport: { width: vp.width, height: vp.height } });
|
|
1731
|
-
const page = await context.newPage();
|
|
1732
|
-
try {
|
|
1733
|
-
await page.goto(opts.url, { waitUntil: 'networkidle', timeout: 20_000 }).catch(() => page.goto(opts.url, { waitUntil: 'domcontentloaded', timeout: 15_000 }));
|
|
1734
|
-
await page.waitForTimeout(2000);
|
|
1735
|
-
await dismissCookieBanners(page);
|
|
1736
|
-
await page.waitForTimeout(500);
|
|
1737
|
-
const screenshotPath = path.join(screenshotDir, `${vp.name}.png`);
|
|
1738
|
-
await page.screenshot({ path: screenshotPath, fullPage: true }).catch(() => page.screenshot({ path: screenshotPath, fullPage: false }));
|
|
1739
|
-
screenshotPaths[vp.name] = screenshotPath;
|
|
1740
|
-
const raw = await page.evaluate(extractTokensFromDOM);
|
|
1741
|
-
Object.assign(allCustomProps, raw.customProperties);
|
|
1742
|
-
for (const c of raw.colors) {
|
|
1743
|
-
const existing = allColors.get(c.value);
|
|
1744
|
-
if (existing) {
|
|
1745
|
-
existing.count += c.count;
|
|
1746
|
-
for (const p of c.properties)
|
|
1747
|
-
existing.properties.add(p);
|
|
1748
|
-
}
|
|
1749
|
-
else {
|
|
1750
|
-
allColors.set(c.value, { count: c.count, properties: new Set(c.properties) });
|
|
1751
|
-
}
|
|
1752
|
-
}
|
|
1753
|
-
for (const f of raw.typography.families) {
|
|
1754
|
-
const existing = allFamilies.get(f.family);
|
|
1755
|
-
if (existing) {
|
|
1756
|
-
for (const w of f.weights)
|
|
1757
|
-
existing.weights.add(w);
|
|
1758
|
-
if (f.headingUse)
|
|
1759
|
-
existing.headingUse = true;
|
|
1760
|
-
if (f.monoUse)
|
|
1761
|
-
existing.monoUse = true;
|
|
1762
|
-
}
|
|
1763
|
-
else {
|
|
1764
|
-
allFamilies.set(f.family, { weights: new Set(f.weights), headingUse: f.headingUse, monoUse: f.monoUse });
|
|
1765
|
-
}
|
|
1766
|
-
}
|
|
1767
|
-
allScaleEntries.push(...raw.typography.scale);
|
|
1768
|
-
if (vp.name === 'desktop' || !brand.title) {
|
|
1769
|
-
brand = raw.brand;
|
|
1770
|
-
}
|
|
1771
|
-
for (const logo of raw.logos) {
|
|
1772
|
-
if (!allLogos.some(l => l.src === logo.src && l.svgContent === logo.svgContent)) {
|
|
1773
|
-
allLogos.push(logo);
|
|
1774
|
-
}
|
|
1775
|
-
}
|
|
1776
|
-
for (const icon of raw.icons) {
|
|
1777
|
-
if (!allIcons.some(i => i.content === icon.content)) {
|
|
1778
|
-
allIcons.push(icon);
|
|
1779
|
-
}
|
|
1780
|
-
}
|
|
1781
|
-
for (const ff of raw.fontFiles) {
|
|
1782
|
-
if (!allFontFiles.has(ff.src)) {
|
|
1783
|
-
allFontFiles.set(ff.src, ff);
|
|
1784
|
-
}
|
|
1785
|
-
}
|
|
1786
|
-
for (const u of raw.imageUrls)
|
|
1787
|
-
allImageUrls.add(u);
|
|
1788
|
-
for (const u of raw.backgroundImageUrls)
|
|
1789
|
-
allBackgroundImageUrls.add(u);
|
|
1790
|
-
for (const u of raw.videoUrls)
|
|
1791
|
-
allVideoUrls.add(u);
|
|
1792
|
-
for (const u of raw.externalStylesheetUrls)
|
|
1793
|
-
allExternalStylesheetUrls.add(u);
|
|
1794
|
-
for (const la of raw.linkAssetUrls)
|
|
1795
|
-
allLinkAssetUrls.set(la.url, la.rel);
|
|
1796
|
-
for (const lib of raw.detectedLibraries)
|
|
1797
|
-
allDetectedLibraries.add(lib);
|
|
1798
|
-
responsiveTokens[vp.name] = {
|
|
1799
|
-
width: vp.width,
|
|
1800
|
-
height: vp.height,
|
|
1801
|
-
spacing: raw.spacing,
|
|
1802
|
-
gridBaseUnit: detectGridUnit(raw.spacing),
|
|
1803
|
-
borders: raw.borders,
|
|
1804
|
-
shadows: raw.shadows,
|
|
1805
|
-
components: raw.components,
|
|
1806
|
-
animations: raw.animations,
|
|
1807
|
-
screenshotPath,
|
|
1808
|
-
};
|
|
1809
|
-
opts.onProgress?.(vp.name, vp.width, vp.height, {
|
|
1810
|
-
colors: raw.colors.length,
|
|
1811
|
-
fonts: raw.typography.families.length,
|
|
1812
|
-
buttons: raw.components.buttons.length,
|
|
1813
|
-
inputs: raw.components.inputs.length,
|
|
1814
|
-
cards: raw.components.cards.length,
|
|
1815
|
-
});
|
|
1816
|
-
}
|
|
1817
|
-
finally {
|
|
1818
|
-
await context.close();
|
|
1819
|
-
}
|
|
1820
|
-
}
|
|
1821
|
-
await browser.close();
|
|
1822
|
-
// Deduplicate type scale
|
|
1823
|
-
const scaleMap = new Map();
|
|
1824
|
-
for (const entry of allScaleEntries) {
|
|
1825
|
-
const key = `${entry.fontSize}|${entry.fontWeight}|${entry.lineHeight}|${entry.fontFamily}`;
|
|
1826
|
-
const existing = scaleMap.get(key);
|
|
1827
|
-
if (existing) {
|
|
1828
|
-
existing.count += entry.count;
|
|
1829
|
-
}
|
|
1830
|
-
else {
|
|
1831
|
-
const isHeading = /^h[1-6]$/.test(entry.tag);
|
|
1832
|
-
const fontSize = parseFloat(entry.fontSize);
|
|
1833
|
-
let usage = 'body';
|
|
1834
|
-
if (isHeading)
|
|
1835
|
-
usage = 'heading';
|
|
1836
|
-
else if (fontSize <= 12)
|
|
1837
|
-
usage = 'caption';
|
|
1838
|
-
else if (fontSize <= 14 && entry.fontWeight >= '500')
|
|
1839
|
-
usage = 'label';
|
|
1840
|
-
scaleMap.set(key, { fontSize: entry.fontSize, fontWeight: entry.fontWeight, lineHeight: entry.lineHeight, letterSpacing: entry.letterSpacing, fontFamily: entry.fontFamily, usage, tag: entry.tag, count: entry.count });
|
|
1841
|
-
}
|
|
1842
|
-
}
|
|
1843
|
-
const mergedColors = Array.from(allColors.entries())
|
|
1844
|
-
.map(([value, data]) => ({ value, count: data.count, properties: Array.from(data.properties) }))
|
|
1845
|
-
.sort((a, b) => b.count - a.count);
|
|
1846
|
-
const clusteredColors = clusterColors(mergedColors);
|
|
1847
|
-
const families = Array.from(allFamilies.entries()).map(([family, data]) => ({
|
|
1848
|
-
family,
|
|
1849
|
-
weights: Array.from(data.weights).sort((a, b) => a - b),
|
|
1850
|
-
classification: data.monoUse ? 'mono' :
|
|
1851
|
-
(data.headingUse && allFamilies.size > 1) ? 'heading' :
|
|
1852
|
-
data.headingUse ? 'display' : 'body',
|
|
1853
|
-
}));
|
|
1854
|
-
// Download font files
|
|
1855
|
-
const downloadedFontFiles = [];
|
|
1856
|
-
if (allFontFiles.size > 0) {
|
|
1857
|
-
const fontDir = path.join(outputDir, 'fonts');
|
|
1858
|
-
fs.mkdirSync(fontDir, { recursive: true });
|
|
1859
|
-
for (const ff of allFontFiles.values()) {
|
|
1860
|
-
try {
|
|
1861
|
-
const res = await fetch(ff.src);
|
|
1862
|
-
if (!res.ok)
|
|
1863
|
-
continue;
|
|
1864
|
-
const buffer = Buffer.from(await res.arrayBuffer());
|
|
1865
|
-
const urlPath = decodeURIComponent(new URL(ff.src).pathname);
|
|
1866
|
-
const filename = path.basename(urlPath) || `${ff.family.replace(/\s+/g, '-')}-${ff.weight}-${ff.style}.${ff.format === 'unknown' ? 'bin' : ff.format}`;
|
|
1867
|
-
const localPath = path.join(fontDir, filename);
|
|
1868
|
-
fs.writeFileSync(localPath, buffer);
|
|
1869
|
-
downloadedFontFiles.push({ ...ff, localPath });
|
|
1870
|
-
}
|
|
1871
|
-
catch {
|
|
1872
|
-
downloadedFontFiles.push({ ...ff });
|
|
1873
|
-
}
|
|
1874
|
-
}
|
|
1875
|
-
}
|
|
1876
|
-
// Download images
|
|
1877
|
-
const downloadedImages = [];
|
|
1878
|
-
const allAssetUrls = new Map();
|
|
1879
|
-
for (const u of allImageUrls)
|
|
1880
|
-
allAssetUrls.set(u, 'img');
|
|
1881
|
-
for (const u of allBackgroundImageUrls)
|
|
1882
|
-
allAssetUrls.set(u, 'background');
|
|
1883
|
-
// Brand assets
|
|
1884
|
-
if (brand.favicon)
|
|
1885
|
-
allAssetUrls.set(brand.favicon, 'favicon');
|
|
1886
|
-
if (brand.ogImage)
|
|
1887
|
-
allAssetUrls.set(brand.ogImage, 'og-image');
|
|
1888
|
-
if (brand.appleTouchIcon)
|
|
1889
|
-
allAssetUrls.set(brand.appleTouchIcon, 'icon');
|
|
1890
|
-
for (const [url, rel] of allLinkAssetUrls) {
|
|
1891
|
-
if (/icon/i.test(rel))
|
|
1892
|
-
allAssetUrls.set(url, 'favicon');
|
|
1893
|
-
}
|
|
1894
|
-
if (allAssetUrls.size > 0) {
|
|
1895
|
-
const imageDir = path.join(outputDir, 'images');
|
|
1896
|
-
fs.mkdirSync(imageDir, { recursive: true });
|
|
1897
|
-
for (const [url, type] of allAssetUrls) {
|
|
1898
|
-
try {
|
|
1899
|
-
const res = await fetch(url);
|
|
1900
|
-
if (!res.ok) {
|
|
1901
|
-
downloadedImages.push({ url, type });
|
|
1902
|
-
continue;
|
|
1903
|
-
}
|
|
1904
|
-
const buffer = Buffer.from(await res.arrayBuffer());
|
|
1905
|
-
const urlPath = decodeURIComponent(new URL(url).pathname);
|
|
1906
|
-
const filename = path.basename(urlPath) || `asset-${downloadedImages.length}.bin`;
|
|
1907
|
-
const localPath = path.join(imageDir, filename);
|
|
1908
|
-
fs.writeFileSync(localPath, buffer);
|
|
1909
|
-
downloadedImages.push({
|
|
1910
|
-
url, type, localPath,
|
|
1911
|
-
mimeType: res.headers.get('content-type') || undefined,
|
|
1912
|
-
sizeBytes: buffer.length,
|
|
1913
|
-
});
|
|
1914
|
-
}
|
|
1915
|
-
catch {
|
|
1916
|
-
downloadedImages.push({ url, type });
|
|
1917
|
-
}
|
|
1918
|
-
}
|
|
1919
|
-
}
|
|
1920
|
-
// Download external stylesheets
|
|
1921
|
-
const downloadedStylesheets = [];
|
|
1922
|
-
if (allExternalStylesheetUrls.size > 0) {
|
|
1923
|
-
const cssDir = path.join(outputDir, 'stylesheets');
|
|
1924
|
-
fs.mkdirSync(cssDir, { recursive: true });
|
|
1925
|
-
for (const url of allExternalStylesheetUrls) {
|
|
1926
|
-
try {
|
|
1927
|
-
const res = await fetch(url);
|
|
1928
|
-
if (!res.ok) {
|
|
1929
|
-
downloadedStylesheets.push({ url });
|
|
1930
|
-
continue;
|
|
1931
|
-
}
|
|
1932
|
-
const text = await res.text();
|
|
1933
|
-
const urlPath = decodeURIComponent(new URL(url).pathname);
|
|
1934
|
-
const filename = path.basename(urlPath) || `style-${downloadedStylesheets.length}.css`;
|
|
1935
|
-
const localPath = path.join(cssDir, filename);
|
|
1936
|
-
fs.writeFileSync(localPath, text);
|
|
1937
|
-
downloadedStylesheets.push({ url, localPath });
|
|
1938
|
-
}
|
|
1939
|
-
catch {
|
|
1940
|
-
downloadedStylesheets.push({ url });
|
|
1941
|
-
}
|
|
1942
|
-
}
|
|
1943
|
-
}
|
|
1944
|
-
// Download videos
|
|
1945
|
-
const downloadedVideos = [];
|
|
1946
|
-
if (allVideoUrls.size > 0) {
|
|
1947
|
-
const videoDir = path.join(outputDir, 'videos');
|
|
1948
|
-
fs.mkdirSync(videoDir, { recursive: true });
|
|
1949
|
-
for (const url of allVideoUrls) {
|
|
1950
|
-
try {
|
|
1951
|
-
const res = await fetch(url);
|
|
1952
|
-
if (!res.ok) {
|
|
1953
|
-
downloadedVideos.push({ url, type: 'video' });
|
|
1954
|
-
continue;
|
|
1955
|
-
}
|
|
1956
|
-
const buffer = Buffer.from(await res.arrayBuffer());
|
|
1957
|
-
const urlPath = decodeURIComponent(new URL(url).pathname);
|
|
1958
|
-
const filename = path.basename(urlPath) || `video-${downloadedVideos.length}.mp4`;
|
|
1959
|
-
const localPath = path.join(videoDir, filename);
|
|
1960
|
-
fs.writeFileSync(localPath, buffer);
|
|
1961
|
-
downloadedVideos.push({
|
|
1962
|
-
url, type: 'video', localPath,
|
|
1963
|
-
mimeType: res.headers.get('content-type') || undefined,
|
|
1964
|
-
sizeBytes: buffer.length,
|
|
1965
|
-
});
|
|
1966
|
-
}
|
|
1967
|
-
catch {
|
|
1968
|
-
downloadedVideos.push({ url, type: 'video' });
|
|
1969
|
-
}
|
|
1970
|
-
}
|
|
1971
|
-
}
|
|
1972
|
-
const tokens = {
|
|
1973
|
-
url: opts.url,
|
|
1974
|
-
extractedAt: new Date().toISOString(),
|
|
1975
|
-
viewportsAudited: viewports.map(v => v.name),
|
|
1976
|
-
customProperties: allCustomProps,
|
|
1977
|
-
colors: clusteredColors,
|
|
1978
|
-
typography: { families, scale: Array.from(scaleMap.values()).sort((a, b) => b.count - a.count) },
|
|
1979
|
-
brand,
|
|
1980
|
-
logos: allLogos,
|
|
1981
|
-
icons: allIcons.slice(0, 50),
|
|
1982
|
-
fontFiles: downloadedFontFiles,
|
|
1983
|
-
images: downloadedImages,
|
|
1984
|
-
videos: downloadedVideos,
|
|
1985
|
-
stylesheets: downloadedStylesheets,
|
|
1986
|
-
responsive: responsiveTokens,
|
|
1987
|
-
detectedLibraries: Array.from(allDetectedLibraries),
|
|
1988
|
-
};
|
|
1989
|
-
const tokenPath = path.join(outputDir, 'tokens.json');
|
|
1990
|
-
fs.writeFileSync(tokenPath, JSON.stringify(tokens, null, 2));
|
|
1991
|
-
return { tokens, outputDir, screenshotPaths };
|
|
1992
|
-
}
|
|
1993
|
-
// ---------------------------------------------------------------------------
|
|
1994
|
-
// Zip packaging
|
|
1995
|
-
// ---------------------------------------------------------------------------
|
|
1996
|
-
async function createZipBundle(outputDir, tokens) {
|
|
1997
|
-
const { execSync } = await import('node:child_process');
|
|
1998
|
-
const zipPath = `${outputDir}.zip`;
|
|
1999
|
-
// Use system zip (available on macOS/Linux)
|
|
2000
|
-
execSync(`cd "${path.dirname(outputDir)}" && zip -r "${path.basename(zipPath)}" "${path.basename(outputDir)}"`, { stdio: 'pipe' });
|
|
2001
|
-
return zipPath;
|
|
2002
|
-
}
|
|
2003
494
|
// ---------------------------------------------------------------------------
|
|
2004
495
|
// CLI wrapper for token extraction
|
|
2005
496
|
// ---------------------------------------------------------------------------
|