@palate/cli 0.0.0-stage → 0.0.2
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/LICENSE +20 -0
- package/README.md +153 -2
- package/bin/palate.mjs +894 -0
- package/kb/anatomy/data-table.md +24 -0
- package/kb/anatomy/empty-state.md +16 -0
- package/kb/anatomy/filter-toolbar.md +19 -0
- package/kb/anatomy/meter.md +16 -0
- package/kb/anatomy/product-window.md +18 -0
- package/kb/anatomy/settings-page.md +18 -0
- package/kb/anatomy/status-indicator.md +17 -0
- package/kb/anatomy/toast-undo.md +16 -0
- package/kb/bench/briefs.json +618 -0
- package/kb/craft/SOURCES.md +48 -0
- package/kb/craft/creativity.md +55 -0
- package/kb/craft/details.md +75 -0
- package/kb/craft/landing-demos.md +46 -0
- package/kb/craft/mistakes.md +73 -0
- package/kb/craft/motion.md +53 -0
- package/kb/craft/principles.md +68 -0
- package/kb/index/discovery.json +1 -0
- package/kb/inspiration/index.json +79 -0
- package/kb/moves/animated-comparison-table.md +58 -0
- package/kb/moves/before-after-slider.md +58 -0
- package/kb/moves/bento-live-tiles.md +57 -0
- package/kb/moves/calm-motion-system.md +63 -0
- package/kb/moves/canvas-grain.md +58 -0
- package/kb/moves/changelog-as-magazine.md +56 -0
- package/kb/moves/code-drawn-diagram.md +56 -0
- package/kb/moves/colophon.md +54 -0
- package/kb/moves/command-palette-site-nav.md +58 -0
- package/kb/moves/confetti-free-celebration.md +58 -0
- package/kb/moves/console-easter-egg.md +57 -0
- package/kb/moves/copy-confirmation.md +57 -0
- package/kb/moves/cursor-reactive-light.md +57 -0
- package/kb/moves/dark-band-contrast-section.md +55 -0
- package/kb/moves/designed-dark-mode.md +58 -0
- package/kb/moves/duotone-photography.md +55 -0
- package/kb/moves/editorial-drop-cap.md +58 -0
- package/kb/moves/editorial-grid-pull-quotes.md +55 -0
- package/kb/moves/engraving-colour-lens.md +64 -0
- package/kb/moves/explorable-control.md +57 -0
- package/kb/moves/feature-films-on-painted-stages.md +58 -0
- package/kb/moves/first-win-onboarding.md +59 -0
- package/kb/moves/footer-live-status.md +56 -0
- package/kb/moves/footer-rewards-the-end.md +58 -0
- package/kb/moves/generative-brand-pattern.md +55 -0
- package/kb/moves/gentle-error-nudge.md +59 -0
- package/kb/moves/halftone-dot-imagery.md +55 -0
- package/kb/moves/hand-drawn-annotations.md +55 -0
- package/kb/moves/hero-paints-itself-in.md +65 -0
- package/kb/moves/honest-progress-narration.md +58 -0
- package/kb/moves/honest-testimonial-wall.md +57 -0
- package/kb/moves/inline-sparklines-in-copy.md +57 -0
- package/kb/moves/inspect-our-taste.md +58 -0
- package/kb/moves/interactive-pricing-calculator.md +59 -0
- package/kb/moves/isometric-illustration-system.md +56 -0
- package/kb/moves/kinetic-variable-type.md +57 -0
- package/kb/moves/konami-secret.md +59 -0
- package/kb/moves/layered-paper-depth.md +56 -0
- package/kb/moves/lightweight-3d-object-hero.md +59 -0
- package/kb/moves/line-engraving-from-image.md +63 -0
- package/kb/moves/link-preview-peek.md +57 -0
- package/kb/moves/live-data-ticker.md +61 -0
- package/kb/moves/made-with-proof.md +51 -0
- package/kb/moves/magnetic-buttons.md +55 -0
- package/kb/moves/marquee-done-right.md +58 -0
- package/kb/moves/moves.yaml +450 -0
- package/kb/moves/museum-painting-backdrop.md +59 -0
- package/kb/moves/one-colour-brand-world.md +56 -0
- package/kb/moves/playful-404.md +58 -0
- package/kb/moves/pointer-highlight-word.md +56 -0
- package/kb/moves/product-in-context.md +56 -0
- package/kb/moves/real-world-data.md +58 -0
- package/kb/moves/referral-reward-reveal.md +59 -0
- package/kb/moves/riso-print-texture.md +57 -0
- package/kb/moves/screenshot-detail-crops.md +56 -0
- package/kb/moves/scroll-progress-chapters.md +57 -0
- package/kb/moves/scroll-scrubbed-product-film.md +62 -0
- package/kb/moves/self-playing-product-demo.md +61 -0
- package/kb/moves/shape-continuity-morph.md +58 -0
- package/kb/moves/sliding-nav-indicator.md +58 -0
- package/kb/moves/sticky-scroll-story.md +56 -0
- package/kb/moves/story-led-long-scroll.md +56 -0
- package/kb/moves/tactile-button.md +63 -0
- package/kb/moves/technical-drawing-lines.md +57 -0
- package/kb/moves/time-of-day-theming.md +56 -0
- package/kb/moves/tiny-game-easter-egg.md +60 -0
- package/kb/moves/type-decode-reveal.md +57 -0
- package/kb/moves/typographic-numeral-hero.md +58 -0
- package/kb/moves/view-transitions-between-pages.md +57 -0
- package/kb/moves/waitlist-position-live.md +60 -0
- package/kb/patterns/app-team-members.md +27 -0
- package/kb/patterns/chat-agent-approvals.md +31 -0
- package/kb/patterns/commerce-product-page.md +34 -0
- package/kb/patterns/dashboard-entity-table.md +37 -0
- package/kb/patterns/dashboard-home-today.md +24 -0
- package/kb/patterns/docs-api-reference.md +31 -0
- package/kb/patterns/marketing-product-led-landing.md +30 -0
- package/kb/patterns/mobile-daily-checklist.md +26 -0
- package/kb/references/index.json +1 -0
- package/kb/references/thumbs/004c5e9072.jpg +0 -0
- package/kb/references/thumbs/008915eb21.jpg +0 -0
- package/kb/references/thumbs/0098cf66c2.jpg +0 -0
- package/kb/references/thumbs/012e61b31f.jpg +0 -0
- package/kb/references/thumbs/014d4e46fc.jpg +0 -0
- package/kb/references/thumbs/01e54f394a.jpg +0 -0
- package/kb/references/thumbs/01e99a579c.jpg +0 -0
- package/kb/references/thumbs/01fd014575.jpg +0 -0
- package/kb/references/thumbs/022778ca72.jpg +0 -0
- package/kb/references/thumbs/02717bf1e1.jpg +0 -0
- package/kb/references/thumbs/02a5cfdbe9.jpg +0 -0
- package/kb/references/thumbs/02c9823f8d.jpg +0 -0
- package/kb/references/thumbs/02f3974613.jpg +0 -0
- package/kb/references/thumbs/032427f1ce.jpg +0 -0
- package/kb/references/thumbs/03ba677d07.jpg +0 -0
- package/kb/references/thumbs/04f2476cfe.jpg +0 -0
- package/kb/references/thumbs/0563e7c52e.jpg +0 -0
- package/kb/references/thumbs/05fa6b8eef.jpg +0 -0
- package/kb/references/thumbs/065bce1517.jpg +0 -0
- package/kb/references/thumbs/06c017f113.jpg +0 -0
- package/kb/references/thumbs/0770e2d8d3.jpg +0 -0
- package/kb/references/thumbs/078a26c4cb.jpg +0 -0
- package/kb/references/thumbs/08679b6e84.jpg +0 -0
- package/kb/references/thumbs/088c45a34c.jpg +0 -0
- package/kb/references/thumbs/089e9fd4e4.jpg +0 -0
- package/kb/references/thumbs/08f773231e.jpg +0 -0
- package/kb/references/thumbs/093ca3e2e3.jpg +0 -0
- package/kb/references/thumbs/0963320f74.jpg +0 -0
- package/kb/references/thumbs/09fc1ded85.jpg +0 -0
- package/kb/references/thumbs/0a368f3963.jpg +0 -0
- package/kb/references/thumbs/0a9e0e5201.jpg +0 -0
- package/kb/references/thumbs/0aace67ee7.jpg +0 -0
- package/kb/references/thumbs/0b61ecf793.jpg +0 -0
- package/kb/references/thumbs/0bde18ae09.jpg +0 -0
- package/kb/references/thumbs/0bf7ee8143.jpg +0 -0
- package/kb/references/thumbs/0ca20a13e0.jpg +0 -0
- package/kb/references/thumbs/0cdc0198c4.jpg +0 -0
- package/kb/references/thumbs/0cf9790b28.jpg +0 -0
- package/kb/references/thumbs/0d0202904b.jpg +0 -0
- package/kb/references/thumbs/0d053aba18.jpg +0 -0
- package/kb/references/thumbs/0d0c2c13d2.jpg +0 -0
- package/kb/references/thumbs/0d30be99b0.jpg +0 -0
- package/kb/references/thumbs/0d503da1b7.jpg +0 -0
- package/kb/references/thumbs/0ddad7b740.jpg +0 -0
- package/kb/references/thumbs/0e0a148101.jpg +0 -0
- package/kb/references/thumbs/0e401b6b75.jpg +0 -0
- package/kb/references/thumbs/0e8f32ba0f.jpg +0 -0
- package/kb/references/thumbs/0ef1966a7d.jpg +0 -0
- package/kb/references/thumbs/0efd416d11.jpg +0 -0
- package/kb/references/thumbs/0f3af263ae.jpg +0 -0
- package/kb/references/thumbs/0f9644c3a5.jpg +0 -0
- package/kb/references/thumbs/1020552737.jpg +0 -0
- package/kb/references/thumbs/1037626587.jpg +0 -0
- package/kb/references/thumbs/10512b7e94.jpg +0 -0
- package/kb/references/thumbs/106dc4a7b5.jpg +0 -0
- package/kb/references/thumbs/108943f6f7.jpg +0 -0
- package/kb/references/thumbs/10a5551d51.jpg +0 -0
- package/kb/references/thumbs/10dd01076c.jpg +0 -0
- package/kb/references/thumbs/11b06e39da.jpg +0 -0
- package/kb/references/thumbs/11baa1ec83.jpg +0 -0
- package/kb/references/thumbs/11bbe12167.jpg +0 -0
- package/kb/references/thumbs/11ca37feb8.jpg +0 -0
- package/kb/references/thumbs/11f14d3f12.jpg +0 -0
- package/kb/references/thumbs/124530ceb5.jpg +0 -0
- package/kb/references/thumbs/1276402336.jpg +0 -0
- package/kb/references/thumbs/1280e5c9d6.jpg +0 -0
- package/kb/references/thumbs/12cac0d172.jpg +0 -0
- package/kb/references/thumbs/138be6623a.jpg +0 -0
- package/kb/references/thumbs/13a0c544d1.jpg +0 -0
- package/kb/references/thumbs/13a32cb94c.jpg +0 -0
- package/kb/references/thumbs/144035d641.jpg +0 -0
- package/kb/references/thumbs/1457e41924.jpg +0 -0
- package/kb/references/thumbs/14972e3a86.jpg +0 -0
- package/kb/references/thumbs/15a47f2e4c.jpg +0 -0
- package/kb/references/thumbs/15aa15d22e.jpg +0 -0
- package/kb/references/thumbs/160dcd24b7.jpg +0 -0
- package/kb/references/thumbs/1610978529.jpg +0 -0
- package/kb/references/thumbs/16db30ba37.jpg +0 -0
- package/kb/references/thumbs/16f19ae4f3.jpg +0 -0
- package/kb/references/thumbs/16f758b03f.jpg +0 -0
- package/kb/references/thumbs/1748e5b751.jpg +0 -0
- package/kb/references/thumbs/175d0b0e69.jpg +0 -0
- package/kb/references/thumbs/178f571a36.jpg +0 -0
- package/kb/references/thumbs/17c43dc025.jpg +0 -0
- package/kb/references/thumbs/17c7f05b50.jpg +0 -0
- package/kb/references/thumbs/17f97e61dd.jpg +0 -0
- package/kb/references/thumbs/1804e17365.jpg +0 -0
- package/kb/references/thumbs/195c162ed1.jpg +0 -0
- package/kb/references/thumbs/19e3699c29.jpg +0 -0
- package/kb/references/thumbs/1a48e0a4d5.jpg +0 -0
- package/kb/references/thumbs/1a6cda265e.jpg +0 -0
- package/kb/references/thumbs/1a7e012091.jpg +0 -0
- package/kb/references/thumbs/1a9173a172.jpg +0 -0
- package/kb/references/thumbs/1b34a6b4d2.jpg +0 -0
- package/kb/references/thumbs/1b5284f3b2.jpg +0 -0
- package/kb/references/thumbs/1b93fbd6d3.jpg +0 -0
- package/kb/references/thumbs/1c6728fef6.jpg +0 -0
- package/kb/references/thumbs/1c6f101618.jpg +0 -0
- package/kb/references/thumbs/1cf7011a94.jpg +0 -0
- package/kb/references/thumbs/1d1769e539.jpg +0 -0
- package/kb/references/thumbs/1d1fd23199.jpg +0 -0
- package/kb/references/thumbs/1d7b6602ac.jpg +0 -0
- package/kb/references/thumbs/1dbbc61b9e.jpg +0 -0
- package/kb/references/thumbs/1dbdd6d21d.jpg +0 -0
- package/kb/references/thumbs/1dd873744b.jpg +0 -0
- package/kb/references/thumbs/1df4dd7cf9.jpg +0 -0
- package/kb/references/thumbs/1ece8c35b8.jpg +0 -0
- package/kb/references/thumbs/1f0e5b9aa4.jpg +0 -0
- package/kb/references/thumbs/1f1419651b.jpg +0 -0
- package/kb/references/thumbs/1f2a6da92d.jpg +0 -0
- package/kb/references/thumbs/1f651f33c0.jpg +0 -0
- package/kb/references/thumbs/1f8b697d18.jpg +0 -0
- package/kb/references/thumbs/1fee3824cd.jpg +0 -0
- package/kb/references/thumbs/1ff7757853.jpg +0 -0
- package/kb/references/thumbs/2043182f01.jpg +0 -0
- package/kb/references/thumbs/2059dabcd5.jpg +0 -0
- package/kb/references/thumbs/209d9dbb95.jpg +0 -0
- package/kb/references/thumbs/20d8923554.jpg +0 -0
- package/kb/references/thumbs/210dd3b671.jpg +0 -0
- package/kb/references/thumbs/2120719116.jpg +0 -0
- package/kb/references/thumbs/21a7384429.jpg +0 -0
- package/kb/references/thumbs/225f187391.jpg +0 -0
- package/kb/references/thumbs/230caf7dd2.jpg +0 -0
- package/kb/references/thumbs/235afad355.jpg +0 -0
- package/kb/references/thumbs/2378066687.jpg +0 -0
- package/kb/references/thumbs/241faf68a1.jpg +0 -0
- package/kb/references/thumbs/24d9f1a9b9.jpg +0 -0
- package/kb/references/thumbs/2505c8b9a0.jpg +0 -0
- package/kb/references/thumbs/25593638ca.jpg +0 -0
- package/kb/references/thumbs/25dd454f18.jpg +0 -0
- package/kb/references/thumbs/2602e123e2.jpg +0 -0
- package/kb/references/thumbs/26095c0b11.jpg +0 -0
- package/kb/references/thumbs/26260d6238.jpg +0 -0
- package/kb/references/thumbs/26bba21836.jpg +0 -0
- package/kb/references/thumbs/26bde3520b.jpg +0 -0
- package/kb/references/thumbs/272e22067c.jpg +0 -0
- package/kb/references/thumbs/27421b3226.jpg +0 -0
- package/kb/references/thumbs/2769ebca41.jpg +0 -0
- package/kb/references/thumbs/27e2f6e8b0.jpg +0 -0
- package/kb/references/thumbs/28432ba373.jpg +0 -0
- package/kb/references/thumbs/2846626f19.jpg +0 -0
- package/kb/references/thumbs/28750e821e.jpg +0 -0
- package/kb/references/thumbs/289481230f.jpg +0 -0
- package/kb/references/thumbs/2897967408.jpg +0 -0
- package/kb/references/thumbs/28c0a5fa17.jpg +0 -0
- package/kb/references/thumbs/28c181fba5.jpg +0 -0
- package/kb/references/thumbs/29806f539f.jpg +0 -0
- package/kb/references/thumbs/2997f56ee7.jpg +0 -0
- package/kb/references/thumbs/29b34a10fb.jpg +0 -0
- package/kb/references/thumbs/29bd78cdb4.jpg +0 -0
- package/kb/references/thumbs/29c8c6c22a.jpg +0 -0
- package/kb/references/thumbs/2ac23f7fae.jpg +0 -0
- package/kb/references/thumbs/2b76e13f79.jpg +0 -0
- package/kb/references/thumbs/2b84226479.jpg +0 -0
- package/kb/references/thumbs/2bf2dbf737.jpg +0 -0
- package/kb/references/thumbs/2bf89abbf6.jpg +0 -0
- package/kb/references/thumbs/2c562cd534.jpg +0 -0
- package/kb/references/thumbs/2cfe9d48c4.jpg +0 -0
- package/kb/references/thumbs/2d49f19b27.jpg +0 -0
- package/kb/references/thumbs/2dc8670da7.jpg +0 -0
- package/kb/references/thumbs/2de947c469.jpg +0 -0
- package/kb/references/thumbs/2e075ca79f.jpg +0 -0
- package/kb/references/thumbs/2eb0820e6b.jpg +0 -0
- package/kb/references/thumbs/2ec56a9dc0.jpg +0 -0
- package/kb/references/thumbs/30bb4ad607.jpg +0 -0
- package/kb/references/thumbs/30d19b8444.jpg +0 -0
- package/kb/references/thumbs/3173c71d73.jpg +0 -0
- package/kb/references/thumbs/31e59e4518.jpg +0 -0
- package/kb/references/thumbs/322b8960e8.jpg +0 -0
- package/kb/references/thumbs/325c9be6f4.jpg +0 -0
- package/kb/references/thumbs/3273338982.jpg +0 -0
- package/kb/references/thumbs/32f8387f38.jpg +0 -0
- package/kb/references/thumbs/332f6b74c3.jpg +0 -0
- package/kb/references/thumbs/334b146b6a.jpg +0 -0
- package/kb/references/thumbs/339abd60e4.jpg +0 -0
- package/kb/references/thumbs/33e88ec295.jpg +0 -0
- package/kb/references/thumbs/340931b5d4.jpg +0 -0
- package/kb/references/thumbs/34165a93bd.jpg +0 -0
- package/kb/references/thumbs/34d0575dd7.jpg +0 -0
- package/kb/references/thumbs/3549e75f0a.jpg +0 -0
- package/kb/references/thumbs/35827f1561.jpg +0 -0
- package/kb/references/thumbs/37c50280ec.jpg +0 -0
- package/kb/references/thumbs/37e66ecf11.jpg +0 -0
- package/kb/references/thumbs/3841a04fdc.jpg +0 -0
- package/kb/references/thumbs/38490c088f.jpg +0 -0
- package/kb/references/thumbs/3850319e9f.jpg +0 -0
- package/kb/references/thumbs/3856a64166.jpg +0 -0
- package/kb/references/thumbs/3881f211c2.jpg +0 -0
- package/kb/references/thumbs/38c7dde2f3.jpg +0 -0
- package/kb/references/thumbs/38ce9e486c.jpg +0 -0
- package/kb/references/thumbs/390ea8e01b.jpg +0 -0
- package/kb/references/thumbs/391b7fc27c.jpg +0 -0
- package/kb/references/thumbs/395f7214dc.jpg +0 -0
- package/kb/references/thumbs/39e81a5db2.jpg +0 -0
- package/kb/references/thumbs/3a67c8d655.jpg +0 -0
- package/kb/references/thumbs/3a89c25e3b.jpg +0 -0
- package/kb/references/thumbs/3aa0837542.jpg +0 -0
- package/kb/references/thumbs/3aae18aa8a.jpg +0 -0
- package/kb/references/thumbs/3bdbe767db.jpg +0 -0
- package/kb/references/thumbs/3c0616fc7f.jpg +0 -0
- package/kb/references/thumbs/3c18e9db1d.jpg +0 -0
- package/kb/references/thumbs/3c5a14076c.jpg +0 -0
- package/kb/references/thumbs/3ca9ec15c6.jpg +0 -0
- package/kb/references/thumbs/3d7756fd35.jpg +0 -0
- package/kb/references/thumbs/3dd45bd33d.jpg +0 -0
- package/kb/references/thumbs/3de72d5414.jpg +0 -0
- package/kb/references/thumbs/3dede597af.jpg +0 -0
- package/kb/references/thumbs/3ea948619e.jpg +0 -0
- package/kb/references/thumbs/402c12054c.jpg +0 -0
- package/kb/references/thumbs/403ab4af90.jpg +0 -0
- package/kb/references/thumbs/40773a8c63.jpg +0 -0
- package/kb/references/thumbs/407d9583db.jpg +0 -0
- package/kb/references/thumbs/415dc1d84c.jpg +0 -0
- package/kb/references/thumbs/420b676a4f.jpg +0 -0
- package/kb/references/thumbs/42a1ee85eb.jpg +0 -0
- package/kb/references/thumbs/43059221aa.jpg +0 -0
- package/kb/references/thumbs/432fa0d22c.jpg +0 -0
- package/kb/references/thumbs/43826f20e3.jpg +0 -0
- package/kb/references/thumbs/43ac55c98a.jpg +0 -0
- package/kb/references/thumbs/43d098f3c3.jpg +0 -0
- package/kb/references/thumbs/4449d80c2c.jpg +0 -0
- package/kb/references/thumbs/44625727dc.jpg +0 -0
- package/kb/references/thumbs/4482bb87a1.jpg +0 -0
- package/kb/references/thumbs/44b078acdc.jpg +0 -0
- package/kb/references/thumbs/44d0305850.jpg +0 -0
- package/kb/references/thumbs/456bb6b59e.jpg +0 -0
- package/kb/references/thumbs/45df73ce48.jpg +0 -0
- package/kb/references/thumbs/45edb45d87.jpg +0 -0
- package/kb/references/thumbs/4671d57cd5.jpg +0 -0
- package/kb/references/thumbs/46cc108518.jpg +0 -0
- package/kb/references/thumbs/470022e614.jpg +0 -0
- package/kb/references/thumbs/4719a669ce.jpg +0 -0
- package/kb/references/thumbs/47404f5ad4.jpg +0 -0
- package/kb/references/thumbs/479d3eb977.jpg +0 -0
- package/kb/references/thumbs/479ed92ceb.jpg +0 -0
- package/kb/references/thumbs/47baed50c7.jpg +0 -0
- package/kb/references/thumbs/47fdcfd7b0.jpg +0 -0
- package/kb/references/thumbs/48fd5e94c5.jpg +0 -0
- package/kb/references/thumbs/493807917b.jpg +0 -0
- package/kb/references/thumbs/49a1c67293.jpg +0 -0
- package/kb/references/thumbs/49e2d7fafe.jpg +0 -0
- package/kb/references/thumbs/49f540fcb6.jpg +0 -0
- package/kb/references/thumbs/4a67ec4028.jpg +0 -0
- package/kb/references/thumbs/4a93275b04.jpg +0 -0
- package/kb/references/thumbs/4aae6f52da.jpg +0 -0
- package/kb/references/thumbs/4b1bae66bd.jpg +0 -0
- package/kb/references/thumbs/4b75da15a9.jpg +0 -0
- package/kb/references/thumbs/4b77789b88.jpg +0 -0
- package/kb/references/thumbs/4bc2f3f3a4.jpg +0 -0
- package/kb/references/thumbs/4c48f27b88.jpg +0 -0
- package/kb/references/thumbs/4c52bd8bfb.jpg +0 -0
- package/kb/references/thumbs/4c9b4acc60.jpg +0 -0
- package/kb/references/thumbs/4cbf0752d4.jpg +0 -0
- package/kb/references/thumbs/4cd2672517.jpg +0 -0
- package/kb/references/thumbs/4dd22a8181.jpg +0 -0
- package/kb/references/thumbs/4ee3447314.jpg +0 -0
- package/kb/references/thumbs/4ef29c8144.jpg +0 -0
- package/kb/references/thumbs/4f82fadfd6.jpg +0 -0
- package/kb/references/thumbs/4fc9b643d8.jpg +0 -0
- package/kb/references/thumbs/50549524b0.jpg +0 -0
- package/kb/references/thumbs/509a615883.jpg +0 -0
- package/kb/references/thumbs/50c03ef855.jpg +0 -0
- package/kb/references/thumbs/50cf9acad6.jpg +0 -0
- package/kb/references/thumbs/51131b025c.jpg +0 -0
- package/kb/references/thumbs/51da1124f0.jpg +0 -0
- package/kb/references/thumbs/524d5e7288.jpg +0 -0
- package/kb/references/thumbs/5251247c81.jpg +0 -0
- package/kb/references/thumbs/526b91b7ec.jpg +0 -0
- package/kb/references/thumbs/52f02f6c2e.jpg +0 -0
- package/kb/references/thumbs/530ab530dc.jpg +0 -0
- package/kb/references/thumbs/5335ee4404.jpg +0 -0
- package/kb/references/thumbs/5385b90939.jpg +0 -0
- package/kb/references/thumbs/53c54654cc.jpg +0 -0
- package/kb/references/thumbs/5420794c6c.jpg +0 -0
- package/kb/references/thumbs/542a9dce8b.jpg +0 -0
- package/kb/references/thumbs/5571530a5f.jpg +0 -0
- package/kb/references/thumbs/56cc68d804.jpg +0 -0
- package/kb/references/thumbs/577b7f7c44.jpg +0 -0
- package/kb/references/thumbs/57adf8f1bb.jpg +0 -0
- package/kb/references/thumbs/57c5f5e3a6.jpg +0 -0
- package/kb/references/thumbs/57ffe4e839.jpg +0 -0
- package/kb/references/thumbs/5817dfc7d6.jpg +0 -0
- package/kb/references/thumbs/581a182a7a.jpg +0 -0
- package/kb/references/thumbs/58807679c8.jpg +0 -0
- package/kb/references/thumbs/589a1d9111.jpg +0 -0
- package/kb/references/thumbs/59167391ab.jpg +0 -0
- package/kb/references/thumbs/5a70420166.jpg +0 -0
- package/kb/references/thumbs/5b10df1b8d.jpg +0 -0
- package/kb/references/thumbs/5b7a712fdb.jpg +0 -0
- package/kb/references/thumbs/5bc46b92ad.jpg +0 -0
- package/kb/references/thumbs/5c0546a120.jpg +0 -0
- package/kb/references/thumbs/5c2b6125a8.jpg +0 -0
- package/kb/references/thumbs/5c536b9e42.jpg +0 -0
- package/kb/references/thumbs/5c8e441d30.jpg +0 -0
- package/kb/references/thumbs/5cb6f1baa5.jpg +0 -0
- package/kb/references/thumbs/5ce6c1f21e.jpg +0 -0
- package/kb/references/thumbs/5cef052c0a.jpg +0 -0
- package/kb/references/thumbs/5d08438209.jpg +0 -0
- package/kb/references/thumbs/5d0f9af9b0.jpg +0 -0
- package/kb/references/thumbs/5d300ed332.jpg +0 -0
- package/kb/references/thumbs/5d708a4794.jpg +0 -0
- package/kb/references/thumbs/5de8826a98.jpg +0 -0
- package/kb/references/thumbs/5df7bff22c.jpg +0 -0
- package/kb/references/thumbs/5e087d0f00.jpg +0 -0
- package/kb/references/thumbs/5e44e70ca0.jpg +0 -0
- package/kb/references/thumbs/5eff5d80ce.jpg +0 -0
- package/kb/references/thumbs/5f015c96b1.jpg +0 -0
- package/kb/references/thumbs/5f3289b7c4.jpg +0 -0
- package/kb/references/thumbs/5f830c40eb.jpg +0 -0
- package/kb/references/thumbs/60c4c3527b.jpg +0 -0
- package/kb/references/thumbs/60e9e594dd.jpg +0 -0
- package/kb/references/thumbs/615421d7be.jpg +0 -0
- package/kb/references/thumbs/6176e4004f.jpg +0 -0
- package/kb/references/thumbs/61f0fe5e06.jpg +0 -0
- package/kb/references/thumbs/6213785db4.jpg +0 -0
- package/kb/references/thumbs/62a670ae09.jpg +0 -0
- package/kb/references/thumbs/63cc12981b.jpg +0 -0
- package/kb/references/thumbs/641f15cc71.jpg +0 -0
- package/kb/references/thumbs/642ab2081d.jpg +0 -0
- package/kb/references/thumbs/644a59bf6e.jpg +0 -0
- package/kb/references/thumbs/644da8a8e3.jpg +0 -0
- package/kb/references/thumbs/6458986897.jpg +0 -0
- package/kb/references/thumbs/64cfbd7b93.jpg +0 -0
- package/kb/references/thumbs/661a57686a.jpg +0 -0
- package/kb/references/thumbs/66355a813a.jpg +0 -0
- package/kb/references/thumbs/664396dd44.jpg +0 -0
- package/kb/references/thumbs/66d832d65f.jpg +0 -0
- package/kb/references/thumbs/66de9e9833.jpg +0 -0
- package/kb/references/thumbs/6750964fd9.jpg +0 -0
- package/kb/references/thumbs/67c7164832.jpg +0 -0
- package/kb/references/thumbs/681600b6cd.jpg +0 -0
- package/kb/references/thumbs/682e75afe4.jpg +0 -0
- package/kb/references/thumbs/69274bc952.jpg +0 -0
- package/kb/references/thumbs/696ec08c52.jpg +0 -0
- package/kb/references/thumbs/69760111cc.jpg +0 -0
- package/kb/references/thumbs/697e32664f.jpg +0 -0
- package/kb/references/thumbs/69df92544b.jpg +0 -0
- package/kb/references/thumbs/69e48c5229.jpg +0 -0
- package/kb/references/thumbs/6a5fb75de1.jpg +0 -0
- package/kb/references/thumbs/6a7389e472.jpg +0 -0
- package/kb/references/thumbs/6a9b041e02.jpg +0 -0
- package/kb/references/thumbs/6ab2fa93ee.jpg +0 -0
- package/kb/references/thumbs/6ae3400319.jpg +0 -0
- package/kb/references/thumbs/6b21344bf3.jpg +0 -0
- package/kb/references/thumbs/6bd1b67a8a.jpg +0 -0
- package/kb/references/thumbs/6c99156c4f.jpg +0 -0
- package/kb/references/thumbs/6cc9b65384.jpg +0 -0
- package/kb/references/thumbs/6d15830a73.jpg +0 -0
- package/kb/references/thumbs/6d4c33d180.jpg +0 -0
- package/kb/references/thumbs/6e36815799.jpg +0 -0
- package/kb/references/thumbs/6eddb9cbfc.jpg +0 -0
- package/kb/references/thumbs/6f97968a7c.jpg +0 -0
- package/kb/references/thumbs/6fa0716a7d.jpg +0 -0
- package/kb/references/thumbs/6facd57184.jpg +0 -0
- package/kb/references/thumbs/6fd57b785a.jpg +0 -0
- package/kb/references/thumbs/700b3cb185.jpg +0 -0
- package/kb/references/thumbs/71197443fa.jpg +0 -0
- package/kb/references/thumbs/72082bc9ee.jpg +0 -0
- package/kb/references/thumbs/72453db479.jpg +0 -0
- package/kb/references/thumbs/72cd64d500.jpg +0 -0
- package/kb/references/thumbs/72d1e75283.jpg +0 -0
- package/kb/references/thumbs/72e13f7f5b.jpg +0 -0
- package/kb/references/thumbs/7459f8e644.jpg +0 -0
- package/kb/references/thumbs/7470d55c79.jpg +0 -0
- package/kb/references/thumbs/74c468f02a.jpg +0 -0
- package/kb/references/thumbs/74fed6eb77.jpg +0 -0
- package/kb/references/thumbs/756e05080d.jpg +0 -0
- package/kb/references/thumbs/75df7e5f49.jpg +0 -0
- package/kb/references/thumbs/76226da6ef.jpg +0 -0
- package/kb/references/thumbs/76a34aa056.jpg +0 -0
- package/kb/references/thumbs/76c7076589.jpg +0 -0
- package/kb/references/thumbs/76cccc396c.jpg +0 -0
- package/kb/references/thumbs/774654789f.jpg +0 -0
- package/kb/references/thumbs/7746cc494d.jpg +0 -0
- package/kb/references/thumbs/7766e36aa3.jpg +0 -0
- package/kb/references/thumbs/78049331ef.jpg +0 -0
- package/kb/references/thumbs/78808a0ca5.jpg +0 -0
- package/kb/references/thumbs/7890b0a310.jpg +0 -0
- package/kb/references/thumbs/78911975e6.jpg +0 -0
- package/kb/references/thumbs/78d289d5a0.jpg +0 -0
- package/kb/references/thumbs/794126267e.jpg +0 -0
- package/kb/references/thumbs/7976eb9025.jpg +0 -0
- package/kb/references/thumbs/798519ec7c.jpg +0 -0
- package/kb/references/thumbs/7a6cc5eecc.jpg +0 -0
- package/kb/references/thumbs/7ac8a7cd4a.jpg +0 -0
- package/kb/references/thumbs/7b1341d61d.jpg +0 -0
- package/kb/references/thumbs/7b9c4031e3.jpg +0 -0
- package/kb/references/thumbs/7ba5ad3779.jpg +0 -0
- package/kb/references/thumbs/7be6eb2bd0.jpg +0 -0
- package/kb/references/thumbs/7c30864f5d.jpg +0 -0
- package/kb/references/thumbs/7c379b5e17.jpg +0 -0
- package/kb/references/thumbs/7c4050eb02.jpg +0 -0
- package/kb/references/thumbs/7ca47b3221.jpg +0 -0
- package/kb/references/thumbs/7d43516bd7.jpg +0 -0
- package/kb/references/thumbs/7da9dbb98a.jpg +0 -0
- package/kb/references/thumbs/7ddda8f57c.jpg +0 -0
- package/kb/references/thumbs/7ece6d215c.jpg +0 -0
- package/kb/references/thumbs/7eebf029de.jpg +0 -0
- package/kb/references/thumbs/7f45483c15.jpg +0 -0
- package/kb/references/thumbs/7fde06eaa5.jpg +0 -0
- package/kb/references/thumbs/80407b97e9.jpg +0 -0
- package/kb/references/thumbs/80f0a6cf4b.jpg +0 -0
- package/kb/references/thumbs/8118b451b8.jpg +0 -0
- package/kb/references/thumbs/816baed4a8.jpg +0 -0
- package/kb/references/thumbs/817d2d15d9.jpg +0 -0
- package/kb/references/thumbs/8260cb430d.jpg +0 -0
- package/kb/references/thumbs/827b562899.jpg +0 -0
- package/kb/references/thumbs/8293cf1e92.jpg +0 -0
- package/kb/references/thumbs/8294653169.jpg +0 -0
- package/kb/references/thumbs/83061f871f.jpg +0 -0
- package/kb/references/thumbs/833df778ca.jpg +0 -0
- package/kb/references/thumbs/83d3acaafc.jpg +0 -0
- package/kb/references/thumbs/844c6efeff.jpg +0 -0
- package/kb/references/thumbs/84563d482a.jpg +0 -0
- package/kb/references/thumbs/848082dd94.jpg +0 -0
- package/kb/references/thumbs/8494b1da94.jpg +0 -0
- package/kb/references/thumbs/84b71014be.jpg +0 -0
- package/kb/references/thumbs/84be4a5f3c.jpg +0 -0
- package/kb/references/thumbs/84fd0fd25a.jpg +0 -0
- package/kb/references/thumbs/853e488e22.jpg +0 -0
- package/kb/references/thumbs/858c91585d.jpg +0 -0
- package/kb/references/thumbs/85903e8fe9.jpg +0 -0
- package/kb/references/thumbs/86597d43fd.jpg +0 -0
- package/kb/references/thumbs/866eb99500.jpg +0 -0
- package/kb/references/thumbs/86ac050340.jpg +0 -0
- package/kb/references/thumbs/86afec7b64.jpg +0 -0
- package/kb/references/thumbs/87238127f5.jpg +0 -0
- package/kb/references/thumbs/87321df792.jpg +0 -0
- package/kb/references/thumbs/87460f70cd.jpg +0 -0
- package/kb/references/thumbs/8770dfeaec.jpg +0 -0
- package/kb/references/thumbs/8798451a8c.jpg +0 -0
- package/kb/references/thumbs/87da1f34a3.jpg +0 -0
- package/kb/references/thumbs/882cdf8c05.jpg +0 -0
- package/kb/references/thumbs/8839f4106f.jpg +0 -0
- package/kb/references/thumbs/8844455212.jpg +0 -0
- package/kb/references/thumbs/88abba0497.jpg +0 -0
- package/kb/references/thumbs/88b372435d.jpg +0 -0
- package/kb/references/thumbs/88bd4f35fa.jpg +0 -0
- package/kb/references/thumbs/88c71a075e.jpg +0 -0
- package/kb/references/thumbs/892e5f048e.jpg +0 -0
- package/kb/references/thumbs/893cc8e920.jpg +0 -0
- package/kb/references/thumbs/896c198814.jpg +0 -0
- package/kb/references/thumbs/8a8e22f756.jpg +0 -0
- package/kb/references/thumbs/8a9d8a35cb.jpg +0 -0
- package/kb/references/thumbs/8ab1d67fec.jpg +0 -0
- package/kb/references/thumbs/8ab201fe12.jpg +0 -0
- package/kb/references/thumbs/8ac372c102.jpg +0 -0
- package/kb/references/thumbs/8b15736d62.jpg +0 -0
- package/kb/references/thumbs/8b1d8d6a05.jpg +0 -0
- package/kb/references/thumbs/8bb6050e20.jpg +0 -0
- package/kb/references/thumbs/8c6c9d1577.jpg +0 -0
- package/kb/references/thumbs/8d337851e0.jpg +0 -0
- package/kb/references/thumbs/8d44b33cc5.jpg +0 -0
- package/kb/references/thumbs/8d4536ff3b.jpg +0 -0
- package/kb/references/thumbs/8d9b99454a.jpg +0 -0
- package/kb/references/thumbs/8dda2303ff.jpg +0 -0
- package/kb/references/thumbs/8dec722d62.jpg +0 -0
- package/kb/references/thumbs/8e0ea4ca74.jpg +0 -0
- package/kb/references/thumbs/8ebba84d69.jpg +0 -0
- package/kb/references/thumbs/8ef09140c0.jpg +0 -0
- package/kb/references/thumbs/8f2928a1b0.jpg +0 -0
- package/kb/references/thumbs/8f88f9ddba.jpg +0 -0
- package/kb/references/thumbs/8fa4451009.jpg +0 -0
- package/kb/references/thumbs/90109b0e02.jpg +0 -0
- package/kb/references/thumbs/90e854e5c9.jpg +0 -0
- package/kb/references/thumbs/914e79f893.jpg +0 -0
- package/kb/references/thumbs/9161369197.jpg +0 -0
- package/kb/references/thumbs/9165c87d6f.jpg +0 -0
- package/kb/references/thumbs/9193f6558a.jpg +0 -0
- package/kb/references/thumbs/91ac7df877.jpg +0 -0
- package/kb/references/thumbs/9216bd4931.jpg +0 -0
- package/kb/references/thumbs/922ae8a26a.jpg +0 -0
- package/kb/references/thumbs/92a70e6867.jpg +0 -0
- package/kb/references/thumbs/92c77ece22.jpg +0 -0
- package/kb/references/thumbs/92e5914fbf.jpg +0 -0
- package/kb/references/thumbs/92ee36419a.jpg +0 -0
- package/kb/references/thumbs/94153a60b0.jpg +0 -0
- package/kb/references/thumbs/9449666640.jpg +0 -0
- package/kb/references/thumbs/944be6a646.jpg +0 -0
- package/kb/references/thumbs/945831476e.jpg +0 -0
- package/kb/references/thumbs/94c1af82f4.jpg +0 -0
- package/kb/references/thumbs/9599d6d589.jpg +0 -0
- package/kb/references/thumbs/960710f4cf.jpg +0 -0
- package/kb/references/thumbs/9608f12d64.jpg +0 -0
- package/kb/references/thumbs/962dabfaa0.jpg +0 -0
- package/kb/references/thumbs/965a3a95c6.jpg +0 -0
- package/kb/references/thumbs/9672ed5ae4.jpg +0 -0
- package/kb/references/thumbs/96891ae9c1.jpg +0 -0
- package/kb/references/thumbs/96e1907c99.jpg +0 -0
- package/kb/references/thumbs/96f25b6578.jpg +0 -0
- package/kb/references/thumbs/97211df9ae.jpg +0 -0
- package/kb/references/thumbs/9769d763a5.jpg +0 -0
- package/kb/references/thumbs/9792bd6d05.jpg +0 -0
- package/kb/references/thumbs/97d2837e3e.jpg +0 -0
- package/kb/references/thumbs/97d90d1d85.jpg +0 -0
- package/kb/references/thumbs/97fcd5774a.jpg +0 -0
- package/kb/references/thumbs/980f68564e.jpg +0 -0
- package/kb/references/thumbs/98511fb393.jpg +0 -0
- package/kb/references/thumbs/98f8fa694d.jpg +0 -0
- package/kb/references/thumbs/9a42c6bb97.jpg +0 -0
- package/kb/references/thumbs/9ab3600a06.jpg +0 -0
- package/kb/references/thumbs/9ab48d9130.jpg +0 -0
- package/kb/references/thumbs/9ad519197d.jpg +0 -0
- package/kb/references/thumbs/9b1382c657.jpg +0 -0
- package/kb/references/thumbs/9b6cc1e077.jpg +0 -0
- package/kb/references/thumbs/9beede8480.jpg +0 -0
- package/kb/references/thumbs/9bf8976952.jpg +0 -0
- package/kb/references/thumbs/9cace7b75a.jpg +0 -0
- package/kb/references/thumbs/9cdcc5745e.jpg +0 -0
- package/kb/references/thumbs/9d0b269074.jpg +0 -0
- package/kb/references/thumbs/9d41788b48.jpg +0 -0
- package/kb/references/thumbs/9d475bd6af.jpg +0 -0
- package/kb/references/thumbs/9e6bc543d0.jpg +0 -0
- package/kb/references/thumbs/9ed0e0a206.jpg +0 -0
- package/kb/references/thumbs/9ef538de13.jpg +0 -0
- package/kb/references/thumbs/9efc373ab4.jpg +0 -0
- package/kb/references/thumbs/9f4a939fa5.jpg +0 -0
- package/kb/references/thumbs/9f8d12d7b9.jpg +0 -0
- package/kb/references/thumbs/9fd5e3efa7.jpg +0 -0
- package/kb/references/thumbs/a0a9b9c572.jpg +0 -0
- package/kb/references/thumbs/a29ca9b185.jpg +0 -0
- package/kb/references/thumbs/a2a42884af.jpg +0 -0
- package/kb/references/thumbs/a2f5064e34.jpg +0 -0
- package/kb/references/thumbs/a3112ce672.jpg +0 -0
- package/kb/references/thumbs/a3437736d1.jpg +0 -0
- package/kb/references/thumbs/a372667e76.jpg +0 -0
- package/kb/references/thumbs/a39bae94e9.jpg +0 -0
- package/kb/references/thumbs/a3fc75d94c.jpg +0 -0
- package/kb/references/thumbs/a440487631.jpg +0 -0
- package/kb/references/thumbs/a4993d5fb5.jpg +0 -0
- package/kb/references/thumbs/a500476a86.jpg +0 -0
- package/kb/references/thumbs/a55506bf46.jpg +0 -0
- package/kb/references/thumbs/a59a5f9843.jpg +0 -0
- package/kb/references/thumbs/a5bce0eede.jpg +0 -0
- package/kb/references/thumbs/a5d921e1f0.jpg +0 -0
- package/kb/references/thumbs/a5e19d4637.jpg +0 -0
- package/kb/references/thumbs/a5f92b97d5.jpg +0 -0
- package/kb/references/thumbs/a6562c210a.jpg +0 -0
- package/kb/references/thumbs/a6f61a8d09.jpg +0 -0
- package/kb/references/thumbs/a719fdf663.jpg +0 -0
- package/kb/references/thumbs/a724e5442f.jpg +0 -0
- package/kb/references/thumbs/a852b4319f.jpg +0 -0
- package/kb/references/thumbs/a896c08448.jpg +0 -0
- package/kb/references/thumbs/a8d190a4b8.jpg +0 -0
- package/kb/references/thumbs/a8dcdd57d7.jpg +0 -0
- package/kb/references/thumbs/a951347e18.jpg +0 -0
- package/kb/references/thumbs/a9a1a0bf7d.jpg +0 -0
- package/kb/references/thumbs/a9b72986a8.jpg +0 -0
- package/kb/references/thumbs/aa11555ba8.jpg +0 -0
- package/kb/references/thumbs/aa4aab9093.jpg +0 -0
- package/kb/references/thumbs/aa89b96658.jpg +0 -0
- package/kb/references/thumbs/aaeb3739b6.jpg +0 -0
- package/kb/references/thumbs/ab25a608ec.jpg +0 -0
- package/kb/references/thumbs/ab34b62268.jpg +0 -0
- package/kb/references/thumbs/ab3b0217bb.jpg +0 -0
- package/kb/references/thumbs/ac70c04afb.jpg +0 -0
- package/kb/references/thumbs/ace0879117.jpg +0 -0
- package/kb/references/thumbs/ad0515ae55.jpg +0 -0
- package/kb/references/thumbs/ad66b25b47.jpg +0 -0
- package/kb/references/thumbs/ad68229d2f.jpg +0 -0
- package/kb/references/thumbs/add6480693.jpg +0 -0
- package/kb/references/thumbs/ae5b8c3b8c.jpg +0 -0
- package/kb/references/thumbs/ae81247b47.jpg +0 -0
- package/kb/references/thumbs/af8aa54379.jpg +0 -0
- package/kb/references/thumbs/afa80dedd4.jpg +0 -0
- package/kb/references/thumbs/afe04d730f.jpg +0 -0
- package/kb/references/thumbs/b09a7dc315.jpg +0 -0
- package/kb/references/thumbs/b0c02629f6.jpg +0 -0
- package/kb/references/thumbs/b1cb4f91de.jpg +0 -0
- package/kb/references/thumbs/b1d4c13c92.jpg +0 -0
- package/kb/references/thumbs/b20eef86db.jpg +0 -0
- package/kb/references/thumbs/b3136ccdde.jpg +0 -0
- package/kb/references/thumbs/b3f5e8d7dd.jpg +0 -0
- package/kb/references/thumbs/b44adb81c9.jpg +0 -0
- package/kb/references/thumbs/b457a5894b.jpg +0 -0
- package/kb/references/thumbs/b461281e0f.jpg +0 -0
- package/kb/references/thumbs/b4942644f0.jpg +0 -0
- package/kb/references/thumbs/b49f8eccad.jpg +0 -0
- package/kb/references/thumbs/b56c69be23.jpg +0 -0
- package/kb/references/thumbs/b59786b33c.jpg +0 -0
- package/kb/references/thumbs/b5d368ce5a.jpg +0 -0
- package/kb/references/thumbs/b5eb442406.jpg +0 -0
- package/kb/references/thumbs/b7fdabf4ab.jpg +0 -0
- package/kb/references/thumbs/b88ee8d235.jpg +0 -0
- package/kb/references/thumbs/b8a4e5be8a.jpg +0 -0
- package/kb/references/thumbs/b8c85b661c.jpg +0 -0
- package/kb/references/thumbs/ba1b8e7435.jpg +0 -0
- package/kb/references/thumbs/ba9a593eed.jpg +0 -0
- package/kb/references/thumbs/baa7afc201.jpg +0 -0
- package/kb/references/thumbs/baf74398f7.jpg +0 -0
- package/kb/references/thumbs/bb3e947c10.jpg +0 -0
- package/kb/references/thumbs/bb4c1bb7c1.jpg +0 -0
- package/kb/references/thumbs/bbaa25cc8a.jpg +0 -0
- package/kb/references/thumbs/bc7e7f88cc.jpg +0 -0
- package/kb/references/thumbs/bc9d5b58f5.jpg +0 -0
- package/kb/references/thumbs/bcc99a51f7.jpg +0 -0
- package/kb/references/thumbs/bce13dd1c1.jpg +0 -0
- package/kb/references/thumbs/bd079a6130.jpg +0 -0
- package/kb/references/thumbs/bd2d70dda7.jpg +0 -0
- package/kb/references/thumbs/bd5895c5f2.jpg +0 -0
- package/kb/references/thumbs/bde8e31d62.jpg +0 -0
- package/kb/references/thumbs/be084e78f3.jpg +0 -0
- package/kb/references/thumbs/be94f170c8.jpg +0 -0
- package/kb/references/thumbs/bed33654f7.jpg +0 -0
- package/kb/references/thumbs/beeb9a0c18.jpg +0 -0
- package/kb/references/thumbs/bef42094dc.jpg +0 -0
- package/kb/references/thumbs/bf625496fa.jpg +0 -0
- package/kb/references/thumbs/bf771f1398.jpg +0 -0
- package/kb/references/thumbs/c0ddefa3d9.jpg +0 -0
- package/kb/references/thumbs/c0f01e536e.jpg +0 -0
- package/kb/references/thumbs/c1d592b678.jpg +0 -0
- package/kb/references/thumbs/c1e83e1b3e.jpg +0 -0
- package/kb/references/thumbs/c2168f8096.jpg +0 -0
- package/kb/references/thumbs/c249cd6b17.jpg +0 -0
- package/kb/references/thumbs/c2e3d4feed.jpg +0 -0
- package/kb/references/thumbs/c3cec01df1.jpg +0 -0
- package/kb/references/thumbs/c47bedb42d.jpg +0 -0
- package/kb/references/thumbs/c4b03b4680.jpg +0 -0
- package/kb/references/thumbs/c4e6d18ec3.jpg +0 -0
- package/kb/references/thumbs/c5477e2877.jpg +0 -0
- package/kb/references/thumbs/c566794674.jpg +0 -0
- package/kb/references/thumbs/c5b945afbb.jpg +0 -0
- package/kb/references/thumbs/c5e4659497.jpg +0 -0
- package/kb/references/thumbs/c622786702.jpg +0 -0
- package/kb/references/thumbs/c6250649ca.jpg +0 -0
- package/kb/references/thumbs/c6261a9526.jpg +0 -0
- package/kb/references/thumbs/c62e609076.jpg +0 -0
- package/kb/references/thumbs/c6602d3e85.jpg +0 -0
- package/kb/references/thumbs/c6c1a16e40.jpg +0 -0
- package/kb/references/thumbs/c77f932157.jpg +0 -0
- package/kb/references/thumbs/c79788f01e.jpg +0 -0
- package/kb/references/thumbs/c7a511ad1e.jpg +0 -0
- package/kb/references/thumbs/c7acfbc3c8.jpg +0 -0
- package/kb/references/thumbs/c7bb6d1bb8.jpg +0 -0
- package/kb/references/thumbs/c829eadcb9.jpg +0 -0
- package/kb/references/thumbs/c891fd9d85.jpg +0 -0
- package/kb/references/thumbs/c89f812ee3.jpg +0 -0
- package/kb/references/thumbs/c8a85b9f61.jpg +0 -0
- package/kb/references/thumbs/c8fdf85c9a.jpg +0 -0
- package/kb/references/thumbs/c961ac1031.jpg +0 -0
- package/kb/references/thumbs/c9fd3f88d8.jpg +0 -0
- package/kb/references/thumbs/ca0d6399c3.jpg +0 -0
- package/kb/references/thumbs/ca316c83e4.jpg +0 -0
- package/kb/references/thumbs/ca3b93aa0d.jpg +0 -0
- package/kb/references/thumbs/cb0596a85b.jpg +0 -0
- package/kb/references/thumbs/cb95a5c3ef.jpg +0 -0
- package/kb/references/thumbs/cbe238b656.jpg +0 -0
- package/kb/references/thumbs/cbf530e608.jpg +0 -0
- package/kb/references/thumbs/cc173494ec.jpg +0 -0
- package/kb/references/thumbs/ccb664bddf.jpg +0 -0
- package/kb/references/thumbs/ccf69d36cd.jpg +0 -0
- package/kb/references/thumbs/ccf759db42.jpg +0 -0
- package/kb/references/thumbs/cd2a285807.jpg +0 -0
- package/kb/references/thumbs/cd4a24a8ca.jpg +0 -0
- package/kb/references/thumbs/cdb3fe8938.jpg +0 -0
- package/kb/references/thumbs/cdce1b9bc6.jpg +0 -0
- package/kb/references/thumbs/cde3aa761a.jpg +0 -0
- package/kb/references/thumbs/ce9d5cd9c2.jpg +0 -0
- package/kb/references/thumbs/ceb01f8a34.jpg +0 -0
- package/kb/references/thumbs/cee15c98d0.jpg +0 -0
- package/kb/references/thumbs/cf6944ddae.jpg +0 -0
- package/kb/references/thumbs/d15dd21cd8.jpg +0 -0
- package/kb/references/thumbs/d1633bf2ed.jpg +0 -0
- package/kb/references/thumbs/d16a838000.jpg +0 -0
- package/kb/references/thumbs/d1cf87e9f0.jpg +0 -0
- package/kb/references/thumbs/d32511d913.jpg +0 -0
- package/kb/references/thumbs/d327e9dedd.jpg +0 -0
- package/kb/references/thumbs/d35bf6c85e.jpg +0 -0
- package/kb/references/thumbs/d37afc9d61.jpg +0 -0
- package/kb/references/thumbs/d3b66655c7.jpg +0 -0
- package/kb/references/thumbs/d40b4172b6.jpg +0 -0
- package/kb/references/thumbs/d4c40d1824.jpg +0 -0
- package/kb/references/thumbs/d4cb55a4b6.jpg +0 -0
- package/kb/references/thumbs/d4f0c30f7e.jpg +0 -0
- package/kb/references/thumbs/d51cce3ae5.jpg +0 -0
- package/kb/references/thumbs/d52948f9ba.jpg +0 -0
- package/kb/references/thumbs/d62bd3cba4.jpg +0 -0
- package/kb/references/thumbs/d668a21c92.jpg +0 -0
- package/kb/references/thumbs/d685072c52.jpg +0 -0
- package/kb/references/thumbs/d6d7b96093.jpg +0 -0
- package/kb/references/thumbs/d7cd874b21.jpg +0 -0
- package/kb/references/thumbs/d7e111736a.jpg +0 -0
- package/kb/references/thumbs/d88325f832.jpg +0 -0
- package/kb/references/thumbs/d9272ded76.jpg +0 -0
- package/kb/references/thumbs/d962e7c654.jpg +0 -0
- package/kb/references/thumbs/d97f000ee2.jpg +0 -0
- package/kb/references/thumbs/d9c1ae2543.jpg +0 -0
- package/kb/references/thumbs/d9ccb3cac8.jpg +0 -0
- package/kb/references/thumbs/da99019680.jpg +0 -0
- package/kb/references/thumbs/da9b8bc615.jpg +0 -0
- package/kb/references/thumbs/daafbee2c1.jpg +0 -0
- package/kb/references/thumbs/db0f11f143.jpg +0 -0
- package/kb/references/thumbs/db5dd367f5.jpg +0 -0
- package/kb/references/thumbs/db69677866.jpg +0 -0
- package/kb/references/thumbs/db87c1956e.jpg +0 -0
- package/kb/references/thumbs/dbb86d8bda.jpg +0 -0
- package/kb/references/thumbs/dbd3a8ff2a.jpg +0 -0
- package/kb/references/thumbs/dbdf4b2794.jpg +0 -0
- package/kb/references/thumbs/dd4c19b4b1.jpg +0 -0
- package/kb/references/thumbs/dd98d9f6fa.jpg +0 -0
- package/kb/references/thumbs/ddb00e3b6a.jpg +0 -0
- package/kb/references/thumbs/dde3541a5c.jpg +0 -0
- package/kb/references/thumbs/de2a4da502.jpg +0 -0
- package/kb/references/thumbs/de4c387a66.jpg +0 -0
- package/kb/references/thumbs/de5618fa82.jpg +0 -0
- package/kb/references/thumbs/de6bec2538.jpg +0 -0
- package/kb/references/thumbs/dea13cbb1f.jpg +0 -0
- package/kb/references/thumbs/deff9202aa.jpg +0 -0
- package/kb/references/thumbs/df12835f8d.jpg +0 -0
- package/kb/references/thumbs/df57acbb3d.jpg +0 -0
- package/kb/references/thumbs/df8265009e.jpg +0 -0
- package/kb/references/thumbs/dfc99a1e8e.jpg +0 -0
- package/kb/references/thumbs/dfc9fde460.jpg +0 -0
- package/kb/references/thumbs/e038348eee.jpg +0 -0
- package/kb/references/thumbs/e04c2ae1d5.jpg +0 -0
- package/kb/references/thumbs/e070345c43.jpg +0 -0
- package/kb/references/thumbs/e0a0c84d47.jpg +0 -0
- package/kb/references/thumbs/e0af1e6b9e.jpg +0 -0
- package/kb/references/thumbs/e1025942c4.jpg +0 -0
- package/kb/references/thumbs/e11b6f5b82.jpg +0 -0
- package/kb/references/thumbs/e1553de3fc.jpg +0 -0
- package/kb/references/thumbs/e162ebe7cd.jpg +0 -0
- package/kb/references/thumbs/e1f1e531d8.jpg +0 -0
- package/kb/references/thumbs/e20842e5c9.jpg +0 -0
- package/kb/references/thumbs/e24e7d949b.jpg +0 -0
- package/kb/references/thumbs/e2a3e5d10a.jpg +0 -0
- package/kb/references/thumbs/e2de02615a.jpg +0 -0
- package/kb/references/thumbs/e305dad7dd.jpg +0 -0
- package/kb/references/thumbs/e52e3ef97f.jpg +0 -0
- package/kb/references/thumbs/e570e92d06.jpg +0 -0
- package/kb/references/thumbs/e583932fb9.jpg +0 -0
- package/kb/references/thumbs/e5da3c038f.jpg +0 -0
- package/kb/references/thumbs/e657634af3.jpg +0 -0
- package/kb/references/thumbs/e67946e3e1.jpg +0 -0
- package/kb/references/thumbs/e69e0f3639.jpg +0 -0
- package/kb/references/thumbs/e6ebe66031.jpg +0 -0
- package/kb/references/thumbs/e7db9563c9.jpg +0 -0
- package/kb/references/thumbs/e84a4c3b7b.jpg +0 -0
- package/kb/references/thumbs/e84cf52c27.jpg +0 -0
- package/kb/references/thumbs/e8707cf6e8.jpg +0 -0
- package/kb/references/thumbs/e888890b40.jpg +0 -0
- package/kb/references/thumbs/e8a502b418.jpg +0 -0
- package/kb/references/thumbs/e8ba68371d.jpg +0 -0
- package/kb/references/thumbs/e9098f54cc.jpg +0 -0
- package/kb/references/thumbs/e9144deae9.jpg +0 -0
- package/kb/references/thumbs/e93e98d757.jpg +0 -0
- package/kb/references/thumbs/e9762dd435.jpg +0 -0
- package/kb/references/thumbs/e9db4544c8.jpg +0 -0
- package/kb/references/thumbs/e9ddffeacd.jpg +0 -0
- package/kb/references/thumbs/e9e2cf1d7e.jpg +0 -0
- package/kb/references/thumbs/ea1e23e623.jpg +0 -0
- package/kb/references/thumbs/ea41200f5e.jpg +0 -0
- package/kb/references/thumbs/eaf4916e1a.jpg +0 -0
- package/kb/references/thumbs/eb2c505fc8.jpg +0 -0
- package/kb/references/thumbs/eb47943693.jpg +0 -0
- package/kb/references/thumbs/eb4ffd6625.jpg +0 -0
- package/kb/references/thumbs/eb82ac0a4d.jpg +0 -0
- package/kb/references/thumbs/ebeb345ade.jpg +0 -0
- package/kb/references/thumbs/ec4e2aba0a.jpg +0 -0
- package/kb/references/thumbs/ed683bc9f7.jpg +0 -0
- package/kb/references/thumbs/ed9e236a84.jpg +0 -0
- package/kb/references/thumbs/ee45e1caea.jpg +0 -0
- package/kb/references/thumbs/ee50bbf2e7.jpg +0 -0
- package/kb/references/thumbs/eed6ad3b7b.jpg +0 -0
- package/kb/references/thumbs/eee247fd70.jpg +0 -0
- package/kb/references/thumbs/ef87b6cb99.jpg +0 -0
- package/kb/references/thumbs/ef87dccf04.jpg +0 -0
- package/kb/references/thumbs/ef941bba1e.jpg +0 -0
- package/kb/references/thumbs/effc14fb2c.jpg +0 -0
- package/kb/references/thumbs/f04aa5a191.jpg +0 -0
- package/kb/references/thumbs/f13f12485f.jpg +0 -0
- package/kb/references/thumbs/f16a5c0fd4.jpg +0 -0
- package/kb/references/thumbs/f1ba7a7a8f.jpg +0 -0
- package/kb/references/thumbs/f1bc4c9c19.jpg +0 -0
- package/kb/references/thumbs/f1ee827878.jpg +0 -0
- package/kb/references/thumbs/f21736aeff.jpg +0 -0
- package/kb/references/thumbs/f22bc41213.jpg +0 -0
- package/kb/references/thumbs/f266aaffcc.jpg +0 -0
- package/kb/references/thumbs/f2c0ed0d65.jpg +0 -0
- package/kb/references/thumbs/f32ac710fd.jpg +0 -0
- package/kb/references/thumbs/f32b697ff6.jpg +0 -0
- package/kb/references/thumbs/f39c883904.jpg +0 -0
- package/kb/references/thumbs/f3b9d55e6e.jpg +0 -0
- package/kb/references/thumbs/f3c1763e14.jpg +0 -0
- package/kb/references/thumbs/f40853fd43.jpg +0 -0
- package/kb/references/thumbs/f4fd07dc4d.jpg +0 -0
- package/kb/references/thumbs/f50b756ee5.jpg +0 -0
- package/kb/references/thumbs/f51272e9b3.jpg +0 -0
- package/kb/references/thumbs/f538895eee.jpg +0 -0
- package/kb/references/thumbs/f5d28081b5.jpg +0 -0
- package/kb/references/thumbs/f6046c9501.jpg +0 -0
- package/kb/references/thumbs/f62e4af316.jpg +0 -0
- package/kb/references/thumbs/f6f6bffeb8.jpg +0 -0
- package/kb/references/thumbs/f736e00af6.jpg +0 -0
- package/kb/references/thumbs/f7932da47d.jpg +0 -0
- package/kb/references/thumbs/f879e118ca.jpg +0 -0
- package/kb/references/thumbs/f88257215d.jpg +0 -0
- package/kb/references/thumbs/f88c45a6d7.jpg +0 -0
- package/kb/references/thumbs/f89a7b646e.jpg +0 -0
- package/kb/references/thumbs/f8a1e7a274.jpg +0 -0
- package/kb/references/thumbs/f9183cf2b6.jpg +0 -0
- package/kb/references/thumbs/f995de5ce9.jpg +0 -0
- package/kb/references/thumbs/f9ada2c3e1.jpg +0 -0
- package/kb/references/thumbs/f9d4616f89.jpg +0 -0
- package/kb/references/thumbs/f9f4307bb0.jpg +0 -0
- package/kb/references/thumbs/fa070122c7.jpg +0 -0
- package/kb/references/thumbs/fa29cc1369.jpg +0 -0
- package/kb/references/thumbs/fa791a4dab.jpg +0 -0
- package/kb/references/thumbs/fa8060509e.jpg +0 -0
- package/kb/references/thumbs/fafeed4003.jpg +0 -0
- package/kb/references/thumbs/fb1c024f38.jpg +0 -0
- package/kb/references/thumbs/fb77aa28b3.jpg +0 -0
- package/kb/references/thumbs/fb9f7f1966.jpg +0 -0
- package/kb/references/thumbs/fba8a8c2da.jpg +0 -0
- package/kb/references/thumbs/fce2754c36.jpg +0 -0
- package/kb/references/thumbs/fce2fb83df.jpg +0 -0
- package/kb/references/thumbs/fd4c51bb47.jpg +0 -0
- package/kb/references/thumbs/fd8c269817.jpg +0 -0
- package/kb/references/thumbs/fdb7b250ca.jpg +0 -0
- package/kb/references/thumbs/fe18088de8.jpg +0 -0
- package/kb/references/thumbs/fe4f742ba7.jpg +0 -0
- package/kb/references/thumbs/fe64742ce7.jpg +0 -0
- package/kb/references/thumbs/feb5d62161.jpg +0 -0
- package/kb/references/thumbs/fed21b3e98.jpg +0 -0
- package/kb/references/thumbs/fed2205951.jpg +0 -0
- package/kb/references/thumbs/ffdacf1715.jpg +0 -0
- package/kb/sections/_buttons.md +93 -0
- package/kb/sections/_finish.md +325 -0
- package/kb/sections/about-founder-note.md +85 -0
- package/kb/sections/about-team-roster.md +86 -0
- package/kb/sections/alternating-rows-live-tiles.md +110 -0
- package/kb/sections/annotated-product-shot.md +105 -0
- package/kb/sections/asymmetric-bento-dominant.md +114 -0
- package/kb/sections/before-after-features.md +104 -0
- package/kb/sections/before-after-repair.md +107 -0
- package/kb/sections/bento-lead-tile.md +112 -0
- package/kb/sections/book-a-slot-honest.md +116 -0
- package/kb/sections/capability-prompt-ledger.md +68 -0
- package/kb/sections/case-study-band.md +113 -0
- package/kb/sections/centred-stack-hero.md +107 -0
- package/kb/sections/closing-demo-final-state.md +81 -0
- package/kb/sections/closing-full-bleed-line-and-form.md +86 -0
- package/kb/sections/closing-result-previewed.md +88 -0
- package/kb/sections/closing-three-real-steps.md +87 -0
- package/kb/sections/command-reference-table.md +70 -0
- package/kb/sections/comparison-old-way-bars.md +88 -0
- package/kb/sections/comparison-same-task-two-ways.md +88 -0
- package/kb/sections/day-of-the-place.md +102 -0
- package/kb/sections/docs-reading-rail.md +88 -0
- package/kb/sections/editorial-columns-hero.md +107 -0
- package/kb/sections/editorial-essay-pull-quote.md +112 -0
- package/kb/sections/events-calendar-list.md +107 -0
- package/kb/sections/explorable-control-demo.md +102 -0
- package/kb/sections/faq-open-answers-editorial.md +85 -0
- package/kb/sections/faq-ruled-accordion-aside.md +87 -0
- package/kb/sections/footer-closing-scene.md +84 -0
- package/kb/sections/footer-editorial-sitemap.md +85 -0
- package/kb/sections/footer-minimal-credits.md +80 -0
- package/kb/sections/full-bleed-image-band.md +103 -0
- package/kb/sections/full-bleed-image-hero.md +105 -0
- package/kb/sections/gallery-first-hero.md +109 -0
- package/kb/sections/horizontal-scroll-strip.md +101 -0
- package/kb/sections/how-i-work-own-voice.md +111 -0
- package/kb/sections/how-it-works-in-place.md +109 -0
- package/kb/sections/impact-numbers-sourced.md +109 -0
- package/kb/sections/install-steps-per-agent.md +77 -0
- package/kb/sections/masonry-real-work.md +109 -0
- package/kb/sections/numbered-editorial-columns.md +105 -0
- package/kb/sections/object-first-hero.md +106 -0
- package/kb/sections/object-timeline.md +105 -0
- package/kb/sections/one-huge-number.md +100 -0
- package/kb/sections/one-person-story.md +103 -0
- package/kb/sections/order-ahead-ticket.md +119 -0
- package/kb/sections/press-strip-quoted.md +106 -0
- package/kb/sections/pricing-comparison-table-sticky.md +94 -0
- package/kb/sections/pricing-free-pro-upgrade-moment.md +85 -0
- package/kb/sections/pricing-plan-beside-product.md +88 -0
- package/kb/sections/pricing-plans-ledger.md +94 -0
- package/kb/sections/pricing-usage-calculator.md +93 -0
- package/kb/sections/printed-menu-card.md +110 -0
- package/kb/sections/process-happening.md +102 -0
- package/kb/sections/project-index-ruled.md +106 -0
- package/kb/sections/proof-logo-cloud-with-reasons.md +82 -0
- package/kb/sections/proof-measured-numbers.md +88 -0
- package/kb/sections/proof-output-wall.md +84 -0
- package/kb/sections/proof-single-honest-quote.md +81 -0
- package/kb/sections/resources-searchable-list.md +110 -0
- package/kb/sections/rooms-and-rates.md +107 -0
- package/kb/sections/ruled-facts-table.md +108 -0
- package/kb/sections/ruled-list-pinned-visual.md +107 -0
- package/kb/sections/scroll-scrubbed-product-demo.md +103 -0
- package/kb/sections/self-playing-run-demo.md +104 -0
- package/kb/sections/services-price-ledger.md +106 -0
- package/kb/sections/signup-form-with-next-steps.md +86 -0
- package/kb/sections/signup-waitlist-place-in-line.md +90 -0
- package/kb/sections/spec-sheet-features.md +108 -0
- package/kb/sections/split-hero-live-object.md +108 -0
- package/kb/sections/sticky-feature-narrative.md +104 -0
- package/kb/sections/todays-board-hero.md +112 -0
- package/kb/sections/touchable-tool-hero.md +111 -0
- package/kb/sections/typographic-statement-hero.md +99 -0
- package/kb/sections/visit-hours-map.md +110 -0
- package/kb/sections/ways-to-help-honest.md +115 -0
- package/kb/sources/design-reference.md +991 -0
- package/kb/sources/product-and-docs.md +37 -0
- package/kb/systems/1win/DESIGN.md +463 -0
- package/kb/systems/LICENSE-awesome-design-md +21 -0
- package/kb/systems/README.md +7 -0
- package/kb/systems/airbnb/DESIGN.md +545 -0
- package/kb/systems/airtable/DESIGN.md +554 -0
- package/kb/systems/aljazeera/DESIGN.md +594 -0
- package/kb/systems/apple/DESIGN.md +562 -0
- package/kb/systems/binance/DESIGN.md +634 -0
- package/kb/systems/bmw/DESIGN.md +544 -0
- package/kb/systems/bmw-m/DESIGN.md +503 -0
- package/kb/systems/bugatti/DESIGN.md +454 -0
- package/kb/systems/cal/DESIGN.md +542 -0
- package/kb/systems/categories.json +87 -0
- package/kb/systems/claude/DESIGN.md +589 -0
- package/kb/systems/clay/DESIGN.md +541 -0
- package/kb/systems/clickhouse/DESIGN.md +544 -0
- package/kb/systems/cohere/DESIGN.md +451 -0
- package/kb/systems/coinbase/DESIGN.md +570 -0
- package/kb/systems/composio/DESIGN.md +506 -0
- package/kb/systems/cursor/DESIGN.md +537 -0
- package/kb/systems/dell-1996/DESIGN.md +632 -0
- package/kb/systems/dji/DESIGN.md +882 -0
- package/kb/systems/elevenlabs/DESIGN.md +504 -0
- package/kb/systems/expo/DESIGN.md +526 -0
- package/kb/systems/ferrari/DESIGN.md +531 -0
- package/kb/systems/figma/DESIGN.md +578 -0
- package/kb/systems/framer/DESIGN.md +544 -0
- package/kb/systems/hashicorp/DESIGN.md +575 -0
- package/kb/systems/hp/DESIGN.md +670 -0
- package/kb/systems/ibm/DESIGN.md +550 -0
- package/kb/systems/intercom/DESIGN.md +546 -0
- package/kb/systems/jobright/DESIGN.md +525 -0
- package/kb/systems/kraken/DESIGN.md +125 -0
- package/kb/systems/lamborghini/DESIGN.md +288 -0
- package/kb/systems/linear.app/DESIGN.md +548 -0
- package/kb/systems/lovable/DESIGN.md +298 -0
- package/kb/systems/mastercard/DESIGN.md +365 -0
- package/kb/systems/mercury/DESIGN.md +568 -0
- package/kb/systems/meta/DESIGN.md +683 -0
- package/kb/systems/minimax/DESIGN.md +746 -0
- package/kb/systems/mintlify/DESIGN.md +852 -0
- package/kb/systems/miro/DESIGN.md +825 -0
- package/kb/systems/mistral.ai/DESIGN.md +773 -0
- package/kb/systems/mongodb/DESIGN.md +767 -0
- package/kb/systems/mydealz/DESIGN.md +551 -0
- package/kb/systems/nerdwallet/DESIGN.md +722 -0
- package/kb/systems/nike/DESIGN.md +575 -0
- package/kb/systems/nintendo-2001/DESIGN.md +649 -0
- package/kb/systems/notion/DESIGN.md +821 -0
- package/kb/systems/nvidia/DESIGN.md +640 -0
- package/kb/systems/ollama/DESIGN.md +539 -0
- package/kb/systems/opencode.ai/DESIGN.md +521 -0
- package/kb/systems/oracle/DESIGN.md +778 -0
- package/kb/systems/oura/DESIGN.md +616 -0
- package/kb/systems/pinterest/DESIGN.md +597 -0
- package/kb/systems/playstation/DESIGN.md +661 -0
- package/kb/systems/posthog/DESIGN.md +690 -0
- package/kb/systems/raycast/DESIGN.md +669 -0
- package/kb/systems/renault/DESIGN.md +589 -0
- package/kb/systems/replicate/DESIGN.md +616 -0
- package/kb/systems/resend/DESIGN.md +585 -0
- package/kb/systems/revolut/DESIGN.md +636 -0
- package/kb/systems/runwayml/DESIGN.md +244 -0
- package/kb/systems/sanity/DESIGN.md +357 -0
- package/kb/systems/sap/DESIGN.md +639 -0
- package/kb/systems/sentry/DESIGN.md +551 -0
- package/kb/systems/shopify/DESIGN.md +516 -0
- package/kb/systems/slack/DESIGN.md +482 -0
- package/kb/systems/spacex/DESIGN.md +363 -0
- package/kb/systems/spotify/DESIGN.md +246 -0
- package/kb/systems/starbucks/DESIGN.md +580 -0
- package/kb/systems/stripe/DESIGN.md +487 -0
- package/kb/systems/supabase/DESIGN.md +462 -0
- package/kb/systems/superhuman/DESIGN.md +448 -0
- package/kb/systems/tesla/DESIGN.md +286 -0
- package/kb/systems/theverge/DESIGN.md +339 -0
- package/kb/systems/together.ai/DESIGN.md +633 -0
- package/kb/systems/uber/DESIGN.md +636 -0
- package/kb/systems/vercel/DESIGN.md +736 -0
- package/kb/systems/vodafone/DESIGN.md +538 -0
- package/kb/systems/voltagent/DESIGN.md +521 -0
- package/kb/systems/warp/DESIGN.md +526 -0
- package/kb/systems/webflow/DESIGN.md +588 -0
- package/kb/systems/wired/DESIGN.md +497 -0
- package/kb/systems/wise/DESIGN.md +544 -0
- package/kb/systems/x.ai/DESIGN.md +465 -0
- package/kb/systems/zapier/DESIGN.md +537 -0
- package/kb/taste/garvit-by-job.md +46 -0
- package/kb/taste/garvit-portfolio.md +94 -0
- package/kb/universal/color-roles.md +26 -0
- package/kb/universal/copy.md +16 -0
- package/kb/universal/details.md +31 -0
- package/kb/universal/first-screen.md +30 -0
- package/kb/universal/fit-the-codebase.md +29 -0
- package/kb/universal/layout-width.md +28 -0
- package/kb/universal/motion-feedback.md +23 -0
- package/kb/universal/signature.md +29 -0
- package/kb/universal/states.md +17 -0
- package/kb/universal/type-app.md +33 -0
- package/kb/universal/type-marketing.md +28 -0
- package/package.json +37 -4
- package/scripts/pack-assets.mjs +58 -0
- package/skill/palate/SKILL.md +261 -0
- package/src/auth-recheck.mjs +4 -0
- package/src/auth.mjs +416 -0
- package/src/capture.mjs +182 -0
- package/src/catalog/classify.mjs +96 -0
- package/src/catalog/curated.mjs +128 -0
- package/src/catalog/index-build.mjs +67 -0
- package/src/catalog/literal.mjs +146 -0
- package/src/catalog/store.mjs +40 -0
- package/src/catalog/vibrant.mjs +126 -0
- package/src/check.mjs +93 -0
- package/src/concept.mjs +157 -0
- package/src/context.mjs +226 -0
- package/src/copy.mjs +188 -0
- package/src/devserver.mjs +135 -0
- package/src/fontgenre.mjs +44 -0
- package/src/guard-agents.mjs +213 -0
- package/src/guard.mjs +377 -0
- package/src/kb.mjs +91 -0
- package/src/look.mjs +312 -0
- package/src/loop/ambition.mjs +620 -0
- package/src/loop/animate.mjs +70 -0
- package/src/loop/artdirection.mjs +264 -0
- package/src/loop/bench.mjs +184 -0
- package/src/loop/benchpick.mjs +155 -0
- package/src/loop/blueprint.mjs +620 -0
- package/src/loop/board.html +180 -0
- package/src/loop/build.mjs +393 -0
- package/src/loop/buttons.mjs +413 -0
- package/src/loop/common.mjs +206 -0
- package/src/loop/cost.mjs +63 -0
- package/src/loop/craft.mjs +152 -0
- package/src/loop/craftchecks.mjs +148 -0
- package/src/loop/critique.mjs +393 -0
- package/src/loop/details.mjs +225 -0
- package/src/loop/direction.html +168 -0
- package/src/loop/direction.mjs +256 -0
- package/src/loop/directions.json +8 -0
- package/src/loop/explore.mjs +219 -0
- package/src/loop/fail.mjs +110 -0
- package/src/loop/fix.mjs +194 -0
- package/src/loop/floor.mjs +677 -0
- package/src/loop/forget.mjs +52 -0
- package/src/loop/gate.mjs +1053 -0
- package/src/loop/grid.html +123 -0
- package/src/loop/growth.mjs +118 -0
- package/src/loop/icons.mjs +109 -0
- package/src/loop/imagery.mjs +1280 -0
- package/src/loop/judge-api.mjs +245 -0
- package/src/loop/judge.mjs +167 -0
- package/src/loop/layouts.mjs +492 -0
- package/src/loop/library.mjs +82 -0
- package/src/loop/lookplan.mjs +182 -0
- package/src/loop/lookvec.mjs +645 -0
- package/src/loop/match.mjs +923 -0
- package/src/loop/matched.mjs +26 -0
- package/src/loop/myrefs.mjs +151 -0
- package/src/loop/pick.mjs +213 -0
- package/src/loop/picker.html +118 -0
- package/src/loop/plan.mjs +289 -0
- package/src/loop/polish.mjs +39 -0
- package/src/loop/research-learn.mjs +41 -0
- package/src/loop/research.mjs +1130 -0
- package/src/loop/rulereview.mjs +146 -0
- package/src/loop/rules-board.html +199 -0
- package/src/loop/rules.mjs +939 -0
- package/src/loop/similar.mjs +276 -0
- package/src/loop/states-source.mjs +258 -0
- package/src/loop/states.mjs +1547 -0
- package/src/loop/stats.mjs +78 -0
- package/src/loop/taste.mjs +81 -0
- package/src/loop/timeline.mjs +149 -0
- package/src/loop/variants.mjs +228 -0
- package/src/loop/vary.mjs +249 -0
- package/src/mcp/server.mjs +1246 -0
- package/src/motion.mjs +207 -0
- package/src/moves.mjs +242 -0
- package/src/novelty.mjs +116 -0
- package/src/page-ambition.js +181 -0
- package/src/page-craft.js +724 -0
- package/src/page-details.js +415 -0
- package/src/page-devices.js +365 -0
- package/src/page-fingerprint.js +281 -0
- package/src/page-gate.js +1307 -0
- package/src/page-look.js +698 -0
- package/src/page-metrics.js +78 -0
- package/src/page-principles.js +1167 -0
- package/src/page-rules.js +308 -0
- package/src/page-sections.js +211 -0
- package/src/page-states.js +703 -0
- package/src/paths.mjs +13 -0
- package/src/png.mjs +107 -0
- package/src/point/auto.mjs +224 -0
- package/src/point/locate.mjs +114 -0
- package/src/point/overlay.js +1516 -0
- package/src/point/server.mjs +1122 -0
- package/src/point/trail.mjs +219 -0
- package/src/pool.mjs +80 -0
- package/src/principles.mjs +279 -0
- package/src/refs.mjs +299 -0
- package/src/robots.mjs +145 -0
- package/src/sections.mjs +210 -0
- package/src/shoot.mjs +235 -0
- package/src/systems/apply.mjs +355 -0
- package/src/systems/load.mjs +251 -0
- package/src/systems/pick.mjs +213 -0
- package/templates/vite-react-tw/index.html +12 -0
- package/templates/vite-react-tw/package.json +25 -0
- package/templates/vite-react-tw/src/App.tsx +3 -0
- package/templates/vite-react-tw/src/index.css +1 -0
- package/templates/vite-react-tw/src/main.tsx +10 -0
- package/templates/vite-react-tw/tsconfig.app.json +26 -0
- package/templates/vite-react-tw/tsconfig.json +7 -0
- package/templates/vite-react-tw/tsconfig.node.json +23 -0
- package/templates/vite-react-tw/vite.config.ts +8 -0
|
@@ -0,0 +1,1053 @@
|
|
|
1
|
+
// `palate gate <route>`: the quality floor, judged by someone who didn't build the page.
|
|
2
|
+
//
|
|
3
|
+
// Three layers, cheapest first (research: reports/AI design quality floor.md):
|
|
4
|
+
// 1. The floor checks palate measures itself (floor.mjs): dead space, half-empty sections, one template
|
|
5
|
+
// repeated, one flat colour with no material, monospace used as decoration, placeholder text, clipped text,
|
|
6
|
+
// a phone section that stacks into an endless list, a system that drifts between sections, a void inside a
|
|
7
|
+
// tile, a photo shown twice; at 1440, 1024, 768 and 390. Two-sided: too empty fails like too busy.
|
|
8
|
+
// 2. A checklist judge: yes/no questions per section (from each section's archetype in kb/sections and a few
|
|
9
|
+
// that hold for every section), answered from screenshots by a fresh judge.
|
|
10
|
+
// 3. A pairwise judge against strong real sites of the same category (the research captures), in both orders,
|
|
11
|
+
// by judges who never saw the build. A page fails only when it is clearly worse in both orders; a split
|
|
12
|
+
// verdict is a tie. Pairwise judging agrees with people far more than 1–10 scores (~70–90% vs 50–64%), and
|
|
13
|
+
// a builder grading its own work skews high (Kiln: 6.7 self-graded, about 5 to a person).
|
|
14
|
+
//
|
|
15
|
+
// palate makes no model calls: it writes the judge packets (blind: "Page A / Page B", no file names that say
|
|
16
|
+
// which is ours) and the calling agent runs each with a fresh subagent; `--answers` reads what they wrote. When
|
|
17
|
+
// the gate fails, palate writes one repair brief for one section (one target per round: fixing everything at
|
|
18
|
+
// once breaks what already worked), snapshots the source first, and after the repair compares the round with
|
|
19
|
+
// the best one so far; a round that got worse is rolled back (`--restore`). Two repair rounds at most, then it
|
|
20
|
+
// says honestly where the page is still below the bar.
|
|
21
|
+
import crypto from "node:crypto";
|
|
22
|
+
import fs from "node:fs";
|
|
23
|
+
import path from "node:path";
|
|
24
|
+
import { cropSections, findSections, openPage, shootFull, slugOf, stamp, withBrowser } from "../look.mjs";
|
|
25
|
+
import { normalizeRoute } from "../shoot.mjs";
|
|
26
|
+
import { readJson, TASTE_DIR } from "./common.mjs";
|
|
27
|
+
import { loadBlueprint } from "./blueprint.mjs";
|
|
28
|
+
import { readResearch } from "./similar.mjs";
|
|
29
|
+
import { activeMinutes, buildDurations, buildTookLine, loopTimeline, mark, minutes } from "./timeline.mjs";
|
|
30
|
+
import { MATCHED_NOTE, matchedRoute } from "./matched.mjs";
|
|
31
|
+
|
|
32
|
+
// The build-mode loop a route belongs to (blueprint.mjs and build.mjs name it the same way).
|
|
33
|
+
const loopName = (route) => (route === "/" ? "home" : String(route).replace(/^\//, "").replace(/[^a-z0-9]+/gi, "-"));
|
|
34
|
+
|
|
35
|
+
const Floor = await import("./floor.mjs").catch(() => null);
|
|
36
|
+
const Sections = await import("../sections.mjs").catch(() => null);
|
|
37
|
+
// The house-look check (lookvec.mjs): a page whose look repeats palate's recent pages in other projects fails.
|
|
38
|
+
const LookVec = await import("./lookvec.mjs").catch(() => null);
|
|
39
|
+
const FINGERPRINT = (() => {
|
|
40
|
+
try {
|
|
41
|
+
return fs.readFileSync(new URL("../page-fingerprint.js", import.meta.url), "utf8").trim().replace(/;\s*$/, "");
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
})();
|
|
46
|
+
|
|
47
|
+
// The similarity gate palate shoot runs (other projects, and the research's references), as floor checks: a page
|
|
48
|
+
// could pass the quality gate and then fail palate shoot (Tracewell drifted from 40% to 43% like axiom.co during its
|
|
49
|
+
// repairs). Nothing is recorded here; palate shoot records the page.
|
|
50
|
+
export async function similarityChecks(dir, route, fp) {
|
|
51
|
+
if (!fp?.skeleton?.length) return [];
|
|
52
|
+
try {
|
|
53
|
+
const { shotRecord } = await import("../novelty.mjs");
|
|
54
|
+
const { similarityGate } = await import("./similar.mjs");
|
|
55
|
+
const g = similarityGate(dir, route, shotRecord(dir, route, fp));
|
|
56
|
+
return [...g.hits, ...g.refHits].map((h) => ({
|
|
57
|
+
id: "SIMILAR",
|
|
58
|
+
width: 1440,
|
|
59
|
+
section: null,
|
|
60
|
+
severity: "fail",
|
|
61
|
+
what: `${Math.round(h.total * 100)}% like ${h.where}${h.parts?.length ? ` (${h.parts.filter((x) => x.score >= 0.6).slice(0, 3).map((x) => x.label).join(", ") || h.parts[0].label})` : ""}: over palate's similarity gate`,
|
|
62
|
+
fix: `Change what palate shoot ${route} names under "Similarity gate": the skeleton, the first screen's shape or the section archetypes, not the colour`,
|
|
63
|
+
}));
|
|
64
|
+
} catch {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const slash = (p) => String(p).replace(/\\/g, "/");
|
|
70
|
+
export const MAX_ROUNDS = 2;
|
|
71
|
+
const POINT_LOG = path.join(TASTE_DIR, "point.jsonl");
|
|
72
|
+
export const GATE_LOG = path.join(TASTE_DIR, "gates.jsonl");
|
|
73
|
+
|
|
74
|
+
// Questions every section answers, whatever its archetype. YES is good.
|
|
75
|
+
export const SECTION_QUESTIONS = [
|
|
76
|
+
{ id: "finished", q: "Does this section look finished and deliberately designed, at the level of a strong product site (not a template or a first draft)?", must: true },
|
|
77
|
+
{ id: "space", q: "Is every empty area clearly intentional (no dead band, no empty half beside the content, no columns that are mostly blank)?" },
|
|
78
|
+
{ id: "focus", q: "Is there one clear focal point, with the rest a step quieter?" },
|
|
79
|
+
{ id: "real", q: "Is the content real and specific to this product (no placeholder text, no generic filler, no fake logos or stats)?" },
|
|
80
|
+
];
|
|
81
|
+
// Questions about the whole page. must: a NO fails the page.
|
|
82
|
+
export const PAGE_QUESTIONS = [
|
|
83
|
+
{ id: "subject", q: "Does the page feel like its subject, so you could tell what kind of product or place it is with the words blurred?", must: true },
|
|
84
|
+
{ id: "material", q: "Does the page have real imagery or material (photography, illustration, texture or a rich product object), not only UI cards on flat colour?", must: true },
|
|
85
|
+
{ id: "variety", q: "Is each section built differently enough that the page doesn't feel like one template repeated?" },
|
|
86
|
+
{ id: "phone", q: "Does the phone version hold up (no endless stacked lists, nothing clipped, comfortable tap targets, a way to reach the navigation)?", must: true },
|
|
87
|
+
{ id: "tablet", q: "Do the tablet views (1024 first screen, 768 whole page) hold up: no photo blown up behind the text, no squeezed or orphaned columns, nothing clipped?" },
|
|
88
|
+
{ id: "finish", q: "Is the finish consistent (one radius family, one shadow style, considered buttons, no default-looking controls)?" },
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
const yes = (v) => v === true || /^(y|yes|true|pass)$/i.test(String(v ?? "").trim());
|
|
92
|
+
const no = (v) => v === false || /^(n|no|false|fail)$/i.test(String(v ?? "").trim());
|
|
93
|
+
|
|
94
|
+
function rootFor(dir, route) {
|
|
95
|
+
return path.join(dir, ".palate", "quality", slugOf(route));
|
|
96
|
+
}
|
|
97
|
+
const roundsFile = (dir, route) => path.join(rootFor(dir, route), "rounds.json");
|
|
98
|
+
export function readRounds(dir, route) {
|
|
99
|
+
return readJson(roundsFile(dir, route), { rounds: [], best: null });
|
|
100
|
+
}
|
|
101
|
+
function writeRounds(dir, route, r) {
|
|
102
|
+
fs.mkdirSync(rootFor(dir, route), { recursive: true });
|
|
103
|
+
fs.writeFileSync(roundsFile(dir, route), JSON.stringify(r, null, 1));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// The strongest references to judge against: the research captures for this project (kept order is palate's
|
|
107
|
+
// ranking), each with its first screen and full page.
|
|
108
|
+
export function exemplars(dir, { limit = 2 } = {}) {
|
|
109
|
+
const r = readResearch(dir);
|
|
110
|
+
// No research for this project (build mode went straight to a build): the user's own saved references are the
|
|
111
|
+
// bar, so the page is still judged beside strong real sites (a Moveday hero got no comparison at all).
|
|
112
|
+
if (!r) return userBar(dir, { limit });
|
|
113
|
+
// The bar is the best of the category: the strongest competitor and the strongest craft reference by
|
|
114
|
+
// palate's craft score (research.json), then the rest in palate's order.
|
|
115
|
+
const raw = readJson(r.file, {});
|
|
116
|
+
const meta = new Map((raw.kept ?? []).map((k) => [k.url, k]));
|
|
117
|
+
const all = r.refs.map((x) => ({ name: x.name, url: x.url, fold: x.fold, full: x.shots.find((f) => /full/i.test(f)) ?? null, craft: meta.get(x.url)?.craft ?? 0, role: meta.get(x.url)?.role ?? null })).filter((x) => x.fold && x.full);
|
|
118
|
+
const byCraft = (a, b) => b.craft - a.craft;
|
|
119
|
+
// The bar is the category's best: references the project named (a benchmark brief's), then competitors by craft;
|
|
120
|
+
// a craft site from another category only when the category has too few (a bakery judged against a mail app
|
|
121
|
+
// tells nobody much).
|
|
122
|
+
const host = (u) => String(u ?? "").replace(/^https?:\/\/(www\.)?/, "").split("/")[0];
|
|
123
|
+
const named = new Set((readJson(path.join(dir, ".palate", "bench.json"), {})?.references ?? []).map((r) => host(r.url ?? r)));
|
|
124
|
+
// Then the strongest remaining reference of any kind, so a niche category's weak sites aren't the only bar.
|
|
125
|
+
const picked = all.filter((x) => named.has(host(x.url))).sort(byCraft).slice(0, limit);
|
|
126
|
+
const comp = all.filter((x) => x.role === "competitor" && !picked.includes(x)).sort(byCraft)[0];
|
|
127
|
+
if (comp && picked.length < limit) picked.push(comp);
|
|
128
|
+
for (const x of [...all].sort(byCraft)) if (picked.length < limit && !picked.includes(x)) picked.push(x);
|
|
129
|
+
return picked;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// The user's saved references (palate refs add) with a first screen and a full page, as the bar: the ones they
|
|
133
|
+
// noted as strong first ("best product storytelling sections"), the project's own site never. They may be from
|
|
134
|
+
// another category, and the pair packet says so: judge the craft, not the category.
|
|
135
|
+
export function userBar(dir, { limit = 2, list } = {}) {
|
|
136
|
+
let refs = list;
|
|
137
|
+
if (!refs) {
|
|
138
|
+
try {
|
|
139
|
+
refs = readJson(path.join(TASTE_DIR, "..", "refs", "refs.json"), []);
|
|
140
|
+
} catch {
|
|
141
|
+
refs = [];
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const host = (u) => String(u ?? "").replace(/^https?:\/\/(www\.)?/, "").split("/")[0].toLowerCase();
|
|
145
|
+
const own = new Set([host(readJson(path.join(dir, "package.json"), {})?.homepage), path.basename(dir).toLowerCase()].filter(Boolean));
|
|
146
|
+
return (Array.isArray(refs) ? refs : [])
|
|
147
|
+
.map((x) => ({ x, fold: (x.files ?? []).find((f) => /fold\.(png|jpe?g)$/i.test(f)), full: (x.files ?? []).find((f) => /full\.(png|jpe?g)$/i.test(f)) }))
|
|
148
|
+
.filter(({ x, fold, full }) => fold && full && fs.existsSync(fold) && fs.existsSync(full) && !own.has(host(x.source)) && !own.has(host(x.source).split(".")[0]))
|
|
149
|
+
.map(({ x, fold, full }) => ({ name: host(x.source) || x.id, url: /^https?:/.test(x.source ?? "") ? x.source : null, fold, full, craft: (x.note ? 2 : 0) + ((x.likes ?? []).length ? 1 : 0), role: "your reference", added: x.added ?? "" }))
|
|
150
|
+
.sort((a, b) => b.craft - a.craft || String(b.added).localeCompare(String(a.added)))
|
|
151
|
+
.slice(0, limit);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// This user's own recent corrections from palate point (what they asked for, before and after), so the judge
|
|
155
|
+
// looks with their eye. Only pairs whose images still exist.
|
|
156
|
+
export function corrections(limit = 3) {
|
|
157
|
+
if (!fs.existsSync(POINT_LOG)) return [];
|
|
158
|
+
const rows = fs
|
|
159
|
+
.readFileSync(POINT_LOG, "utf8")
|
|
160
|
+
.split("\n")
|
|
161
|
+
.filter(Boolean)
|
|
162
|
+
.map((l) => {
|
|
163
|
+
try {
|
|
164
|
+
return JSON.parse(l);
|
|
165
|
+
} catch {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
.filter((x) => x?.kind === "point-done" && x.prompt && x.before && x.after && fs.existsSync(x.before) && fs.existsSync(x.after));
|
|
170
|
+
return rows.slice(-limit).reverse();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Snapshot of the project's source (src/, app/, components/, styles), so a repair that makes things worse can be
|
|
174
|
+
// rolled back. Small projects only: skipped above 8 MB.
|
|
175
|
+
const SRC_DIRS = ["src", "app", "components", "styles", "pages", "lib"];
|
|
176
|
+
export function snapshot(dir, into) {
|
|
177
|
+
let bytes = 0;
|
|
178
|
+
const files = [];
|
|
179
|
+
const walk = (d) => {
|
|
180
|
+
if (!fs.existsSync(d)) return;
|
|
181
|
+
for (const e of fs.readdirSync(d, { withFileTypes: true })) {
|
|
182
|
+
if (/^(node_modules|\.next|dist|build|\.palate|\.git)$/.test(e.name)) continue;
|
|
183
|
+
const p = path.join(d, e.name);
|
|
184
|
+
if (e.isDirectory()) walk(p);
|
|
185
|
+
else {
|
|
186
|
+
bytes += fs.statSync(p).size;
|
|
187
|
+
files.push(p);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
for (const s of SRC_DIRS) walk(path.join(dir, s));
|
|
192
|
+
if (bytes > 8e6) return { ok: false, why: `source is ${Math.round(bytes / 1e6)} MB; no snapshot` };
|
|
193
|
+
// public/ too (a repair swaps photos there), under its own budget: a page's photos can run to tens of MB.
|
|
194
|
+
const code = files.length;
|
|
195
|
+
const codeBytes = bytes;
|
|
196
|
+
walk(path.join(dir, "public"));
|
|
197
|
+
if (bytes - codeBytes > 60e6) files.length = code;
|
|
198
|
+
for (const f of files) {
|
|
199
|
+
const to = path.join(into, path.relative(dir, f));
|
|
200
|
+
fs.mkdirSync(path.dirname(to), { recursive: true });
|
|
201
|
+
fs.copyFileSync(f, to);
|
|
202
|
+
}
|
|
203
|
+
return { ok: true, files: files.length };
|
|
204
|
+
}
|
|
205
|
+
export function restore(dir, route, round) {
|
|
206
|
+
const rr = readRounds(dir, route);
|
|
207
|
+
const r = rr.rounds.find((x) => x.n === Number(round)) ?? null;
|
|
208
|
+
if (!r?.snapshot) throw new Error(`No snapshot for round ${round} of ${route}.`);
|
|
209
|
+
const from = r.snapshot;
|
|
210
|
+
let n = 0;
|
|
211
|
+
const walk = (d) => {
|
|
212
|
+
for (const e of fs.readdirSync(d, { withFileTypes: true })) {
|
|
213
|
+
const p = path.join(d, e.name);
|
|
214
|
+
if (e.isDirectory()) walk(p);
|
|
215
|
+
else {
|
|
216
|
+
const to = path.join(dir, path.relative(from, p));
|
|
217
|
+
fs.mkdirSync(path.dirname(to), { recursive: true });
|
|
218
|
+
fs.copyFileSync(p, to);
|
|
219
|
+
n++;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
walk(from);
|
|
224
|
+
// The restored round's verdict is the page's verdict again: palate check and bench record read it, not the
|
|
225
|
+
// rolled-back round's.
|
|
226
|
+
try {
|
|
227
|
+
const v = readJson(path.join(r.at, "verdict.json"), null);
|
|
228
|
+
const st = readJson(path.join(r.at, "state.json"), null);
|
|
229
|
+
if (v && st) {
|
|
230
|
+
const prev = readJson(path.join(dir, ".palate", "quality.json"), {})[route];
|
|
231
|
+
saveQuality(dir, route, v, st, { final: Boolean(prev?.final) });
|
|
232
|
+
}
|
|
233
|
+
rr.best = r.n;
|
|
234
|
+
fs.writeFileSync(roundsFile(dir, route), JSON.stringify(rr, null, 1));
|
|
235
|
+
} catch {}
|
|
236
|
+
return `Restored ${n} files (source and public/) from round ${round} (${slash(from)}); round ${round}'s verdict is the page's verdict again. Files added since then are still there: delete them if they belong only to the reverted repair.`;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// A full-page screenshot cut into screen-sized parts (at most `max`, from the top), so a judge sees a long page
|
|
240
|
+
// at a legible size instead of shrunk to a strip (a judge saw Missive's full page at a fifth of its size).
|
|
241
|
+
export async function sliceTall(file, { height = 1600, max = 4, width = 1440 } = {}) {
|
|
242
|
+
const { acquireBrowser } = await import("../pool.mjs");
|
|
243
|
+
const { browser, release } = await acquireBrowser();
|
|
244
|
+
try {
|
|
245
|
+
const page = await browser.newPage();
|
|
246
|
+
try {
|
|
247
|
+
const type = /\.jpe?g$/i.test(file) ? "image/jpeg" : "image/png";
|
|
248
|
+
const parts = await page.evaluate(
|
|
249
|
+
async ([src, h, n, w]) => {
|
|
250
|
+
const img = new Image();
|
|
251
|
+
img.src = src;
|
|
252
|
+
await img.decode();
|
|
253
|
+
const scale = Math.min(1, w / img.naturalWidth);
|
|
254
|
+
const sh = h / scale;
|
|
255
|
+
const res = [];
|
|
256
|
+
for (let y = 0, i = 0; y < img.naturalHeight && i < n; y += sh, i++) {
|
|
257
|
+
const c = document.createElement("canvas");
|
|
258
|
+
const take = Math.min(sh, img.naturalHeight - y);
|
|
259
|
+
c.width = Math.round(img.naturalWidth * scale);
|
|
260
|
+
c.height = Math.round(take * scale);
|
|
261
|
+
c.getContext("2d").drawImage(img, 0, y, img.naturalWidth, take, 0, 0, c.width, c.height);
|
|
262
|
+
res.push(c.toDataURL("image/jpeg", 0.85));
|
|
263
|
+
}
|
|
264
|
+
return res;
|
|
265
|
+
},
|
|
266
|
+
[`data:${type};base64,${fs.readFileSync(file).toString("base64")}`, height, max, width],
|
|
267
|
+
);
|
|
268
|
+
return parts.map((d) => Buffer.from(d.split(",")[1], "base64"));
|
|
269
|
+
} finally {
|
|
270
|
+
await page.close();
|
|
271
|
+
}
|
|
272
|
+
} finally {
|
|
273
|
+
await release();
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const JSON_RULE = "Answer only from what you see in the images. Be strict: a strong human designer is the bar, not other AI output. Write the JSON file exactly as specified and nothing else.";
|
|
278
|
+
|
|
279
|
+
// The project's own facts (its README), for the judges: a quote, a name or a price the project gave is real, and a
|
|
280
|
+
// section that shows only what the facts give is honest, not thin. Without this, judges called a README's own quote
|
|
281
|
+
// "invented" and failed honest sections for lacking a photo or organisation the project never had.
|
|
282
|
+
export function projectFacts(dir, { max = 3000 } = {}) {
|
|
283
|
+
for (const f of ["README.md", "readme.md", "README", "BRIEF.md"]) {
|
|
284
|
+
try {
|
|
285
|
+
const t = fs.readFileSync(path.join(dir, f), "utf8").replace(/\r\n/g, "\n").trim();
|
|
286
|
+
if (t) return t.length > max ? `${t.slice(0, max)}\n…` : t;
|
|
287
|
+
} catch {}
|
|
288
|
+
}
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export const FACTS_RULE = "Content that appears in these facts is the project's own: never call it invented, fake or unsourced. When a question asks for a detail the facts don't give (a photo of the person, an organisation, a source link, opening hours), answer YES if the section shows only what the facts give and invents nothing; answer NO if it fills the gap with something made up (an invented surname, a stock face, a fake source).";
|
|
293
|
+
|
|
294
|
+
function checklistPacket({ route, job, sections, phone, firstScreen, questions, page, out, mine, bar = [], tablet = {}, facts = null }) {
|
|
295
|
+
return [
|
|
296
|
+
"# Design review: section checklist",
|
|
297
|
+
"",
|
|
298
|
+
"You are an independent design reviewer. You did not make this page and you have no stake in it. Open every image with your image tool before answering.",
|
|
299
|
+
job ? `The page is the ${route} page for: ${job}` : null,
|
|
300
|
+
"",
|
|
301
|
+
`- The first screen at 1440: ${slash(firstScreen)}`,
|
|
302
|
+
`- The whole page on a phone (390): ${slash(phone)}`,
|
|
303
|
+
tablet.fold ? `- The first screen on a tablet, landscape (1024): ${slash(tablet.fold)}` : null,
|
|
304
|
+
tablet.full ? `- The whole page on a tablet, portrait (768): ${slash(tablet.full)}` : null,
|
|
305
|
+
"",
|
|
306
|
+
...(facts ? ["## The project's facts (from its README)", "```", facts, "```", FACTS_RULE, ""] : []),
|
|
307
|
+
...(bar.length
|
|
308
|
+
? [
|
|
309
|
+
"## The bar",
|
|
310
|
+
"Strong real sites in the same category, for calibration only (they are not being reviewed). A section answers YES to \"finished\" only if it would hold up next to these:",
|
|
311
|
+
...bar.map((b, i) => `- Site ${i + 1}, full page: ${slash(b)}`),
|
|
312
|
+
"",
|
|
313
|
+
]
|
|
314
|
+
: []),
|
|
315
|
+
...(mine.length
|
|
316
|
+
? [
|
|
317
|
+
"## How this user judges (their own recent corrections: what they asked for, then before → after)",
|
|
318
|
+
...mine.map((m) => `- "${m.prompt}" (${m.route}): ${slash(m.before)} → ${slash(m.after)}`),
|
|
319
|
+
"Hold the page to the standard these corrections show.",
|
|
320
|
+
"",
|
|
321
|
+
]
|
|
322
|
+
: []),
|
|
323
|
+
"## Each section",
|
|
324
|
+
"Answer every question YES or NO (YES = good), and give each section a `worst` line: the one thing most below the bar, in under 20 words.",
|
|
325
|
+
...sections.map((s) => [`### Section ${s.n}: ${s.name}${s.archetype ? ` (meant to be: ${s.archetype})` : ""}`, `Image: ${slash(s.file)}`, ...questions(s).map((q) => `- ${q.id}: ${q.q}`)].join("\n")),
|
|
326
|
+
"",
|
|
327
|
+
"## The whole page",
|
|
328
|
+
...page.map((q) => `- ${q.id}: ${q.q}`),
|
|
329
|
+
"",
|
|
330
|
+
JSON_RULE,
|
|
331
|
+
`Write ${slash(out)} as {"sections": [{"n": 1, "answers": {"finished": true, ...}, "worst": "..."}, ...], "page": {"subject": true, ...}, "summary": "two sentences"}`,
|
|
332
|
+
]
|
|
333
|
+
.filter((l) => l != null)
|
|
334
|
+
.join("\n");
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function pairPacket({ comparisons, out, sameCategory = true }) {
|
|
338
|
+
return [
|
|
339
|
+
"# Design review: which page is better designed?",
|
|
340
|
+
"",
|
|
341
|
+
sameCategory
|
|
342
|
+
? "You are an independent design reviewer comparing landing pages for different products in the same category. You made none of them. Open every image with your image tool before answering. Judge the design, not the product or the brand's fame."
|
|
343
|
+
: "You are an independent design reviewer comparing product landing pages. The products may be in different categories: judge the design craft, not the category, the product or the brand's fame. You made none of them. Open every image with your image tool before answering.",
|
|
344
|
+
"For each comparison, look at both first screens and both full pages, top to bottom. Weigh: the first impression and hierarchy; the finish (type, spacing, surfaces, buttons, details); imagery and material; whether every section is considered and different (not one template repeated); and whether the lower sections hold up as well as the first screen.",
|
|
345
|
+
"",
|
|
346
|
+
...comparisons.map((c) => {
|
|
347
|
+
const full = (p, who) => (Array.isArray(p.parts) && p.parts.length ? p.parts.map((f, i) => `- Page ${who}, part ${i + 1} of ${p.parts.length} (top to bottom): ${slash(f)}`) : [`- Page ${who} full page: ${slash(p.full)}`]);
|
|
348
|
+
return [`## ${c.id}`, `- Page A first screen: ${slash(c.a.fold)}`, ...full(c.a, "A"), `- Page B first screen: ${slash(c.b.fold)}`, ...full(c.b, "B")].join("\n");
|
|
349
|
+
}),
|
|
350
|
+
"",
|
|
351
|
+
"For each: `winner` A, B or tie; `margin` clearly or slightly (omit for a tie); `weakestA` and `weakestB`: each page's single weakest section and why, in under 25 words, whoever wins.",
|
|
352
|
+
JSON_RULE,
|
|
353
|
+
`Write ${slash(out)} as {"comparisons": [{"id": "${comparisons[0]?.id ?? "c1"}", "winner": "A", "margin": "clearly", "weakestA": {"section": "...", "why": "..."}, "weakestB": {"section": "...", "why": "..."}}, ...]}`,
|
|
354
|
+
].join("\n");
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// The blueprint section each crop shows (or null). By the blueprint id first (the element's id, a word of its name,
|
|
358
|
+
// heading or selector); the page's own header and nav are chrome unless the blueprint plans a section with that id;
|
|
359
|
+
// the rest in order, but only when what's left lines up one to one. A crop palate can't place gets the generic
|
|
360
|
+
// questions: asking a pre-order form the day-of-the-place questions (a header counted as section 1 shifted every
|
|
361
|
+
// archetype by one) failed a good page.
|
|
362
|
+
export function matchCrops(crops, plan) {
|
|
363
|
+
const low = (v) => String(v ?? "").toLowerCase();
|
|
364
|
+
const words = (v) => low(v).split(/[^a-z0-9]+/).filter(Boolean);
|
|
365
|
+
const idOf = (s) => low(s.id);
|
|
366
|
+
const hit = (c, s) => Boolean(idOf(s)) && (low(c.id) === idOf(s) || [c.id, c.selector].some((v) => words(v).includes(idOf(s)) || low(v).includes(`#${idOf(s)}`)) || words(c.name).join("-") === idOf(s) || words(c.heading).join("-") === idOf(s));
|
|
367
|
+
const used = new Set();
|
|
368
|
+
const out = crops.map((c) => {
|
|
369
|
+
const s = plan.find((p) => !used.has(p) && hit(c, p));
|
|
370
|
+
if (s) used.add(s);
|
|
371
|
+
return s ?? null;
|
|
372
|
+
});
|
|
373
|
+
const chrome = (c) => ["header", "nav"].includes(low(c.tag)) || ["header", "navigation"].includes(low(c.name));
|
|
374
|
+
const open = crops.map((c, i) => i).filter((i) => !out[i] && !chrome(crops[i]));
|
|
375
|
+
const left = plan.filter((p) => !used.has(p) && !["header", "nav", "navigation"].includes(idOf(p)));
|
|
376
|
+
if (open.length === left.length) open.forEach((i, k) => (out[i] = left[k]));
|
|
377
|
+
return out;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Renders the page, runs the floor checks, crops every section and writes the judge packets.
|
|
381
|
+
async function prepare(dir, route, { url, job } = {}) {
|
|
382
|
+
const rounds = readRounds(dir, route);
|
|
383
|
+
const n = rounds.rounds.length;
|
|
384
|
+
const at = path.join(rootFor(dir, route), `round-${n}-${stamp()}`);
|
|
385
|
+
fs.mkdirSync(at, { recursive: true });
|
|
386
|
+
// The tablet views come from the floor's own renders at 1024 and 768 (no extra page loads).
|
|
387
|
+
const tablet = { fold: path.join(at, "page-1024-fold.png"), full: path.join(at, "page-768.png") };
|
|
388
|
+
const floorShots = { 1024: [{ path: tablet.fold }], 768: [{ path: tablet.full, full: true }] };
|
|
389
|
+
const floor = Floor?.runFloor ? await Floor.runFloor(dir, route, { url, shots: floorShots }).catch((e) => ({ pass: true, checks: [], error: e.message })) : { pass: true, checks: [], error: "floor checks not installed" };
|
|
390
|
+
for (const k of Object.keys(tablet)) if (!fs.existsSync(tablet[k])) delete tablet[k];
|
|
391
|
+
// A route built from the user's own design (palate match) isn't held to palate's house look.
|
|
392
|
+
const userDesign = Boolean(matchedRoute(dir, route));
|
|
393
|
+
if (LookVec?.houseLookCheck && !userDesign) await LookVec.houseLookCheck(dir, route, floor, { url });
|
|
394
|
+
const bp = loadBlueprint(dir, { route });
|
|
395
|
+
const shots = await withBrowser(dir, { url }, async ({ base, browser }) => {
|
|
396
|
+
const { page, ctx } = await openPage(browser, base, route, { width: 1440, reducedMotion: true });
|
|
397
|
+
try {
|
|
398
|
+
const fold = path.join(at, "page-fold.png");
|
|
399
|
+
await page.screenshot({ path: fold });
|
|
400
|
+
const fp = FINGERPRINT ? await page.evaluate(`(${FINGERPRINT})()`).catch(() => null) : null;
|
|
401
|
+
const full = path.join(at, "page-full.png");
|
|
402
|
+
const image = await shootFull(page, full);
|
|
403
|
+
const sections = await findSections(page);
|
|
404
|
+
// The crops are cut from that same capture, so they match it.
|
|
405
|
+
const crops = await cropSections(page, sections, path.join(at, "sections"), "w1440", { image });
|
|
406
|
+
// The copy lock: every string the page showed before an existing-page change, checked against now.
|
|
407
|
+
const strings = bp?.before?.strings && fs.existsSync(bp.before.strings) ? await page.evaluate(() => {
|
|
408
|
+
const out = new Set();
|
|
409
|
+
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
|
|
410
|
+
for (let n = walker.nextNode(); n; n = walker.nextNode()) {
|
|
411
|
+
const el = n.parentElement;
|
|
412
|
+
if (!el || el.closest("script,style,noscript,template,[aria-hidden=true]")) continue;
|
|
413
|
+
const cs = getComputedStyle(el);
|
|
414
|
+
if (cs.display === "none" || cs.visibility === "hidden") continue;
|
|
415
|
+
const t = n.textContent.replace(/\s+/g, " ").trim();
|
|
416
|
+
if (t.length >= 2) out.add(t);
|
|
417
|
+
}
|
|
418
|
+
for (const el of document.querySelectorAll("[alt],[aria-label],[placeholder],[title]")) for (const a of ["alt", "aria-label", "placeholder", "title"]) { const v = (el.getAttribute(a) ?? "").trim(); if (v.length >= 2) out.add(v); }
|
|
419
|
+
return [...out];
|
|
420
|
+
}).catch(() => null) : null;
|
|
421
|
+
const p = await openPage(browser, base, route, { width: 390, reducedMotion: true });
|
|
422
|
+
const phone = path.join(at, "page-390.png");
|
|
423
|
+
try {
|
|
424
|
+
await shootFull(p.page, phone);
|
|
425
|
+
} finally {
|
|
426
|
+
await p.ctx.close();
|
|
427
|
+
}
|
|
428
|
+
return { fold, full, phone, crops, strings, fp };
|
|
429
|
+
} finally {
|
|
430
|
+
await ctx.close();
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
for (const c of userDesign ? [] : await similarityChecks(dir, route, shots.fp)) {
|
|
434
|
+
floor.checks = [...(floor.checks ?? []), c];
|
|
435
|
+
floor.pass = false;
|
|
436
|
+
}
|
|
437
|
+
// Match crops to the blueprint's sections by order (the page's sections, header and footer included, are
|
|
438
|
+
// matched to the blueprint where the counts allow).
|
|
439
|
+
const plan = bp?.sections ?? [];
|
|
440
|
+
const matched = matchCrops(shots.crops, plan);
|
|
441
|
+
const sections = shots.crops.map((c, i) => {
|
|
442
|
+
const b = matched[i];
|
|
443
|
+
return { n: i + 1, name: c.heading || c.name || `section ${i + 1}`, file: c.file, archetype: b?.archetype ?? null, id: b?.id ?? null };
|
|
444
|
+
});
|
|
445
|
+
const questions = (s) => [...SECTION_QUESTIONS, ...(s.archetype && Sections?.floorChecks ? Sections.floorChecks(s.archetype).map((q, k) => ({ id: `a${k + 1}`, q })) : [])];
|
|
446
|
+
const mine = corrections();
|
|
447
|
+
const packets = [];
|
|
448
|
+
const checklistOut = path.join(at, "answers-checklist.json");
|
|
449
|
+
// Pairwise, both orders, each order with a different judge: packet 1 has ours as A for ref 1 and as B for
|
|
450
|
+
// ref 2; packet 2 the reverse.
|
|
451
|
+
const refs = exemplars(dir);
|
|
452
|
+
// An existing page's change is also judged against the page as it was (blueprint.mjs saved it): it may not get worse.
|
|
453
|
+
if (bp?.before?.full && fs.existsSync(bp.before.full) && fs.existsSync(bp.before.fold)) refs.unshift({ name: "the page before this change", url: null, fold: bp.before.fold, full: bp.before.full, before: true });
|
|
454
|
+
// Blind: every image in a comparison gets a random neutral name, so no file name says which page is ours or
|
|
455
|
+
// whose site a reference is (the first judges read "ours-fold.png" and knew).
|
|
456
|
+
const blindDir = path.join(at, "blind");
|
|
457
|
+
fs.mkdirSync(blindDir, { recursive: true });
|
|
458
|
+
const blind = (file) => {
|
|
459
|
+
const to = path.join(blindDir, `${crypto.randomBytes(5).toString("hex")}${path.extname(file)}`);
|
|
460
|
+
fs.copyFileSync(file, to);
|
|
461
|
+
return to;
|
|
462
|
+
};
|
|
463
|
+
const parts = async (file) => {
|
|
464
|
+
const bufs = await sliceTall(file).catch(() => []);
|
|
465
|
+
return bufs.map((b) => {
|
|
466
|
+
const to = path.join(blindDir, `${crypto.randomBytes(5).toString("hex")}.jpg`);
|
|
467
|
+
fs.writeFileSync(to, b);
|
|
468
|
+
return to;
|
|
469
|
+
});
|
|
470
|
+
};
|
|
471
|
+
const ours = { fold: blind(shots.fold), full: blind(shots.full), parts: await parts(shots.full) };
|
|
472
|
+
const refViews = [];
|
|
473
|
+
for (const ref of refs) refViews.push({ fold: blind(ref.fold), full: blind(ref.full), parts: await parts(ref.full) });
|
|
474
|
+
fs.writeFileSync(path.join(at, "packet-checklist.md"), checklistPacket({ route, job, sections, phone: shots.phone, firstScreen: shots.fold, questions, page: PAGE_QUESTIONS, out: checklistOut, mine, bar: refViews.flatMap((v) => (v.parts.length ? v.parts.slice(0, 2) : [v.full])), tablet, facts: projectFacts(dir) }));
|
|
475
|
+
packets.push({ file: path.join(at, "packet-checklist.md"), out: checklistOut });
|
|
476
|
+
const key = [];
|
|
477
|
+
if (refs.length) {
|
|
478
|
+
const order = [[], []];
|
|
479
|
+
refs.forEach((ref, i) => {
|
|
480
|
+
const r = refViews[i];
|
|
481
|
+
const id = `c${i + 1}`;
|
|
482
|
+
const oursFirst = { id, a: ours, b: r };
|
|
483
|
+
const refFirst = { id, a: r, b: ours };
|
|
484
|
+
order[0].push(i % 2 === 0 ? oursFirst : refFirst);
|
|
485
|
+
order[1].push(i % 2 === 0 ? refFirst : oursFirst);
|
|
486
|
+
key.push({ id, ref: ref.name, url: ref.url, before: Boolean(ref.before), oursIs: [i % 2 === 0 ? "A" : "B", i % 2 === 0 ? "B" : "A"] });
|
|
487
|
+
});
|
|
488
|
+
order.forEach((comparisons, k) => {
|
|
489
|
+
const out = path.join(at, `answers-pair-${k + 1}.json`);
|
|
490
|
+
const file = path.join(at, `packet-pair-${k + 1}.md`);
|
|
491
|
+
fs.writeFileSync(file, pairPacket({ comparisons, out, sameCategory: !refs.some((x) => x.role === "your reference") }));
|
|
492
|
+
packets.push({ file, out });
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
mark(dir, loopName(route), "gate_first");
|
|
496
|
+
mark(dir, loopName(route), "gate_round", { round: n });
|
|
497
|
+
const copy = shots.strings && bp?.before?.strings ? copyDiff(readJson(bp.before.strings, []), shots.strings, { locked: bp.copy !== "edit" }) : null;
|
|
498
|
+
delete shots.strings;
|
|
499
|
+
const state = { route, job, at, n, floor, shots, sections, refs, key, packets, copy, blueprint: bp?.file ?? null, created: new Date().toISOString() };
|
|
500
|
+
fs.writeFileSync(path.join(at, "state.json"), JSON.stringify(state, null, 1));
|
|
501
|
+
return state;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// The copy lock: what an existing page showed before that it doesn't now (and what's new). Whitespace and case of
|
|
505
|
+
// spacing are ignored; a string counts as kept when it appears whole inside a longer one now (a sentence merged
|
|
506
|
+
// into a paragraph). locked: the blueprint didn't say copy: "edit".
|
|
507
|
+
export function copyDiff(before, after, { locked = true } = {}) {
|
|
508
|
+
const norm = (x) => String(x).replace(/\s+/g, " ").trim();
|
|
509
|
+
const now = after.map(norm);
|
|
510
|
+
const joined = now.join(" \u0001 ");
|
|
511
|
+
const prev = before.map(norm);
|
|
512
|
+
const missing = prev.filter((x) => x.length >= 2 && !now.includes(x) && !joined.includes(x));
|
|
513
|
+
const was = new Set(prev);
|
|
514
|
+
const was2 = prev.join(" \u0001 ");
|
|
515
|
+
const added = now.filter((x) => !was.has(x) && !was2.includes(x));
|
|
516
|
+
return { locked, kept: prev.length - missing.length, total: prev.length, missing, added };
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// The verdict from the floor checks and the judges' answers.
|
|
520
|
+
export function verdict(state, answers) {
|
|
521
|
+
const fails = [];
|
|
522
|
+
const warns = [];
|
|
523
|
+
for (const c of state.floor?.checks ?? []) (c.severity === "fail" ? fails : warns).push({ layer: "floor", id: c.id, section: c.section ?? null, what: c.what, fix: c.fix });
|
|
524
|
+
// The copy lock (an existing page): strings the page showed before and doesn't now.
|
|
525
|
+
if (state.copy?.missing?.length) (state.copy.locked ? fails : warns).push({ layer: "floor", id: "COPY_LOST", section: null, what: `${state.copy.missing.length} string${state.copy.missing.length > 1 ? "s" : ""} the page showed before ${state.copy.missing.length > 1 ? "are" : "is"} gone: ${state.copy.missing.slice(0, 6).map((x) => `"${x.slice(0, 60)}"`).join(", ")}${state.copy.missing.length > 6 ? ` and ${state.copy.missing.length - 6} more` : ""}`, fix: state.copy.locked ? "Put them back word for word (the copy is locked unless the blueprint says copy: edit)" : "Check each was meant to go" });
|
|
526
|
+
if (state.copy?.added?.length) warns.push({ layer: "floor", id: "COPY_NEW", section: null, what: `${state.copy.added.length} new string${state.copy.added.length > 1 ? "s" : ""} (list them in the hand-off): ${state.copy.added.slice(0, 6).map((x) => `"${x.slice(0, 40)}"`).join(", ")}` });
|
|
527
|
+
const cl = answers.checklist;
|
|
528
|
+
const weakVotes = {};
|
|
529
|
+
if (cl) {
|
|
530
|
+
for (const s of cl.sections ?? []) {
|
|
531
|
+
const sec = state.sections.find((x) => x.n === Number(s.n));
|
|
532
|
+
const entries = Object.entries(s.answers ?? {});
|
|
533
|
+
const nos = entries.filter(([, v]) => no(v)).map(([k]) => k);
|
|
534
|
+
const mustFail = SECTION_QUESTIONS.filter((q) => q.must).some((q) => no(s.answers?.[q.id]));
|
|
535
|
+
if (mustFail || nos.length >= 2) fails.push({ layer: "checklist", id: "SECTION_BELOW_BAR", section: { n: Number(s.n), name: sec?.name }, what: `section ${s.n} (${sec?.name ?? "?"}) answered NO to ${nos.join(", ")}${s.worst ? `: ${s.worst}` : ""}`, fix: s.worst ?? null });
|
|
536
|
+
else if (nos.length) warns.push({ layer: "checklist", id: "SECTION_NO", section: { n: Number(s.n), name: sec?.name }, what: `section ${s.n} (${sec?.name ?? "?"}): NO to ${nos.join(", ")}${s.worst ? `: ${s.worst}` : ""}` });
|
|
537
|
+
if (s.worst) weakVotes[s.n] = (weakVotes[s.n] ?? 0) + (nos.length ? 1 : 0);
|
|
538
|
+
}
|
|
539
|
+
for (const q of PAGE_QUESTIONS) {
|
|
540
|
+
if (!no(cl.page?.[q.id])) continue;
|
|
541
|
+
(q.must ? fails : warns).push({ layer: "checklist", id: `PAGE_${q.id.toUpperCase()}`, section: null, what: `the page: NO to "${q.q}"` });
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
// Pairwise: per reference, ours in each order.
|
|
545
|
+
const pairs = [];
|
|
546
|
+
for (const k of state.key ?? []) {
|
|
547
|
+
const res = [answers.pair1, answers.pair2].map((p, idx) => {
|
|
548
|
+
const c = p?.comparisons?.find((x) => x.id === k.id);
|
|
549
|
+
if (!c) return null;
|
|
550
|
+
const oursSide = k.oursIs[idx];
|
|
551
|
+
const w = String(c.winner ?? "").trim().toUpperCase();
|
|
552
|
+
const outcome = w === "TIE" || !w ? "tie" : w === oursSide ? "win" : "lose";
|
|
553
|
+
const mineW = c[`weakest${oursSide}`] ?? (c.weakest && String(c.weakest.page ?? "").toUpperCase() === oursSide ? c.weakest : null);
|
|
554
|
+
return { outcome, margin: outcome === "tie" ? null : String(c.margin ?? "slightly").toLowerCase(), oursWeakest: mineW ?? null };
|
|
555
|
+
});
|
|
556
|
+
const [a, b] = res;
|
|
557
|
+
let result = "incomplete";
|
|
558
|
+
if (a && b) {
|
|
559
|
+
if (a.outcome === b.outcome) result = a.outcome === "lose" && a.margin === "clearly" && b.margin === "clearly" ? "clearly worse" : a.outcome === "lose" ? "worse" : a.outcome === "win" ? "better" : "tie";
|
|
560
|
+
else result = "tie (the two orders disagree)";
|
|
561
|
+
}
|
|
562
|
+
pairs.push({ ref: k.ref, url: k.url, result, orders: res });
|
|
563
|
+
for (const o of res) if (o?.oursWeakest) weakVotes[o.oursWeakest.section] = (weakVotes[o.oursWeakest.section] ?? 0) + 1;
|
|
564
|
+
if (k.before) {
|
|
565
|
+
if (/worse/.test(result)) fails.push({ layer: "pairwise", id: "WORSE_THAN_BEFORE", section: null, what: `${result} than the page before this change, in both orders: the change made it worse${res.find((o) => o?.oursWeakest) ? `; weakest now: ${res.find((o) => o?.oursWeakest).oursWeakest.section}: ${res.find((o) => o?.oursWeakest).oursWeakest.why}` : ""}` });
|
|
566
|
+
else if (/^tie/.test(result)) warns.push({ layer: "pairwise", id: "NO_BETTER", section: null, what: `no better than the page before this change (${result})` });
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
if (result === "clearly worse") fails.push({ layer: "pairwise", id: "CLEARLY_WORSE", section: null, what: `clearly worse than ${k.ref} in both orders${res.find((o) => o?.oursWeakest) ? `; weakest: ${res.find((o) => o?.oursWeakest).oursWeakest.section}: ${res.find((o) => o?.oursWeakest).oursWeakest.why}` : ""}` });
|
|
570
|
+
else if (result === "worse") warns.push({ layer: "pairwise", id: "BEHIND", section: null, what: `behind ${k.ref} in both orders (slightly)` });
|
|
571
|
+
}
|
|
572
|
+
const pass = !fails.length;
|
|
573
|
+
// A round's standing, for "keep the best round": fewer fails, then fewer pairwise losses, then fewer warns.
|
|
574
|
+
const losses = pairs.filter((p) => /worse/.test(p.result)).length;
|
|
575
|
+
const score = fails.length * 100 + losses * 10 + warns.length;
|
|
576
|
+
return { pass, fails, warns, pairs, score, weakVotes };
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// The one thing to repair this round: a failing section first (floor fails before judge fails), else the section
|
|
580
|
+
// the judges named weakest most often.
|
|
581
|
+
export function repairTarget(state, v) {
|
|
582
|
+
const bySection = {};
|
|
583
|
+
for (const f of v.fails) {
|
|
584
|
+
const n = f.section?.n ?? f.section?.index ?? null;
|
|
585
|
+
if (n == null) continue;
|
|
586
|
+
(bySection[n] ??= []).push(f);
|
|
587
|
+
}
|
|
588
|
+
const worst = Object.entries(bySection).sort((a, b) => b[1].length - a[1].length || (b[1].some((x) => x.layer === "floor") ? 1 : 0) - (a[1].some((x) => x.layer === "floor") ? 1 : 0))[0];
|
|
589
|
+
if (worst) return { n: Number(worst[0]), why: worst[1] };
|
|
590
|
+
const voted = Object.entries(v.weakVotes ?? {}).sort((a, b) => b[1] - a[1])[0];
|
|
591
|
+
if (voted) {
|
|
592
|
+
const n = Number(voted[0]);
|
|
593
|
+
const sec = Number.isFinite(n) ? state.sections.find((s) => s.n === n) : state.sections.find((s) => s.name.toLowerCase().includes(String(voted[0]).toLowerCase()));
|
|
594
|
+
if (sec) return { n: sec.n, why: v.fails.filter((f) => !f.section) };
|
|
595
|
+
}
|
|
596
|
+
const pageFail = v.fails.find((f) => !f.section);
|
|
597
|
+
return pageFail ? { n: null, why: [pageFail] } : null;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Every section below the bar this round (up to max), each with its own findings; page-wide findings go with the
|
|
601
|
+
// first. Each is repaired by its own builder in parallel: they touch different sections, so they don't undo each
|
|
602
|
+
// other, and one round replaces what used to be two (the benchmark spent ~15 minutes in serial repair rounds).
|
|
603
|
+
export function repairTargets(state, v, { max = 3 } = {}) {
|
|
604
|
+
const bySection = {};
|
|
605
|
+
for (const f of v.fails) {
|
|
606
|
+
const n = f.section?.n ?? f.section?.index ?? null;
|
|
607
|
+
if (n == null) continue;
|
|
608
|
+
(bySection[n] ??= []).push(f);
|
|
609
|
+
}
|
|
610
|
+
const ranked = Object.entries(bySection).sort((a, b) => b[1].length - a[1].length || (b[1].some((x) => x.layer === "floor") ? 1 : 0) - (a[1].some((x) => x.layer === "floor") ? 1 : 0));
|
|
611
|
+
const out = ranked.slice(0, max).map(([n, why]) => ({ n: Number(n), why }));
|
|
612
|
+
const page = v.fails.filter((f) => !f.section);
|
|
613
|
+
if (!out.length) {
|
|
614
|
+
const t = repairTarget(state, v);
|
|
615
|
+
return t ? [t] : [];
|
|
616
|
+
}
|
|
617
|
+
if (page.length) out[0].why = [...out[0].why, ...page];
|
|
618
|
+
return out;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// What this job may change. The gate judges the whole page, so on a page still being built ("add the pricing section")
|
|
622
|
+
// the judges say the page "just stops, no footer": a repair then built a closing section the user never asked for.
|
|
623
|
+
// On an existing page the blueprint's add/upgrade sections are the scope; the rest is a suggestion for the user.
|
|
624
|
+
export function scopeLines(bp) {
|
|
625
|
+
if (bp?.mode !== "existing") return [];
|
|
626
|
+
const ids = (bp.sections ?? []).filter((s) => ["add", "upgrade"].includes(s.action)).map((s) => s.id);
|
|
627
|
+
if (!ids.length) return [];
|
|
628
|
+
return [
|
|
629
|
+
"## This job's scope",
|
|
630
|
+
`${bp.job ? `The user asked: "${String(bp.job).slice(0, 240)}". ` : ""}This build adds or changes only: ${ids.join(", ")}. Fix what's below the bar inside those (and the tokens they use).`,
|
|
631
|
+
"If the judges say something outside them is missing or weak (an ending, a footer, a section the page doesn't have yet, another section's look), don't build or change it: tell the user in one line as a suggestion and leave it for them to ask.",
|
|
632
|
+
"",
|
|
633
|
+
];
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function repairBrief(dir, state, v, target, { round, of = 1 }) {
|
|
637
|
+
const sec = target?.n ? state.sections.find((s) => s.n === target.n) : null;
|
|
638
|
+
const bp = loadBlueprint(dir, { route: state.route });
|
|
639
|
+
const planned = sec?.id ? bp?.sections?.find((s) => s.id === sec.id) : null;
|
|
640
|
+
const arche = sec?.archetype && Sections?.sectionBrief ? Sections.sectionBrief(sec.archetype) : null;
|
|
641
|
+
return [
|
|
642
|
+
`# Repair round ${round} of ${MAX_ROUNDS}: ${sec ? `section ${sec.n}, ${sec.name}` : "the page"}`,
|
|
643
|
+
"",
|
|
644
|
+
of > 1 ? `One of ${of} sections repaired in parallel this round, each by its own builder. Change only this section's own files (and nothing another section uses); palate snapshotted the source before this round.` : "One target this round. Change only this section (and the tokens it needs). palate snapshotted the source before this round.",
|
|
645
|
+
sec ? `- Its crop now: ${slash(sec.file)}` : null,
|
|
646
|
+
planned ? `- Its job in the blueprint: ${planned.job}. Content: ${planned.content ?? "-"}. Surface: ${planned.surface}. Imagery: ${planned.imagery}.` : null,
|
|
647
|
+
"",
|
|
648
|
+
...scopeLines(bp),
|
|
649
|
+
"## What's below the bar",
|
|
650
|
+
...(target?.why?.length ? target.why : v.fails).map((f) => `- [${f.layer}] ${f.what}${f.fix ? ` → ${f.fix}` : ""}`),
|
|
651
|
+
...v.pairs.filter((p) => p.orders.some((o) => o?.oursWeakest)).map((p) => `- Judges comparing with ${p.ref}: ${p.orders.filter((o) => o?.oursWeakest).map((o) => `${o.oursWeakest.section}: ${o.oursWeakest.why}`).join("; ")}`),
|
|
652
|
+
"",
|
|
653
|
+
...(arche ? ["## The archetype this section follows (rebuild it to this, completely)", arche, ""] : []),
|
|
654
|
+
"Facts come only from the project (its README and existing copy): where the archetype asks for something the project doesn't have (a photo of the person, an organisation, a source, opening hours), leave it out and make the section strong without it. Never invent a surname, a face, a source or a number.",
|
|
655
|
+
Sections?.finishStandard ? `The shared finish: kb/sections/_finish.md.` : null,
|
|
656
|
+
"",
|
|
657
|
+
`Then: \`palate gate ${state.route}\` again (it renders, re-checks and writes new judge packets; run them with fresh subagents, then --answers). If the round got worse, palate says so and rolls it back with --restore.`,
|
|
658
|
+
]
|
|
659
|
+
.filter((l) => l != null)
|
|
660
|
+
.join("\n");
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function logGate(dir, state, v) {
|
|
664
|
+
try {
|
|
665
|
+
fs.mkdirSync(TASTE_DIR, { recursive: true });
|
|
666
|
+
fs.appendFileSync(GATE_LOG, JSON.stringify({ project: path.basename(path.resolve(dir)), route: state.route, round: state.n, pass: v.pass, fails: v.fails.map((f) => f.id), pairs: v.pairs.map((p) => ({ ref: p.ref, result: p.result })), at: new Date().toISOString() }) + "\n");
|
|
667
|
+
} catch {}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Saves the route's latest quality verdict (palate check reads it).
|
|
671
|
+
function saveQuality(dir, route, v, state, { final = false } = {}) {
|
|
672
|
+
const f = path.join(dir, ".palate", "quality.json");
|
|
673
|
+
const all = readJson(f, {});
|
|
674
|
+
all[route] = { pass: v.pass, final, at: new Date().toISOString(), round: state.n, fails: v.fails.map((x) => x.what), pairs: v.pairs.map((p) => `${p.result} vs ${p.ref}`) };
|
|
675
|
+
fs.mkdirSync(path.dirname(f), { recursive: true });
|
|
676
|
+
fs.writeFileSync(f, JSON.stringify(all, null, 1));
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function readAnswers(state, given) {
|
|
680
|
+
const pick = (re) => {
|
|
681
|
+
const cands = [...(given ?? []), ...state.packets.map((p) => p.out)].filter((f) => re.test(path.basename(f)) && fs.existsSync(f));
|
|
682
|
+
return cands.length ? readJson(cands[0], null) : null;
|
|
683
|
+
};
|
|
684
|
+
return { checklist: pick(/checklist/i), pair1: pick(/pair-?1/i), pair2: pick(/pair-?2/i) };
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// judge: "subagent" (default: the agent runs each packet with a fresh subagent), "api" (palate runs the packets
|
|
688
|
+
// itself with a model API from judge-api.mjs, for agents without subagents: Codex, Cursor), or "self" (no
|
|
689
|
+
// subagents and no key: the builder answers, and the verdict says so; a builder grades its own work too kindly).
|
|
690
|
+
export async function gate(dir, rawRoute, { url, answers, job, restore: restoreRound, judge = "subagent" } = {}) {
|
|
691
|
+
const route = normalizeRoute(rawRoute);
|
|
692
|
+
if (restoreRound != null) return restore(dir, route, restoreRound);
|
|
693
|
+
// The user's own design is the spec there: fidelity is checked by palate match, not palate's taste.
|
|
694
|
+
if (matchedRoute(dir, route) && !answers) return [`# Quality gate ${route}: not for this route`, "", MATCHED_NOTE(route), "That check renders the build beside the design and keeps contrast and clipping as warnings; it never changes the design."].join("\n");
|
|
695
|
+
const rr = readRounds(dir, route);
|
|
696
|
+
if (!answers && judge === "api") {
|
|
697
|
+
const J = await import("./judge-api.mjs");
|
|
698
|
+
const provider = J.judgeProvider();
|
|
699
|
+
if (!provider) return "No judge API key (GEMINI_API_KEY or GOOGLE_API_KEY, OPENAI_API_KEY, ANTHROPIC_API_KEY), or PALATE_JUDGE=off. Run the packets with fresh subagents instead, or --judge self (marked as self-judged).";
|
|
700
|
+
const meta = loadBlueprint(dir, { route });
|
|
701
|
+
const state = await prepare(dir, route, { url, job: job ?? meta?.job });
|
|
702
|
+
const res = await J.runPackets(state.packets, { provider });
|
|
703
|
+
state.judge = { mode: "api", provider: provider.name, model: provider.model };
|
|
704
|
+
fs.writeFileSync(path.join(state.at, "state.json"), JSON.stringify(state, null, 1));
|
|
705
|
+
const failed = res.filter((x) => !x.ok);
|
|
706
|
+
if (failed.length) return [`# Quality gate ${route}: the API judge failed on ${failed.length} packet(s)`, ...failed.map((f) => `- ${slash(f.file?.file ?? f.file)}: ${f.error}`), "", `Run those packets with fresh subagents, then palate gate ${route} --answers ${slash(state.at)}.`].join("\n");
|
|
707
|
+
return gate(dir, rawRoute, { url, answers: [state.at], job });
|
|
708
|
+
}
|
|
709
|
+
if (!answers) {
|
|
710
|
+
const meta = loadBlueprint(dir, { route });
|
|
711
|
+
const state = await prepare(dir, route, { url, job: job ?? meta?.job });
|
|
712
|
+
if (judge === "self") {
|
|
713
|
+
state.judge = { mode: "self" };
|
|
714
|
+
fs.writeFileSync(path.join(state.at, "state.json"), JSON.stringify(state, null, 1));
|
|
715
|
+
}
|
|
716
|
+
const lines = [
|
|
717
|
+
`# Quality gate ${route}: round ${state.n}`,
|
|
718
|
+
"",
|
|
719
|
+
`## Floor checks (palate measured): ${state.floor.pass ? "pass" : "FAIL"}${state.floor.error ? ` (${state.floor.error})` : ""}`,
|
|
720
|
+
...(Floor?.floorLines ? [].concat(Floor.floorLines(state.floor)) : (state.floor.checks ?? []).map((c) => `- ${c.severity === "fail" ? "✗" : "!"} ${c.id}: ${c.what}`)),
|
|
721
|
+
"",
|
|
722
|
+
"## The judges (independent: never the agent that built the page)",
|
|
723
|
+
...state.packets.map((p, i) => `${i + 1}. ${slash(p.file)}`),
|
|
724
|
+
state.refs.length ? `Compared with: ${state.refs.map((r) => r.name).join(", ")} (each in both orders, by different judges).` : "No research captures to compare with: only the checklist judge runs. Run palate research for this page to add the comparison.",
|
|
725
|
+
"",
|
|
726
|
+
`Run each packet with its own fresh subagent, in parallel: "Read <packet> and follow it exactly." Don't judge them yourself (a builder grades its own work too kindly), and don't tell the judges which page is ours. Then: \`palate gate ${route} --answers ${slash(state.at)}\`.`,
|
|
727
|
+
];
|
|
728
|
+
return lines.join("\n");
|
|
729
|
+
}
|
|
730
|
+
// --answers: the latest prepared round (or the directory given).
|
|
731
|
+
const dirGiven = answers.length === 1 && fs.existsSync(answers[0]) && fs.statSync(answers[0]).isDirectory() ? answers[0] : null;
|
|
732
|
+
const latest = dirGiven ?? fs.readdirSync(rootFor(dir, route)).filter((n) => n.startsWith("round-")).map((n) => path.join(rootFor(dir, route), n)).sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs)[0];
|
|
733
|
+
const state = readJson(path.join(latest, "state.json"), null);
|
|
734
|
+
if (!state) throw new Error(`No prepared gate round for ${route}. Run palate gate ${route} first.`);
|
|
735
|
+
const files = dirGiven ? fs.readdirSync(dirGiven).map((f) => path.join(dirGiven, f)) : answers;
|
|
736
|
+
const ans = readAnswers(state, files);
|
|
737
|
+
if (!ans.checklist) throw new Error(`No checklist answers yet: ${slash(state.packets[0].out)}`);
|
|
738
|
+
const v = verdict(state, ans);
|
|
739
|
+
v.judge = state.judge ?? { mode: "subagent" };
|
|
740
|
+
// A page that gained sections since the last round is a new build (Moveday's features under the hero): its rounds
|
|
741
|
+
// start again, so it isn't "final" on arrival and is never rolled back to a round without those sections. The old
|
|
742
|
+
// series is kept beside it.
|
|
743
|
+
const last = rr.rounds.at(-1);
|
|
744
|
+
const lastState = last ? readJson(path.join(last.at, "state.json"), null) : null;
|
|
745
|
+
const key = (s) => String(s.id ?? s.name ?? "").toLowerCase().trim();
|
|
746
|
+
if (lastState?.sections && state.sections.some((s) => key(s) && !lastState.sections.some((p) => key(p) === key(s)))) {
|
|
747
|
+
const old = path.join(rootFor(dir, route), `rounds-before-${stamp()}.json`);
|
|
748
|
+
fs.writeFileSync(old, JSON.stringify(rr, null, 1));
|
|
749
|
+
rr.rounds = [];
|
|
750
|
+
rr.best = null;
|
|
751
|
+
}
|
|
752
|
+
// Rounds: record this one; compare with the best so far.
|
|
753
|
+
const prevBest = rr.best != null ? rr.rounds.find((x) => x.n === rr.best) : null;
|
|
754
|
+
// Worse means more fails: not a warning more, and not a "slightly behind" against a reference, which flips with
|
|
755
|
+
// judge noise (the benchmark rolled back rounds that had fixed their target over 413 vs 406 and 317 vs 304). With
|
|
756
|
+
// as many fails, the newer round wins: it carries the repairs.
|
|
757
|
+
const worse = prevBest && v.fails.length > prevBest.fails;
|
|
758
|
+
const snap = path.join(state.at, "source");
|
|
759
|
+
const snapped = v.pass ? { ok: false } : snapshot(dir, snap);
|
|
760
|
+
const entry = { n: state.n, at: state.at, pass: v.pass, score: v.score, fails: v.fails.length, snapshot: snapped.ok ? snap : null };
|
|
761
|
+
rr.rounds = [...rr.rounds.filter((x) => x.n !== state.n), entry];
|
|
762
|
+
if (!prevBest || !worse) rr.best = state.n;
|
|
763
|
+
writeRounds(dir, route, rr);
|
|
764
|
+
saveQuality(dir, route, v, state, { final: rr.rounds.length - 1 >= MAX_ROUNDS });
|
|
765
|
+
logGate(dir, state, v);
|
|
766
|
+
fs.writeFileSync(path.join(state.at, "verdict.json"), JSON.stringify(v, null, 1));
|
|
767
|
+
const repairs = rr.rounds.length - 1;
|
|
768
|
+
const head = [
|
|
769
|
+
`# Quality gate ${route}, round ${state.n}: ${v.pass ? "PASS" : "below the bar"}${v.judge.mode === "self" ? " (self-judged: weaker; a builder grades its own work too kindly)" : ""}`,
|
|
770
|
+
"",
|
|
771
|
+
...(v.judge.mode === "api" ? [`Judged by ${v.judge.provider} (${v.judge.model}) through its API${v.judge.provider === "anthropic" ? ": the same model family as a Claude builder" : ""}.`, ""] : []),
|
|
772
|
+
...v.pairs.map((p) => `- Against ${p.ref}: ${p.result}`),
|
|
773
|
+
...v.fails.map((f) => `- ✗ [${f.layer}] ${f.what}`),
|
|
774
|
+
...v.warns.slice(0, 8).map((w) => `- ! [${w.layer}] ${w.what}`),
|
|
775
|
+
"",
|
|
776
|
+
];
|
|
777
|
+
mark(dir, loopName(route), "gate_verdict", { round: state.n, pass: v.pass });
|
|
778
|
+
let took = buildTookLine(loopTimeline(dir, loopName(route)));
|
|
779
|
+
const active = activeMinutes(dir, loopName(route));
|
|
780
|
+
const wall = buildDurations(loopTimeline(dir, loopName(route)))?.total;
|
|
781
|
+
// Long pauses (a stall, a break) aren't build time: say the active minutes when they differ.
|
|
782
|
+
if (took && active != null && wall && wall - active > 10 * 60_000) took = took.replace(/^This build took [\d.]+ min/, `This build took ${minutes(active)} min of active work (${minutes(wall)} min on the clock, with pauses)`);
|
|
783
|
+
if (took) head.push(took, "");
|
|
784
|
+
if (v.pass) return [...head, "The page clears the floor. Finish with palate states, details and animate, then offer palate point."].join("\n");
|
|
785
|
+
if (worse && prevBest?.snapshot) {
|
|
786
|
+
return [...head, `This round is worse than round ${prevBest.n} (score ${v.score} vs ${prevBest.score}). Roll it back: \`palate gate ${route} --restore ${prevBest.n}\`, then try a different repair of the same target, or stop if this was the last round.`].join("\n");
|
|
787
|
+
}
|
|
788
|
+
if (repairs >= MAX_ROUNDS) {
|
|
789
|
+
return [...head, `That was the last repair round (${MAX_ROUNDS}). Tell the user honestly where the page is still below the bar (the lines above) and offer palate point so they can direct the fix. Best round: ${rr.best}.`].join("\n");
|
|
790
|
+
}
|
|
791
|
+
const targets = repairTargets(state, v);
|
|
792
|
+
const briefs = targets.map((t, i) => {
|
|
793
|
+
const file = path.join(state.at, targets.length > 1 ? `repair-${i + 1}.md` : "repair.md");
|
|
794
|
+
fs.writeFileSync(file, repairBrief(dir, state, v, t, { round: repairs + 1, of: targets.length }));
|
|
795
|
+
return file;
|
|
796
|
+
});
|
|
797
|
+
return [
|
|
798
|
+
...head,
|
|
799
|
+
targets.length > 1 ? `Repair briefs (${targets.length} sections, in parallel):` : "Repair brief (one section):",
|
|
800
|
+
...briefs.map((f) => `- ${slash(f)}`),
|
|
801
|
+
targets.length > 1 ? `Run each with its own fresh subagent at the same time: "Read <brief> and follow it exactly." Then palate gate ${route} again.` : `Do it yourself or with one subagent: "Read ${slash(briefs[0])} and follow it exactly." Then palate gate ${route} again.`,
|
|
802
|
+
].join("\n");
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// What palate check reports: routes whose latest quality gate is below the bar.
|
|
806
|
+
export function qualityBlockers(dir) {
|
|
807
|
+
const all = readJson(path.join(dir, ".palate", "quality.json"), {});
|
|
808
|
+
return Object.entries(all)
|
|
809
|
+
.filter(([, v]) => !v.pass)
|
|
810
|
+
.map(([route, v]) => ({ route, what: `below the quality bar (round ${v.round}): ${v.fails.slice(0, 2).join("; ")}` }));
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// A benchmark row from the latest gate round of this project (palate bench record).
|
|
814
|
+
export function benchResultFrom(dir, { from } = {}) {
|
|
815
|
+
const q = path.join(dir, ".palate", "quality");
|
|
816
|
+
let at = from ? path.resolve(from) : null;
|
|
817
|
+
if (!at && fs.existsSync(q)) {
|
|
818
|
+
const rounds = fs.readdirSync(q).flatMap((slug) => (fs.statSync(path.join(q, slug)).isDirectory() ? fs.readdirSync(path.join(q, slug)).filter((n) => n.startsWith("round-")).map((n) => path.join(q, slug, n)) : []));
|
|
819
|
+
const judged = rounds.filter((r) => fs.existsSync(path.join(r, "verdict.json")));
|
|
820
|
+
// The round the source is at: after a --restore that's the best round, not the latest one judged.
|
|
821
|
+
const best = judged.map((r) => ({ r, rr: readJson(path.join(path.dirname(r), "rounds.json"), null) })).find(({ r, rr }) => rr?.best != null && path.basename(r).startsWith(`round-${rr.best}-`));
|
|
822
|
+
at = best?.r ?? judged.sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs)[0] ?? null;
|
|
823
|
+
}
|
|
824
|
+
const v = at ? readJson(path.join(at, "verdict.json"), null) : null;
|
|
825
|
+
if (!v) throw new Error("No judged gate round yet: run palate gate <route> and --answers first.");
|
|
826
|
+
const count = (re) => v.pairs.filter((p) => re.test(p.result)).length;
|
|
827
|
+
return {
|
|
828
|
+
floor: { pass: v.pass, fails: v.fails.map((f) => f.id) },
|
|
829
|
+
vsRefs: { wins: count(/^better/), ties: count(/^tie$/), losses: count(/worse/), inconsistent: count(/disagree/) },
|
|
830
|
+
gate: slash(at),
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// ---- best of two: the hero (or any section) built two ways, judged blind in both orders.
|
|
835
|
+
// The page renders option B when the URL carries ?palate-alt=<section> (the builder wires that switch). The judges
|
|
836
|
+
// see two first screens labelled A and B; the user decides only when the two orders disagree.
|
|
837
|
+
export async function duel(dir, rawRoute, { section = "hero", url, answers } = {}) {
|
|
838
|
+
const route = normalizeRoute(rawRoute);
|
|
839
|
+
const root = path.join(rootFor(dir, route), `duel-${section}`);
|
|
840
|
+
if (!answers) {
|
|
841
|
+
fs.mkdirSync(root, { recursive: true });
|
|
842
|
+
const shots = await withBrowser(dir, { url }, async ({ base, browser }) => {
|
|
843
|
+
const out = {};
|
|
844
|
+
for (const [k, q] of [["one", ""], ["two", `?palate-alt=${encodeURIComponent(section)}`]]) {
|
|
845
|
+
for (const width of [1440, 390]) {
|
|
846
|
+
const { page, ctx } = await openPage(browser, base, route + q, { width, reducedMotion: true });
|
|
847
|
+
try {
|
|
848
|
+
const f = path.join(root, `${k}-${width}.png`);
|
|
849
|
+
await page.screenshot({ path: f, fullPage: width === 390 ? false : false });
|
|
850
|
+
out[`${k}${width}`] = f;
|
|
851
|
+
} finally {
|
|
852
|
+
await ctx.close();
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
return out;
|
|
857
|
+
});
|
|
858
|
+
const packet = (first, second, k) => {
|
|
859
|
+
const out = path.join(root, `answers-${k}.json`);
|
|
860
|
+
const file = path.join(root, `packet-${k}.md`);
|
|
861
|
+
fs.writeFileSync(
|
|
862
|
+
file,
|
|
863
|
+
[
|
|
864
|
+
`# Design review: two versions of a ${section}`,
|
|
865
|
+
"",
|
|
866
|
+
"You are an independent design reviewer; you made neither. Open every image with your image tool. Same page, same product, two versions of its first screen.",
|
|
867
|
+
`- Version A at 1440: ${slash(shots[`${first}1440`])} · on a phone: ${slash(shots[`${first}390`])}`,
|
|
868
|
+
`- Version B at 1440: ${slash(shots[`${second}1440`])} · on a phone: ${slash(shots[`${second}390`])}`,
|
|
869
|
+
"",
|
|
870
|
+
"Which is better designed: clearer, more distinctive, better finished, and truer to what the product is? Judge both widths.",
|
|
871
|
+
JSON_RULE,
|
|
872
|
+
`Write ${slash(out)} as {"winner": "A" | "B" | "tie", "margin": "clearly" | "slightly", "why": "under 25 words"}`,
|
|
873
|
+
].join("\n"),
|
|
874
|
+
);
|
|
875
|
+
return { file, out };
|
|
876
|
+
};
|
|
877
|
+
const p = [packet("one", "two", 1), packet("two", "one", 2)];
|
|
878
|
+
fs.writeFileSync(path.join(root, "duel.json"), JSON.stringify({ route, section, packets: p }, null, 1));
|
|
879
|
+
return [
|
|
880
|
+
`# Best of two: the ${section} of ${route}`,
|
|
881
|
+
"",
|
|
882
|
+
...p.map((x, i) => `${i + 1}. ${slash(x.file)}`),
|
|
883
|
+
"",
|
|
884
|
+
`Run each packet with its own fresh subagent, in parallel, then: palate gate ${route} --duel ${section} --answers ${slash(root)}`,
|
|
885
|
+
].join("\n");
|
|
886
|
+
}
|
|
887
|
+
const d = readJson(path.join(root, "duel.json"), null);
|
|
888
|
+
if (!d) throw new Error(`No duel for ${section} on ${route}: run palate gate ${route} --duel ${section} first.`);
|
|
889
|
+
const [a, b] = d.packets.map((p) => readJson(p.out, null));
|
|
890
|
+
if (!a || !b) throw new Error("Both judges' answers are needed.");
|
|
891
|
+
// Packet 1: A = current, B = alternative. Packet 2 the reverse.
|
|
892
|
+
const pick = (ans, flip) => {
|
|
893
|
+
const w = String(ans.winner ?? "").toUpperCase();
|
|
894
|
+
if (w !== "A" && w !== "B") return "tie";
|
|
895
|
+
return (w === "A") !== flip ? "current" : "alternative";
|
|
896
|
+
};
|
|
897
|
+
const [r1, r2] = [pick(a, false), pick(b, true)];
|
|
898
|
+
const verdictLine = r1 === r2 && r1 !== "tie" ? `The ${r1} version wins in both orders (${a.why ?? ""}${b.why ? `; ${b.why}` : ""}).` : `The judges split (${r1} vs ${r2}): a matter of taste.`;
|
|
899
|
+
const next =
|
|
900
|
+
r1 === r2 && r1 === "alternative"
|
|
901
|
+
? `Make the alternative the ${section} (move it into place, delete the current one and the ?palate-alt switch).`
|
|
902
|
+
: r1 === r2 && r1 === "current"
|
|
903
|
+
? `Keep the current ${section}; delete the alternative and the ?palate-alt switch.`
|
|
904
|
+
: `Ask the user in one line which they prefer, showing both 1440 shots (${slash(path.join(root, "one-1440.png"))}, ${slash(path.join(root, "two-1440.png"))}); their answer is taste palate learns from.`;
|
|
905
|
+
try {
|
|
906
|
+
fs.mkdirSync(TASTE_DIR, { recursive: true });
|
|
907
|
+
fs.appendFileSync(GATE_LOG, JSON.stringify({ kind: "duel", project: path.basename(path.resolve(dir)), route, section, result: [r1, r2], at: new Date().toISOString() }) + "\n");
|
|
908
|
+
} catch {}
|
|
909
|
+
return [`# Best of two: the ${section} of ${route}`, "", verdictLine, next].join("\n");
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// ---- `palate section <route> <name>`: a section builder's own check, in seconds. One section cropped at 1440
|
|
913
|
+
// and 390 and palate's floor checks for that section only, instead of a full-page shoot per builder (on Kiln v2
|
|
914
|
+
// six builders each ran full shoots: about 15 minutes each). Also at 768: tablet is where a hero photo gets
|
|
915
|
+
// blown up behind its text.
|
|
916
|
+
const SECTION_WIDTHS = [1440, 768, 390];
|
|
917
|
+
export async function sectionCheck(dir, rawRoute, query, { url, judge = false } = {}) {
|
|
918
|
+
const route = normalizeRoute(rawRoute);
|
|
919
|
+
if (!query) throw new Error("Usage: palate section <route> <section id, name or selector>");
|
|
920
|
+
try {
|
|
921
|
+
mark(dir, loopName(route), "section_check", { id: String(query) });
|
|
922
|
+
} catch {}
|
|
923
|
+
const { matchSection } = await import("../look.mjs");
|
|
924
|
+
const out = path.join(rootFor(dir, route), "sections-check", String(query).toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 40));
|
|
925
|
+
fs.mkdirSync(out, { recursive: true });
|
|
926
|
+
const found = await withBrowser(dir, { url }, async ({ base, browser }) => {
|
|
927
|
+
const res = {};
|
|
928
|
+
// The three widths side by side, each measured on the page it already loaded (no second load for the floor).
|
|
929
|
+
await Promise.all(SECTION_WIDTHS.map(async (width) => {
|
|
930
|
+
const { page, ctx, errors } = await openPage(browser, base, route, { width, reducedMotion: true });
|
|
931
|
+
try {
|
|
932
|
+
const sections = await findSections(page).catch(() => []);
|
|
933
|
+
// A page that didn't render (a build error in another builder's file, a crash) isn't "section not found".
|
|
934
|
+
const text = await page.evaluate(() => document.body?.innerText?.trim().length ?? 0).catch(() => 0);
|
|
935
|
+
const overlay = await page.evaluate(() => document.querySelector("vite-error-overlay")?.shadowRoot?.querySelector(".message")?.textContent ?? null).catch(() => null);
|
|
936
|
+
if (overlay || (!sections.length && text < 20)) {
|
|
937
|
+
res[width] = { failed: true, why: overlay?.trim().slice(0, 300) ?? errors?.[0] ?? "the page rendered nothing" };
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
// By the blueprint id (the section's component carries it in its name, id or heading), else by words.
|
|
941
|
+
const s = (await matchSection(page, sections, String(query))) ?? sections.find((x) => String(x.selector ?? "").toLowerCase().includes(String(query).toLowerCase())) ?? null;
|
|
942
|
+
if (!s) {
|
|
943
|
+
res[width] = { missing: true, names: sections.map((x) => x.name) };
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
const [crop] = await cropSections(page, [s], out, `w${width}`, { maxHeight: 6000 });
|
|
947
|
+
const checks = Floor?.floorOnPage ? await Floor.floorOnPage(page, { width, sections }).catch(() => []) : [];
|
|
948
|
+
res[width] = { index: s.index, name: s.name, file: crop?.file ?? null, height: s.box.height, checks };
|
|
949
|
+
} finally {
|
|
950
|
+
await ctx.close();
|
|
951
|
+
}
|
|
952
|
+
}));
|
|
953
|
+
return res;
|
|
954
|
+
});
|
|
955
|
+
if (found[1440]?.failed) return [`# Section check ${route}: the page didn't render`, `- ${found[1440].why}`, "Another section's file may be mid-edit or broken. Wait a moment and check again; if the error names your file, fix it first."].join("\n");
|
|
956
|
+
const floor = { checks: SECTION_WIDTHS.flatMap((w) => (found[w]?.checks ?? []).map((c) => ({ ...c, width: c.width ?? w }))) };
|
|
957
|
+
// Same-template checks compare neighbours, which other builders may still be writing: left to the whole-page gate.
|
|
958
|
+
const mine = (floor?.checks ?? []).filter((c) => c.id !== "SAME_TEMPLATE").filter((c) => {
|
|
959
|
+
const w = found[c.width ?? 1440];
|
|
960
|
+
return w && !w.missing && !w.failed && (c.section?.index === w.index || (c.section?.heading && w.name && w.name.startsWith(String(c.section.heading).slice(0, 20))));
|
|
961
|
+
});
|
|
962
|
+
if (found[1440]?.missing) return [`# Section check ${route}: "${query}" not found`, `Sections on the page: ${found[1440].names.join(" · ")}`, "Give the section's component an id or heading that names it, or pass one of these names."].join("\n");
|
|
963
|
+
const packet = judge ? earlyPacket(dir, route, String(query), out, found) : null;
|
|
964
|
+
return [
|
|
965
|
+
`# Section check ${route}: ${found[1440].name}`,
|
|
966
|
+
"",
|
|
967
|
+
`- At 1440: ${slash(found[1440].file)} (${found[1440].height}px tall)`,
|
|
968
|
+
found[768]?.file ? `- At 768 (tablet): ${slash(found[768].file)} (${found[768].height}px tall)` : "- At 768: not found",
|
|
969
|
+
found[390]?.file ? `- At 390: ${slash(found[390].file)} (${found[390].height}px tall${found[390].height > 1900 ? ": over two phone screens, check it isn't one long stacked list" : ""})` : "- At 390: not found",
|
|
970
|
+
"",
|
|
971
|
+
mine.length ? "## palate's floor checks for this section" : "## palate's floor checks: nothing wrong in this section",
|
|
972
|
+
...mine.map((c) => `- ${c.severity === "fail" ? "✗" : "!"} ${c.id} @${c.width}: ${c.what}${c.fix ? ` → ${c.fix}` : ""}`),
|
|
973
|
+
"",
|
|
974
|
+
"Open the crops and compare them with the brief's \"Done when\" questions. Fix once, check again, then report done.",
|
|
975
|
+
...(packet
|
|
976
|
+
? [
|
|
977
|
+
"",
|
|
978
|
+
"## The early judge",
|
|
979
|
+
`Packet for a fresh judge (not the builder): ${slash(packet.file)}. Put this line in your report: the main build runs the judge as soon as you're done, then \`palate section ${route} ${query} --verdict\`.`,
|
|
980
|
+
]
|
|
981
|
+
: []),
|
|
982
|
+
].join("\n");
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// ---- The early judge: one section judged by a fresh subagent as soon as its builder is done, on the same
|
|
986
|
+
// questions the gate asks (the section questions plus its archetype's checks), so a section below the bar is fixed
|
|
987
|
+
// while the others are still being built instead of in a repair round after the whole page (on a benchmark page the
|
|
988
|
+
// two repair rounds cost 16 minutes). The final gate still judges the whole page; this only means it finds less.
|
|
989
|
+
function earlyPacket(dir, route, id, out, found) {
|
|
990
|
+
const bp = loadBlueprint(dir, { route });
|
|
991
|
+
const planned = (bp?.sections ?? []).find((x) => String(x.id).toLowerCase() === id.toLowerCase());
|
|
992
|
+
const checks = planned?.archetype && Sections?.floorChecks ? Sections.floorChecks(planned.archetype) : [];
|
|
993
|
+
const questions = [...SECTION_QUESTIONS, ...checks.map((q, k) => ({ id: `a${k + 1}`, q }))];
|
|
994
|
+
const answers = path.join(out, "judge-answers.json");
|
|
995
|
+
fs.rmSync(answers, { force: true });
|
|
996
|
+
const facts = projectFacts(dir);
|
|
997
|
+
const file = path.join(out, "judge-packet.md");
|
|
998
|
+
fs.writeFileSync(
|
|
999
|
+
file,
|
|
1000
|
+
[
|
|
1001
|
+
"# Design review: one section",
|
|
1002
|
+
"",
|
|
1003
|
+
"You are an independent design reviewer. You did not make this section and you have no stake in it. Open every image with your image tool before answering.",
|
|
1004
|
+
`It is one section (${found[1440].name}${planned?.archetype ? `, meant to be: ${planned.archetype}` : ""}) of the ${route} page${planned?.job ? `; its job: ${planned.job}` : ""}.`,
|
|
1005
|
+
"",
|
|
1006
|
+
`- At 1440: ${slash(found[1440].file)}`,
|
|
1007
|
+
found[768]?.file ? `- At 768 (tablet): ${slash(found[768].file)}` : null,
|
|
1008
|
+
found[390]?.file ? `- At 390 (phone): ${slash(found[390].file)}` : null,
|
|
1009
|
+
"",
|
|
1010
|
+
...(facts ? ["## The project's facts (from its README)", "```", facts, "```", FACTS_RULE, ""] : []),
|
|
1011
|
+
"## Questions",
|
|
1012
|
+
"Answer every question YES or NO (YES = good), and a `worst` line: the one thing most below the bar, in under 20 words. Then `fix`: up to three concrete changes that would turn each NO into YES.",
|
|
1013
|
+
...questions.map((q) => `- ${q.id}: ${q.q}`),
|
|
1014
|
+
"",
|
|
1015
|
+
JSON_RULE,
|
|
1016
|
+
`Write ${slash(answers)} as {"answers": {"finished": true, ...}, "worst": "...", "fix": ["...", "..."]}`,
|
|
1017
|
+
]
|
|
1018
|
+
.filter((l) => l != null)
|
|
1019
|
+
.join("\n"),
|
|
1020
|
+
);
|
|
1021
|
+
fs.writeFileSync(path.join(out, "judge-state.json"), JSON.stringify({ route, id, questions: questions.map(({ id: q, q: text, must }) => ({ id: q, q: text, must: Boolean(must) })), at: new Date().toISOString() }, null, 1));
|
|
1022
|
+
return { file, answers };
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
// `palate section <route> <id> --verdict`: the early judge's answers, by the gate's own rule (the "finished" question
|
|
1026
|
+
// NO, or two NOs, is below the bar). Recorded per section in the route's quality folder.
|
|
1027
|
+
export function sectionVerdict(dir, rawRoute, id) {
|
|
1028
|
+
const route = normalizeRoute(rawRoute);
|
|
1029
|
+
const out = path.join(rootFor(dir, route), "sections-check", String(id).toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 40));
|
|
1030
|
+
const st = readJson(path.join(out, "judge-state.json"), null);
|
|
1031
|
+
if (!st) return `No early-judge packet for "${id}" on ${route} yet: run palate section ${route} ${id} --early first.`;
|
|
1032
|
+
const a = readJson(path.join(out, "judge-answers.json"), null);
|
|
1033
|
+
if (!a?.answers) return `The judge hasn't written ${slash(path.join(out, "judge-answers.json"))} yet: give a fresh subagent ${slash(path.join(out, "judge-packet.md"))}.`;
|
|
1034
|
+
const nos = Object.entries(a.answers).filter(([, v]) => no(v)).map(([k]) => k);
|
|
1035
|
+
const mustFail = st.questions.some((q) => q.must && no(a.answers[q.id]));
|
|
1036
|
+
const pass = !mustFail && nos.length < 2;
|
|
1037
|
+
const file = path.join(rootFor(dir, route), "early.json");
|
|
1038
|
+
const all = readJson(file, {});
|
|
1039
|
+
all[id] = { pass, nos, worst: a.worst ?? null, at: new Date().toISOString() };
|
|
1040
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
1041
|
+
fs.writeFileSync(file, JSON.stringify(all, null, 1));
|
|
1042
|
+
try {
|
|
1043
|
+
mark(dir, loopName(route), "section_verdict", { id, pass });
|
|
1044
|
+
} catch {}
|
|
1045
|
+
const q = (k) => st.questions.find((x) => x.id === k)?.q ?? k;
|
|
1046
|
+
if (pass) return `Section ${id} passes the early judge${nos.length ? ` (one NO, a warning: ${q(nos[0])})` : ""}. Leave it; the final gate judges the whole page.`;
|
|
1047
|
+
return [
|
|
1048
|
+
`Section ${id} is below the bar (NO to ${nos.join(", ")})${a.worst ? `: ${a.worst}` : ""}.`,
|
|
1049
|
+
...nos.map((k) => `- ${k}: ${q(k)}`),
|
|
1050
|
+
...(Array.isArray(a.fix) && a.fix.length ? ["Fix:", ...a.fix.slice(0, 3).map((f) => `- ${f}`)] : []),
|
|
1051
|
+
`Send this back to the section's builder (or a fresh one with its brief) now, while the other sections build: fix it once, palate section ${route} ${id}, done. One repair here; the final gate judges again anyway.`,
|
|
1052
|
+
].join("\n");
|
|
1053
|
+
}
|